Manage the allowlist

Read, derive, and publish signed workload policy through an attested CDS connection, then check the served policy and node propagation.

Use c8s allowlist to admit workloads before deploying them and to update their launch policy. c8s publishes each change as one complete, operator-signed document. For the workload model and enforcement boundaries, see The image allowlist.

Before you begin

Have these inputs available:

  • The c8s CLI, OpenSSL for key generation, sha256sum for export checks, jq for grant-only edits, and crane on PATH with registry access when deriving or inspecting images.
  • A reachable Certificate Distribution Service (CDS) endpoint and its independently approved image/configuration pin, obtained through Obtaining launch measurements.
  • For writes, the private operator key whose public half the CDS configuration authorizes.
  • For derivation, deploy.json: your Pod, Deployment, StatefulSet, or DaemonSet as JSON, with every application init and main container image pinned to its platform manifest digest.
  • For propagation checks, authorized access to each node's plugin logs, or a deployment operator who can supply per-node refresh records containing the timestamp, digest, and version. Health-only records cannot establish the document digest.

This guide calls the measured c8s node image baked nodes (--cvm-mode=bare-metal). Chart-managed nodes use --cvm-mode=gke or --cvm-mode=aks; the chart installs their plugin into the host. The launch-bundle examples use a directory named bundle.

Reads need no operator credential, but network reads still verify the endpoint. derive, lint, and inspect-image do not contact CDS; derivation, image inspection, and online lint can read registries. The CLI reference lists the command options.

Create the operator credential

For a new CDS configuration, generate a private key and its public PEM:

umask 077
openssl ecparam -name prime256v1 -genkey -noout -out operator.key
openssl ec -in operator.key -pubout -out operator.pub

Keep operator.key private. Add operator.pub to the measured c8s-cds/v1 configuration through Install c8s. CDS verifies the exact configuration bytes before serving and refuses a configuration with no operator keys. Changing the authorized key set requires a new measured configuration, corresponding CDS pin, and the authorized CDS lifecycle procedure. Setting Helm's cds.operatorKeys alone does not change the external VM's authority.

For baked nodes, c8s node launch-config new authorizes the generated bundle/server.key, or the existing key supplied through its --operator-key. Use that private key for writes. The generator's required --cds-allowlist-seed supplies component entries; optional --tenant-workloads supplies a full c8s.allowlist/v1 document of tenant entries with distinct names. For chart-managed nodes, generate the component seed using the exact release, namespace, image tag, values, and component options used for installation.

The measured CDS seed initializes served policy; live edits use the signed commands below. The node's base allowlist independently admits bootstrap components. Removing a served entry cannot revoke an image still admitted by that base. See Seeding and bootstrap.

Reach the allowlist API

Use the external CDS address supplied to installation or launch generation and the pin that binds its image and launch configuration:

URL='https://<CDS_HOST>:<PORT>'
PIN='bundle/cds-pin.json'
c8s allowlist list --url "$URL" --image-policy-file "$PIN"

Replace <CDS_HOST> and <PORT> with that CDS endpoint. Set PIN to your retained CDS pin; bundle/cds-pin.json is the baked-node launch-bundle example, while the installation guide creates cds-bootstrap/cds-pin.json. The connection verifies Remote-Attestation TLS (RA-TLS). A successful command prints the served version and workload table.

Alternatively, use a router front door whose discovery document reports public_tls.mode=cds and whose allowlist route is enabled. Set URL to https://<ROUTER_HOST>, replacing <ROUTER_HOST> with that front door's hostname, and set PIN to your approved router policy, such as router-pin.json. bundle/server.json is suitable only when it names the node identity hosting that router. The router verifies its connection to CDS against the separate CDS pin.

Allowlist commands also accept --measurements or --measurements-file for a router launch digest. Use those instead of --image-policy-file, never as a substitute for the complete CDS image/configuration pin on the direct path. Discovery verification failures are fatal; WebPKI and ACME router certificates do not provide the discovery binding this CLI requires. Use the direct CDS path for those front doors. Unpinned HTTPS is refused, including reads. Plain HTTP requires --insecure, which skips attestation and is for development only.

Keep the selected URL and PIN for the remaining allowlist commands.

Read it

c8s allowlist list --url "$URL" --image-policy-file "$PIN"
c8s allowlist get vllm-llama --url "$URL" --image-policy-file "$PIN"
c8s allowlist export allowlist.json --url "$URL" --image-policy-file "$PIN"
c8s allowlist diff allowlist.json --url "$URL" --image-policy-file "$PIN"

Replace vllm-llama with the workload entry's name. list prints the version and a summary; -o json prints the document without that version heading. get prints one entry as formatted JSON. export writes the complete canonical bytes, suitable for backup or upload; retain that file unchanged when using its digest as a policy pin.

diff compares a complete document with live policy; --exit-code returns nonzero for a reported difference. Its field comparison covers labels and container policies, but omits secret grants and egress. Inspect those fields in the JSON directly.

Change workload policy

Use apply to replace selected entries while preserving the other entries in the live document. Each supplied entry replaces its entire previous value, including grants and egress. The operator key signs the complete resulting canonical document.

Derive the entry

For an application with no non-platform mounts:

c8s allowlist derive vllm-llama deploy.json --env=any --mounts=deny > entry.json

vllm-llama is the name you choose for this workload. The output is a bare name-keyed map, which apply accepts. derive pins effective argv, consulting the image's baked entrypoint and command through crane when needed. It drops injected c8s containers and reports them on stderr. Re-derive when application images or commands change.

Choose --env=any, --env=deny, or --env-file env.json. The file maps every derived init and main container name to its environment policy. For a container whose complete launch environment consists of PATH and HOSTNAME, save this shape as env.json:

{
  "<APP_CONTAINER>": {
    "policy": "match",
    "variables": {
      "PATH": { "exact": "<EXPECTED_PATH>" },
      "HOSTNAME": { "present": true }
    }
  }
}

Replace <APP_CONTAINER> with its manifest name and <EXPECTED_PATH> with the approved image/runtime value. Add every other environment name the image and runtime introduce, and an entry for each other derived container. match requires the complete name set; use exact for fixed values and present only for values permitted to vary. Set the pod template's enableServiceLinks: false. See Environment policy for constraints.

For volumes, replace --mounts=deny with --mounts-file mounts.json, a JSON map covering every derived init and main container by name. For one container mounting a ConfigMap, save this as mounts.json:

{
  "<APP_CONTAINER>": {
    "policy": "exact",
    "rules": [{ "destination": "/mnt/c8s-data/config", "kind": "data" }]
  }
}

Replace <APP_CONTAINER> with the manifest's container name and the destination with its actual mount path. ConfigMap, Secret, and persistent-volume data mounts belong under /mnt/c8s-data/; other mount kinds have their own rules. See Mount policy. Private mesh credentials belong to injected platform containers; application rules do not include /etc/c8s/certs.

Lint and review

c8s allowlist lint entry.json --strict
c8s allowlist apply entry.json --dry-run --url "$URL" --image-policy-file "$PIN"

Require lint success and review the JSON as well as the displayed diff. Offline lint accepts the same document or entry-map shapes as apply; --strict makes warnings fatal. It checks unlaunchable policies, admission widened by an unconstrained duplicate entry, ambiguous or shadowed entries, and unsafe grants or mount/search-path combinations. A standalone unconstrained entry need not produce a warning. --online additionally checks digests in their registries. The dry run checks collisions with the live document and sends no write.

When replacing an existing entry, retain its intended grants and egress in entry.json. derive emits only its requested grant fields and has no egress flag.

Publish the change

c8s allowlist apply entry.json \
  --url "$URL" --image-policy-file "$PIN" --operator-key operator.key

Use the authorized key from the credential step; for a generated baked-node bundle, substitute bundle/server.key. C8S_OPERATOR_KEY supplies the key path when --operator-key is absent. Success prints applied vllm-llama.

add, apply, edit, and delete read the current version and retry conflicts up to five conditional replacement attempts. Each attempt can resend HTTP requests for 429 or 503 responses. They preserve concurrent changes to other entries; entries named by your operation take your requested value. upload refuses a conflict so you can rerun it and review the new diff. If a response is uncertain, the CLI reads back the document; follow its diagnostic and check served policy before retrying when that read also fails.

Confirm the served policy

c8s allowlist get vllm-llama --url "$URL" --image-policy-file "$PIN"
c8s allowlist export allowlist-after.json --url "$URL" --image-policy-file "$PIN" &&
sha256sum allowlist-after.json

Check the intended fields in the export's workloads.vllm-llama, including secrets and egress; concurrent writes can change the document between get and export. Retain the unchanged export and the printed SHA-256 hash. Prefix the 64 hexadecimal characters with sha256: to form the exact document digest used for node propagation. If the export lacks your intended policy, resolve that change before proceeding.

Each successful replacement advances the CDS version, even when the document bytes match. A separate list read reports the version at that read; it does not pair a version with this export. Record each node's version from the same refresh event that names the export digest.

Edit, delete, or replace the document

To edit one entry in $EDITOR and confirm its publication:

c8s allowlist edit vllm-llama \
  --url "$URL" --image-policy-file "$PIN" --operator-key operator.key

To remove the named entry:

c8s allowlist delete vllm-llama \
  --url "$URL" --image-policy-file "$PIN" --operator-key operator.key

On baked nodes (--cvm-mode=bare-metal), replace the full document with an edited allowlist.json export:

c8s allowlist upload allowlist.json --cvm-mode=bare-metal \
  --url "$URL" --image-policy-file "$PIN" --operator-key operator.key

When a confirmed edit cannot be published, the CLI reports a saved file for retry with apply and the same flags.

For chart-managed nodes, use --cvm-mode=gke or --cvm-mode=aks. Those modes require image labels naming armtls-mesh, nri-image-policy, attestation-api, and nginx. --cvm-mode=bare-metal omits the two measured host services, nri-image-policy and attestation-api. Lint errors always block upload; --strict also blocks warnings. Review component requirements before overriding them with --require or --force.

add <IMAGE_DIGEST> <IMAGE_REF> creates an unconstrained one-container entry named from the image basename and digest prefix. <IMAGE_DIGEST> is the sha256: platform manifest digest; <IMAGE_REF> is the corresponding image reference used as a label. Use it only when every launch policy may be any; derive and apply provide the pinned policy needed for grants and egress.

For a custom API client, all mutations use PUT /allowlist with the complete document. Convert GET's weak ETag W/"N" to the strong numeric If-Match: "N"; a mismatch or invalid condition, including *, returns 412. Omitting the condition makes the write unconditional. Only exact canonical request bytes retain the operator token as a document signature. Use the API contract when implementing a client.

Grant secret paths

Every container in an entry with a secret grant must pin its argv and mounts. Environment policy may remain any, although lint warns about grants with unpinned environment values and --strict makes that warning fatal.

For an application consuming injected secret files, its exact mount rules include { "destination": "/run/c8s/secrets", "kind": "emptyDir" } plus its actual application mounts. Containers without such mounts use deny. Proven read-only nodeRootFile rules can qualify; arbitrary host paths and GPU sockets cannot. Follow Application secrets for consumer setup and release diagnosis.

Derive a new entry with a grant

With deploy.json, env.json, and mounts.json prepared as above, including each consuming container's secret mount, derive the complete entry:

c8s allowlist derive vllm-llama deploy.json \
  --env-file env.json --mounts-file mounts.json \
  --secret-read '/tenant-a/model/**' > entry.json

Replace /tenant-a/model/** with the intended absolute secret-store path or subtree. Then follow Lint and review, publish with apply, and confirm the served policy. Derivation creates a new entry; use the next procedure to preserve an existing one.

Change only an existing grant

Fetch the current entry and wrap it in the name-keyed map that lint and apply accept. This example replaces only secrets, retaining all other fields from that read:

c8s allowlist get vllm-llama --url "$URL" --image-policy-file "$PIN" > live-entry.json &&
jq '{"vllm-llama": .} | .["vllm-llama"].secrets = {"policy":"allow","read":["/tenant-a/model/**"]}' \
  live-entry.json > grant-entry.json &&
c8s allowlist lint grant-entry.json

Use your workload name in both the command and JSON key, and replace the read path with its intended grant. Review grant-entry.json and all lint findings. Coordinate writers to this same entry: apply replaces its whole value, so a later concurrent edit to that name can be overwritten. Publish the reviewed file and read it back:

c8s allowlist apply grant-entry.json \
  --url "$URL" --image-policy-file "$PIN" --operator-key operator.key &&
c8s allowlist get vllm-llama --url "$URL" --image-policy-file "$PIN"

Use apply for grant-only edits: edit treats them as no changes, while apply can print unchanged and still write the grant. Set the grant's policy to deny, or remove it from the replacement entry, to deny subsequent release under that entry. Previously delivered secret bytes remain with their recipients.

Release rechecks current policy against the sandbox's attested admission inventory and needs a unique matching workload with a qualifying grant. The inventory retains admitted launches, including exited or removed containers. Every required main-container policy needs a matching record; matching does not establish current liveness or distinguish init from main lifecycle.

Egress uses the same argv and mount pinning requirements. Review Egress and workload composition before adding exits: the sandbox's retained admission records must satisfy every main-container policy of one unshadowed entry. Foreign additions are refused, and changed or removed exits require pod recreation. An exit supplies no payload encryption or inspection. Use apply and read back egress-only edits too.

Check node propagation

Obtain a recent pull loop: allowlist refreshed or initial allowlist pulled from CDS record for every target node whose digest equals sha256:<EXPORT_SHA256>, replacing <EXPORT_SHA256> with the hash of the reviewed allowlist-after.json. Record the node, timestamp, digest, and version from that same event. Inspect later refresh records for superseding policy. These are operational diagnostics; a separate CDS list version is not the export's version.

On baked nodes, an administrator with authorized host-local access can run:

sudo journalctl --boot --identifier=nri-image-policy --output=cat
sudo curl --unix-socket /run/nri-image-policy/health.sock \
  --silent --show-error --max-time 5 http://localhost/healthz

The baked plugin sends its records to /dev/log with tag nri-image-policy and serves health on the shown Unix socket. These commands require journalctl, curl, and permission to read the node journal and socket. If host access is unavailable, obtain the records from your deployment operator through the administrative channel arranged in the prerequisites.

On chart-managed nodes, the installer deploys the plugin onto the host; its DaemonSet's plugin-ready logs are not the plugin's refresh records. The generated configuration uses plugin stdout for logging, so ask the deployment operator for the host runtime's collection of that stream. Its plugin.health_addr supplies the health address; chart defaults use the same socket above, derived from nriImagePolicy.hostPaths.runtimeDir and nriImagePolicy.healthSocketName. If the runtime does not retain the stream, the digest-based propagation check remains unverified until the operator supplies refresh records.

The health response reports allowlist_version and any allowlist_stall, but carries no document digest. It supplements the matching log event; it cannot replace that check. CDS publication alone does not establish that every node has applied the document.

Pull failures retain the previous policy. During a plugin process lifetime, versions below its applied version are rejected; equal versions can be applied again. Restart establishes a new baseline. Rechecking running containers depends on enforcement configuration and can fail to stop a container; a version check alone does not prove revocation. See Enforcement.

Keep any required audit archive separately. CDS serves retained publication objects and signatures; HA pruning targets the newest 1,000 entries and 64 MiB while preserving the current document. Removed objects and signatures become unavailable.

Node policy measurement is optional and Intel TDX-only; AMD SEV-SNP has no policy-measurement effect. The stock baked image leaves it disabled, and Helm refuses opt-in for node.baked. Supported enabled configurations require journal/API mounts and retain the old policy if measurement fails. For that deployment path, follow Verify a deployment.

Verify who can write

The CDS pin binds the measured configuration's operator keys. Optionally compare its reported key set with your independently trusted public-key bundle using c8s cds verify --operator-keys. That command uses the direct CDS URL and CDS pin even when allowlist operations use a router. The comparison supplements the image/configuration pin.

Getting an image's digest

Use a platform-specific manifest digest and put the same digest-pinned reference in the workload manifest. For an amd64 workload, with registry access configured for crane:

crane digest --platform linux/amd64 '<IMAGE_REF>'
c8s allowlist inspect-image '<IMAGE_REPOSITORY>@sha256:<64_HEX_DIGEST>'

<IMAGE_REF> is the image tag or reference to resolve; <IMAGE_REPOSITORY> is its repository without the tag, and <64_HEX_DIGEST> is the returned digest's hexadecimal part. Choose the platform that matches the node. inspect-image prints the digest and baked entrypoint/command needed to review argv. A multi-platform index and its platform manifest are different objects; admission matches the exact digest reported by the runtime.

Automating the allowlist

Give automation a private key already authorized by the measured CDS configuration, using distinct keys per cluster. Keep the key in your credential system and expose it as a protected file only to the release job. Retirement follows the CDS configuration and lifecycle change; editing a Helm value does not retire CDS authority.

For a reviewed entry.json, the endpoint variables above, and that key mounted at /run/secrets/operator.key:

c8s allowlist lint entry.json --strict &&
c8s allowlist apply entry.json \
  --url "$URL" --image-policy-file "$PIN" \
  --operator-key /run/secrets/operator.key

Require command success, read the entry back, and retain the reviewed canonical export, its SHA-256 digest, and matching per-node refresh records. The CLI's request token lasts 60 seconds and binds the method, path, and exact body; the private key remains the authority for future writes.

See also