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/**"] }
}
}
}schemamust bec8s.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.workloadsmaps 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'sconfidential.ai/cwannotation and matching label value, or the workload name recorded in an issued certificate. Each entry listsinitContainersandcontainers; each container binds adigestto itscommand,args,env, andmountspolicy. The entrylabeland each container'simageare informational. An image that may run however it is invoked is an entry with every policyany, which is whatc8s allowlist addwrites.secretsis an optional grant carried by the whole entry, not by a container: the secret-store paths the workload it names may read and write.policyisallowordeny— there is noany. Paths are absolute and clean, and the only wildcard is a trailing/**, matching strictly beneath its base, so/tenant-a/**does not grant/tenant-aitself. A grant carryingwritepaths must carry at least onereadpath. Anallowgrant also requires every container to pin its argv (commandandargsexactordeny) and its mounts (deny, orexactwith no arbitraryhostrule; proven read-onlynodeRootFilerules 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 adenygrant 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:
commandis matched as an exact prefix of the argv. It may be several tokens (/bin/sh -c,/docker-entrypoint.sh nginx,python3).argsgoverns the remainder of the argv after that prefix.
policy | command (a prefix) | args (the remainder) |
|---|---|---|
exact | argv must start with its argv | the remainder must equal its argv |
any | no prefix constraint | the remainder is unconstrained |
deny | the whole argv must be empty | there 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:
policy | Admits |
|---|---|
exact | exactly the listed names and values |
match | exactly the listed names, each matched on its own: exact pins the value, present: true accepts any value |
deny | an empty environment |
any | any 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:
| Kind | Observed source | Rule exact requires |
|---|---|---|
platform | the 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 sidecars | none — always admitted |
emptyDir | the pod's own emptyDir, on tmpfs or on the node's encrypted scratch | an emptyDir rule at that destination |
data | a ConfigMap, Secret, projected, PVC, CSI, or opened encrypted volume, on memory or encrypted storage | a data rule at a destination under /mnt/c8s-data/ |
host | a host path, or another pod's emptyDir | a host rule naming the exact source path and read-only mode |
host, with node-root provenance | a read-only bind of a regular file reached directly through the node's read-only dm-verity root | a 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, andaddrefuse 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
- Manage the allowlist — the
c8s allowlistCLI: read it, change the floor, edit workload policy, grant secret paths. - CDS HTTP API → Allowlist — the HTTP requests those commands send.
- CLI reference →
c8s allowlist— every flag. - Application secrets — what an entry's
secretsgrant releases.