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.
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
| Piece | Where | Privilege |
|---|---|---|
Enclave running chainos-edge enclave-signer from an EIF built from the release image | Carved out of the instance | Holds the seed. Nothing else does |
Launcher — nitro-cli plus a small Unix-socket bridge | A container in the Edge's task | Privileged: it needs /dev/nitro_enclaves |
Edge — chainos-edge with EDGE_KEYSTORE=nitro | A container in the same task | Unprivileged, read-only root, non-root user |
| Allocator and a KMS vsock-proxy | Host services, from user-data.sh | Host |
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)
- 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, anddeploy/nitro/user-data.shas user data with your cluster name. It installsaws-nitro-enclaves-cli, reserves 2 vCPUs and 1.5 GiB for enclaves, and runs the KMS proxy on vsock port 8000. - 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 tonitro-cli build-enclave.deploy/nitro/installer/reconcile.shdoes 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. - The KMS key. A symmetric key with
deploy/nitro/key-policy.template.json, filled in:EdgeUnsealAttestedOnlyallowskms:Decryptto the Edge task role only whenkms:RecipientAttestation:ImageSha384(PCR0) is the image you built and the encryption context'senvironmentis right. Pin PCR8 instead only for an image signing key you hold;DenyDecryptWithoutAttestationdenieskms:Decryptto 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.
- Seal with
chainos-edge seal-mnemonic --provider=nitro --key=<arn> --policy=policy.json …— the same ceremony asawskms(Mnemonic management), plus the autonomous-spend policy the enclave will enforce:{"spendCaps": { "ETH": "50000000000000000", "TRON": "20000000" },"spendWindowSeconds": 86400,"treasuryAllowlist": ["0x…your treasury…"],"forwarderContracts": ""}seal-mnemonicvalidates 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 setEDGE_SPEND_CAP_*,EDGE_SPEND_WINDOW,EDGE_SWEEP_TREASURY_ALLOWLISTorCHAINOS_FORWARDER_CONTRACTSon the Edge in this mode — it refuses to start, naming them. - 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, withEDGE_NITRO_SOCKET=/run/chainos/enclave.sock;edge:chainos-edge,user: 65534, read-only root, depends onlauncherstarting, withEDGE_KEYSTORE=nitro,PASS_PHRASE_SEALED_FILE,EDGE_AWSKMS_KEY_ID,AWS_REGIONandEDGE_NITRO_SOCKET=/run/chainos/enclave.sock;EDGE_DATA_DIRon 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 asenclave-ledger.json.
- Verify. The Edge logs
seed unsealed inside the enclavewith the enclave's PCR0, and its identity. Compare the PCR0 with the one your key policy names, and the identity with the oneseal-mnemonicprinted. 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 oneDecryptper start, by the task role, with the enclave's image digest underadditionalEventData.
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:
| Scenario | Result |
|---|---|
| The Edge image, signed with the pinned test key | Unsealed; identity, 250 addresses, 8 xpubs and 10 chain signatures identical to mnemonic mode |
The parent's own role calling Decrypt without attestation | Denied |
| 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 pinned | Released — the trade-off of pinning the signer |
| The same, PCR0 pinned | Denied |
Decrypt revoked while the Edge runs | The running Edge keeps signing; a restart refuses to start, naming the denial |
| Signing latency through the enclave | secp256k1 p99 ≈ 1.1 ms; Ed25519 p99 ≈ 0.5 ms; a 1,000-address pool ≈ 45 ms |
And for the signing policy inside the enclave:
| Scenario | Result |
|---|---|
| First start, policy sealed in the envelope | Policy applied from the header; no snapshot, so capped spending frozen for one window |
| Restart 25 s later | Snapshot restored; the freeze carried over |
| Snapshot tampered with on disk | bad signature; frozen |
| Restart after 2 min 46 s down | Snapshot too old; frozen |
| Gas top-up within the sealed cap | Signed; a second, over the cap, refused inside the enclave |
| A deposit address paying a stranger | Refused inside the enclave |
| Raw signing, or signing at an arbitrary path, from the parent | Refused |
EDGE_SPEND_CAP_ETH and EDGE_SWEEP_TREASURY_ALLOWLIST set on the parent | The Edge refuses to start, naming them |
| Withdrawal signing through the policy | p99 ≈ 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.