Keystores
Every keystore mode answers one question: where does the recovery phrase live before the Edge starts, and who can get at it? The modes differ in that; after boot, every mode available today holds the seed in locked memory inside the Edge process and derives child keys as it needs them.
| Mode | Where the phrase lives at rest | Status |
|---|---|---|
mnemonic | A file (PASS_PHRASE_FILE) or variable you mount, in plaintext. Default | Available |
awskms | A sealed envelope, decrypted at boot with AWS KMS | Available |
azurekv | A sealed envelope, decrypted at boot with Azure Key Vault or Managed HSM | Available |
nitro | A sealed envelope released only to an attested AWS Nitro Enclave, where the seed then lives | Available — guided installer for macOS, Linux and Windows |
azureskr | A sealed envelope released only to an attested Azure confidential container group, where the whole Edge runs | Available — not yet validated on Azure |
gcpcs | A sealed envelope released only to an attested GCP Confidential Space workload, where the whole Edge runs | Available — not yet validated on GCP |
pkcs11 | Inside an HSM that derives BIP-32 keys itself | Planned — core built, no device certified yet; hsm-probe available |
The Edge refuses to start with any value it does not implement, rather than falling back to
mnemonic.

pkcs11, awskms and azurekv were accepted and ignoredEarlier versions of this page described those three as available. They were never built: an
Edge of that era accepted the values, loaded the mnemonic exactly as mnemonic mode does, and
reported the requested keystore on its status page. If you set one of them on such a version,
your seed was in process memory the whole time. awskms now exists and means something
different from what those pages described, and so does azurekv — see
Sealed seed.
Two different threats
It helps to keep two things apart, because no keystore addresses both.
- Someone obtains the key material itself. That is permanent and silent: they can sign anything, from anywhere, for ever, and the only remedy is moving every asset to a new phrase. This is what keystore modes are about.
- Someone with live access makes the Edge sign something it should not, without ever
learning the key. That is bounded by how long they keep access and shows up in logs, but it
moves funds. No keystore closes it — an HSM signs whatever the process holding its PIN asks
it to. It is closed by who holds
zkl_*keys, approval policies in ChainOS, the Edge's spend caps and its sender and destination checks.
"The key is in a KMS" means the first threat is narrowed. It does not mean an operator with a shell on the Edge's node cannot move funds.
What mnemonic mode does
- The phrase is read once at boot, from
PASS_PHRASE_FILEorPASS_PHRASE, and never re-read. Use the file — see Configuration. - On Linux the seed lives in a small buffer outside the Go heap that is locked so it cannot
be paged to swap and excluded from core dumps. That needs a few KiB of
RLIMIT_MEMLOCKand no capability; if the kernel refuses, the Edge refuses to start. - At startup the process makes itself non-dumpable and disables core files. Another process
running as the same user — including a
kubectl debugcontainer — then cannot read/proc/<pid>/environ,/proc/<pid>/memor/proc/<pid>/rootwithoutCAP_SYS_PTRACE. - Child private keys are derived on demand, used to sign, and zeroed. Never persisted.
- Nothing derived from the phrase is written to the data directory.
- The image runs as a non-root uid from a
scratchbase with no shell. A read-only root filesystem and dropping every capability are settings your deployment applies; the image does not enforce them. The Edge needs no added capability, so it runs under Pod Securityrestricted.
What it does not protect against
The seed exists in the Edge's address space for as long as the process runs, and the mnemonic file stays mounted beside it. Anyone who can do one of these can read it:
- read the secret the mnemonic is mounted from — in Kubernetes, anyone with
getonsecretsin the Edge's namespace; - open a shell beside the process —
kubectl debugwith an ephemeral container can mount the pod's secret volume and read the file. Because the Edge is non-dumpable, an ordinary debug container can no longer read/proc/<pid>/environor the process's memory; a privileged one (--profile=sysadmin) still can, unless Pod Securityrestrictedrejects it; - read the host's memory or collect a core dump — root on the node, or the hypervisor.
So mnemonic mode does not keep the seed from the people who operate the Edge's
infrastructure. A sealed mode removes the first two routes. For
mnemonic mode, it is a matter of who can reach the Edge:
- Deliver
PASS_PHRASE_FILEfrom an external secret store (through the Secrets Store CSI driver or equivalent) so that no KubernetesSecretobject holds the phrase, and restrict that store to the Edge's workload identity and a small named group. - Give nobody routine
pods/exec,pods/attach,pods/ephemeralcontainersorsecretsaccess in the Edge's namespace; route those through an audited break-glass role. - Run the Edge on dedicated nodes that ordinary engineers cannot log in to, and consider confidential nodes (AMD SEV-SNP or Intel TDX) if the host or cloud operator is in your threat model.
- Separate who generates and stores the mnemonic from who deploys the Edge.
Sealed seed (awskms, azurekv)
The phrase is encrypted once, by a seed custodian, into an envelope: the phrase under a
one-time AES-256 data key, with the data key wrapped by a KMS key you own. The Edge is
deployed with the envelope instead of the phrase. At boot it asks KMS to unwrap the data key,
decrypts the phrase into locked memory, checks it derives the identity recorded in the
envelope, and from then on behaves exactly as mnemonic mode. It never calls KMS again until
it restarts.
Neither KMS can derive BIP-32 child keys, so neither can sign for the Edge — see why. What sealing changes is everything before the Edge starts.
What it closes
- Reading the Kubernetes
Secret, a volume snapshot, an etcd backup or a CI log yields ciphertext. The envelope is safe to store in Git or an ordinarySecret. - A debug container that mounts the Edge's volumes reads ciphertext.
- Every decryption is a
kms:Decryptevent in CloudTrail (anUnwrapKeyevent in Azure Monitor), attributed to the identity that made it, and revoking the Edge role'skms:Decryptstops the next start. - Duties separate: the custodian who seals cannot decrypt, the administrators of the key cannot decrypt, and the Edge can only decrypt.
What it does not close — say this to your auditors in these words
- Anyone who can act as the Edge's cloud identity can call
Decryptthemselves — for example by running a pod with the Edge's ServiceAccount. Restrict who can create pods with that ServiceAccount to your deploy pipeline. The attestednitromode closes this. - Root on the node, or the hypervisor, can read the Edge's memory after boot, where the
seed is in plaintext.
nitrocloses this; so would an HSM. - A KMS key administrator can rewrite the key policy to grant themselves
Decrypt. That cannot be prevented by policy; alarm on it (below).
Configuration
| Variable | |
|---|---|
EDGE_KEYSTORE=awskms | |
PASS_PHRASE_SEALED_FILE | Path to the envelope |
EDGE_AWSKMS_KEY_ID | The key ARN. Must equal the envelope's keyRef |
AWS_REGION | Must equal the key's region |
PASS_PHRASE and PASS_PHRASE_FILE must not be set: the Edge refuses to start if either
is present beside a sealed mode. AWS credentials come only from the workload identity — EKS
Pod Identity, IRSA, an ECS task role or an instance profile. If AWS_ACCESS_KEY_ID is in the
environment the Edge refuses to start. Shared config and credentials files are ignored.
See Configuration for timeouts and retries, and Mnemonic management for the sealing ceremony.
Reference key policy
A symmetric KMS key (SYMMETRIC_DEFAULT) with three roles. Replace ACCOUNT and the role
names with yours.
{
"Version": "2012-10-17",
"Statement": [
{ "Sid": "KeyAdministration", "Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::ACCOUNT:role/chainos-key-admin" },
"Action": ["kms:Describe*", "kms:Get*", "kms:List*", "kms:PutKeyPolicy",
"kms:EnableKeyRotation", "kms:TagResource", "kms:ScheduleKeyDeletion",
"kms:CancelKeyDeletion"],
"Resource": "*" },
{ "Sid": "SealOnly", "Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::ACCOUNT:role/chainos-seed-custodian" },
"Action": "kms:GenerateDataKey",
"Resource": "*",
"Condition": { "StringEquals": { "kms:EncryptionContext:purpose": "chainos-edge-mnemonic" } } },
{ "Sid": "EdgeUnsealLive", "Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::ACCOUNT:role/chainos-edge-live" },
"Action": "kms:Decrypt",
"Resource": "*",
"Condition": { "StringEquals": {
"kms:EncryptionContext:purpose": "chainos-edge-mnemonic",
"kms:EncryptionContext:environment": "live" } } }
]
}
- Administrators get no
kms:Decrypt, the custodian gets nokms:Decrypt, and the Edge role gets onlykms:Decrypt. - The
environmentcondition means a sandbox Edge's role cannot decrypt a live envelope even if someone points it at one. The Edge also refuses an envelope whose environment differs from its API key's before it calls KMS at all. - An administrator who can
PutKeyPolicycan widen this policy. Alarm on everyPutKeyPolicy,CreateGrant,DisableKeyandScheduleKeyDeletionon the key, and on anykms:Decryptby a principal other than the Edge role. - Automatic key rotation of the same key needs no re-seal.
Azure Key Vault (azurekv)
The same design, with the data key generated by the Edge's custodian tooling and wrapped with
RSA-OAEP-256 under an RSA key in Key Vault.
| Variable | |
|---|---|
EDGE_KEYSTORE=azurekv | |
PASS_PHRASE_SEALED_FILE | Path to the envelope |
EDGE_AZUREKV_KEY_ID | The full key identifier including its version: https://<vault>.vault.azure.net/keys/<name>/<version>, or the managedhsm.azure.net equivalent. Must equal the envelope's keyRef |
- Use Managed HSM, or at least a Premium vault. The key must be
RSA-HSM, 3072 bits or more, with only thewrapKeyandunwrapKeyoperations;seal-mnemonicrefuses a software-protected or shorter key. A standard vault's software keys are not what an auditor means by "HSM". - The version is pinned because an unversioned identifier means "the latest": a key rotated under the Edge would silently stop opening the envelope it sealed. Re-seal after rotating.
- Credentials come only from Azure Workload Identity (a federated token) or a managed
identity.
AZURE_CLIENT_SECRETorAZURE_CLIENT_CERTIFICATE_PATHin the environment is a startup error. - Key Vault has no encryption context. The environment and label are bound into the envelope instead, so a wrapped key moved into another envelope unwraps and then fails to decrypt — but a single key policy cannot stop a sandbox identity unwrapping a live envelope. Use a separate key, or a separate vault, per environment.
Least privilege. The built-in Key Vault Crypto User role also allows decrypt, sign and more. Create two custom roles instead and assign them on the key, not the vault:
{
"Name": "ChainOS Edge Unseal",
"Description": "Unwrap the ChainOS Edge seed data key. Nothing else.",
"Actions": [],
"DataActions": ["Microsoft.KeyVault/vaults/keys/unwrap/action"],
"AssignableScopes": ["/subscriptions/<subscription>/resourceGroups/<group>"]
}
{
"Name": "ChainOS Seed Custodian",
"Description": "Seal a ChainOS Edge seed: read the key and wrap. No unwrap.",
"Actions": [],
"DataActions": ["Microsoft.KeyVault/vaults/keys/wrap/action",
"Microsoft.KeyVault/vaults/keys/read"],
"AssignableScopes": ["/subscriptions/<subscription>/resourceGroups/<group>"]
}
On Managed HSM, define the equivalent roles in its local RBAC with
Microsoft.KeyVault/managedHsm/keys/unwrap/action for the Edge, and
Microsoft.KeyVault/managedHsm/keys/wrap/action plus
Microsoft.KeyVault/managedHsm/keys/read/action for the custodian. (The built-in Managed HSM
Crypto Service Encryption User grants read, wrap and unwrap — too much for either role.)
Alarm, in Azure Monitor, on UnwrapKey by any principal other than the Edge's identity, and on
every role assignment or key change on the key.
Status
The status page shows the keystore, the key the envelope was sealed under, its label, and the
Edge's full identity fingerprint. The custodian compares that fingerprint with the one
seal-mnemonic printed. If they differ the Edge would not have started: it refuses an
envelope whose phrase does not derive the identity the envelope records.
Attested enclave (nitro)
The seed never exists outside an AWS Nitro Enclave: a VM carved out of the EC2 instance, with no network, no disk, no shell, and memory the instance itself — its root user, its other containers, the ECS agent — cannot read. The Edge process on the instance (the parent) holds no key material at all. It asks the enclave for public keys and signatures over a local vsock channel, and the enclave answers.
How the seed gets in, once per enclave start:
- The parent sends the enclave the envelope (sealed with
--provider=nitro) and its own temporary role credentials. - Inside, the enclave generates a throwaway RSA key and asks the Nitro Secure Module for an attestation document: a statement signed by AWS that names the exact image running (PCR0) and who signed it (PCR8), and carries that RSA public key.
- The enclave calls KMS
Decryptwith the attestation document attached. KMS checks the measurements against the key policy and, if they match, returns the data key encrypted to the enclave's RSA key — never in plaintext. - The enclave decrypts it, opens the envelope, checks the phrase derives the identity the custodian recorded, and discards the RSA key.
What this closes, beyond the sealed modes
- The Edge's cloud identity is no longer enough. The parent's role makes the KMS call, but
KMS answers only to the attested enclave; the key policy denies
Decryptwithout an attestation document to every principal, administrators included. Someone with the role but outside the enclave gets nothing. - Root on the instance cannot read the seed. It is only in enclave memory.
- A modified image cannot get the seed. Its measurements differ, so KMS refuses it. A debug-mode enclave reports all-zero measurements and is refused too.
The signing policy runs inside the enclave too. The enclave is not a signing oracle for the parent: it signs only through the same checks the Edge applies in every other mode, and it applies them itself.
- No raw signing. The parent cannot ask for a signature over arbitrary bytes or at an arbitrary path. It can ask for a withdrawal, a sweep, a consolidation, a gas top-up, a forward or a proof of control, and the enclave decodes the transaction and runs the sender and destination checks before it signs. The one exception is the Edge's identity key, which signs the Cloud handshake.
- The autonomous-spend policy is sealed with the seed. Spend caps, the spend window, the
sweep treasury allowlist and the forwarder contracts go into the envelope header at
seal-mnemonic --policy=<file>time and are bound to the ciphertext, so changing them means resealing — a custodian ceremony, not an environment variable. In this mode the Edge refuses to start ifEDGE_SPEND_CAP_*,EDGE_SPEND_WINDOW,EDGE_SWEEP_TREASURY_ALLOWLISTorCHAINOS_FORWARDER_CONTRACTSis set, because the parent could otherwise appear to configure a policy the enclave ignores. - The spend ledger lives in the enclave. The enclave keeps what it has spent in memory and
hands the parent a snapshot, signed with a key only it can derive, every 30 seconds and
after every capped spend. After a restart, a valid snapshot at most two minutes old is
trusted. Anything else — none, tampered, or older — freezes capped spending (gas top-ups and
forwards) on every capped chain for one window, by charging each its full cap at the moment
of restore. A rollback to an old snapshot therefore cannot replay spends; the residual
exposure is the spends of the last two minutes before a restart, and every restart is a KMS
Decryptin CloudTrail. KeepEDGE_DATA_DIRon a durable volume so an ordinary restart restores instead of freezing.
What still remains — say this in these words: anyone who controls the parent instance, or
holds a valid zkl_* key for the Edge's local API, can still ask the enclave for a
withdrawal that passes its checks. The enclave decides what may be signed — never a
deposit address's funds to a stranger, never above the sealed caps — but a withdrawal to an
external destination is, by design, something an authorised caller may request. Having the
enclave verify a signature from Cloud on every withdrawal it signs is the planned next step.
Registered key sources — wallets brought over in a migration — do not load in this mode: their envelopes could only be opened in the parent, which would put their phrases exactly where this mode keeps keys out. Each is reported on the status page with that reason, and a withdrawal from one of their addresses is refused.
Installing it
The guided installer — chainos-edge-nitro.sh for macOS and Linux,
chainos-edge-nitro.ps1 for Windows — builds the whole deployment from a released image:
the enclave-enabled instance, the enclave image and its measurement, both KMS keys with the
policies below, and the ECS service. It ends by printing the private endpoint your application
calls. Attested deployment is the same deployment by hand.
A seed nobody knows
In this mode the phrase does not have to come from a person at all.
seal-mnemonic --generate creates it inside the sealing process and seals it at once, twice:
- for the enclave, under the seal key, which releases it only to the measured image;
- for recovery, under a separate KMS key whose
kms:Decryptbelongs to one break-glass principal.
It is never printed, logged or written in plaintext. The installer does this on the Edge's own instance, so no person ever knows the phrase. Then:
- The recovery envelope is the backup. No paper copy exists, so losing both the recovery
envelope and its key loses the wallet. Keep a copy of the envelope outside the cloud account,
never schedule the recovery key for deletion while the wallet holds funds, and alert on any
kms:Decryptwith it in CloudTrail. - Changing the sealed policy is a reseal from the recovery copy
(
seal-mnemonic --from-envelope), run by that principal. The identity — and every address — stays the same. - A paper backup is still possible, on purpose. The installer's
revealcommand shows the phrase once, to the recovery principal, on their own screen, after a typed confirmation, and checks the paper before it finishes — for an offline backup, or to leave. Each run is akms:Decryptin CloudTrail. See A paper backup. - Reinstalling reuses it. Before generating anything, the installer lists the seeds sealed by earlier installs in the account and region, and reusing one is the default. A new seed is a new wallet, so generating one is always an explicit choice.
A phrase you already have — from an earlier Edge, or generated offline — works exactly as
before: seal it with seal-mnemonic --provider=nitro, and keep its paper backup.
What to pin: PCR0
| Pin | Means | Upgrade |
|---|---|---|
| PCR0, the image itself | Only that exact build gets the seed | The policy gains the new PCR0 per release — the guided installer does it |
| PCR8, the EIF signing certificate | Any image signed by that key gets the seed | No policy change |
PCR0 trusts nobody; PCR8 trusts whoever holds the signing key to ship only reviewed builds. Both were measured on real hardware: under a PCR8 pin a one-byte-different image signed by the same key is released the seed, and under a PCR0 pin it is refused.
Ziklag does not sign Edge enclave images, and you should pin PCR0. A signing key held by Ziklag would let whoever holds it — or steals it — ship an image your seed is released to. That is the one thing a compromised Ziklag must never be able to do. If you want upgrades that need no policy change, sign the images yourself with a key you hold, and pin its PCR8.
How you know what you are pinning. The Edge is not open source, and its release binary is obfuscated, so the measurement is checked against the release rather than rebuilt from source. The guided installer builds the enclave image in your account from the release image — deterministically, so every installation of the same release measures the same — and the key releases the seed to that measurement only. Each instance records what it built: the release image digest, the binary's SHA-256 and the resulting PCR0. An upgrade adds the new PCR0 beside the current one, so a rollback needs no policy change.
The status page and the portal show nitro and the PCR0 the running enclave reports, so you
can check it against the PCR0 your key policy names.
Confidential containers (azureskr, gcpcs)
These two modes follow Microsoft's and Google's published documentation and pass the Edge's tests against stand-ins for every service they call. Neither has yet run on a real Azure or GCP account. Treat them as ready to evaluate, and run your own end-to-end test before a live wallet depends on one.
Both run the whole Edge inside a confidential VM — AMD SEV-SNP or Intel TDX — whose memory
the host cannot read, and both release the seed's wrapping key only to a workload the platform
has attested. There is no process split as in nitro: the seed is in locked memory in the Edge
process, and that process is inside the TEE.
azureskr — Azure | gcpcs — GCP | |
|---|---|---|
| Runs as | A confidential container group on Azure Container Instances | The workload container of a Confidential Space VM |
| What is measured | The group's confidential computing enforcement (CCE) policy: every image, command and mount, and whether exec is allowed | The workload image's digest, on Google's hardened production image |
| Wrapping key | An exportable RSA-HSM key in Key Vault Premium or Managed HSM, with a release policy naming the policy hash | A Cloud KMS HSM key whose decrypt permission belongs only to a Workload Identity Pool whose condition names the image digest |
| How the seed gets in | Microsoft's SKR sidecar, in the same group, attests to Microsoft Azure Attestation and asks Key Vault to release the key; the Edge unwraps the data key with it and drops it | The launcher writes an attestation token; the Edge exchanges it at Google's STS for a short-lived token and calls Cloud KMS decrypt |
| Debug refused | The release policy requires x-ms-sevsnpvm-is-debuggable false, and a CCE policy generated without --debug-mode denies exec | The pool requires dbgstat disabled-since-boot; the Edge also refuses a debug image's token itself, before sending it anywhere |
| Measurement reported | hostdata:<policy hash> | image:<digest> |
What this closes, beyond the sealed modes: the Edge's cloud identity outside the TEE gets nothing — Key Vault releases only to an attested group, and the pool accepts only an attested workload; node root and the hypervisor cannot read the process's memory; and a modified image changes the measurement, so the key is not released to it.
What still remains: as in every mode, anyone who can call the Edge's local API with a valid
zkl_* key can ask it to sign. And unlike nitro, the autonomous-spend policy here is the
Edge's ordinary environment configuration (EDGE_SPEND_CAP_* and the rest), not sealed with the
seed. On Azure the CCE policy fixes those values, so changing one changes the measurement and
the key is not released; on GCP the attestation token records the workload's environment
overrides (submods.container.env_override), and the pool's condition should pin the ones that
matter.
Upgrades change the measurement — a new image digest, and on Azure a new CCE policy hash —
so the release policy or the pool's condition must name the new value before the new release
starts, exactly as a nitro PCR0 pin does. The deployment steps are on
Confidential containers on Azure and GCP.
AKS Confidential Containers, which an earlier plan named for azureskr, was a preview that
Microsoft retired in March 2026. Azure Container Instances is the supported home, with the
same attestation and release.
HSM (pkcs11) — evaluating a device
EDGE_KEYSTORE=pkcs11 keeps the seed and every key derived from it inside an HSM, as
non-extractable objects; the Edge holds only a PIN. It works only on a device that derives
BIP-32 (secp256k1) and SLIP-10 (Ed25519) keys in hardware. Without that, the choice is
between deriving in the Edge — which puts the seed back in memory — and one key object per
address, which does not scale. The Edge refuses both, and refuses the mode until a vendor's
device has passed certification. AWS CloudHSM, Azure Managed HSM, YubiHSM 2 and SoftHSM do
not derive BIP-32 keys and are not eligible.
To evaluate a device, run the capability probe from the -pkcs11 image with your vendor's
library mounted:
docker run --rm -v /opt/vendor:/opt/vendor:ro -v "$PWD/pin:/run/pin:ro" \
-e EDGE_PKCS11_LIBRARY=/opt/vendor/libcryptoki.so -e EDGE_PKCS11_TOKEN_LABEL=edge \
-e EDGE_PKCS11_PIN_FILE=/run/pin -e EDGE_PKCS11_VENDOR=<vendor> \
ziklag/chainos-edge:<version>-pkcs11 hsm-probe --kat
It prints the device against requirements R1–R10 — seed import by unwrap, BIP-32 and SLIP-10
derivation, CKM_ECDSA on secp256k1, CKM_EDDSA, session objects, HA, audit, usage policies —
and with --kat runs the published BIP-32 and SLIP-10 test vectors through the device, using a
throwaway test seed that never touches yours. A device that fails R2–R6 is not eligible; one
without SLIP-10 Ed25519 cannot hold the Edge identity key and is refused.
Why not sign in the KMS
BIP-32 derivation is the constraint that decides everything. The Edge derives a child key per address from one seed. An HSM or KMS that signs but does not derive BIP-32 children leaves two arrangements:
- Derivation inside the Edge, signing in the HSM. The child private keys are still in process memory, so this protects the master seed only.
- One HSM key object per address. Fully protected, and it does not scale — a device with a few thousand key slots caps how many addresses you can have.
AWS KMS and Azure Key Vault asymmetric keys sign; they do not derive BIP-32 children. The
workable KMS arrangement is the one awskms and azurekv implement: a key that encrypts the seed at rest,
decrypted by the Edge at boot into memory. An HSM that derives BIP-32 keys internally is what
the planned pkcs11 mode needs, and the Edge will refuse that mode on a device that cannot.
A keystore of any kind also does not solve backup — whatever holds the master material still has to be backed up, and the sealed envelope is not a backup: lose the KMS key and you lose that copy, so the paper or metal backup of the phrase remains the recovery path. The one exception is a phrase nobody knows: there is no paper copy, and the recovery envelope under its own key is the backup instead. No keystore makes Ziklag able to recover keys. Cloud holds only public keys either way.
Read Mnemonic management for backup and storage of the phrase itself.