Runbook: key rotation
What actually rotates in 2QUIC today, the honest recovery path for identity key compromise or KMS key loss, and what is not automated.
Scope: what rotates today
There is no rotate-identity command. The full validator-cli surface is
fetch-release, register, set-payout-wallet, security, and
emergency-stop. This table is the honest state of rotation in v1:
| Key | Where it lives | Rotation today |
|---|---|---|
Marketplace JWT signing key (ES256, kid in every JWT header) | Marketplace AWS KMS, never leaves KMS | Marketplace-driven on its schedule. Your part when a rotation is announced: add one [[jwt.keys]] entry to the host config and restart host-proxy. |
| Validator identity key (Ed25519 seed) | Inside the Nitro Enclave, plus the KMS-encrypted ciphertext on the EC2 host | No rotation command in v1. Changing it means destroy, redeploy, re-register (procedure below). |
| Per-validator KMS CMK (seals the seed) | Your AWS account, created by deploy.sh | No in-place rotation. If the CMK or its attestation policy is lost, the ciphertext is unrecoverable: destroy and redeploy. |
| Payout wallet | Marketplace listing | validator-cli set-payout-wallet, applies to future bookings only. |
| Trader lease API key | Marketplace | Traders rotate it themselves via POST /v1/leases/{id}/api-keys, see Authentication. |
Older docs and the original design referenced validator-cli rotate-identity
and rotate-marketplace. Those commands were removed before launch and
nothing replaces them in v1. Any procedure built on them is dead.
Marketplace JWT signing key (kid rotation)
The marketplace signs every lease JWT with an ECDSA P-256 key held in AWS KMS
(alg: ES256). The JWT header carries a kid, and your host-proxy verifies
incoming JWTs against a static kid -> pubkey trust set in
/etc/staked-quic/config.toml. The trust set normally has one entry and two
during a rotation overlap. The design budgets a 24-hour overlap window during
which both the current and previous marketplace pubkeys verify.
The rotation is coordinated by the marketplace:
-
The marketplace generates the new KMS key and publishes the new
(kid, pubkey)pair to operators. -
Your action: append a second
[[jwt.keys]]entry on the host (reachable over SSM) and restart host-proxy. Downtime is seconds, and both kids verify during the overlap.# /etc/staked-quic/config.toml [[jwt.keys]] kid = "mkt-jwt-v1-2026q2" pubkey_pem = """ -----BEGIN PUBLIC KEY----- ...new marketplace P-256 public key... -----END PUBLIC KEY----- """ -
The marketplace flips its signing
kid. New trader JWTs (default TTL 600 seconds) carry the new header, old in-flight JWTs still verify against the old entry. -
After the overlap you remove the old
[[jwt.keys]]entry at leisure and restart host-proxy again.
Traders do nothing: their SDK refreshes JWTs automatically and the new tokens
just carry the new kid.
If the marketplace signing key is compromised, the marketplace disables it in
KMS immediately. No new JWTs can be signed, and every already-issued JWT dies
at its exp, so within 10 minutes at the default TTL. The token endpoint
returns 500 jwt-sign-failed until the replacement key is live (the design
doc's incident plan says 503, but the shipped code maps a kms:Sign failure
to 500, so alert on the 500s). No operator action is needed for that path.
Payout wallet rotation
The only true self-service rotation in the product. Use it to move earnings to a cold wallet or split custody:
validator-cli set-payout-wallet \
--identity-keypair /path/to/validator-keypair.json \
--payout-wallet <BASE58_ADDRESS> \
--dry-run # remove to apply
The CLI signs a dedicated set-payout challenge with your identity keypair
file (never through the enclave) and calls PUT /v1/validators/payout-wallet.
- Applies to bookings made after the update only. The payout wallet is frozen onto each lease at booking time, so already-booked leases keep paying the old address.
- The payout wallet card on the dashboard shows the current address and a prefilled copy of this command.
Validator identity key: there is no rotation command
The identity seed exists in exactly two places: inside the running enclave and as a KMS-encrypted ciphertext on the EC2 host. There is no supported way to swap it in place, and re-keying your Solana validator identity on-chain is an operator action outside the marketplace entirely.
The honest marketplace-side procedure for both scenarios below is
destroy + redeploy + re-register, because deploy.sh is what
KMS-encrypts the seed and provisions it into the enclave. There is no
shortcut.
First, cut traffic with emergency-stop
For a suspected compromise, stop inbound traffic before anything else:
# Preview (the DEFAULT is dry-run): prints current ingress rules
# and the planned revoke, changes nothing
validator-cli emergency-stop --security-group-id sg-0123abcd --aws-region us-east-1
# Actually cut traffic
validator-cli emergency-stop --security-group-id sg-0123abcd --aws-region us-east-1 --dry-run=false
emergency-stop defaults to dry-run. If you do not pass --dry-run=false,
nothing changes and traffic keeps flowing. Verify the output says the rules
were revoked.
What it does: revokes every ingress rule on the EC2 security group, so
inbound traffic stops within seconds at the hypervisor level. The enclave
keeps running and the KMS envelope stays sealed. A local NDJSON audit line is
appended under $XDG_CONFIG_HOME/staked-quic/audit/.
What it does not do: it never talks to the marketplace. It does not delist you, does not freeze or revoke leases, and refunds nothing. Restoring ingress afterwards is manual (re-add the security group rules yourself). See Incident response for the full triage flow.
Scenario A: identity key compromise (new keypair)
-
Cut traffic with
emergency-stop --dry-run=falseas above. -
Revoke active leases from the dashboard: each revocation requires a reason and books a prorated refund of
floor(remaining_epochs * total / total_epochs)for the trader. Their JWTs die at expiry, within 10 minutes. -
Tear down the deployment from the sibling repo checkout:
./deploy/scripts/destroy.shThis destroys the EC2 instance, EIP, KMS key, S3 bucket and VPC, and wipes local secrets. It preserves
artefacts/andtrader-keypair.json, so the redeploy is fast. -
Re-key your validator identity on-chain with your normal Solana operator tooling. This step is outside the marketplace.
-
Redeploy with the new keypair (the
MARKETPLACE_JWT_PUBKEY_PEM,MARKETPLACE_JWT_AUDandMARKETPLACE_JWT_VAL_PKenvironment variables are required, withMARKETPLACE_JWT_VAL_PKset to the new pubkey):./deploy/scripts/deploy.sh \ --identity-keypair /path/to/NEW-validator-keypair.json \ --region us-east-1deploy.shKMS-encrypts the new seed under a fresh CMK, provisions the enclave over SSM, and verifies gRPCHealthreportsprovisioned=truewith the matching pubkey. -
Register the new identity. A new pubkey means a new listing and a new
validator_id:validator-cli register \ --identity-keypair /path/to/NEW-validator-keypair.json \ --endpoint-url https://<NEW_EIP>:443 \ --price 2.5 \ --bio "Your listing bio" \ --region us-east-1The new API key is written once to
<validator_id>.api-key(mode 0600) next to the keypair and never shown again. -
The old listing stays tied to the compromised pubkey and you cannot delete it yourself. Contact the marketplace so an admin suspends it.
Scenario B: KMS key loss (same keypair)
If the per-validator CMK is deleted, disabled, or its attestation policy is
broken, the seed ciphertext on the host can never be decrypted again and
reprovision.sh cannot recover. Confirm the diagnosis first:
validator-cli security --kms-key-id <key-id-or-alias> --aws-region us-east-1 --json
This exits 1 if the CMK is disabled or unreadable, or if any Allow on
kms:Decrypt lacks the kms:RecipientAttestation condition. The check runs
locally and never reports to the marketplace. The KMS drift badge on the
dashboard security strip is a v1 placeholder hardcoded to "In sync" and does
not detect CMK policy drift, so trust the CLI exit code, not the badge.
Recovery, keeping the same identity keypair:
./deploy/scripts/destroy.sh./deploy/scripts/deploy.sh --identity-keypair /path/to/validator-keypair.json --region <region>with the sameMARKETPLACE_JWT_*environment variables as the original install.- Do not re-register.
POST /v1/validatorsreturns409 validator-already-registeredfor a pubkey that already has a listing. Your listing, history and ratings survive. - The redeploy allocates a new EIP. If your registered
endpoint_urlembeds the old IP, update it from the dashboard listing editor, and make sureMARKETPLACE_JWT_AUDon the host equals the registeredendpoint_urlbyte for byte (the__PUBLIC_IP__placeholder inMARKETPLACE_JWT_AUDresolves to the new EIP automatically during deploy). A mismatch rejects every lease JWT withPERMISSION_DENIED: wrong audience.
What is NOT automated
Be explicit with yourself about the manual surface:
- There is no scheduled or on-demand identity key rotation. The only path is destroy + redeploy + re-register.
- Marketplace JWT key rotation requires a config edit and a host-proxy restart on every validator host. Nothing pushes the new key to you.
emergency-stopnever delists you, never freezes leases, never refunds. Re-opening ingress afterwards is manual.- Lease revocation is per-lease and manual, from the dashboard or
POST /v1/validators/{id}/leases/{lease_id}/revoke. - After an EC2 reboot the enclave boots unprovisioned by design. Run
./deploy/scripts/reprovision.sh --region <region>yourself, nothing auto-provisions. See Incident response. - A TEE upgrade does not re-wrap the seed.
update.shonly changes the PCR conditions on the same CMK and re-provisions from the same on-host ciphertext. See TEE upgrade.