This document covers key management, incident response, and the admin transfer procedure for Trivela's Soroban contracts.
Do not open a public GitHub issue for security vulnerabilities. Email security@trivela.com with:
- A description of the vulnerability and the component affected
- Steps to reproduce (or a proof-of-concept)
- Potential impact
You will receive an acknowledgement within 48 hours. We aim to release a fix within 14 days for critical issues and 30 days for others. Please allow us to coordinate disclosure before publishing.
| Key / Secret | Purpose | Storage requirement |
|---|---|---|
Admin keypair (G...) |
Contract admin calls (propose_admin, upgrades) |
Hardware wallet or HSM |
STELLAR_SECRET_KEY |
SEP-10 / sponsored account signing | Secrets manager (Vault, AWS SM) |
TRIVELA_MASTER_KEY |
Privileged API operations | Secrets manager |
TRIVELA_API_KEYS |
Standard API access | Secrets manager |
TRIVELA_JWT_SECRET |
JWT signing | Secrets manager, min 32 chars |
DATABASE_URL |
PostgreSQL credentials | Secrets manager |
VAPID_PRIVATE_KEY |
Web Push signing | Secrets manager |
Rules:
- Secrets must never be committed to git, logged, or sent over unencrypted channels.
- Production secrets must differ from testnet secrets.
- Rotate all secrets at least annually or immediately after any suspected compromise.
Follow this procedure to rotate any backend secret (JWT secret, API keys, database password, etc.) without downtime.
- Generate the new secret:
openssl rand -hex 32
- Add the new value to your secrets manager alongside the old one (if the system supports multi-key validation, enable it so both old and new tokens are accepted temporarily).
- Update the environment variable in your deployment platform (Kubernetes secret, Helm values,
etc.) and roll out the new backend pods:
kubectl rollout restart deployment/trivela-backend -n trivela-prod kubectl rollout status deployment/trivela-backend -n trivela-prod
- Verify that the new pods are healthy (
/healthreturns 200) and that API clients can authenticate with the new key. - Revoke the old secret from the secrets manager.
- Document the rotation in your change log with the date and reason.
TRIVELA_API_KEYS accepts a comma-separated list, so you can add the new key before removing the
old one:
# Phase 1 — add new key (old key still works)
TRIVELA_API_KEYS="old-key,new-key"
# Distribute new key to all consumers and confirm they have switched.
# Phase 2 — remove old key
TRIVELA_API_KEYS="new-key"The STELLAR_SECRET_KEY is a Stellar keypair used for server-side signing (SEP-10, sponsored
accounts). It does not control any contract admin authority.
- Generate a new Stellar keypair:
stellar keys generate trivela-server-new --network mainnet stellar keys address trivela-server-new
- Fund the new account (minimum 1 XLM base reserve).
- Update
STELLAR_SECRET_KEYin the secrets manager with the new secret key. - Roll out the backend (step 3 of routine rotation above).
- Verify SEP-10 challenges are signed by the new key.
- The old keypair can be decommissioned (it holds no special on-chain authority).
The admin keypair is the highest-privilege credential in the system. If you believe it has been compromised, act immediately.
- Revoke any cloud credentials that may have given access to the secret (rotate AWS/GCP IAM keys, Vault tokens, etc.).
- If the key is in a hardware wallet that has been lost or stolen, treat the key as compromised regardless of PIN protection.
Use a backup admin keypair or a multisig key that was established during initial setup. If no backup key exists, you will need to coordinate with the Stellar network — a compromised admin with no backup is a critical situation; contact security@trivela.com immediately.
Assuming you have a backup key (trivela-admin-backup):
# 1. Propose the backup key as the new admin (called from the COMPROMISED key if still accessible,
# or from any key that has been granted emergency authority).
stellar contract invoke \
--id <REWARDS_CONTRACT_ID> \
--network mainnet \
--source trivela-admin-backup \
-- propose_admin \
--current_admin <CURRENT_ADMIN_PUBLIC_KEY> \
--new_admin <BACKUP_ADMIN_PUBLIC_KEY>
# Repeat for each contract (campaign, badges, nullifiers, voting).# 2. Accept from the backup key.
stellar contract invoke \
--id <REWARDS_CONTRACT_ID> \
--network mainnet \
--source trivela-admin-backup \
-- accept_admin \
--new_admin <BACKUP_ADMIN_PUBLIC_KEY># 3. Confirm the transfer succeeded.
stellar contract invoke \
--id <REWARDS_CONTRACT_ID> \
--network mainnet \
--source trivela-admin-backup \
-- admin
# Should return BACKUP_ADMIN_PUBLIC_KEY.Once the backup key is in control, generate a fresh keypair on a hardware wallet and transfer admin to it using the normal two-step procedure (see below).
- Audit contract event logs for any unauthorized calls between the estimated compromise time and the transfer.
- Rotate all backend secrets as a precaution (the admin keypair is separate from backend secrets but assume full breach).
- Write an incident report covering timeline, root cause, and remediation.
Both the rewards and campaign contracts (and all other Trivela contracts) implement a
propose-then-accept pattern. A one-step transfer to a wrong address cannot brick the contract
because the current admin retains control until the new admin explicitly accepts.
| Function | Called by | Effect |
|---|---|---|
propose_admin(current_admin, new_admin) |
Current admin | Writes new_admin to pending_admin; no change to admin slot |
cancel_admin_transfer(current_admin) |
Current admin | Clears pending_admin; no transfer occurs |
accept_admin(new_admin) |
New admin | Moves new_admin into admin slot; clears pending_admin |
- Generate the new admin keypair on the target hardware wallet. Do not copy the secret over the wire.
- Fund the new account (minimum 1 XLM).
- Test that the new keypair can sign a no-op transaction on mainnet.
- Confirm
pending_admin()returnsNone(no in-flight transfer from a previous attempt).
stellar contract invoke \
--id <CONTRACT_ID> \
--network mainnet \
--source <CURRENT_ADMIN_IDENTITY> \
-- propose_admin \
--current_admin <CURRENT_ADMIN_PUBLIC_KEY> \
--new_admin <NEW_ADMIN_PUBLIC_KEY>Verify the aproposed event appears on-chain with the correct new_admin address.
stellar contract invoke \
--id <CONTRACT_ID> \
--network mainnet \
--source <CURRENT_ADMIN_IDENTITY> \
-- pending_admin
# Must return NEW_ADMIN_PUBLIC_KEYIf the address is wrong, call cancel_admin_transfer and start over. The current admin slot is
unchanged until accept_admin is called.
The new admin must call accept_admin within 30 days (the instance-storage TTL). After that
window pending_admin is cleared automatically by the ledger, and the proposal expires.
stellar contract invoke \
--id <CONTRACT_ID> \
--network mainnet \
--source <NEW_ADMIN_IDENTITY> \
-- accept_admin \
--new_admin <NEW_ADMIN_PUBLIC_KEY>stellar contract invoke \
--id <CONTRACT_ID> \
--network mainnet \
--source <NEW_ADMIN_IDENTITY> \
-- admin
# Must return NEW_ADMIN_PUBLIC_KEY
stellar contract invoke \
--id <CONTRACT_ID> \
--network mainnet \
--source <NEW_ADMIN_IDENTITY> \
-- pending_admin
# Must return NoneRepeat steps 1–2 and verify for each deployed contract (rewards, campaign, badges, nullifiers, voting).
- Multisig: Consider a multisig signer policy (e.g., 2-of-3) on the admin keypair so that no single person can initiate a transfer.
- Time-lock: For non-emergency rotations, delay the
accept_admincall by 24–48 hours to give team members time to detect unauthorized proposals. - Monitoring: Alert on any
propose_adminoraccept_admincontract events in production. These should be rare and always expected. - Backup key: Maintain a cold-storage backup admin key in a separate geographic location. Test it annually.
- MAINNET_DEPLOY.md — production deployment guide
- DEPLOYMENT.md — admin transfer and blue/green deployment
- MAINNET_CHECKLIST.md — launch sign-off checklist
- RUNBOOK.md — incident response and rollback procedures