Mnemonic management
This is the page with no undo. Everything else in ChainOS can be re-derived, restored from a backup or rebuilt from the chain. The mnemonic cannot.
Ziklag never receives your mnemonic and holds nothing from which it can be reconstructed. There is no reset flow, no support escalation and no key escrow — because any of those would mean Ziklag held something that could reconstruct your keys, which is the property the product exists to deny.
Lose the phrase and the funds derived from it are unreachable. By anyone, permanently.
Treat it as you would an HSM master key, because that is what it is.
Generating
Offline, on a machine you trust, with a tool you trust.
- 12 or 24 words. Both are accepted. 24 words is 256 bits of entropy against 128 for 12; 128 bits is already beyond brute force, so the practical argument for 24 is margin against a future nobody has predicted rather than a present weakness.
- Use a real generator. Any BIP-39 implementation from a hardware wallet, a reputable offline tool, or your language's audited library. Do not compose a phrase from words you chose; human-chosen "random" words are guessable at a rate that has emptied real wallets.
- Do not use a passphrase-derived seed, brain wallet or any scheme where the entropy comes from something memorable.
- Do not reuse a mnemonic that has ever been typed into a website, an online generator, a note-taking app, or a wallet on a machine that has been compromised.
- Generate a separate one for sandbox. Cloud actively refuses the same identity in both
environments (
409 CROSS_ENVIRONMENT_IDENTITY), because sharing means your CI Edge holds the keys to live funds. See Environments.
Storing it for the Edge
The Edge reads the phrase from a file, once, at boot.
# On the host, outside any image build context.
umask 077
printf '%s' "$MNEMONIC" > /etc/chainos/mnemonic
chmod 0400 /etc/chainos/mnemonic
chown 10001 /etc/chainos/mnemonic # the uid the container runs as
Then mount it read-only, and set PASS_PHRASE_FILE to the mount path.
Things not to do, each of which has a specific leak:
| Don't | Because |
|---|---|
Pass it as PASS_PHRASE | Visible in docker inspect, /proc/<pid>/environ, crash dumps and most orchestrator UIs |
| Bake it into an image | Present in every layer, every registry copy, and every machine that pulls the image |
| Put it in a Compose file you commit | It is in git history forever, including after you delete it |
| Write it with a trailing newline you did not intend | Some tools include it in the phrase; BIP-39 validation then fails, which is at least loud |
echo it | Shell history. Use printf with the value from your secret manager, or a heredoc with a leading space where your shell supports it |
Secret managers. Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager, Kubernetes Secrets with encryption-at-rest enabled and RBAC narrowed to the workload — any of these is appropriate. The Edge does not integrate with them directly; it reads a file, and your platform's existing secret-to-file mechanism is what you should use, because it is the one your operators already audit.
Sealing the phrase
In a sealed mode the Edge is never given the phrase. It is given an envelope — the phrase encrypted under a key in your KMS — and decrypts it once at boot. Sealing is a short ceremony with two roles that should be different people:
| Role | Holds | May |
|---|---|---|
| Seed custodian | The phrase and its offline backups | Seal (kms:GenerateDataKey; Key Vault wrap and read). Not decrypt |
| Deployer (DevOps) | The envelope, which is ciphertext | Deploy the Edge. Never sees the phrase |
| The Edge's workload identity | Nothing at rest | Decrypt (kms:Decrypt; Key Vault unwrap) and nothing else |
-
The custodian generates the phrase offline (
chainos-edge mnemonic) or retrieves the existing one, and makes the paper or metal backups described below. -
On a hardened workstation, with the custodian role, the custodian seals it:
chainos-edge seal-mnemonic --provider=awskms \--key=arn:aws:kms:eu-west-1:123456789012:key/… \--environment=live --label=primary --out=edge-seed.sealed.jsonFor Azure,
--provider=azurekv --key=https://<vault>.vault.azure.net/keys/<name>/<version>.The phrase is typed with echo off (or piped in); there is no flag for it, and the command refuses to run if
PASS_PHRASEorPASS_PHRASE_FILEis set. It prints only public values: the key, the context, the Edge identity fingerprint, and the first five ETH addresses. Keep that output. -
The custodian hands the envelope to DevOps. It is ciphertext: store it in Git, a ConfigMap or an ordinary
Secret. -
DevOps deploys with
EDGE_KEYSTORE,PASS_PHRASE_SEALED_FILEand the key variable, and noPASS_PHRASE*. The custodian confirms that the identity fingerprint on the Edge's status page matches the oneseal-mnemonicprinted, and that the first addresses match. -
Re-sealing — a new KMS key, or a new label — repeats steps 2 to 4. It does not change a single address, because the phrase is the same. AWS automatic rotation of the same key needs no re-seal.
seal-mnemonic --verify=edge-seed.sealed.json tries to open an envelope with the caller's own
credentials. On the custodian's workstation it should fail, because the custodian has no
Decrypt; if it succeeds, the key policy is wider than it should be.
Lose the KMS key — deleted, or the account closed — and the envelope is unreadable. That loses one copy, not the wallet: the offline backup of the phrase is still the recovery path. Never destroy the paper backup because "the seed is in KMS now".
A phrase nobody sees (--generate)
seal-mnemonic --generate makes a new 24-word phrase inside the command and seals it at once.
It is never printed, logged or written in plaintext, so no person ever knows it. The
guided installer uses it on the Edge's own instance.
With no paper backup, the backup has to be a second envelope, so --generate refuses to run
without one:
chainos-edge seal-mnemonic --generate --provider=nitro --key=<seal key ARN> \
--environment=live --label=primary --policy=policy.json --out=primary.json \
--recovery-key=<recovery key ARN> --recovery-out=recovery.json --summary=summary.json
| Flag | |
|---|---|
--recovery-key, --recovery-out | A second envelope of the same phrase, under a separate AWS KMS key (provider awskms, or --recovery-provider). Written first, so a primary envelope without its backup never exists. Give only a break-glass role kms:Decrypt on that key |
--summary | The public results — identity, first ETH addresses, keys, policy — as JSON |
--from-envelope=recovery.json | Instead of --generate or typing a phrase: open the recovery envelope with your own credentials and reseal its phrase — a new policy, a new key, a new enclave measurement. The identity printed must not change |
To write the phrase on paper — an offline backup, or a way to take the wallet elsewhere —
chainos-edge reveal-mnemonic --from-envelope=recovery.json shows it once, at a terminal only,
after a typed confirmation, and checks three words of what you wrote before it finishes. The
guided installer's reveal command runs it for you; see
A paper backup.
The recovery envelope and its key are the backup in this mode: lose both and the wallet is
gone. 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:Decrypt with it in CloudTrail.
Backing it up
The backup is the part people get wrong, and they get it wrong in one of two directions: too few copies, or copies somewhere online.
- At least two copies, geographically separated, both offline. Paper in a safe, or a steel plate, or an encrypted volume on removable media held in different locations.
- Never in a password manager that syncs, in cloud storage, in email, in a screenshot, or in a photograph — a phone photo is in a cloud backup within minutes.
- Split it if your risk model calls for it. A 2-of-3 Shamir split (SLIP-39) or a documented split-custody arrangement is reasonable at a bank. What is not reasonable is an undocumented split where one holder leaves and nobody knows the reconstruction procedure.
- Restore it once, as a test, before you go live. An untested backup is not a backup. Do the restore into a throwaway sandbox Edge and confirm it derives the addresses you expect.
Export the address mapping too
The mnemonic recovers funds. It does not recover the mapping from address to customer — that is yours, and ChainOS is the only place it currently lives.
curl -s "$EDGE/addresses?size=1000" \
-H "X-API-Key: $CHAINOS_EDGE_KEY" \
| jq -r '.data.content[] | [.chain, .address, .derivationPath, .userRef, .tag] | @csv' \
>> chainos-addresses.csv
Run that on a schedule, page through if you have more than a thousand, and keep the result in your own systems. The console also exports CSV per screen.
With the mnemonic and this file you can reconstruct everything without ChainOS: the paths are deterministic and documented in HD derivation.
More than one wallet
If you arrived with wallets that predate ChainOS, your Edge can hold those too. They are
called key sources, they are registered with chainos-edge import-mnemonic, and
Migration covers the flow. Three
operational points belong here rather than there.
They are supplied the same way PASS_PHRASE is. A file your secret management mounts,
read at boot. The Edge does not invent a keystore for them and does not write key material
anywhere — which is the same rule as for the primary phrase, and the reason there is no
second thing to back up.
In a sealed keystore mode, they must be sealed too. On an Edge running awskms or
azurekv, a key source whose reference is a plain path or env: is not loaded: reading
it would put a plaintext phrase back on a machine whose primary was sealed to remove exactly
that. The Edge still starts, and list-key-sources --check and the boot log give the reason.
Seal each foreign wallet with the same key the primary uses, under its own label, and import
it with --sealed:
chainos-edge seal-mnemonic --provider=awskms --key=<arn> --environment=live \
--label=old-metamask --out=old-metamask.sealed.json
chainos-edge import-mnemonic --label=old-metamask --sealed=old-metamask.sealed.json
# a raw key file, one key (or address,key) per line, read from stdin
chainos-edge seal-mnemonic --provider=awskms --key=<arn> --environment=live \
--label=hot-keys --raw-keys --chain=eth --out=hot-keys.sealed.json < keys.txt
chainos-edge import-keys --label=hot-keys --chain=eth --sealed=hot-keys.sealed.json
The manifest then records sealed:/path/to/envelope, and every replica opens it at boot with
its own KMS identity, exactly as it opens the primary. The import commands refuse a plaintext
--file or --env on a sealed Edge, so the mistake surfaces before the registration rather
than at the next restart.
Mount each one on every replica that should be able to sign with it. A key source registered from one machine is registered for the account; the material is on whichever machines you put it on. A replica without it refuses a withdrawal from those addresses, which is correct and is not the failure you want to discover under load.
The manifest is metadata. chainos-edge import-mnemonic writes key-sources.json into
EDGE_DATA_DIR: a label, a fingerprint, the chains, the base path, and the path of the
file to read each phrase from. It contains no key material, it is safe to back up, and losing
it costs you the registration record on that machine and nothing else — re-running the import
rebuilds it, and ChainOS recognises the same wallet by its fingerprint rather than creating a
second one.
chainos-edge list-key-sources
shows what this machine knows about, and flags any source whose material is not configured here — the case where the phrase was piped in once and nothing kept it.
Signing with them happens at boot, or not at all. The Edge reads each source's secret when it starts, opens the wallet beside the primary one, and derives the addresses it registered so it knows which of them it holds keys for. A source whose secret is missing on that replica is reported at startup and its addresses are refused there — it is not retried later, because adding a wallet to a running signer would mean the same withdrawal got different answers depending on when it arrived. Mount the secret and restart.
chainos-edge list-key-sources --check
is the same work on demand: it loads every source and reports what this machine can actually
sign for, rather than what it was once told about. Run it on a new replica before you send
traffic to it. Without --check the command reads the manifest and nothing else, which is
why it is the default — a listing should not be a reason to read every recovery phrase on the
box.
Signing routes by address, never by what Cloud says. When a transaction arrives, the Edge
re-derives the sender from the primary phrase; if it does not match, it looks the address up
among the key sources it loaded and signs only if one of them actually derives it. ChainOS
knows which wallet owns which address and is never asked — the same principle as
chainos-edge prove-control, which searches for a derivation index rather than being handed
one. A withdrawal from an address no wallet on the machine derives is refused, and the
refusal names the sources whose material is not mounted there, because that is almost always
the cause.
Only EVM chains can be signed from a key source today. The Edge carries address codecs for Ethereum, BSC, Polygon, Avalanche and Base and deliberately not for Bitcoin, TRON or XRP — those addresses are derived in Cloud from the extended public key you registered. That is fine for watching and for reporting, and it means the Edge cannot tell whose address a Bitcoin sender is. So if you register a key source covering Bitcoin, TRON or XRP, withdrawals on that chain are refused on this Edge rather than signed with the primary phrase's key, which would produce a valid-looking signature for the wrong wallet. Sweep or withdraw those chains from the wallet that holds them.
Rotating
Prefer not to. Rotating a mnemonic is not like rotating an API key. Addresses derived from the old phrase stay valid on-chain and stay funded; addresses derived from the new one are different. Any deposit sent to an old address after rotation is recoverable only with the old phrase, so "rotation" in practice means "run two mnemonics indefinitely".
ChainOS handles that correctly — addresses derived from prior extended public keys stay monitored forever, so nothing already issued goes dark — but the operational burden of two sets of keys is real and permanent.
Rotate when the old phrase may be compromised. Do not rotate on a schedule.
The procedure
- Generate the new mnemonic and back it up, fully, before touching anything.
- Sweep or plan to sweep funds from addresses derived from the old phrase into your treasury, using the old phrase. Do this first — after step 4 you are running two Edges.
- Tell your customers which deposit addresses are changing, if you re-issue. If you do not re-issue, old addresses keep working and this step is unnecessary.
- Start an Edge with the new mnemonic. Cloud sees a different identity key, refuses to derive against it, quarantines the new extended public keys, and pages your account owner. This is the expected behaviour, not a failure.
- Approve the rotation in the console, with a fresh TOTP code, as an owner. Read Identity rotation before you do — that page is what explains why the gate is there.
- Keep the old phrase. Forever. It is the only key to funds already sitting at old addresses, including deposits that arrive after the rotation.
What must never happen
Do not start an Edge with a different mnemonic while expecting it to serve the same account, in the belief that ChainOS will "just use the new keys". It will not: it will refuse authentication before reading the payload, quarantine the keys, and alert. That refusal is the feature. An implementation that accepted the new phrase silently would derive new addresses for existing customers while their deposits kept arriving at addresses whose keys your new Edge does not hold.
If the Edge cannot read the file
The failure modes, in the order they occur:
| Symptom | Cause |
|---|---|
mnemonic file not readable | File mode or ownership. The container's uid must be able to read it |
invalid BIP-39 checksum | A typo, a wrong word, or a trailing newline included in the phrase |
cannot lock key material into memory | The runtime's memlock limit is (nearly) zero. The Edge needs a few KiB and no IPC_LOCK; it refuses to start rather than hold a seed that could be paged to swap |
| Derives the wrong addresses | The phrase is right but a word is transposed. Check against the published test vectors in Installation |
the envelope was sealed for a different environment | A live envelope on a sandbox Edge, or the reverse. Seal again with the right --environment |
the envelope was sealed under …, and this Edge is configured for … | The key variable and the envelope name different keys. One of them is wrong |
does not derive the identity recorded in its envelope | The wrong envelope is mounted, or it was sealed from a different phrase |
may not call kms:Decrypt | The Edge's role lacks Decrypt, or the key policy's context conditions do not match the envelope |
Troubleshooting has the diagnostics for each.