skip to content

A rule blocking secrets as container env vars missed a chart using envFrom — what went wrong?

level: middleimportance: must knowfreq 54%

answer

  1. the fact, not the field
  2. more than one way to say the same thing
  3. envFrom injects a whole Secret
  4. initContainers is a separate list
  5. under-matching fails silently

basics

~20 s

The same fact has several representations in the document. A secret reaches the environment through env[].valueFrom.secretKeyRef and through envFrom[].secretRef, and the rule inspected only the first. The contract must enumerate every field the fact can appear in.

solid answer

~50 s

The rule was written against one shape of the fact rather than the fact itself. A secret lands in a container's environment through at least two fields — an entry in `env` whose `valueFrom.secretKeyRef` points at a Secret, and an `envFrom` entry with a `secretRef` that injects a whole Secret — and a literal `value` can carry a credential too. Inspecting `env` alone leaves the other paths open, and because a missing match is silent, the suite stays green. The same mistake repeats across container lists: a rule that walks `spec.template.spec.containers` misses `initContainers`. The fix is a contract change first: enumerate every field where the fact can appear and every list of containers, then cover each one with its own deny fixture, so adding a representation later is a visible edit to both the contract and the rule.

code

yaml · 15 lines
yaml
# rendered output, spec.template.spec.containers[0]
- name: api
  image: registry.example.com/api
  env:
    - name: LOG_LEVEL
      value: info
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: db-creds
          key: password
  envFrom:
    - secretRef:
        name: api-tokens
  ...

go deeper

for a junior

Know that a secret can reach a container's environment in more than one way, and be able to name env with a secretKeyRef and envFrom with a secretRef as two of them.

for a middle

Explain why the miss was silent rather than an error, and walk through the container lists a rule must enumerate. Expect to be asked what the fixture matrix looks like afterwards.

for a senior

Demonstrate that your remedy lands in the input contract where reviewers read it, and be ready to argue when normalising the input beats enumerating inside the rule.

for a principal

Be prepared to defend a policy that is deliberately narrower than the platform allows — for instance forbidding one representation outright so the interesting rules get a simpler contract — and to say who maintains that enumeration as the platform grows.

## The bug is a modelling bug, not a syntax bug The rule asked "is there an entry under `env` that references a Secret?" The policy it was meant to encode is "can a secret value be read from this container's process environment?" Those are not the same question, and the document offers more than one way to make the second one true. ## The representations, concretely For a workload manifest, a secret value reaches the environment through: - **`env[].valueFrom.secretKeyRef`** — one named key of a Secret bound to one variable. - **`envFrom[].secretRef`** — an entire Secret injected, one variable per key, with names the manifest never spells out. - **`env[].value`** — a literal. Nothing marks it as sensitive; the credential is simply sitting in the document. And for each of those, the container itself can live in more than one place: `spec.template.spec.containers`, `spec.template.spec.initContainers`, and `ephemeralContainers` where the surface permits them. A rule that hard-codes one list has the same hole in a different dimension. Charts that inject a sidecar during rendering are the usual way this is discovered, because the injected container is appended to a list the author never walked. ## Why it fails quietly An under-matching rule produces no error. There is no missing field to complain about, no exception, no denial: the rule is evaluated, finds nothing at the path it was told to look at, and returns no violation. From outside the gate that is indistinguishable from a compliant chart. This is the reason under-matching is worse than over-matching in practice — an over-matching rule blocks somebody's change and is reported within the hour, while an under-matching rule reports success for a year. ## The fix belongs in the contract, not only in the rule Patching the rule to also read `envFrom` closes today's hole and leaves the process unchanged, so the next representation is found the same way. What actually prevents recurrence is writing the enumeration down as part of the input contract: > The fact *a secret is readable from this container's environment* is expressed by: `env[].valueFrom.secretKeyRef`; `envFrom[].secretRef`; a literal `env[].value`. Containers are enumerated from `containers`, `initContainers` and `ephemeralContainers`. That list becomes reviewable. A reviewer who knows the platform can say "you missed a list" while reading three lines, instead of reverse-engineering it from rule logic. It also gives the tests their structure: one deny fixture per representation, per container list — a small matrix that fails loudly when someone simplifies the rule. ## Normalising instead of enumerating A stronger pattern, when you control the step that builds the input, is to normalise before deciding: derive one flat collection of "environment sources" per container from all of the above, and have the rule decide over that. The rule then has one shape to reason about, and the mapping from raw document to normalised form is the thing tested for completeness. The trap is that normalisation is itself code that can under-match, so it needs the same enumeration and the same per-representation fixtures — you have moved the problem to a place where it is easier to review, not removed it. ## Two things this is not It is not an argument for text matching. Scanning the rendered document for the word "secret" catches the `envFrom` case and also every comment, image name and unrelated label, and a rule that fires on comments loses the room the first time it blocks a legitimate change. It is also not solved by a convention. "Teams should use `env` and never `envFrom`" is only true while it is enforced, and enforcing it is another rule with the same problem: it has to recognise `envFrom` to forbid it. If you do take that route, the forbidding rule is the one that must be complete, and the secrets rule then legitimately depends on a narrowed contract — but say so in the contract, because that dependency is invisible otherwise. ## What an interviewer is listening for That you separate *the fact* from *the field*; that you recognise silence as the failure signature of an under-matching rule; that your remedy is enumerated in a place a reviewer reads, with a fixture per representation; and that you check container lists as carefully as you check field names.

  • Why is an under-matching rule more dangerous than an over-matching one?
    An over-matching rule blocks a legitimate change, so somebody complains the same day and you fix it. An under-matching rule returns no violation, which is indistinguishable from a compliant estate: the dashboard is green, the auditor sees a passing control, and nobody learns anything until an incident makes someone re-read the rule.
  • How would you prove the rule now covers every representation?
    Write one deny fixture per representation and per container list — a small matrix — and assert each is refused. Then check the rule has no path left unexercised, so a later simplification that drops a branch fails a test rather than passing quietly. Coverage over the enumeration in the contract, not over the rule's line count, is what the assertion is really about.
  • Would normalising the input into one list of environment sources be better than enumerating in the rule?
    Often yes, when you own the step that builds the input: the rule then decides over one flat shape and is much easier to read. But the normaliser inherits the completeness problem, so it needs the same enumeration and the same per-representation fixtures. You have made the risky part small and reviewable rather than eliminated it.

saying these in an interview costs you the question

  • Patches the one missed field and calls it done
  • Believes a single field expresses the whole fact
  • Falls back to text-matching the rendered document
  • Walks only spec.template.spec.containers
  • Relies on a team convention nothing enforces

context