fix(synapse): keep credentials out of the config ConfigMap - #32
Conversation
|
Verified on a live k3s cluster, not just from source. The precedence claim was the load-bearing assumption in this PR, so it needed a runtime check. SetupRendered this branch with the credential only in the Secret, and a deliberately different
Results1. The Secret reaches synapse. The worker logs 2. Env wins over config.yaml. References to The authcheck attempt confirms the api_key path is live too — that request only happens with a key set. 3. The ConfigMap is clean. Grepping the full in-cluster ConfigMap YAML (which includes the This also confirms the two Test resources removed afterwards. |
Follow-up: per-endpoint credentials needed more than env injectionThe env-var approach above only reaches fields synapse exposes an env override for. It does not cover per-endpoint keys. There are three
So a separate key for geoip or model/threat downloads could only be written into the file — straight back into a world-readable ConfigMap. Fix
Off by default. One combination is refused outrightThe operator's NetVarsResolver reads and rewrites That is not cosmetic — an unfilled Verified on a cluster
Boundary worth stating: the geoip fetch itself did not run during the test (0 hits on the bogus endpoint), so I proved the credential is stored and the file is loaded from a Secret, not that geoip transmits that header. That is synapse behaviour and unchanged by this PR. Follow-up worth filing separatelyTeaching synapse |
The chart templated platform.api_key, captcha.secret_key and
captcha.jwt_secret into synapse.config, which renders into a ConfigMap. That
exposes them three ways: readable by anyone with `get configmap` in the
namespace, copied verbatim into the last-applied-configuration annotation,
and returned by `helm get values`. None of that is true of a Secret.
Adds a `secrets` block injected as env vars via envFrom:
secrets.existingSecret reference a Secret managed out of band (External
Secrets, sealed-secrets, Vault). Preferred: no
credential ever enters values.
secrets.create render a Secret from values. Better than a
ConfigMap, but the credential is then in values.
This needs no synapse change. synapse parses config.yaml and then runs
apply_env_overrides, which assigns unconditionally (cli.rs:2907 then :2914),
so API_KEY / CAPTCHA_SECRET_KEY / CAPTCHA_JWT_SECRET override the file. The
Secret's keys are those env var names.
Corrects two values.yaml comments that claimed the opposite - "YAML config
has higher priority than env vars" and "environment variable overrides
(lowest priority)". Env wins. Both would have talked someone out of exactly
this fix.
Also fails the render when a credential is still set inside synapse.config,
with the migration spelled out. A hard fail rather than a warning is
deliberate: the silent version of this is how a production api_key ended up
in cleartext.
Note the remaining exposure: env vars are visible in /proc/<pid>/environ
inside the container and in crash dumps. File-mounted secrets would be
tighter but need synapse to learn to read them, so that is a follow-up.
Env injection only reaches fields synapse exposes an env override for. It
does not reach the three `headers` maps - telemetry exporter, geoip sources,
threat/download source - which exist to carry per-endpoint credentials
(`Authorization: "Bearer ..."`). There is no env override for any of them and
config.yaml has no ${VAR} interpolation, so a per-endpoint key for geoip or
model/threat downloads could only be written into the file, and therefore
into a world-readable ConfigMap.
`synapse.configSecret` renders config.yaml into a Secret instead. synapse
reads a path and does not care whether the bytes come from a ConfigMap or a
Secret, so this needs no synapse change. upstreams.yaml is not sensitive and
stays a ConfigMap so the --ingress-mode operator can keep owning it.
Off by default, and the chart refuses one combination outright: the
operator's NetVarsResolver reads and rewrites config.yaml in a ConfigMap only
(netvars_resolver.go takes a corev1.ConfigMap and calls r.Update on it). With
config.yaml in a Secret it would find nothing to rewrite and quietly stop
filling ids.address_vars.HOME_NET/EXTERNAL_NET - and an unfilled HOME_NET is
what lets inline IDS blocking ban an internal source IP. Failing the render
beats discovering that from a banned node.
Verified on a cluster: with configSecret on, synapse logs "Using config file:
/etc/synapse/config.yaml" and names keys from that file as unused, so it
parsed the Secret-sourced content; a header credential appears once in the
Secret and zero times across every ConfigMap in the namespace; the ConfigMap
retains only upstreams.yaml.
9f042f0 to
1f5e8f7
Compare
|
Rebased onto main, which now carries #33 (Dragonfly auto-wire) and #34 (content_scanning keys + appVersion). Five conflicts, all version bumps or adjacent additions rather than disagreements:
One thing I changed rather than just merging. The credential guard's comment said "a warning, not a hard fail: existing installs must still be able to render while they migrate" — but the code called Verified after the rebase — all four guards fire on their own trigger and nothing else:
And the two features coexist: with
|
Problem
platform.api_key,captcha.secret_keyandcaptcha.jwt_secretare templated intosynapse.config, which renders into a ConfigMap. That exposes them three ways:get configmapin the namespace;kubectl.kubernetes.io/last-applied-configurationannotation;helm get values.The chart had no Secret template at all. Putting them in
env:is no better — that block renders literal name/value pairs into the Deployment spec.Fix
A
secretsblock, injected viaenvFrom:existingSecret— reference a Secret managed out of band (External Secrets, sealed-secrets, Vault). No credential ever enters values. Preferred.create— render a Secret from values. Better than a ConfigMap, but the credential then lives in your values file.The Secret's keys are the env var names synapse reads:
API_KEY,CAPTCHA_SECRET_KEY,CAPTCHA_JWT_SECRET.Why no synapse change is needed
synapse parses
config.yamland then applies env overrides, assigning unconditionally:So env wins over the file, and the ConfigMap can simply stay empty.
Two documentation bugs, corrected
values.yamlclaimed the exact opposite in two places:Both are backwards, and either would have talked someone out of this fix. Corrected, with the mechanism named.
Guard
Rendering now fails if a credential is still set inside
synapse.config:A hard fail rather than a warning is deliberate: the silent version of this is how a production
api_keyended up in cleartext.This is a breaking upgrade for anyone currently setting a credential in
synapse.config— by design. Migration is: blank the field, move the value tosecrets.Verification
existingSecret=my-credsenvFrom: [{secretRef: {name: my-creds}}], no Secret renderedcreate=trueAPI_KEY,CAPTCHA_JWT_SECRET;envFrompoints at itapi_key: ""synapse.configAll three charts lint; stack and both overlays render.
Known remaining exposure
Env vars are visible in
/proc/<pid>/environinside the container and in crash dumps. File-mounted secrets would be tighter, but that needs synapse to learn to read a credential from a file — worth a follow-up issue rather than blocking this.Precedence was verified by reading the source, not at runtime.