This document outlines the procedures for rotating oracle keypair, admin keypair, and JWT secret without service interruption.
The CarbonLedger platform implements secure key rotation procedures to ensure:
- Zero-downtime operations during key transitions
- Multi-sig protection for admin key changes
- Gradual transition for JWT secrets
- Audit trail for all rotation activities
-
Preparation
- Generate new oracle keypair
- Validate new keypair cryptographic properties
- Backup current oracle keys securely
-
Registration Phase
- New oracle key is registered on-chain via
rotate_oraclefunction - Old oracle key remains active during this phase
- System validates new oracle functionality
- New oracle key is registered on-chain via
-
Transition Phase
- Environment variables are updated with new keys
- Service restart with new configuration
- Old oracle key is deprecated on-chain
-
Verification
- Test oracle operations with new keypair
- Monitor system for any issues
- Archive old keys securely
POST /api/v1/key-rotation/oracle
{
"newOraclePublicKey": "GABC...",
"newOracleSecretKey": "SABC...",
"reason": "Quarterly security rotation",
"scheduledAt": "2024-01-15T10:00:00Z"
}✅ New key registered on-chain before old key deactivated ✅ Service interruption prevented ✅ Audit trail maintained
-
Multi-Sig Requirement
- Admin key rotation requires multi-signature approval
- Time-lock can be enforced (24-168 hours)
- Multiple admins must approve the rotation
-
Time-Lock Phase
- Rotation request is created with scheduled execution
- Time-lock prevents immediate changes
- Allows for review and approval period
-
Execution Phase
- After time-lock expires, rotation is executed
- New admin key is activated
- Old admin key is deprecated
POST /api/v1/key-rotation/admin
{
"newAdminPublicKey": "GXYZ...",
"newAdminSecretKey": "SXYZ...",
"reason": "Annual security update",
"multiSigRequired": true,
"timeLockHours": 48
}✅ Multi-sig required for admin changes ✅ Time-lock protection implemented ✅ Audit trail maintained
Superseded. This section describes the original
JWT_SECRET/JWT_SECRET_NEWenvironment-variable approach, triggered viaPOST /api/v1/key-rotation/jwt. It required editing env vars and a service restart to finalize, and the "old secret still valid" window was open-ended rather than time-boxed. It has been replaced by the automated AWS Secrets Manager rotation described in Automated Secrets Manager Rotation (JWT / Postgres / Redis) below. This section is kept for historical reference only — the/api/v1/key-rotation/jwtendpoint and its underlying env vars are deprecated and will be removed in a future release.
-
Dual Secret Mode
- New JWT secret is added to environment
- Both old and new secrets are accepted during transition
- Zero-downtime authentication maintained
-
Transition Period
- Default 24-hour transition period (configurable)
- New tokens issued with new secret
- Existing tokens remain valid until expiration
-
Finalization
- After transition period, old secret is removed
- Environment updated to use only new secret
- System continues normal operation
POST /api/v1/key-rotation/jwt
{
"newJWTSecret": "new-super-secret-key-32-chars-min",
"reason": "Quarterly security rotation",
"transitionPeriodHours": 24
}✅ Zero-downtime authentication ✅ Two valid secrets during transition ✅ Gradual token migration
This is the current, automated process for the three secrets that require coordinated restarts across NestJS, the oracle, and the frontend if handled manually: the JWT signing secret, PostgreSQL credentials, and the Redis AUTH token. Unlike the oracle/admin rotation above (which stays a deliberate, admin-triggered API call), these three now rotate automatically on a schedule with no manual steps and no restart.
| Secret | Mechanism | Terraform resource |
|---|---|---|
| JWT signing secret | Custom Lambda, dual-secret overlap | infra/main/secrets.tf → aws_lambda_function.rotate_jwt |
| PostgreSQL credentials | AWS-managed RDS single-user rotation template (via Serverless Application Repository) | aws_serverlessapplicationrepository_cloudformation_stack.rotate_postgres |
| Redis AUTH token | Custom Lambda, ElastiCache ROTATE strategy |
aws_lambda_function.rotate_redis |
All three follow the standard Secrets Manager four-step lifecycle
(createSecret → setSecret → testSecret → finishSecret), wired
via aws_secretsmanager_secret_rotation on a 30-day schedule (or
on-demand: aws secretsmanager rotate-secret --secret-id <arn>).
The JWT secret is stored as a JSON document rather than a bare string:
{ "current": "...", "previous": "...", "previous_expires_at": "2026-08-01T03:15:00Z" }createSecret generates a new current value and carries the
outgoing value forward as previous, stamped with an expiry 15
minutes out. Tokens signed with the old secret keep validating until
that timestamp passes, which is what lets in-flight requests survive
a rotation with zero downtime.
backend/src/key-rotation/secrets-refresh.service.ts
(SecretsRefreshService) keeps the live JWT/Postgres/Redis values in
memory and refreshes them:
- On
SIGHUP— sent as part of the Lambda'sfinishSecretstep, or manually:kill -HUP <pid>. - On a 5-minute poll — a fallback in case a
SIGHUPis ever missed.
backend/src/auth/jwt-rotation.strategy.ts (JWTRotationStrategy)
reads from SecretsRefreshService.getJwtVerificationSecrets(), which
returns the current secret plus the previous one only while still
inside its 15-minute window — replacing the old
JWT_SECRET/JWT_SECRET_NEW env-var pair described above.
Oracle services use the same pattern in Python, via
oracle/secrets_manager.py: start_refresh_loop() does an initial
fetch, registers a SIGHUP handler, and starts a 5-minute poll
fallback thread. verification_listener.py calls
secrets_manager.get_database_url() fresh on every psycopg2.connect()
(never cached), and tracks a refresh_generation counter to detect
when the Redis AUTH token has rotated and reconnect — closing a bug
where the previous lazily-cached Redis client had no way to pick up a
rotated password short of a full process restart.
scripts/test-key-rotation-staging.sh now includes
test_secrets_manager_rotation(), which:
- Forces a rotation of each of the three secrets via
aws secretsmanager rotate-secret. - Probes an authenticated staging endpoint continuously through the rotation window and fails on any dropped request.
- Confirms the rotation Lambda reports the secret fully settled
(no
AWSPENDINGversion left).
This runs nightly via .github/workflows/key-rotation-test.yml
(03:00 UTC) and can be triggered manually from the Actions tab.
Required env vars: STAGING_JWT_SECRET_ARN, STAGING_POSTGRES_SECRET_ARN,
STAGING_REDIS_SECRET_ARN, STAGING_API_URL.
RDS's built-in single-user rotation template updates the database
password directly via the RDS API — it does not go through Terraform.
That means var.db_password in infra/main/variables.tf reflects
only the bootstrap password from the first terraform apply; after
the first rotation, the real password lives in Secrets Manager, not
in Terraform state or tfvars. This is expected AWS behavior for RDS
rotation and doesn't require any Terraform changes to accommodate.
- All secrets are encrypted at rest
- Environment variables are used for runtime configuration
- Backup procedures follow security best practices
- All rotation events are logged
- Database maintains rotation history
- System events are emitted for monitoring
- Only authorized users can initiate rotations
- Role-based access control enforced
- Multi-sig protection for critical operations
-
Oracle Rotation Test
# Test oracle key rotation in staging curl -X POST http://localhost:3001/api/v1/key-rotation/oracle \ -H "Authorization: Bearer <token>" \ -d '{ "newOraclePublicKey": "GTEST...", "newOracleSecretKey": "STEST...", "reason": "Staging test rotation" }'
-
Admin Rotation Test
# Test admin key rotation with time-lock curl -X POST http://localhost:3001/api/v1/key-rotation/admin \ -H "Authorization: Bearer <token>" \ -d '{ "newAdminPublicKey": "GTEST...", "newAdminSecretKey": "STEST...", "reason": "Staging test rotation", "multiSigRequired": true, "timeLockHours": 1 }'
-
JWT Rotation Test
# Test JWT secret rotation curl -X POST http://localhost:3001/api/v1/key-rotation/jwt \ -H "Authorization: Bearer <token>" \ -d '{ "newJWTSecret": "test-secret-key-32-chars-minimum", "reason": "Staging test rotation", "transitionPeriodHours": 2 }'
-
Monitor Rotation Status
curl -X GET http://localhost:3001/api/v1/key-rotation/<rotation-id>
-
Verify System Operations
- Oracle submissions continue working
- Admin operations function normally
- Authentication remains valid
-
Check Audit Logs
- Review rotation events
- Verify security measures
- Confirm system integrity
- Rotation success/failure rates
- Time taken for rotation completion
- System performance during rotation
- Rotation failures
- Extended rotation times
- Unusual access patterns
- Detailed rotation logs
- Security event logging
- Performance metrics
-
Identify Issue
- Check rotation status
- Review error logs
- Verify system state
-
Rollback Plan
- Restore previous keys if needed
- Revert environment changes
- Verify system recovery
-
Post-Incident Review
- Document root cause
- Update procedures
- Improve monitoring
-
Immediate Rotation
- Initiate emergency rotation
- Use accelerated procedures
- Notify security team
-
System Hardening
- Review access logs
- Update security measures
- Enhance monitoring
# Oracle Configuration
ORACLE_PUBLIC_KEY=GABC...
ORACLE_SECRET_KEY=SABC...
# Admin Configuration
ADMIN_PUBLIC_KEY=GXYZ...
ADMIN_SECRET_KEY=SXYZ...
# JWT Configuration
JWT_SECRET=original-secret-key-32-chars-minimum
JWT_SECRET_NEW=new-secret-key-32-chars-minimum # During rotation
# Stellar Configuration
CARBON_ORACLE_CONTRACT_ID=CA...
STELLAR_NETWORK=testnet- Default transition period: 24 hours
- Maximum time-lock: 168 hours (7 days)
- Minimum secret length: 32 characters
- Audit retention: 1 year
The key rotation procedures ensure secure, zero-downtime maintenance of critical system keys. Regular testing in staging environments is required before production deployment.
For questions or issues, contact the security team at security@carbonledger.com.