Skip to main content

Attested deployment (AWS Nitro Enclaves)

This page is the deployment recipe for EDGE_KEYSTORE=nitro. Read that section first for what it protects against and what it does not.

Most deployments should use the guided installer

The guided installer — chainos-edge-nitro.sh for macOS and Linux, chainos-edge-nitro.ps1 for Windows — does every step below in your AWS account, generates and seals the recovery phrase without anyone seeing it, and prints the endpoint your application calls. This page is the same deployment by hand, for when you need to see or change each piece.

The reference files are in the repository under chainos-edge/deploy/nitro/.

The pieces​

PieceWherePrivilege
Enclave running chainos-edge enclave-signer from an EIF built from the release imageCarved out of the instanceHolds the seed. Nothing else does
Launcher — nitro-cli plus a small Unix-socket bridgeA container in the Edge's taskPrivileged: it needs /dev/nitro_enclaves
Edge — chainos-edge with EDGE_KEYSTORE=nitroA container in the same taskUnprivileged, read-only root, non-root user
Allocator and a KMS vsock-proxyHost services, from user-data.shHost

The Edge container cannot open vsock itself: Docker's default seccomp profile refuses the AF_VSOCK socket family, and ECS has no way to attach another profile. The launcher therefore bridges a Unix socket on a volume private to the task (EDGE_NITRO_SOCKET), which keeps the Edge container unprivileged and exposes nothing on any network.

The enclave reaches KMS through the host's vsock-proxy, allow-listed to kms.<region>.amazonaws.com:443 and nothing else. TLS runs end to end inside the enclave, so the host relays ciphertext it cannot read or alter. The enclave trusts only Amazon's root CAs, pinned in the image.

Steps (ECS on EC2)​

  1. Instances. An enclave-capable type with at least 4 vCPUs (for example m5.xlarge), the ECS-optimized Amazon Linux 2023 AMI, --enclave-options Enabled=true, IMDSv2, and deploy/nitro/user-data.sh as user data with your cluster name. It installs aws-nitro-enclaves-cli, reserves 2 vCPUs and 1.5 GiB for enclaves, and runs the KMS proxy on vsock port 8000.
  2. The EIF. Build it on the instance from the release image — the binary inside ziklag/chainos-edge:<release>, plus the pinned Amazon CA bundle — as a sorted tarball with fixed times and owners, docker imported and passed to nitro-cli build-enclave. deploy/nitro/installer/reconcile.sh does exactly this; follow it. Built that way, every build of one release measures the same. Ziklag does not sign or publish EIFs (§ What to pin on the Keystores page): the measurement you pin is the one you built from the release.
  3. The KMS key. A symmetric key with deploy/nitro/key-policy.template.json, filled in:
    • EdgeUnsealAttestedOnly allows kms:Decrypt to the Edge task role only when kms:RecipientAttestation:ImageSha384 (PCR0) is the image you built and the encryption context's environment is right. Pin PCR8 instead only for an image signing key you hold;
    • DenyDecryptWithoutAttestation denies kms:Decrypt to every principal when there is no attestation document — without it the role could simply decrypt on the instance. Never pin an all-zero value: that is what a debug-mode enclave reports.
  4. Seal with chainos-edge seal-mnemonic --provider=nitro --key=<arn> --policy=policy.json … — the same ceremony as awskms (Mnemonic management), plus the autonomous-spend policy the enclave will enforce:
    {
    "spendCaps": { "ETH": "50000000000000000", "TRON": "20000000" },
    "spendWindowSeconds": 86400,
    "treasuryAllowlist": ["0x…your treasury…"],
    "forwarderContracts": ""
    }
    seal-mnemonic validates the file and prints the policy it sealed. Caps are in the chain's smallest unit; a chain with no cap gets no autonomous spending. Do not set EDGE_SPEND_CAP_*, EDGE_SPEND_WINDOW, EDGE_SWEEP_TREASURY_ALLOWLIST or CHAINOS_FORWARDER_CONTRACTS on the Edge in this mode — it refuses to start, naming them.
  5. The task definition. Bridge networking, the task role from step 3, a shared volume at /run/chainos, and two containers from the parent image (deploy/nitro/Dockerfile.parent):
    • launcher: chainos-enclave-launcher, privileged: true, essential, with EDGE_NITRO_SOCKET=/run/chainos/enclave.sock;
    • edge: chainos-edge, user: 65534, read-only root, depends on launcher starting, with EDGE_KEYSTORE=nitro, PASS_PHRASE_SEALED_FILE, EDGE_AWSKMS_KEY_ID, AWS_REGION and EDGE_NITRO_SOCKET=/run/chainos/enclave.sock;
    • EDGE_DATA_DIR on a volume that survives a task restart (an EBS-backed host path, not the task's scratch space): the enclave's signed ledger snapshot lives there as enclave-ledger.json.
  6. Verify. The Edge logs seed unsealed inside the enclave with the enclave's PCR0, and its identity. Compare the PCR0 with the one your key policy names, and the identity with the one seal-mnemonic printed. It also logs the sealed policy and the ledger restore: restored N spend record(s) after an ordinary restart, or an error naming the chains whose capped spending is frozen and until when. CloudTrail shows one Decrypt per start, by the task role, with the enclave's image digest under additionalEventData.

On plain EC2, without ECS, run the same two processes on the instance: nitro-cli run-enclave and chainos-edge with EDGE_NITRO_CID=16 instead of a socket — a host process may open vsock.

Measured on AWS​

The scenario below was run on an m5.xlarge in eu-north-1 as an ECS task, against a key pinned as above:

ScenarioResult
The Edge image, signed with the pinned test keyUnsealed; identity, 250 addresses, 8 xpubs and 10 chain signatures identical to mnemonic mode
The parent's own role calling Decrypt without attestationDenied
The same image signed with another key (PCR8 differs)Denied
The Edge image in debug mode (PCRs zero)Denied
A one-byte-different image signed with the pinned key, PCR8 pinnedReleased — the trade-off of pinning the signer
The same, PCR0 pinnedDenied
Decrypt revoked while the Edge runsThe running Edge keeps signing; a restart refuses to start, naming the denial
Signing latency through the enclavesecp256k1 p99 ≈ 1.1 ms; Ed25519 p99 ≈ 0.5 ms; a 1,000-address pool ≈ 45 ms

And for the signing policy inside the enclave:

ScenarioResult
First start, policy sealed in the envelopePolicy applied from the header; no snapshot, so capped spending frozen for one window
Restart 25 s laterSnapshot restored; the freeze carried over
Snapshot tampered with on diskbad signature; frozen
Restart after 2 min 46 s downSnapshot too old; frozen
Gas top-up within the sealed capSigned; a second, over the cap, refused inside the enclave
A deposit address paying a strangerRefused inside the enclave
Raw signing, or signing at an arbitrary path, from the parentRefused
EDGE_SPEND_CAP_ETH and EDGE_SWEEP_TREASURY_ALLOWLIST set on the parentThe Edge refuses to start, naming them
Withdrawal signing through the policyp99 ≈ 1.7 ms

Upgrading​

With PCR0 pinned, build the new release's EIF, add its PCR0 to the policy before rolling out (the condition takes a list), roll out, then remove the old one when you no longer need to roll back. With PCR8 pinned to your own signing key, a new release you sign needs no policy change. Either way the enclave is restarted, so each replica unseals once more — the KMS must be reachable during a rollout. Roll replicas within two minutes of their last checkpoint, or expect capped spending to pause for one window.

Changing the spend policy is a reseal: seal-mnemonic --provider=nitro --policy=<new file> with the phrase — or --from-envelope=<recovery envelope> for a phrase nobody has seen — then replace the envelope and restart.