Skip to content

Signing Configs

Sign a Containment Chamber config for signed-Nitro deployment with YubiKey PIV or software Ed25519

A signed-Nitro enclave boots only from a config whose signature verifies against the operator/CI public keys baked into the enclave image (CONFIG_SIGNERS, measured into PCR0). You sign one config file: the enclave verifies it, and the chart reads the same file’s values to wire the parent-side proxies — one source of truth, no parent/enclave drift.

direction: down
author: "author config.yaml" { style.fill: "#FFF6EF" }
sign: "sign payload (YubiKey PIV / OpenSSL Ed25519)" { style.fill: "#FFF6EF" }
helm: "helm upgrade --set-file signedConfig=signed.yaml" { style.fill: "#FFF6EF" }
render: "Helm render (single input)"
cm: "ConfigMap: signed bytes, verbatim"
derived: "parent values: egress allowlist + ports (fromYaml)"
parent: "Parent proxy: forwards bytes, NO verify"
enclave: "Enclave: verify vs PCR0-baked CONFIG_SIGNERS"
author -> sign
sign -> helm
helm -> render
render -> cm
render -> derived
cm -> parent: "mount /config/config.yaml"
parent -> enclave: "forward over vsock"
derived -> parent: "wire vsock-proxy + ports"
enclave -> enclave: "boot iff quorum verifies, else refuse"
  1. Signer keys. An operator key on a YubiKey PIV (ECDSA P-256, slot 9c) and/or a software Ed25519 key for CI. Operators use P-256 (not Ed25519) so signing works on any YubiKey 5.x; Ed25519-in-PIV needs firmware ≥ 5.7.

  2. Trust manifest baked into the EIF. Your signers’ public keys, threshold, and required IDs live in custody/config-signers.ron, baked into the enclave image at build time (CHAMBER_SIGNERS_FILE) and measured into PCR0. See the manifest format in EKS Enclave Deployment. A signed-Nitro EIF built without it refuses to boot.

  1. Freeze the config and stamp a generation. bundle --create edits in place, normalizes the signed region to end in exactly one LF, and writes a trailer carrying generation N (monotonic, >= 1). Keep the unsigned original.

    Terminal window
    cp config.yaml signed.yaml
    containment-chamber config bundle --file signed.yaml --create --generation N
  2. Emit the exact bytes to sign. This is the canonical message DOMAIN ‖ generation ‖ signed_region — domain- and generation-bound, so a signature cannot be replayed across generations.

    Terminal window
    containment-chamber config sign-payload --file signed.yaml > payload.bin
  3. Sign the payload with each signer. Helper scripts wrap the exact algorithm so you cannot accidentally produce a signature the enclave will reject:

    Terminal window
    # YubiKey PIV operator (ECDSA P-256, slot 9c) -> raw DER signature
    scripts/sign-config-piv.sh payload.bin operator-a.sig
    # software Ed25519 (CI) -> raw 64-byte signature
    scripts/sign-config-ed25519.sh ci-ed25519.key payload.bin > github-ci.sig
  4. Embed each signature into the trailer (base64). Repeatable — run once per signer. The <signer-id> must match an id in config-signers.ron.

    Terminal window
    containment-chamber config bundle --file signed.yaml --add operator-a=operator-a.sig
    containment-chamber config bundle --file signed.yaml --add github-ci=github-ci.sig
  5. Verify locally (optional, but see the caveat below).

    Terminal window
    containment-chamber config verify --file signed.yaml
  6. Deploy. --set-file loads the file’s bytes as the signedConfig value, verbatim.

    Terminal window
    helm upgrade containment-chamber ./k8s/charts/containment-chamber \
    --set-file signedConfig=signed.yaml
Role Key Signature emitted Helper
operator YubiKey PIV, ECDSA P-256 DER, over SHA-256(message) scripts/sign-config-piv.sh
ci software Ed25519 raw 64-byte, over message scripts/sign-config-ed25519.sh

The alg is bound to the signer’s manifest entry and is never read from the wire.

--set-file signedConfig=signed.yaml injects the file’s bytes as an opaque string value — it does not parse the YAML and does not compute any hash. The rolling restart on a config change comes from a separate, chart-side annotation on the pod template:

checksum/config: {{`{{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}`}}

When signedConfig changes, the rendered ConfigMap changes, so its sha256sum changes, so the pod template changes, so helm upgrade performs a rolling restart. Editing a ConfigMap alone never restarts pods — this checksum is what turns a byte change into a redeploy.