The image allowlist

How measured bootstrap policy and signed workload entries govern container admission, secret grants, egress, and policy history.

The allowlist describes permitted container launches. It is a map of named workload entries. Each entry lists the containers a workload runs, and each container binds an image digest to the command line, environment, and mounts it may run with.

Every decision keys on the image digest, never the image reference. The reference a pod presents (docker.io/vllm/vllm-openai:v0.6.3) is chosen by the untrusted host; the digest is bound to the bytes that execute. References in the allowlist are labels for humans.

The Certificate Distribution Service (CDS) stores the allowlist and authorizes its updates. The node nri-image-policy plugin enforces it at container creation. Operators manage it with the c8s allowlist CLI; the CDS HTTP API defines the request contract.

Data model

{
  "schema": "c8s.allowlist/v1",
  "workloads": {
    "vllm-llama": {
      "label": "docker.io/vllm/vllm-openai:v0.6.3",
      "initContainers": [],
      "containers": [
        {
          "digest": "sha256:<vllm>",
          "image": "docker.io/vllm/vllm-openai:v0.6.3",
          "command": { "policy": "exact", "argv": ["python3"] },
          "args": { "policy": "exact", "argv": ["-m", "vllm.entrypoints.openai.api_server"] },
          "env": { "policy": "any" },
          "mounts": {
            "policy": "exact",
            "rules": [
              { "destination": "/run/c8s/secrets", "kind": "emptyDir" }
            ]
          }
        }
      ],
      "secrets": { "policy": "allow", "read": ["/tenant-a/**"] }
    }
  }
}
  • schema must be c8s.allowlist/v1. It is the first field of the canonical form, so a foreign or malformed document fails loudly instead of parsing as an empty (deny-everything) allowlist.
  • workloads maps an operator-chosen name to one entry. The name must match [A-Za-z0-9][A-Za-z0-9._-]* and is at most 63 bytes, so it can also be a pod's confidential.ai/cw annotation and matching label value, or the workload name recorded in an issued certificate. Each entry lists initContainers and containers; each container binds a digest to its command, args, env, and mounts policy. The entry label and each container's image are informational. An image that may run however it is invoked is an entry with every policy any, which is what c8s allowlist add writes.
  • secrets is an optional grant carried by the whole entry, not by a container: the secret-store paths the workload it names may read and write. policy is allow or deny — there is no any. Paths are absolute and clean, and the only wildcard is a trailing /**, matching strictly beneath its base, so /tenant-a/** does not grant /tenant-a itself. A grant carrying write paths must carry at least one read path. An allow grant also requires every container to pin its argv (command and args exact or deny) and its mounts (deny, or exact with no arbitrary host rule; proven read-only nodeRootFile rules are eligible). The process policy and mount policy sections explain these rules. Environment pinning is optional. The CDS refuses an entry that breaks this on write and refuses release for one stored earlier. An entry with no grant releases nothing, and a deny grant normalizes away entirely, so it never reaches the canonical document. See Secrets.

Digests are validated as sha256:<64 hex chars> (the OCI image-digest format). The store is a SQLite database holding the canonical workload entries, a digest index over them, and the publication history. One version counter, bumped on each successful replacement, is served as the ETag.

The serialization is canonical: fixed field order, sorted map keys, sorted container lists. Any holder of an equivalent document reproduces the same bytes, so what c8s allowlist export writes round-trips as an upload or as the workloads document in measured CDS configuration. The JSON above illustrates the fields; publication uses canonical bytes, including real digests. The application mounts its secret directory; injected platform containers own the private mesh credentials, which the webhook refuses to mount into application containers.

Process policy: command and args

A digest already pins the image's baked ENTRYPOINT/CMD — they are in the OCI config the digest covers. A workload policy constrains what a pod may run for those bytes. Without it, an image with an overridable entrypoint can be pointed at an arbitrary command — credential extraction, a reverse shell — while still presenting an allowlisted digest.

The two fields mirror the Kubernetes container fields: command overrides the image ENTRYPOINT, args overrides CMD. The enforcers do not see the override as an override — they see the container's effective argv (the OCI process.args, already merged from the image config and any pod-spec override) and match policy against that:

  • command is matched as an exact prefix of the argv. It may be several tokens (/bin/sh -c, /docker-entrypoint.sh nginx, python3).
  • args governs the remainder of the argv after that prefix.
policycommand (a prefix)args (the remainder)
exactargv must start with its argvthe remainder must equal its argv
anyno prefix constraintthe remainder is unconstrained
denythe whole argv must be emptythere must be no remainder

exact requires a non-empty argv; any and deny take none. Every combination is well-defined: command exact + args any pins the executable and lets flags vary, and exact + exact pins the whole argv. With command: any, no prefix is consumed: args checks the whole argv. For example, argv ["/app", "serve"] matches command: any with args: exact only when the args rule lists both tokens. command: any with args: deny requires an empty argv.

An absent policy normalizes to deny, so a minimally specified container is maximally restrictive. That also means command: deny requires an empty argv and can therefore never start — write command: any if you mean "any argv". c8s allowlist lint flags it.

Under command: exact and mounts: any, the final launch check rejects a non-platform mount at or above an absolute executable path in argv. For a relative name such as python3, this check does not resolve PATH and cannot detect that shadowing. The example above uses mounts: exact, which constrains mounts through its declared rules instead.

Environment policy

env constrains the complete OCI launch environment, image defaults and runtime additions included:

policyAdmits
exactexactly the listed names and values
matchexactly the listed names, each matched on its own: exact pins the value, present: true accepts any value
denyan empty environment
anyany environment (the default when env is absent)

Missing env evidence fails exact, match, and deny. lint rejects present on variables that choose which code loads (PATH, LD_PRELOAD, PYTHONPATH, NODE_OPTIONS, and others). match needs per-variable evidence, which the node keeps only for environments of at most 128 variables and 4 KiB of names; set enableServiceLinks: false on such pods. The webhook already sets it on every pod it injects into, and warns unless the pod template sets it too.

Mount policy

mounts constrains the bind mounts in the final OCI spec. The node classifies each mount by its source, not its destination alone, into one of four kinds, and a fifth rule kind admits a measured host file:

KindObserved sourceRule exact requires
platformthe kubelet and containerd files every container gets (/etc/hosts, /etc/resolv.conf, the service-account token, …), and the admission inventory's socket directory the node mounts into C8s sidecarsnone — always admitted
emptyDirthe pod's own emptyDir, on tmpfs or on the node's encrypted scratchan emptyDir rule at that destination
dataa ConfigMap, Secret, projected, PVC, CSI, or opened encrypted volume, on memory or encrypted storagea data rule at a destination under /mnt/c8s-data/
hosta host path, or another pod's emptyDira host rule naming the exact source path and read-only mode
host, with node-root provenancea read-only bind of a regular file reached directly through the node's read-only dm-verity roota nodeRootFile rule naming the exact source path, with readOnly: true

A nodeRootFile rule, such as {"destination": "/usr/bin/nvidia-smi", "kind": "nodeRootFile", "source": "/usr/bin/nvidia-smi", "readOnly": true}, admits a file whose bytes the node launch measurement already covers, so it is not treated as operator-supplied executable content and lint permits it on PATH and loader search paths. The node proves the provenance itself: the root must be one strict read-only dm-verity device whose root hash matches roothash= on the measured kernel command line. A node that cannot prove this provenance denies the rule. Proven read-only nodeRootFile rules are eligible for secret grants; arbitrary host paths and GPU socket mounts are not.

An absent policy is deny, which admits platform mounts alone. exact requires the observed non-platform set to equal its rules; any leaves mounts open. An emptyDir or data mount on storage the node cannot prove to be memory-backed or encrypted matches no rule, and missing mount evidence fails exact and deny closed.

c8s allowlist derive builds an entry from a Pod or Deployment, with --env/--env-file and --mounts/--mounts-file supplying these two policies.

A digest may run several ways

One digest can appear under several containers, within an entry or across entries. At the per-container gate, admission is the union: the container runs if its effective argv and other launch fields satisfy some listing container's policy. A shared base image (busybox, a distroless runtime) is legitimately invoked with different command lines by different workloads.

Whole-workload matching checks one entry at a time. Every observed workload launch must satisfy a container policy in that entry's containers or initContainers, and every policy in containers must have a satisfying observation. Declared init policies may be absent; observations admitted by neither list are foreign. A unique match means exactly one entry meets these conditions.

For example, an entry with distinct server and helper main policies needs observations satisfying both; another admitted helper observation does not invalidate the match. Matching does not count container copies or retain Kubernetes init/main identity: one observation can satisfy several overlapping main policies, including an init launch identical to a main policy.

Two consequences to know, both of which lint reports:

  • An unconstrained entry widens admission for a shared digest everywhere, because the host chooses which pod pairs a digest with which argv.
  • An entry whose every allowed launch another entry also admits is shadowed: every pod it describes matches both, so it is never the unique match. lint, apply, and add refuse it. To tighten a seeded any-argv entry, edit it rather than adding a narrower entry beside it.

Authorizing mutations

An operator key is an ECDSA keypair authorized to change policy. Its public half is in CDS's measured c8s-cds/v1 launch configuration; its private half signs writes locally. CDS checks the exact configuration bytes against its own attestation evidence before serving and refuses startup with no operator keys. A client independently pins the CDS image and configuration. See The trust root.

All CLI mutations publish a complete document. add, apply, edit, and delete read the current document, compose the requested change, canonicalize it, and sign a conditional replacement. They retry conflicts within a bound. upload replaces the document from a file and refuses a conflict so the operator can review the new difference.

Operator                                      Attested CDS
private signing key                           measured public keys
   |                                                |
   | GET /allowlist: document and version            |
   |<-----------------------------------------------|
   | compose change; canonicalize; sign exact bytes |
   |                                                |
   | PUT /allowlist: complete document + token       |
   | If-Match: version read                         |
   |----------------------------------------------->|
   |                      verify token and version  |
   |                      atomically replace policy |
   |<-----------------------------------------------|
   |                      204 No Content             |

A subsequent GET /allowlist supplies the updated document and version.

The token is a JSON Web Token (JWT) signed with ES256, ES384, or ES512, matching the key's curve. The CLI gives it a 60-second lifetime and a random identifier (jti). The pbh claim binds SHA-256 of the exact body, htm binds the HTTP method, and htu binds the URL path. CDS checks the signature, validity window, method, path, and body hash. It caps token validity at five minutes and spends each token once; replay returns 409.

Spent-token records are bounded: pruning uses the highest spent issuance timestamp minus a one-hour margin, and tokens whose expiry precedes that boundary remain refused. Singleton operation with a volatile allowlist store additionally requires the token's issuance time (iat) to be at or after startup plus configured clock skew. Replicas share token-spending state through their replicated store. See the operator token contract.

Canonical encoding matters after authorization too. CDS retains the token as a document signature only when the request body exactly equals the canonical document it serves. Valid noncanonical JSON can be accepted without a retained signature; seeded policy can also be unsigned. The API reference defines version conditions and canonical encoding.

These keys also authorize route-manifest writes and operator-supplied secret values. A key holder can widen workload policy, so access to the private key is policy authority. Changing the authorized public-key set requires a new measured configuration and corresponding CDS pin. Clients that continue trusting the previous configuration have not adopted that change.

The request token has no cluster audience (aud) binding. Two clusters trusting the same key can each accept a captured, otherwise valid token once within its validity window. Distinct operator keys per cluster prevent that cross-cluster reuse. The measured configuration binds initial operator authority and workload policy; signed workload updates and signed HA node-policy changes can change live policy after launch.

Seeding and bootstrap

CDS seeds workload entries from its measured launch configuration before serving requests. Seeding adds missing ordinary entries by name and preserves stored edits; it also retires recognized generated component entries superseded by the seed. An unchanged seed leaves the version unchanged. A seed error prevents startup.

The launch configuration contains public inputs, including operator keys, initial workloads, node-image policy, inventory address ranges, and certificate naming rules. It carries no private keys or secret values. Changing a file on the host does not publish policy: startup requires its exact measured bytes, and live allowlist changes use signed replacements.

The node also has a base allowlist in its boot configuration. It admits the platform launches needed before CDS is reachable and remains an independent admission path after a served policy arrives. Removing an entry from CDS cannot revoke a launch still admitted by that base. On a measured node image, changing the base requires changing the measured image and its pins. CDS's seed and the node's base serve different roles even when they describe some of the same component images.

State lifetime depends on deployment mode. A singleton's allowlist and publication history follow its database lifetime; its CA and secret state are volatile. Replicated CDS carries allowlist state and history alongside CA, secrets, and bindings in replicated snapshots. A restarting replica can recover from surviving peers under the cluster's serving rules. Total loss of replica memory loses volatile state. See Mesh CA bootstrap.

Enforcement

The node's nri-image-policy plugin runs inside the confidential node's TEE. At container creation it resolves the image digest and checks effective argv, then checks argv, environment, and mounts in the final OCI specification before start. With no served policy, the base allowlist supplies bootstrap admission. Audit mode records denials while admitting containers; enforcement mode applies denials. Configured namespace exemptions admit only digests in their captured running-container set, rather than every image a namespace names.

Ordinary container admission is a union of permitted launches. It does not require that all containers in an entry start together. Capabilities and other pod-spec fields have their own controls outside this allowlist. Rechecking already running containers requires enforce_existing outside audit mode, respects exemptions, and attempts to stop denied containers; stop failures are logged.

Certificate issuance and secret release ask different questions. For a sandbox-bound certificate request, CDS fetches inventory from an attested node and requires every reported digest to be allowlisted. It can issue a certificate before the workload is complete. When the inventory uniquely matches an entry, CDS stamps its name and policy version/digest onto the leaf. An incomplete or ambiguous match can yield an unnamed leaf.

Secret release rechecks current policy against attested inventory and requires one unique workload match with a qualifying grant, including its argv and mounts. An allowlisted digest or an earlier certificate stamp alone is insufficient. Platform roles excluded from workload matching are established by measured, finalized launches and owned injected mounts; naming a container like a sidecar grants no exemption. See Application secrets.

Egress and workload composition

An entry can carry egress rules: explicit TCP destinations that the pod may reach outside mesh capture. An exit is the network exception installed for one such rule. A rule names a CIDR address range and TCP port. Each entry permits at most eight unique rules, with masked IPv4 prefixes of /16 or narrower and IPv6 prefixes of /48 or narrower. Single-address /32 or /128 rules keep the destination narrow. Validation and the node's network gate exclude special addresses and protected platform or cluster ranges.

Egress requires every container's argv and mounts to be pinned by the same rules as secret grants. Environment policy may still be any. A permitted environment can influence application behavior, so egress eligibility alone does not establish an environment-pinned launch. derive does not add egress rules automatically.

The node grants exits only when the observed launches satisfy every main-container policy of one unshadowed entry and the sandbox contains no foreign workload launches. It then binds the sandbox to that entry and refuses additions its container policies do not admit. Exits remain unavailable until the observed set satisfies every main policy; an init launch that matches a main policy can satisfy it. These composition checks apply to egress-bearing entries, beyond ordinary per-container admission.

An exit bypasses capture; it supplies neither payload encryption nor inspection. The workload must protect its outbound protocol. Changing or removing exits requires pod recreation to apply the new exit set.

Policy updates

The node replaces the served policy as a unit and rejects a version below the last version it applied. An equal-version document can be applied again. A fetch failure retains the previous policy. The version high-water mark is process-local: it rejects older versions during that process lifetime, but the first version after restart establishes a new baseline. The node health endpoint's allowlist_version reports its applied served version; it does not prove every running container has stopped after a tightening.

Allowlist history

CDS publication history records canonical documents and any retained operator signatures. Its read API exposes retained versions, document digests, publication times, objects, and signatures. All CLI mutation forms can produce signed documents; a seed or noncanonical manual write can produce unsigned policy. See the history API.

The HA leader periodically prunes history toward the newest 1,000 documents and a 64 MiB budget, preserving at least the current document. These are retention targets; writes can exceed them before pruning applies. Pruned objects and signatures become unavailable. History follows the state lifecycle described above; keep any required audit archive separately.

Node policy measurement is a separate record. When enabled on TDX, the policy-measurement hook must succeed before the node accepts a pulled policy. It checks journal capacity and calls MeasureOnce for the policy digest against runtime measurement register RTMR3; a measurement failure retains the previous policy. At capacity, the caller refuses a digest absent from the journal until reboot. This history records node acceptance, while CDS history records publication.

Measurement is optional and is a no-op on SNP. The stock baked node image leaves it disabled, and Helm refuses to enable it for node.baked. A supported configuration that enables it also needs the runtime journal and API mounts. A stock baked node therefore supplies no measured policy-history guarantee.

c8s verify establishes both the measured history and whether the verifier accepts its policies. The history anchor is the operator public key whose bytes seed RTMR3 at launch, supplied through --operator-pkey or an image-policy pin's approver_key. The verifier replays the reported policy digests from that seed and compares the result with attested RTMR3. Acceptance uses explicit --pin-policy digests or --trust-operator --trust-operator-keys; these choices are mutually exclusive and cover every digest proven by that replay plus the policy under which the certificate was issued. --operator-keys has a separate CDS/route verification role.

--fetch-allowlists retrieves digest-checked documents for inspection. Fetching documents or replaying RTMR3 alone does not accept their policy. Missing acceptance, stale evidence, or an unauthenticated leaf stamp can leave verification partial. The CLI distinguishes verified (exit 0), partial (4), policy rejection (2), and unavailable evidence (3). See Verify a deployment for the evidence and trust inputs.

See also