Keychain
Set 3AM up once, and every install of yours has it. Site keys, pairing and sealed files; what it carries and never carries; changing the home; when an install is lost; managing it; the cryptography.
One Keychain, every door. Set 3AM up once, on one install, the home. Every other install of yours, a member, takes the same setup from it and stays in step: connections and their secrets, sign-in, mail, policy and settings, branding, people and teams. Enterprise and Strategic.
┌──────────── Mumbai DR (member, in step)
Production ──────┼──────────── Frankfurt staging (member, catching up)
(the home) └ ─ ─ ─ ─ ─ ─ Dev pod (gone: not synced for a day)It moves only between your own installs, encrypted end to end. 3AM's licence authority never sees a connection, a secret or a person.
| Tier | Installs on the licence | Active instances | Keychain |
|---|---|---|---|
| Starter | Production + DR | 1 | |
| Enterprise | Up to 10, any purpose | 1 per install | Yes |
| Strategic | Any install of yours | Unlimited | Yes |
Quick start
# Console: Keychain → Make this the home (or in setup: the Keychain step)
curl -s -X POST -H "Authorization: Bearer $T" -d '{"label":"Production · Mumbai"}' $PROD/v1/keychain/home# Console: Keychain → Add a site → A new host → Make its key
curl -s -X POST -H "Authorization: Bearer $T" \
-d '{"labels":["Mumbai DR"],"purpose":"dr","licence":true,"hours":24}' $PROD/v1/keychain/site-keysprintf '%s' '3AMSITE1.…' > site.key
./install.sh --keychain-join https://3am-prod.bank.internal --keychain-site-key-file site.key \
--model-url … --model … --model-key-file …The new site installs, takes everything the Keychain carries within a minute, and (with "licence": true) joins the
licence with the home vouching. Nobody types a code.
What it carries, and what never leaves an install
| Carried (the home chooses each part) | Never carried |
|---|---|
| Licence: taken only where it is good for that install | The audit log: each install keeps its own, untouched |
| Connections and their secrets | Incidents and their evidence |
| Mail: the provider and its key | Sessions and the admin token |
| Sign-in: single sign-on, who may sign in | Each install's own keys: every install remains its own identity |
| People and teams | The halt, and actions taken: stopping one install never stops another |
| Policy and settings: rules, budgets, autonomy per service, the fix mode, services always covered | |
| Branding | |
| Your numbers and estate fixes: the business figures, corrections to the estate |
Connections a member adds itself stay its own.
Three ways in
| Way | For | How |
|---|---|---|
| Site key (recommended) | A new host, where 3AM isn't installed yet | The home makes a key for one site; the site's installer uses it and sets itself up, licence included |
| Pairing | An install already running that can reach the home | The home opens for 10 minutes; both consoles show six digits; each person types what the other screen shows |
| Sealed file | A site that can't reach the home | The Keychain in one file, encrypted under a passphrase; carry the file and the passphrase apart |
In setup, a new site answers We run 3AM elsewhere already on the licence step and joins by any of the three.
Site keys
On the home: Keychain → Add a site → A new host.
-
Describe the site: its name, what it's for (production, DR, staging, development, test), how long the key works (1 hour, 1 day, 3 days or 7 days), and whether to add it to the licence.
-
Run one line on the new host, shown once, in the folder where you unpacked the bundle:
printf '%s' '3AMSITE1.<pin>.<secret>' > site.key ./install.sh --keychain-join https://3am-prod.bank.internal --keychain-site-key-file site.key3AM already installed there? Its console: Keychain → Join a Keychain → Site key, with the address and the key.
-
It arrives: the step updates by itself when the site is set up. It shows under Members and syncs every minute.
The key's rules. 3AMSITE1.<pin>.<secret> works once, until it expires, and can be withdrawn. The home keeps
only its hash. The pin is a hash of the home's signing key, so the new site checks it reached the right home: a key
pointed at another install is refused there (security.keychain_wrong_home). An expired, withdrawn or reused key is
refused by the home (security.keychain_site_key_refused). The new site retries every 30 seconds for up to two hours
while the home is unreachable.
Pairing
- On the home: Keychain → Add a site → An install already running → Open for 10 minutes.
- On the other install: Keychain → Join a Keychain → Pair, with the home's console address.
- Both consoles show six digits. Type, on each, what the other shows. If they differ, someone is between the two installs: nothing is paired.
Sealed file
-
On the home: Keychain → Add a site → No network between them → a passphrase of 12 characters or more → Seal and download (
3am-keychain.json). -
Carry the file and the passphrase by different routes.
-
On the other site: Keychain → Join a Keychain → Sealed file, or on a new host:
./install.sh --keychain 3am-keychain.json --keychain-passphrase-file pass.txtTaken once per version; an upgrade doesn't repeat it. A sealed file is a copy, not a member: it doesn't sync afterwards.
From then on
- Each member syncs with the home every minute and takes what changed.
- What the Keychain carries is read-only on a member ("managed by Keychain from Production"). Change it once, on the home.
- Leave keeps everything a member has, and makes it editable there again.
Managing it
Who can change it. Everyone signed in sees the Keychain and Installs tabs. Owners and admins change them; the server refuses anyone else. Taking over as the home is for owners only.
What it carries. On the home: Keychain → What it carries, a switch per part. A member keeps what it has of a part the home stops carrying, as its own. Nothing is deleted anywhere.
Keeping a member's own copy. On a member: Keychain → Each part → Keep my own. That part becomes editable there, and the home's changes to it no longer arrive. Take the Keychain's later replaces the member's copy with the home's. The home's Members list shows what each member keeps.
Names. The home and each member can be renamed where the name is shown (the pencil).
Moving a member. On a member: Keychain → Move to another home, then pair with the new home. It keeps everything it has.
Changing the home
When the home itself must move (new hardware, a datacentre exit), hand it to a member:
- On the home: Members → Make it the home, on a member that is in step, keeps nothing of its own, and has a
console address (
--console-url). - The home shows six digits for 10 minutes. The member sees the offer at its next sync; an owner or admin there types the digits.
- The member signs its acceptance and sends it to the home. The home stands down and becomes a member of the new home.
- Every other member is answered, at its next sync, with the home's signed offer and the new home's signed acceptance. It checks both against keys it already trusts, and moves.
A wrong code moves nothing. If the answer is lost halfway, the next sync finishes the move. A forged move (one not
signed by both homes) is ignored and recorded (security.keychain_bad_handover).
When an install is lost
Every member syncs every minute, so the Keychain knows when a key stops: a destroyed VM, a pod that didn't come back, a host rebuilt with a new identity.
| After | The ring shows | Audit event |
|---|---|---|
| 3 minutes without a sync | Not answering, on the home and in every member's view | keychain.member_silent / keychain.home_silent |
| A day | Gone?, with Remove beside it | keychain.member_gone |
| The home silent 15 minutes | On members: If the home is gone, with Take over | keychain.home_gone |
| It syncs again | Back in step | keychain.member_back / keychain.home_back |
Nothing is removed by itself. A person removes a gone install (and takes it off the licence on the Installs tab).
The home lost. After 15 minutes of silence, an owner on a member can take over as the home. It becomes the home with everything it has. No other install follows it on its say-so: each pairs with it again (Keychain → Move to another home), six digits each. If the old home comes back, it is no longer anyone's home.
The cryptography
| Part | How | Why |
|---|---|---|
| Identity | Each install signs with its audit-log key (Ed25519, or ECDSA P-256 under FIPS) and has a P-256 key for key agreement (data/keychain/agree.pem, 0600) | Each install is a key, not a name |
| Pairing | The home opens a window; the member offers its keys and a nonce; the home answers with its own, signed. Six digits from the hash of the whole exchange, compared by people | A man in the middle must substitute keys, which changes the digits |
| Site key | Single use, expiring, stored as a hash; pins the home's signing key | The site can't be pointed at another home; a copied key works once |
| A sync | Signed by the member (who, when, a nonce, its version, its install-log head); the home checks the pinned key, the time (2 minutes) and that the nonce is new | Replays, forgeries, members that left |
| The answer | A fresh P-256 key per answer; ECDH with the member's key; HKDF-SHA256 bound to both fingerprints, the version and the nonce; AES-256-GCM; header and ciphertext hash signed by the home | Only that member can read it; a changed byte is found; a stolen home key opens no past answer |
| Handover | The home signs an offer naming the new home's keys and every member's; the new home signs its acceptance | Members re-pin only on both homes' signatures |
| Sealed file | scrypt (N=2^17, r=8, p=1), or PBKDF2-HMAC-SHA256 600,000 rounds under FIPS; AES-256-GCM; signed by the sealing install | Brute-forcing the passphrase is expensive; a changed file is found |
| Gossip | Install-log heads compared at every sync, both ways | A forked history of the licence's installs is noticed |
Every algorithm is FIPS-approved (P-256, ECDSA, SHA-256, HKDF, AES-GCM, PBKDF2).
API
| Call | Who | Purpose |
|---|---|---|
GET /v1/keychain | viewer | Role (off, home, member), what it carries, members and their health, site keys, the ring, offers; can says what you may change |
POST /v1/keychain/home {"label"} | admin | Make this install the home |
POST /v1/keychain/site-keys {"labels","purpose","licence","hours"} | admin | Make site keys (shown once) |
POST /v1/keychain/site-keys/revoke {"id"} | admin | Withdraw a key |
POST /v1/keychain/enrol {"address","key"} | admin | Set this install up with a site key |
POST /v1/keychain/window | admin | Open pairing for 10 minutes |
POST /v1/keychain/members/confirm {"fingerprint","code"} / /members/remove | admin | Pair a member by its code / remove one |
POST /v1/keychain/join {"address","label"}, /join/confirm {"code"}, /join/cancel | admin | Pair with a home |
POST /v1/keychain/joins/approve {"request"} | admin | Add a paired member to the licence |
POST /v1/keychain/scope {"skip": [parts]} | admin | On the home: the parts left out |
POST /v1/keychain/own {"part","own"} | admin | On a member: keep its own copy, or take the Keychain's again |
POST /v1/keychain/rename {"label","fingerprint"?} | admin | Rename this install, or a member |
POST /v1/keychain/handover {"fingerprint"}, /handover/cancel, /handover/accept {"code"} | admin | Change the home |
POST /v1/keychain/take-over | owner | Take over as the home (after 15 minutes of silence) |
POST /v1/keychain/pull, /leave | admin | Sync now; leave |
POST /v1/keychain/seal {"passphrase"}, /unseal {"file","passphrase"} | admin | The sealed file |
POST /v1/keychain/peer/* | installs, by key | Between installs (no session: authenticated by pinned keys, throttled per address) |
Parts: licence, connections, mail, signin, people, policy, branding, numbers.
Audit events
keychain.home, keychain.window, keychain.member_paired, keychain.member_enrolled, keychain.member_removed,
keychain.joined, keychain.enrolled, keychain.left, keychain.applied, keychain.sealed, keychain.refused,
keychain.site_keys, keychain.site_key_revoked, keychain.auto_vouch_failed, keychain.scope,
keychain.override, keychain.override_dropped, keychain.renamed, keychain.handover_offered,
keychain.handover_cancelled, keychain.home_handed_over, keychain.home_received, keychain.home_moved,
keychain.home_taken_over, keychain.member_silent, keychain.member_gone, keychain.member_back,
keychain.home_silent, keychain.home_gone, keychain.home_back, keychain.pairing_cancelled;
security.keychain_code_mismatch, security.keychain_site_key_refused, security.keychain_wrong_home,
security.keychain_bad_handover, security.install_log_fork.