Skip to main content

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.

There is no recovery path

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'tBecause
Pass it as PASS_PHRASEVisible in docker inspect, /proc/<pid>/environ, crash dumps and most orchestrator UIs
Bake it into an imagePresent in every layer, every registry copy, and every machine that pulls the image
Put it in a Compose file you commitIt is in git history forever, including after you delete it
Write it with a trailing newline you did not intendSome tools include it in the phrase; BIP-39 validation then fails, which is at least loud
echo itShell 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:

RoleHoldsMay
Seed custodianThe phrase and its offline backupsSeal (kms:GenerateDataKey; Key Vault wrap and read). Not decrypt
Deployer (DevOps)The envelope, which is ciphertextDeploy the Edge. Never sees the phrase
The Edge's workload identityNothing at restDecrypt (kms:Decrypt; Key Vault unwrap) and nothing else
  1. The custodian generates the phrase offline (chainos-edge mnemonic) or retrieves the existing one, and makes the paper or metal backups described below.

  2. 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.json

    For 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_PHRASE or PASS_PHRASE_FILE is set. It prints only public values: the key, the context, the Edge identity fingerprint, and the first five ETH addresses. Keep that output.

  3. The custodian hands the envelope to DevOps. It is ciphertext: store it in Git, a ConfigMap or an ordinary Secret.

  4. DevOps deploys with EDGE_KEYSTORE, PASS_PHRASE_SEALED_FILE and the key variable, and no PASS_PHRASE*. The custodian confirms that the identity fingerprint on the Edge's status page matches the one seal-mnemonic printed, and that the first addresses match.

  5. 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.

The sealed envelope is not a backup

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-outA 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
--summaryThe public results — identity, first ETH addresses, keys, policy — as JSON
--from-envelope=recovery.jsonInstead 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​

  1. Generate the new mnemonic and back it up, fully, before touching anything.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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:

SymptomCause
mnemonic file not readableFile mode or ownership. The container's uid must be able to read it
invalid BIP-39 checksumA typo, a wrong word, or a trailing newline included in the phrase
cannot lock key material into memoryThe 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 addressesThe phrase is right but a word is transposed. Check against the published test vectors in Installation
the envelope was sealed for a different environmentA 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 envelopeThe wrong envelope is mounted, or it was sealed from a different phrase
may not call kms:DecryptThe Edge's role lacks Decrypt, or the key policy's context conditions do not match the envelope

Troubleshooting has the diagnostics for each.