Issue #1656 — Secrets are hardcoded. This document describes the secrets management architecture for QuorumProof: how secrets are stored, rotated, audited, and consumed by application services.
Prior to this change, secrets (Stellar deploy keys, database URLs, JWT signing
keys) were stored in plaintext environment files, CI variables, or Kubernetes
Secret objects with no rotation, no access audit trail, and no central
inventory. This is a high-severity security finding.
QuorumProof secrets are managed via AWS Secrets Manager using a HashiCorp Vault-compatible path convention. The architecture supports a future migration to a self-hosted Vault cluster with minimal application changes.
All secrets follow the Vault KV-v2 path structure:
quorumproof/<environment>/<service>/<key>
| Path | Contents |
|---|---|
quorumproof/<env>/stellar/deploy-key |
Stellar account secret key for contract deployments |
quorumproof/<env>/database/url |
PostgreSQL connection URL |
quorumproof/<env>/api-server/jwt-secret |
HS256 JWT signing secret |
quorumproof/<env>/api-server/webhook-signing-key |
HMAC-SHA256 webhook signing key |
quorumproof/<env>/monitoring/pagerduty-routing-key |
PagerDuty Events API v2 routing key |
quorumproof/<env>/monitoring/slack-webhook-url |
Slack incoming webhook URL |
A single script for all secrets operations. Supports both AWS Secrets Manager
(default) and HashiCorp Vault (set VAULT_ADDR + VAULT_TOKEN).
# Check all required secrets exist
ENVIRONMENT=production ./scripts/secrets_manager.sh check
# Get a secret value
./scripts/secrets_manager.sh get quorumproof/production/stellar/deploy-key
# Set (create or update) a secret
./scripts/secrets_manager.sh set \
quorumproof/production/api-server/jwt-secret \
"$(openssl rand -hex 32)"
# Trigger immediate rotation
./scripts/secrets_manager.sh rotate quorumproof/production/database/url
# List all secrets in an environment
./scripts/secrets_manager.sh list quorumproof/production
# View audit log (last 24h)
./scripts/secrets_manager.sh audit --since 24hTerraform module that creates all secret objects in AWS Secrets Manager, configures KMS encryption, sets up automatic rotation, and applies resource-based access policies.
module "secrets" {
source = "../../modules/secrets"
environment = var.environment
kms_key_arn = module.kms.key_arn
rotation_lambda_arn = module.rotation_lambda.arn
rotation_days = 30
allowed_principal_arns = [module.ecs_task_role.arn]
}The secrets Terraform module configures automatic rotation for credential secrets
(stellar/deploy-key, database/url, api-server/jwt-secret) when
rotation_lambda_arn is provided.
Rotation intervals:
- Stellar deploy key: 30 days (configurable via
rotation_days) - Database URL (password rotation): 30 days
- JWT signing secret: 30 days
# Immediately rotate a specific secret
./scripts/secrets_manager.sh rotate quorumproof/production/stellar/deploy-keyJWT secret rotation requires dual-key support during the transition window. The API server reads the current and previous versions from Secrets Manager and accepts tokens signed with either key during a 15-minute overlap window:
# 1. Generate new secret and store as next version
NEW_SECRET=$(openssl rand -hex 32)
./scripts/secrets_manager.sh set quorumproof/production/api-server/jwt-secret-next "$NEW_SECRET"
# 2. Deploy the new API server version that reads both keys
# 3. Wait for old tokens to expire (≥ JWT max TTL)
# 4. Promote: rename next → current
./scripts/secrets_manager.sh set quorumproof/production/api-server/jwt-secret "$NEW_SECRET"Every secrets operation (get, set, rotate, list) is appended as a JSON
Line to $AUDIT_LOG_FILE (default: /tmp/quorumproof-secrets-audit.log).
{
"timestamp": "2026-09-28T08:00:00Z",
"action": "get",
"secret_path": "quorumproof/production/stellar/deploy-key",
"status": "ok",
"actor": "arn:aws:iam::123456789012:role/quorumproof-ci",
"environment": "production",
"hostname": "ci-runner-abc123"
}# All entries
./scripts/secrets_manager.sh audit
# Last 24 hours
./scripts/secrets_manager.sh audit --since 24h
# Last 7 days
./scripts/secrets_manager.sh audit --since 7dAWS CloudTrail automatically records all GetSecretValue, PutSecretValue,
and RotateSecret API calls for Secrets Manager. Combined with the script-level
audit log, this provides two independent audit trails.
To query CloudTrail for secrets access:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventSource,AttributeValue=secretsmanager.amazonaws.com \
--start-time "$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)" \
--query 'Events[].{Time:EventTime,Who:Username,What:EventName,Resource:Resources[0].ResourceName}' \
--output tableSecrets are injected into pods via the AWS Secrets Manager CSI driver or by the
CI pipeline creating Kubernetes Secret objects at deploy time (never stored in
Git).
# k8s/external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: quorumproof-api-server-env
namespace: quorumproof
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: quorumproof-api-server-env
data:
- secretKey: STELLAR_SECRET_KEY
remoteRef:
key: quorumproof/production/stellar/deploy-key
- secretKey: DATABASE_URL
remoteRef:
key: quorumproof/production/database/url
- secretKey: JWT_SECRET
remoteRef:
key: quorumproof/production/api-server/jwt-secretIn GitHub Actions, retrieve secrets immediately before use and never store them in environment files:
- name: Inject secrets
run: |
STELLAR_KEY=$(aws secretsmanager get-secret-value \
--secret-id quorumproof/production/stellar/deploy-key \
--query SecretString --output text)
echo "::add-mask::$STELLAR_KEY"
echo "STELLAR_SECRET_KEY=$STELLAR_KEY" >> "$GITHUB_ENV"| Control | Implementation |
|---|---|
| Encryption at rest | AWS KMS (customer-managed key via kms_key_arn) |
| Encryption in transit | TLS enforced by AWS Secrets Manager API |
| Access control | IAM resource-based policies on each secret |
| Audit trail | CloudTrail + script-level JSON audit log |
| Rotation | Automatic via Lambda + immediate via rotate command |
| Secret recovery | 7-day recovery window before permanent deletion |
| No plaintext in Git | All secret values stored in Secrets Manager only |
| Masking in CI | ::add-mask:: annotation on retrieved values |
- Identify all hardcoded secrets:
grep -r "STELLAR_SECRET\|DATABASE_URL\|JWT_SECRET" .env* scripts/ k8s/ - For each secret, run:
./scripts/secrets_manager.sh set quorumproof/<env>/<service>/<key> "<value>" - Remove from
.envfiles and replace with Secrets Manager lookups. - Update Kubernetes
Secretmanifests to use External Secrets Operator. - Rotate all migrated secrets immediately to invalidate the old values.
- Verify with:
ENVIRONMENT=production ./scripts/secrets_manager.sh check
infra/terraform/modules/secrets/— Terraform modulescripts/secrets_manager.sh— CLI operations tooldocs/security-best-practices.md— broader security guidedocs/issuer-security-checklist.md— secrets checklist for issuers