Configuration precedence
Why the API prefix is
/instellingen/and not/config/. Do not "tidy" this back. The shared nginx ingress controller carries a global Nextcloud hardening snippet withlocation ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)/ { deny all; }. It applies to every host on the cluster, so every request under/config/was answered with nginx's own 403 and never reached the pod — invisible in both the application and the oauth2-proxy logs. Measured 2026-08-17; the same snippet also blocks any path starting withautotest,occ,issue,indie,db_orconsole(no trailing slash, so/issuesand/indienenmatch too). Renaming our own routes is the only fix that does not touch infrastructure shared with other tenants.
All configuration is resolved by one loader, iso_audit.config.settings.load_config.
Nothing reads os.environ directly for configuration: that would lose the provenance,
and provenance is the part an auditor asks about.
Order
| Priority | Source | Intended for |
|---|---|---|
| 1 (wins) | environment | Secrets and per-environment values. Injected by the deployment, CI, or a .env on a workstation. |
| 2 | config.yaml |
Non-secret defaults you want in version control — a Jira URL, a model choice. |
| 3 | UI store | What an auditor links in the portal. |
| 4 | built-in default | Only where a sensible default exists (currently the model). |
The first source with a non-empty value wins. An empty or whitespace-only value does not count as set, so it never blocks a lower source.
Why environment first. A deployment must never silently run on a value someone typed into the UI. Conversely, if the UI could override the manifest, an auditor could change a deployment without it being visible in Git.
Provenance
Every resolved value carries its own source (ui-override, env, yaml, ui,
default, leeg). Provenance is a property of the value, not a separate bookkeeping
table — that way it cannot fall away between resolving and using.
- At startup, one audit-log line per field records which source won. Never the value.
GET /instellingen/herkomstreturns the same information, so an auditor can check it without cluster access.- In the portal, each input shows a badge with its source. Fields set from the environment
or
config.yamlare read-only, because a value typed over them would have no effect.
Overriding an administrator value
A field set from the environment can still be replaced from the portal, but only as an
explicit action (POST /instellingen/bronnen/{bron}?overschrijf=true). Its provenance then
becomes ui-override, and the change trail records overschrijft_omgeving: true with who
and when — never the value. Clearing the field restores the environment value.
This exists for one reason: a credential that expires or is revoked must be replaceable by
the person doing the audit. If that requires a cluster administrator, the audit capability
is tied to one person again — see
credential-rotatie-door-auditor.
If the environment value changes after an override was made — an administrator rotating a Secret, for instance — the portal says so on that field. The override stays in effect until someone removes it; the comparison uses a fingerprint, so neither value is shown.
Secrets
- A secret value is never returned by the API, never written to a log, and never shown in
the text representation of the object holding it —
repr()prints<geheim>and the source. That is a structural boundary, not a discipline: a secret cannot reach a log file through an f-string or a stack trace. - Where the UI shows an existing secret it is masked with a fixed-length prefix, so the masking does not reveal the length of the token.
- A secret in
config.yamlworks, but logs a warning. Refusing to start would block a third party on a file they can fix themselves; making it visible is enough.
Schema version
config_version is explicit so a future migration is traceable. A file carrying a higher
version than the code knows is reported and then read anyway — an auditor who cannot see
their configuration cannot repair it either.
Anthropic auth modes
api_key |
sso |
|
|---|---|---|
| Classification, themes, memo text | yes | yes |
| Headless (cron, CI, a runtime in the cluster) | yes | no — needs a browser step, and the refresh token hard-expires |
| Cost traceable on an organisation invoice | yes | no — runs on a personal subscription |
sso uses the Anthropic CLI profile, which the SDK already resolves as a credential
source; no second call path exists for the classifier.
One trap worth knowing. A set ANTHROPIC_API_KEY takes precedence over the CLI
profile — including an empty string. In sso mode the loader therefore removes that
variable from the environment rather than merely skipping it. Without that, a run fails on
a credential the auditor did not choose, and the error points at Anthropic instead of at
the configuration.
Because sso is personal by definition, it is a working step for interactive use, not an
end state for an organisation that wants the capability to outlive one person.
Model choice and cost
The model is configurable, default claude-haiku-4-5. Every selectable model must have a
price row; a test fails otherwise. A model without a price row would make a run report a
cost of zero — worse than no cost line, because it looks complete.
Rates carry a review date (PRIJZEN_PEILDATUM) because prices change outside this repo.