- security
- l2
- runbook
- secrets-management --- Portal | Level: L2: Operations | Topics: Secrets Management | Domain: Security
Runbook: Credential Rotation (Exposed Secret)¶
| Field | Value |
|---|---|
| Domain | Security |
| Alert | Secret detected in git/logs/public location, or rotation triggered by policy |
| Severity | P1 (if exposed), P2 (routine rotation) |
| Est. Resolution Time | 30-60 minutes |
| Escalation Timeout | 20 minutes — page if not resolved (if actively exploited) |
| Last Tested | 2026-03-19 |
| Prerequisites | Access to credential vault/system, ability to update Kubernetes secrets, application restart permissions, IAM/cloud console access |
Quick Assessment (30 seconds)¶
# Run this first — it tells you the scope of the problem
# Identify which credential was exposed and where it was found
git log --all --oneline -20
# Then check if the secret is still visible in recent commits:
git log -p --all -- <FILE_WHERE_SECRET_WAS_FOUND> | head -50
Step 1: REVOKE IMMEDIATELY — Before Everything Else¶
Why: Every second the credential is active is a second an attacker can use it. Revoke first; investigate scope after. This is the single most important rule in credential exposure response.
# AWS — disable or delete the access key:
aws iam update-access-key --access-key-id <ACCESS_KEY_ID> --status Inactive
# Or delete it entirely:
aws iam delete-access-key --access-key-id <ACCESS_KEY_ID>
# GCP — disable a service account key:
gcloud iam service-accounts keys disable <KEY_ID> \
--iam-account=<SERVICE_ACCOUNT_EMAIL>
# GitHub — revoke a personal access token or OAuth token:
# Go to GitHub → Settings → Developer settings → Personal access tokens → Delete
# Database password — reset it immediately:
# MySQL/Postgres: ALTER USER '<USERNAME>'@'<HOST>' IDENTIFIED BY '<NEW_PASSWORD>';
# API key (generic) — use the issuing service's admin UI or API to revoke/rotate
echo "REVOKE THE CREDENTIAL NOW — use the issuing system's console"
AWS: no output on success (or confirmation message).
GCP: "Updated key [<KEY_ID>]."
After revocation, any system using the old credential should start failing with auth errors —
this is expected and confirms the revocation worked.
Step 2: Check Audit Logs for Unauthorized Use¶
Why: After revoking, determine if the credential was already used by an attacker. Scope defines the blast radius — you need this to assess impact and decide whether a broader incident response is needed.
# AWS CloudTrail — look for unexpected API calls using this credential:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=Username,AttributeValue=<IAM_USERNAME_OR_ROLE> \
--start-time $(date -d '48 hours ago' --iso-8601=seconds) \
--output json | jq '.Events[] | {Username, EventName, EventTime, SourceIPAddress}'
# GCP audit logs:
gcloud logging read \
'resource.type="service_account" AND protoPayload.authenticationInfo.principalEmail="<SERVICE_ACCOUNT_EMAIL>"' \
--limit=50 \
--format="json" | jq '.[] | {time: .timestamp, method: .protoPayload.methodName, ip: .httpRequest.remoteIp}'
# Kubernetes audit logs (if the secret was a kube credential):
# Check with your cluster provider — EKS: CloudTrail; GKE: Cloud Audit Logs; self-managed: /var/log/kube-audit.log
# GitHub — check audit log for token usage:
# Go to Organization → Settings → Audit log → filter by actor or token
If no unauthorized use: all events will show expected IPs, usernames, and API calls.
If unauthorized use detected: you will see unexpected source IPs, unusual API calls
(ListBuckets, DescribeInstances, CreateUser, etc.), or actions at unusual times.
Step 3: Generate a New Credential¶
Why: Applications that depended on the old credential need a replacement. Create this before updating application secrets to minimize downtime.
# AWS — create new access key:
aws iam create-access-key --user-name <IAM_USERNAME>
# Note the AccessKeyId and SecretAccessKey — you cannot retrieve the secret again later
# GCP — create new service account key:
gcloud iam service-accounts keys create <OUTPUT_KEY_FILE>.json \
--iam-account=<SERVICE_ACCOUNT_EMAIL>
# Database password — generate a strong random password:
openssl rand -base64 32
# API key — use the issuing service's admin UI to generate a new key
echo "Save the new credential securely — you will need it in the next step"
AWS: JSON with "AccessKeyId" and "SecretAccessKey" fields.
GCP: key file written to <OUTPUT_KEY_FILE>.json.
openssl: a 32-character base64-encoded random string.
Step 4: Update the Kubernetes Secret¶
Why: Applications read credentials from Kubernetes secrets at startup or periodically. The secret must be updated so the application can authenticate after you restart it.
# Update an existing Kubernetes generic secret:
kubectl create secret generic <SECRET_NAME> \
--from-literal=<KEY_NAME>=<NEW_CREDENTIAL_VALUE> \
-n <NAMESPACE> \
--dry-run=client -o yaml | kubectl apply -f -
# If the secret is a Docker pull secret:
kubectl create secret docker-registry <SECRET_NAME> \
--docker-server=<REGISTRY_HOST> \
--docker-username=<USERNAME> \
--docker-password=<NEW_PASSWORD> \
-n <NAMESPACE> \
--dry-run=client -o yaml | kubectl apply -f -
# Verify the secret was updated:
kubectl get secret <SECRET_NAME> -n <NAMESPACE> -o jsonpath='{.metadata.resourceVersion}'
# The resourceVersion should have changed from before the update
secrets write permission in that namespace. Ask the platform team to apply the change or grant you temporary elevated permissions.
Step 5: Restart Affected Deployments¶
Why: Most applications only read secrets at startup — updating the Kubernetes secret does not cause running pods to pick up the new value. You must restart them.
# Rolling restart (no downtime if replicas > 1):
kubectl rollout restart deployment/<DEPLOY_NAME> -n <NAMESPACE>
# Monitor the restart:
kubectl rollout status deployment/<DEPLOY_NAME> -n <NAMESPACE>
# Check logs to confirm the new credential is being used successfully:
kubectl logs deployment/<DEPLOY_NAME> -n <NAMESPACE> --since=5m | grep -i "auth\|connect\|login\|error"
"deployment.apps/<DEPLOY_NAME> restarted"
"deployment "<DEPLOY_NAME>" successfully rolled out"
Logs should show successful authentication with no credential errors.
kubectl get secret <SECRET_NAME> -n <NAMESPACE> -o jsonpath='{.data.<KEY_NAME>}' | base64 -d
Step 6: Remove the Old Credential from All Cached Locations¶
Why: Revoking the credential and updating Kubernetes is not enough if the credential is still cached in CI/CD variables, .env files, or git history — it must be purged from all locations.
# Check CI/CD system environment variables (update manually in the UI):
# GitHub: Settings → Secrets → update or rotate the relevant secret
# GitLab: Settings → CI/CD → Variables → update
# Jenkins: Manage Jenkins → Credentials → rotate
# Check for .env files with the old credential (local only — never commit these):
find . -name ".env*" -not -path "*/.git/*" | xargs grep -l "<PARTIAL_SECRET_VALUE>" 2>/dev/null
# Scan git history for the exposed value (use a partial, non-sensitive fragment):
git log -p --all | grep -i "<PARTIAL_SECRET_VALUE_SAFE_TO_PRINT>"
CI/CD variables updated — confirm by triggering a test pipeline run.
find output: any .env files containing the old credential (update manually, never commit).
git log: if the secret appears in history, you need to rewrite history (see below — requires coordination).
Step 7: Verify the New Credential Works End-to-End¶
Why: All the previous steps are worthless if the new credential doesn't actually work — verify before closing the incident.
# Test the new credential directly (example: AWS):
AWS_ACCESS_KEY_ID=<NEW_ACCESS_KEY_ID> \
AWS_SECRET_ACCESS_KEY=<NEW_SECRET_ACCESS_KEY> \
aws sts get-caller-identity
# Check application health after restart:
kubectl get pods -n <NAMESPACE> -l app=<APP_LABEL>
# All pods should be Running with READY containers
# Check application logs for authentication success:
kubectl logs deployment/<DEPLOY_NAME> -n <NAMESPACE> --since=10m | grep -i "error\|auth\|connect" | head -20
aws sts get-caller-identity: JSON with "Account", "UserId", and "Arn" fields — confirms new key works.
All pods: Running with READY status.
Logs: no authentication errors.
Verification¶
# Confirm the issue is resolved
kubectl get pods -n <NAMESPACE> -l app=<APP_LABEL>
aws sts get-caller-identity # or equivalent for your credential type
aws iam get-access-key-last-used --access-key-id <OLD_KEY_ID> should show no recent use after revocation).
If still broken: Escalate — see below.
Escalation¶
| Condition | Who to Page | What to Say |
|---|---|---|
| Not resolved in 20 min (active exploit) | Security on-call | "P1 Security incident: credential exposed and actively exploited — need immediate incident response" |
| Evidence of unauthorized access | Security on-call | "Security incident: audit logs show unauthorized use of exposed credential — potential breach" |
| Cannot revoke credential | Security on-call + Cloud Admin | "Unable to revoke exposed credential |
| Scope expanding | Security on-call | "Credential exposure may have led to lateral movement — scope expanding beyond original service" |
Post-Incident¶
- Update monitoring if alert was noisy or missing
- File postmortem if P1/P2
- Update this runbook if steps were wrong or incomplete
- Rewrite git history if credential is in commit history (coordinate with team first — force push required)
- Review how the credential was exposed and add pre-commit hooks or scanning to prevent recurrence
- Add secret scanning to CI pipeline (GitHub: Secret Scanning; GitLab: Secret Detection; or tools like
gitleaks,trufflehog) - Audit all other credentials managed the same way — if one leaked, others may be at risk
Common Mistakes¶
- Investigating before revoking: Every second you spend investigating before revoking is a second the attacker can act. Revoke immediately — you can investigate with the logs after.
- Not checking all places the old secret was used: Applications often cache credentials in multiple places — Kubernetes secrets, CI/CD variables, .env files, Helm values, and running pods. Missing one location means the old credential is still in play.
- Forgetting CI/CD environment variables: Kubernetes secrets and application restarts are not enough if the CI/CD system still has the old credential as an environment variable for future builds.
- Not scanning git history: If the secret was committed even once, it's in git history forever unless you rewrite it.
git rmdoes not remove it from history. - Treating routine rotation the same as exposure: For P1 exposures, follow all steps including audit log review. For P2 routine rotation, Steps 2 is optional but Steps 1, 3-7 are still required.
Cross-References¶
- Topic Pack:
training/library/topic-packs/security-fundamentals/(deep background on secrets management) - Related Runbook: unauthorized-access.md — if audit logs show the credential was used by an attacker
- Related Runbook: cve-response.md — if the exposure was via a vulnerable dependency
- Related Runbook:
../kubernetes/secret_rotation.md— Kubernetes-specific secret rotation steps
Wiki Navigation¶
Related Content¶
- HashiCorp Vault (Topic Pack, L2) — Secrets Management
- Interview: Secret Leaked to Git (Scenario, L2) — Secrets Management
- Interview: Vault Token Expired (Scenario, L2) — Secrets Management
- Runbook: Secret Rotation (Runbook, L2) — Secrets Management
- Secrets Management (Topic Pack, L2) — Secrets Management
- Secrets Management Drills (Drill, L2) — Secrets Management
- Secrets Management Flashcards (CLI) (flashcard_deck, L1) — Secrets Management
- Skillcheck: Secrets Management (Assessment, L2) — Secrets Management
Pages that link here¶
- HashiCorp Vault - Primer
- Hashicorp Vault
- Operational Runbooks
- Runbook: CVE Response (Critical Vulnerability)
- Runbook: Secret Rotation (Zero Downtime)
- Runbook: Unauthorized Access Investigation
- Scenario: Secret Leaked to Git
- Scenario: Vault Tokens Expired Across All Services
- Secrets Management
- Secrets Management - Primer
- Secrets Management - Skill Check
- Secrets Management Drills