Key Rotation Procedures¶
Organization: ZKProva System: ZKP-Powered Portable Credit Union Identity SOC 2 Criteria: CC6.6 (Management of Credentials) Document Version: 1.0 Effective Date: 2026-02-28 Classification: Confidential Review Cadence: Quarterly (next review: 2026-05-31)
Table of Contents¶
- Key Inventory
- Rotation Procedures
- Application Cache Caveat
- Audit Trail
- Emergency Rotation
- Document Control
Key Inventory¶
| Key / Secret | Algorithm | Storage Location | Rotation Schedule | Last Rotated | Next Rotation |
|---|---|---|---|---|---|
| JWT Signing Key | HS256 | AWS Secrets Manager (zkprova-secrets) |
90 days | Initial deployment | Q2 2026 |
| AES-256-GCM Encryption Key | AES-256-GCM | AWS Secrets Manager (zkprova-secrets) |
180 days | Initial deployment | Q2 2026 |
| Ed25519 Issuer Signing Key | Ed25519 (DID:key) | AWS Secrets Manager (zkprova-secrets) |
Annual | Initial deployment | Q1 2027 |
| TLS Certificates | RSA 2048 / ECDHE 256 | AWS Certificate Manager (ACM) | Automatic (ACM managed) | Auto-renewed | Auto-renewed |
| GHCR PAT (CI/CD) | N/A | GitHub Actions Secrets | 90 days | Per CI setup | Q2 2026 |
| Redis Auth Token | N/A | AWS Secrets Manager / Terraform variable | 180 days | Initial deployment | Q2 2026 |
| Database Password | N/A | AWS Secrets Manager / Terraform variable (TF_VAR_db_password) |
180 days | Initial deployment | Q2 2026 |
| Webhook HMAC Secrets | HMAC-SHA256 | PostgreSQL (per-lender) | Per lender request | N/A | On request |
| SES SMTP Credentials | N/A | IAM (zkprova-ses-smtp user) |
180 days | Initial deployment | Q2 2026 |
Key Validation at Startup¶
The application enforces key strength at startup via Pydantic validators in backend/app/config.py:
jwt_secret_key: Minimum 32 characters. Known development values rejected whendebug=False.encryption_key: Exactly 64 hex characters (32 bytes). Known development values rejected whendebug=False.issuer_private_key_hex: Exactly 64 hex characters (32 bytes Ed25519 seed).
Rotation Procedures¶
JWT Signing Key (90-day rotation)¶
Risk: A compromised JWT key allows an attacker to forge authentication tokens for any user role (member, lender, admin).
Procedure — Zero-Downtime Dual-Accept Window:
-
Generate new key:
-
Update Secrets Manager with both keys:
-
Deploy with dual-accept logic:
- The application first validates incoming JWTs against the primary key
- If validation fails, it retries against
jwt_secret_key_previous -
New tokens are always signed with the primary key
-
Rolling restart to pick up new secrets:
-
Verify:
- Existing sessions continue to work (validated against previous key)
- New logins receive tokens signed with the new key
-
Run smoke tests:
./scripts/smoke-test.sh -
Remove old key after the dual-accept window expires (access token lifetime = 30 minutes, refresh token = 7 days; wait at least 7 days):
-
Final restart:
-
Log rotation event in audit service and update this document's "Last Rotated" column.
Rollback: If issues arise, swap the primary and previous keys in Secrets Manager and restart.
AES-256-GCM Encryption Key (180-day rotation)¶
Risk: A compromised encryption key allows decryption of all credential data stored in PostgreSQL.
Procedure — Re-Encryption Migration:
-
Generate new key:
-
Update Secrets Manager with both keys:
-
Deploy with dual-key decryption:
- Decrypt attempts use the primary key first
- If decryption fails (wrong key), retry with
encryption_key_previous -
All new encryptions use the primary key
-
Rolling restart:
-
Run re-encryption migration:
-
Verify: Spot-check credential decryption with new key only. Confirm no credentials remain encrypted with the old key.
-
Remove old key from Secrets Manager after re-encryption is verified complete.
-
Final restart and audit log entry.
Rollback: Keep the old key in Secrets Manager until re-encryption is verified. Both keys remain valid during the migration window.
Ed25519 Issuer Signing Key (Annual rotation)¶
Risk: A compromised Ed25519 key allows forging W3C Verifiable Credentials and DID:key identifiers. This is the most sensitive key in the system.
Procedure — Coordinated DID:key Update:
-
Generate new Ed25519 keypair:
-
Derive new DID:key identifier from the new public key (per W3C DID:key specification).
-
Notify all partner credit unions and lenders:
- Provide the new DID:key identifier
- Announce the transition timeline (minimum 30-day notice)
-
Both old and new DIDs will be accepted during transition
-
Update Secrets Manager:
-
Deploy with dual-key verification:
- New credentials signed with the new key and new DID:key
- Verification accepts proofs from credentials signed by either key
-
Old credentials remain valid until their natural expiry
-
Rolling restart:
-
Transition period (90 days):
- Monitor verification success rates for old vs. new credentials
-
Encourage members to request re-issuance of credentials with the new key
-
Decommission old key after transition period. Remove from Secrets Manager. Update DID document to revoke old DID:key.
-
Audit log entry and document update.
Rollback: During transition period, revert to old key as primary if issues with new key are discovered.
Future improvement: HSM integration for Ed25519 keys (Gap #6, target Q3 2026) will provide hardware-backed key protection and simplified rotation.
TLS Certificates (Automatic via ACM)¶
AWS Certificate Manager handles certificate renewal automatically for *.zkprova.com. DNS validation via Route53 ensures seamless renewal.
Verification: Periodically check certificate expiry:
No manual rotation required.
GHCR PAT (90-day rotation)¶
- Generate new Personal Access Token in GitHub Settings with
read:packagesandwrite:packagesscopes - Update GitHub Actions repository secret
GHCR_PAT - Verify CI pipeline can push/pull images: trigger a test workflow run
- Revoke old token in GitHub Settings
Redis Auth Token (180-day rotation)¶
-
Generate new token:
-
Update ElastiCache auth token:
This enables a dual-accept window where both old and new tokens are accepted. -
Update application secret:
-
Rolling restart:
-
Finalize rotation:
Database Password (180-day rotation)¶
-
Generate new password:
-
Update RDS master password:
-
Update application secret with new
DATABASE_URLin Secrets Manager. -
Rolling restart:
-
Verify:
/healthendpoint confirms DB connectivity.
Application Cache Caveat¶
The application uses @lru_cache decorators in two critical locations:
backend/app/services/secrets.py—get_secrets_backend()caches the secrets backend singletonbackend/app/config.py—get_settings()caches the PydanticSettingsobject (which reads secrets at initialization)
Implication: After rotating any secret in AWS Secrets Manager or environment variables, running pods will continue using the cached (old) values. A kubectl rollout restart is required to force pods to re-read secrets.
Procedure for every rotation:
# After updating the secret in Secrets Manager:
kubectl rollout restart deployment/zkprova-backend -n default
kubectl rollout status deployment/zkprova-backend -n default
# PDB (minAvailable=2) ensures at least 2 pods remain serving during restart
# Rolling restart replaces pods one at a time — no downtime
Monitoring: After restart, verify:
- All pods reach Running state: kubectl get pods
- Health check passes: curl https://api.zkprova.com/health
- Smoke tests pass: ./scripts/smoke-test.sh
- No error spike in Sentry
Audit Trail¶
Every key rotation event is recorded through two channels:
1. Application Audit Log (PostgreSQL)¶
The audit service (backend/app/services/audit.py) records rotation events with:
| Field | Value |
|---|---|
event_type |
key.rotated |
actor_id |
Engineer performing the rotation |
actor_type |
admin |
resource_type |
secret |
resource_id |
Key identifier (e.g., jwt_secret_key, encryption_key) |
details |
{"reason": "<scheduled/emergency>", "rotation_date": "<ISO 8601>"} |
ip_address |
Source IP of the engineer |
Audit logs are append-only with 7-year retention.
2. Structured Logs (structlog)¶
The audit service dual-writes to structured JSON logs regardless of DB write success. These logs are forwarded to CloudWatch Logs for centralized analysis.
3. AWS CloudTrail¶
All AWS Secrets Manager API calls (UpdateSecret, GetSecretValue) are automatically logged in CloudTrail with IAM principal, timestamp, and source IP.
Emergency Rotation¶
In the event of a confirmed or suspected key compromise (P0 incident):
- Do not wait for the standard rotation window. Initiate immediate rotation.
- Follow the standard procedure for the compromised key type, but skip the dual-accept window transition period.
- For JWT key compromise: Immediate rotation invalidates all active sessions. All users must re-authenticate. Accept the service disruption.
- For AES key compromise: Immediate rotation + emergency re-encryption. If the attacker has exfiltrated encrypted data, the old key must be considered compromised — re-encrypted data with the new key is safe.
- For Ed25519 key compromise: Immediately revoke the old DID:key. Issue new credentials to all members. Notify all lenders to reject proofs from the compromised DID. This is the highest-severity key rotation scenario.
- Document the emergency rotation as part of the incident post-mortem (see Incident Response Plan).
Document Control¶
| Version | Date | Author | Description |
|---|---|---|---|
| 1.0 | 2026-02-28 | ZKProva Engineering | Initial key rotation procedures |
This document satisfies SOC 2 Trust Service Criteria CC6.6 (Management of Credentials). It is reviewed quarterly and updated after each key rotation event.