Skip to content

Accept every sensitive config key as a value or a -path file - #722

Draft
coopernetes wants to merge 1 commit into
mainfrom
feature/secrets-file-sourcing
Draft

coopernetes wants to merge 1 commit into
mainfrom
feature/secrets-file-sourcing

Conversation

@coopernetes

Copy link
Copy Markdown
Member

Sensitive config keys were split between value-only and file-only forms, and the OAuth client secret and token encryption key were read from disk by the code that used them, so a missing or rotated file surfaced as a runtime error from a controller or a refresh rather than at startup.

  • Every key listed in Accept secrets as either a value or a file path, consistently across sensitive config keys #671 now takes <key> or <key>-path. The existing providers.<name>.oauth.client-secret-path and scm-oauth.token-encryption-key-path keep their names and meaning.
  • SecretsResolver resolves all of them once, after binding, and writes the value back; ScmOAuthLinkController, ScmOAuthTokenService and TokenCipherProvider no longer read secret files.
  • Both forms of one secret, an unreadable -path file, or a provider with oauth.enabled and a client-id but no client secret in either form, fails startup.
  • Startup logs the form of each configured secret (file, environment, YAML literal), never the value. WARN only for a YAML literal and the existing auto-generated ./.data/ token key.
  • scm-oauth.token-encryption-key accepts 32 raw bytes or the base64 of 32 bytes; the error names both. A configured key that fails to load fails startup; the auto-generated dev key still only disables linking.
  • secrets.require-file-sourcing (default false) refuses to start if any secret came from a value form.
  • Every resolved secret field is @ToString.Exclude, so logging a config object never prints one.
  • Docs: new docs/configuration/secrets.md, plus the key tables in providers.md and scm-oauth.md.
  • The Playwright profile gains a placeholder client-secret next to its placeholder client-id, and test/capture/secrets.env.example clears it alongside CLIENT_SECRET_PATH.

Tests: SecretsResolverTest (every pair, both forms, unreadable file, missing OAuth client secret, key formats, enforcement, toString), FogwallConfigLoaderTest (-path bound from YAML and from env, YAML vs env attribution including a mixed-case provider name, an empty env value clearing a YAML literal, secrets resolved on a hot-reload config), AesGcmTokenCipherTest, TokenCipherProviderTest. The OAuth paths that now take the resolved secret are covered end to end by BrokeredPushE2ETest / DeferredForwardingE2ETest, which configure client-secret-path and token-encryption-key-path.

Decisions for review:

  • YAML vs environment is decided by comparing the bound value with the merged file layers at that key, not by asking Gestalt (it does not report a value's source). A ${...} substitution in YAML therefore counts as environment, and an env override equal to the YAML literal counts as YAML. Provider names are lowercased in both the file tree and the bound map, so the lookup matches mixed-case names.
  • Hot reload: composeReload resolves secrets again, without a provenance log, because building the reloaded sections reaches buildProviderRegistry (via validateProviderReferences and resolveProviderName), which constructs providers with api-token. Reload still applies only policy sections, so a changed secret takes effect on restart, contrary to the issue's "covered by hot reload" line.
  • Every configured -path file is read at startup even if the feature using it is off (e.g. a token key with no provider offering linking). A bad file is a startup error regardless.
  • The resolved token key is held in scm-oauth.token-encryption-key as base64 text, so raw binary key files survive the String config field.
  • require-file-sourcing does not refuse the auto-generated dev token key; it still logs WARN. FOGWALL_RELOAD_GIT_AUTH_PASSWORD is read straight from the environment, outside the config loader, and is not covered.
  • An existing local test/capture/secrets.env that sets only CLIENT_SECRET_PATH now fails startup against the Playwright profile's placeholder client-secret until it also sets CLIENT_SECRET="".
  • OAuthClient overrides toString to redact the secret now that it carries the value.

🤖 Generated with Claude Code

Sensitive keys were split between value-only and file-only forms, and the OAuth client secret and token key files were read by the code that used them, so a missing file surfaced as a runtime error instead of a startup one.

Each sensitive key now takes <key> or <key>-path. The loader resolves them once after binding, logs which form supplied each one (WARN for a YAML literal), fails on both forms, an unreadable file, or an OAuth client with no secret, and with secrets.require-file-sourcing refuses any value form. Resolved secrets are excluded from the config classes' toString. The token encryption key accepts 32 raw bytes or base64, and a configured key that fails to load fails startup; only the auto-generated dev key still degrades.

closes #671

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant