From ct
Advises on secret-management tooling (sops+age, Vault, Sealed Secrets, ESO, KMS) with guidance on rotation, threat boundaries, leak detection, and bootstrap trust.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ct:secrets-managementThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Concise pointers for choosing and operating secret-management systems.
Concise pointers for choosing and operating secret-management systems.
Assumes you know what an env var, a k8s Secret, and an IAM role are. This skill covers the operational layer — the parts models tend to gloss over: which tool fits which threat model, the workflow specifics of sops / Vault / ESO / Sealed Secrets, rotation mechanics, leak-detection tooling, and boot-strap-trust pitfalls.
Load when the question is about:
sops workflow (.sops.yaml, encrypted_regex, updatekeys, age recipients)SecretStore/ExternalSecret CRs, refresh, generatorsimmutable, mount propagationDo NOT load for: "store this password somewhere", trivial kubectl create secret, .env file basics, password-manager UX. Those don't need this skill.
| Approach | When it fits | What it costs |
|---|---|---|
| sops + age in repo | GitOps, low infra, small team, encrypts at rest in Git | Manual recipient list mgmt; no audit trail; no dynamic creds |
| sops + cloud KMS | Want central key authority + IAM-based access | Network call to KMS on every decrypt; KMS key deletion = data loss |
| HashiCorp Vault | Dynamic creds, audit, multi-team, certificate issuance, transit encryption | Operates a stateful HA cluster; seal/unseal ceremony; full-time operator |
| AWS SM / GCP SM / Azure KV | Already in that cloud, want minimal ops | Cloud lock-in; per-secret cost; cross-region replication is extra |
| Sealed Secrets | Pure-k8s GitOps; secrets must live in Git encrypted | Controller-only decryption; lose master key = lose all secrets; namespace+name binding by default |
| External Secrets Operator | Already running a secret store; want k8s-native sync | Bootstrap-trust problem (auth to backend); k8s Secret object still on cluster |
| Doppler / Infisical / 1Password Automation | Small team, want polished UX, no infra | Vendor outage = your secrets are gone; data residency questions |
Rule of thumb: pick the simplest tool that satisfies the threat model. sops+age beats Vault for a 3-person team; Vault beats sops the moment you need dynamic database creds or PKI.
.sops.yaml creation_rules: list evaluated top-down, first match wins. Each rule has path_regex, recipient set (age: / kms: / pgp: / hc_vault_transit_uri:), and optional encrypted_regex / unencrypted_regex (mutually exclusive).
creation_rules:
- path_regex: secrets/prod/.*\.yaml$
age: age1abc...
kms: arn:aws:kms:eu-west-1:111:key/xxx
encrypted_regex: '^(password|token|secret|key|credential)'
encrypted_regex 'password|token|secret|key' encrypts only matching keys; the rest stays plaintext. Useful for k8s Secret manifests where metadata should remain navigable.sops -e file.yaml encrypt; sops file.yaml open in $EDITOR (auto decrypt/re-encrypt); sops -d file.yaml decrypt to stdout; sops -i -e file.yaml in-place encrypt..sops.yaml recipient list, then sops updatekeys file.yaml (re-encrypts the data key to new recipients without changing the data key itself). For a full data-key rotation use sops --rotate -i file.yaml or sops rotate.key_groups + shamir_threshold: N → require N of M groups to decrypt. Use when no single team should be able to read prod alone.SOPS_AGE_KEY_FILE env var → SOPS_AGE_KEY (raw key) → SOPS_AGE_KEY_CMD (command output) → ~/.config/sops/age/keys.txt.age-keygen -o key.txt writes private key to file; line 2 is # public key: age1... (the recipient).age -e -r age1abc... -o out.age in.txt encrypt to one recipient.age -d -i key.txt out.age decrypt with identity file.-r is repeatable; resulting file decrypts with ANY one identity. Pattern: encrypt to every developer's age pubkey + a CI/CD key.age -R ~/.ssh/authorized_keys -o out.age in.txt — supports ssh-ed25519 and ssh-rsa. Decrypt with age -d -i ~/.ssh/id_ed25519.-R recipients.txt (one per line, comments OK)..sops.yaml and running sops updatekeys does NOT erase historical Git-committed encrypted files — the old recipient can still decrypt those revisions. Treat removed recipient = compromised data.vault kv put secret/foo k=v, vault kv get secret/foo. Overwrites lose history.max_versions. vault kv get -version=3 secret/foo. Old versions retained until destroy.creation_statements SQL with {{username}}/{{password}} placeholders), then vault read database/creds/<role> returns ephemeral user with lease_id + lease_duration. Default TTL 1h, max 24h. Supports Postgres, MySQL/MariaDB, Mongo, MSSQL, Oracle, Cassandra, Redis, Snowflake, Elasticsearch, +20.vault write pki/issue/<role> common_name=.... CRL/OCSP managed by Vault.role_id (selector, ~UUID) + secret_id (credential). Recommended pattern: trusted orchestrator wraps secret_id (vault write -wrap-ttl=120s -force auth/approle/role/<r>/secret-id), hands wrapping token to consumer; consumer unwraps once, gets secret_id. TTL knobs: secret_id_ttl, secret_id_num_uses, token_ttl, token_max_ttl.tokenreviews). Tied to namespace + SA via role. No long-lived secret in cluster.sts:GetCallerIdentity; Vault verifies signature.vault operator init, e.g. -key-shares=5 -key-threshold=3). Each operator runs vault operator unseal with one share.-dev mode: in-memory, root token printed to stdout, no TLS. NEVER production. Resets on restart.vault audit enable file file_path=/var/log/vault_audit.log.cubbyhole): vault wrap returns a one-time token in a per-token cubbyhole. Used for hand-off (e.g., delivering secret_id to a worker without trusting the courier).vault list sys/leases/lookup/..., vault lease renew <id>, vault lease revoke <id>. Dynamic creds revoke on lease expiry — design apps to fetch fresh creds + retry on permission denied, not to cache forever.kubeseal --fetch-cert. Only the controller can decrypt.kubeseal -f secret.yaml -w sealed.yaml produces a SealedSecret CR safe to commit. Apply: kubectl apply -f sealed.yaml → controller decrypts → creates real Secret.--scope):
strict (default): bound to exact namespace + name. Move/rename = fails.namespace-wide: any name within namespace.cluster-wide: any namespace, any name. Loosest; only when you must.--key-renew-period). Old keys retained to decrypt existing SealedSecrets. To re-encrypt with the latest key without exposing plaintext: kubeseal --re-encrypt < old.yaml > new.yaml.kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml > master.key. Store offline. Without this, a wiped cluster cannot decrypt any committed SealedSecret.Secret shape — for non-Secret resources you need ESO or sops.SecretStore (namespace-scoped) / ClusterSecretStore (cluster) — describes a backend (Vault, AWS SM, GCP SM, Azure KV, 1Password Connect, Akeyless, Doppler, GitLab, IBM, +40).ExternalSecret CR pulls keys from the store and creates/updates a k8s Secret:
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: db-creds }
spec:
refreshInterval: 1h
secretStoreRef: { kind: SecretStore, name: vault-prod }
target: { name: db-creds, creationPolicy: Owner }
data:
- secretKey: PASSWORD
remoteRef: { key: secret/data/db, property: password }
refreshInterval: re-fetches and updates the k8s Secret. 0 = create once, never refresh.dataFrom: bulk import; takes whole JSON object as keys, or supports extract / find patterns.creationPolicy: Owner (default — k8s Secret is owned by the CR, deletes cascade) / Merge (update existing, no owner) / Orphan (create no owner refs) / None (injector use only).deletionPolicy: Retain (default) / Delete (drop k8s Secret when remote keys gone) / Merge (drop only the keys, keep Secret).SecretStore needs creds to talk to the backend. Solutions in order of preference:
Secret (chicken-and-egg; only acceptable for boot-strap).Password, ECRAuthorizationToken, GCRAccessToken, ACRAccessToken, Webhook, ClusterGenerator. Password produces a generated password into a Secret; ECRAuthorizationToken produces a 12-hour Docker pull cred without static creds in cluster.base64 is encoding, NOT encryption. By default Secrets land in etcd as plaintext (base64 in YAML, plaintext on disk).--encryption-provider-config=/etc/k8s/enc.yaml on kube-apiserver. Provider chain — first encrypts, all decrypt:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources: [secrets]
providers:
- kms: { apiVersion: v2, name: cloudkms, endpoint: unix:///run/kmsplugin/socket.sock, timeout: 3s }
- aescbc: { keys: [{ name: fallback, secret: <base64-32B> }] }
- identity: {} # plaintext fallback — last
aescbc (AES-CBC + PKCS#7 + HMAC), secretbox (XChaCha20-Poly1305), kms v1/v2, identity (none). KMS v1 deprecated since 1.28, disabled by default 1.29 — use kms v2 (automatic seed rotation, KDF-derived per-write DEKs, no manual cachesize).kubectl get secret -A -o json | kubectl replace -f -), THEN remove old.immutable: true (1.21+): Secret cannot be updated or have data changed; reduces apiserver/etcd watch load. Useful for rarely-changed long-lived secrets.| Mount type | Updates? | Latency |
|---|---|---|
| Volume mount | yes | ~kubelet sync, default 60s |
Volume mount + subPath | NO | requires pod restart |
Env var (valueFrom.secretKeyRef) | NO | baked at pod start |
/proc/PID/environ leak: any process with same UID can read another's env vars. Volume mounts (mode: 0400) are safer than env vars for sensitive material.AWSPREVIOUS / version=N-1; consumers can pin or follow latest. Rotation = create new version, then update consumer pointer.rotate-root): Vault rotates the DB root password to a value only Vault knows. Subsequent DB engine ops still work; humans permanently lose root unless they break-glass.InsecureSkipVerify=true defeats the design.gitleaks: regex + entropy + composite rules; pre-commit hook (pre-commit-config.yaml) and history scan (gitleaks git --log-opts="--all"). Bypass: SKIP=gitleaks git commit. Config precedence: -c flag > GITLEAKS_CONFIG env > GITLEAKS_CONFIG_TOML env > .gitleaks.toml.trufflehog: similar surface, plus active credential verification — calls the suspected provider to check if the leaked key actually works. Lower false positive rate at cost of network calls.git-secrets: AWS-pattern focused (AKIA..., etc.); lighter, narrower scope.gitleaks / trufflehog / detect-secrets to fail commits matching patterns./proc/PID/environ (same UID). Prefer file mounts with 0400.docker build --build-arg PASSWORD=...: build args persist in image layers / docker history. Use BuildKit --mount=type=secret,id=foo instead — secret only available during the RUN instruction at /run/secrets/foo, not in image. CLI: docker build --secret id=foo,src=./secret.txt.kubectl edit secret shows base64: a quick base64 -d reveals plaintext in terminal scrollback. Clear scrollback / use kubectl create secret --dry-run=client -o yaml | kubeseal instead.kube-system/sealed-secrets-key* immediately on day 1.SecretStore static-token auth in cluster: chicken-and-egg, defeats the point. Use IRSA / Workload Identity / Vault k8s auth instead.sops + age committed without .sops.yaml: new contributors can decrypt (their key is in the file) but cannot encrypt new files correctly because they don't know the recipient list. Always commit .sops.yaml.age recipient list does not retroactively re-key history: removed recipients still decrypt old Git revisions. Treat any removed recipient as having permanent access to anything they could read at removal time.Secret updates don't propagate to subPath mounts or env vars — surprise stale creds after rotation. Use plain volume mounts when rotation matters.age(1) man pageBefore recommending a secret-management approach or a non-trivial config change:
creationPolicy: Orphan, sops updatekeys, --scope strict)A secret manager you cannot operate is worse than a .env file you respect.
npx claudepluginhub pvillega/claude-templates --plugin ctDeploys and configures secrets management with HashiCorp Vault, AWS Secrets Manager, or Kubernetes Secrets for secure credential storage, rotation, and auditing.
Guides managing every phase of a secret's lifecycle: generation, distribution, rotation, and revocation. Useful for designing credential retrieval, rotation policies, or responding to exposures.
Manages Kubernetes secrets with SealedSecrets for GitOps, External Secrets Operator for cloud secret stores, encryption at rest, RBAC, and rotation.