Module 3.5: Secrets in GitOps
Цей контент ще не доступний вашою мовою.
Discipline Module | Complexity:
[COMPLEX]| Time: 35-40 min
Prerequisites
Section titled “Prerequisites”Before starting this module:
- Required: Module 3.1: What is GitOps? — GitOps fundamentals
- Required: Security Principles Track — Security fundamentals
- Recommended: Understanding of Kubernetes Secrets
- Helpful: Basic cryptography concepts (encryption, keys)
What You’ll Be Able to Do
Section titled “What You’ll Be Able to Do”After completing this module, you will be able to:
- Design a secrets management strategy that keeps sensitive data out of Git while maintaining GitOps workflows
- Implement Sealed Secrets, SOPS, or External Secrets Operator for encrypted secret storage in Git
- Evaluate secrets management tools against security requirements — rotation, auditing, access control
- Build secret rotation workflows that update credentials without service disruption
Why This Module Matters
Section titled “Why This Module Matters”GitOps says: “Everything in Git.”
Security says: “Never commit secrets.”
This is the GitOps secrets problem.
If you can’t put secrets in Git, how do you manage them with GitOps? This isn’t a minor inconvenience — it’s a fundamental challenge that every GitOps adoption must solve.
Get it wrong:
- Secrets in Git history (forever)
- Security breaches
- Compliance violations
- Lost credentials
Get it right:
- Secrets managed declaratively
- Full GitOps workflow preserved
- Audit trail maintained
- Security enhanced
This module shows you the patterns and tools for handling secrets in GitOps.
The Secrets Problem
Section titled “The Secrets Problem”Why You Can’t Just Commit Secrets
Section titled “Why You Can’t Just Commit Secrets”# DON'T DO THIS - EverapiVersion: v1kind: Secretmetadata: name: database-credentialstype: Opaquedata: username: YWRtaW4= # base64 of "admin" password: cGFzc3dvcmQ= # base64 of "password123"Problems:
- Git history is forever: Even if you delete, it’s in history
- Base64 is not encryption: Anyone can decode it
- Repository access = secret access: Everyone with repo access sees secrets
- Leaked to forks: Forks get the secrets too
- Compliance violations: Many regulations forbid this
The GitOps Secret Dilemma
Section titled “The GitOps Secret Dilemma”┌─────────────────────────────────────────────────────────────┐│ GitOps Promise ││ ││ "Git is the source of truth for all resources" │└─────────────────────────────────────────────────────────────┘ vs┌─────────────────────────────────────────────────────────────┐│ Security Requirement ││ ││ "Never store plaintext secrets in version control" │└─────────────────────────────────────────────────────────────┘
How do we reconcile these?Solution Categories
Section titled “Solution Categories”Three main approaches to GitOps secrets:
1. Encrypt Secrets in Git
Section titled “1. Encrypt Secrets in Git”Store encrypted secrets in Git. Decrypt at deploy time.
Tools: Sealed Secrets, SOPS, git-crypt
Git Repo Cluster │ │ │ Encrypted Secret │ │ (safe to commit) │ │ │ │ │ ▼ │ │ GitOps Agent │ │ │ │ │ ▼ │ │ Decrypt │ │ │ │ │ ▼ │ │ Kubernetes Secret │ │ (plaintext) │2. Reference External Secrets
Section titled “2. Reference External Secrets”Store secrets in external manager. Reference them in Git.
Tools: External Secrets Operator, Secrets Store CSI Driver
Git Repo External Store Cluster │ │ │ │ Secret Reference │ │ │ (not actual secret) │ │ │ │ │ │ │ ▼ │ │ │ GitOps Agent ─────────┼──── Fetch ──────────▶ │ │ │ │ ▼ │ │ Actual Secret │ │ │ │ │ ▼ │ │ Kubernetes Secret │3. Inject at Runtime
Section titled “3. Inject at Runtime”Don’t put secrets in Kubernetes at all. Inject directly to pods.
Tools: Vault Agent, Secrets Store CSI Driver (mounted)
Git Repo Vault Pod │ │ │ │ Pod spec with │ │ │ Vault annotations │ │ │ │ │ │ │ ▼ │ │ │ Pod created ────────┼───── Auth ─────────▶ │ │ │ │ ▼ │ │ Secret injected │ │ into pod filesystem │Pattern 1: Sealed Secrets
Section titled “Pattern 1: Sealed Secrets”The most GitOps-native solution. Secrets encrypted before Git, decrypted in cluster.
How It Works
Section titled “How It Works”┌─────────────────────────────────────────────────────────────┐│ Encryption Flow ││ ││ 1. Create regular Kubernetes Secret ││ 2. Use kubeseal CLI to encrypt with cluster's public key ││ 3. Commit SealedSecret to Git ││ 4. Sealed Secrets controller decrypts in cluster ││ 5. Regular Secret created for pods to use │└─────────────────────────────────────────────────────────────┘
Developer Git Cluster │ │ │ │ kubeseal │ │ │─────────▶ │ │ │ │ │ │ SealedSecret │ │ │──────────────────▶ │ │ │ │ │ │ │ GitOps sync │ │ │──────────────────────▶│ │ │ │ │ │ Controller │ │ │ decrypts │ │ │ │ │ │ │ ▼ │ │ │ K8s Secret │Installing Sealed Secrets
Section titled “Installing Sealed Secrets”# Add controller to clusterkubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml
# Install kubeseal CLIbrew install kubeseal # macOS# or download from GitHub releasesCreating a Sealed Secret
Section titled “Creating a Sealed Secret”# 1. Create regular secret (don't commit this!)kubectl create secret generic db-creds \ --from-literal=username=admin \ --from-literal=password=supersecret \ --dry-run=client -o yaml > secret.yaml
# 2. Seal itkubeseal --format yaml < secret.yaml > sealed-secret.yaml
# 3. Delete plaintextrm secret.yaml
# 4. Commit sealed secretcat sealed-secret.yamlSealed Secret YAML
Section titled “Sealed Secret YAML”apiVersion: bitnami.com/v1alpha1kind: SealedSecretmetadata: name: db-creds namespace: defaultspec: encryptedData: # These are encrypted - safe to commit username: AgBy8hC7...long encrypted string... password: AgCtr4Hx...long encrypted string... template: metadata: name: db-creds type: OpaquePros and Cons
Section titled “Pros and Cons”Pros:
- True GitOps: encrypted secrets in Git
- Simple workflow
- Controller handles decryption
- Works with any GitOps tool
Cons:
- Cluster-specific encryption (can’t share between clusters)
- Key management (backup the sealing key!)
- Rotation requires re-sealing
Pattern 2: SOPS (Secrets OPerationS)
Section titled “Pattern 2: SOPS (Secrets OPerationS)”Encrypt specific values within YAML files, not entire files.
How It Works
Section titled “How It Works”# Original file (secret-values.yaml)database: username: admin password: supersecret # This needs encryption
# After SOPS encryptiondatabase: username: admin password: ENC[AES256_GCM,data:...,type:str]
sops: kms: - arn: arn:aws:kms:... created_at: "2024-01-15T10:00:00Z" enc: ...Using SOPS
Section titled “Using SOPS”# Installbrew install sops
# Configure with AWS KMS (or GCP KMS, Azure Key Vault, PGP)export SOPS_KMS_ARN="arn:aws:kms:us-east-1:123456789:key/abc-123"
# Encrypt a filesops -e secrets.yaml > secrets.enc.yaml
# Decrypt (for viewing/editing)sops -d secrets.enc.yaml
# Edit in place (decrypts, opens editor, re-encrypts)sops secrets.enc.yamlSOPS with GitOps
Section titled “SOPS with GitOps”Flux native SOPS support:
# Kustomization with SOPS decryptionapiVersion: kustomize.toolkit.fluxcd.io/v1kind: Kustomizationmetadata: name: my-appspec: decryption: provider: sops secretRef: name: sops-age # Age key for decryptionArgoCD with SOPS plugin:
# argocd-cm ConfigMapdata: configManagementPlugins: | - name: kustomize-sops generate: command: ["sh", "-c"] args: ["kustomize build . | sops -d /dev/stdin"]Pros and Cons
Section titled “Pros and Cons”Pros:
- Partial encryption (see structure, hide values)
- Multi-cloud KMS support
- Can work across clusters
- Mature, widely used
Cons:
- Requires KMS access from cluster
- More complex setup
- GitOps tool integration needed
Pattern 3: External Secrets Operator
Section titled “Pattern 3: External Secrets Operator”Don’t store secrets in Git at all. Reference them.
How It Works
Section titled “How It Works”# This goes in Git - just a referenceapiVersion: external-secrets.io/v1beta1kind: ExternalSecretmetadata: name: db-credsspec: refreshInterval: 1h secretStoreRef: kind: ClusterSecretStore name: vault-backend target: name: db-creds # K8s Secret to create data: - secretKey: username remoteRef: key: database/creds property: username - secretKey: password remoteRef: key: database/creds property: passwordGit Operator Vault Cluster │ │ │ │ │ ExternalSecret │ │ │ │ (reference) │ │ │ │───────────────────────▶ GitOps syncs │ │ │ │ │ │ │ │ Fetch secret │ │ │ │────────────────▶│ │ │ │ │ │ │ │◀────────────────│ │ │ │ Secret data │ │ │ │ │ │ │ │ Create K8s Secret │ │ │────────────────────────────────▶│Supported Backends
Section titled “Supported Backends”External Secrets Operator supports:
- AWS Secrets Manager
- Azure Key Vault
- Google Secret Manager
- HashiCorp Vault
- 1Password
- And many more
Setting Up ESO
Section titled “Setting Up ESO”# Install ESOhelm repo add external-secrets https://charts.external-secrets.iohelm install external-secrets external-secrets/external-secrets
# Create SecretStore (example: AWS Secrets Manager)kubectl apply -f - <<EOFapiVersion: external-secrets.io/v1beta1kind: ClusterSecretStoremetadata: name: aws-secrets-managerspec: provider: aws: service: SecretsManager region: us-east-1 auth: jwt: serviceAccountRef: name: external-secrets namespace: external-secretsEOFPros and Cons
Section titled “Pros and Cons”Pros:
- No secrets in Git at all
- Central secret management
- Automatic rotation
- Multi-cluster friendly
Cons:
- Dependency on external system
- More moving parts
- Requires secret store setup
Try This: Choose Your Approach
Section titled “Try This: Choose Your Approach”Answer these questions to help choose:
1. Do you already have a secrets manager (Vault, AWS SM, etc.)? [ ] Yes → Consider External Secrets Operator [ ] No → Sealed Secrets or SOPS
2. How many clusters do you have? [ ] One → Sealed Secrets is simple [ ] Many → SOPS or External Secrets (shareable)
3. Who manages secrets? [ ] Same team as infrastructure → Any approach [ ] Security team separately → External Secrets (separation)
4. Do you need secrets in Git history for audit? [ ] Yes → Sealed Secrets or SOPS [ ] No → External Secrets
5. Cloud provider preference? [ ] AWS/GCP/Azure → Use their KMS with SOPS [ ] Multi-cloud → Vault + External Secrets [ ] On-prem → Sealed Secrets or VaultDid You Know?
Section titled “Did You Know?”-
GitHub scans for secrets in commits and will alert you (and potentially revoke them if from known providers). This is a last-resort safety net, not a security strategy.
-
The Sealed Secrets controller key is the crown jewel. If you lose it, you lose all secrets. If it’s leaked, all encrypted secrets are compromised. Back it up securely.
-
SOPS was created by Mozilla for their internal infrastructure. It’s designed for GitOps-style workflows from the start.
-
GitGuardian’s 2023 report found over 10 million secrets exposed in public GitHub commits that year alone. The most common: API keys, database credentials, and cloud provider secrets. Prevention beats detection every time.
War Story: The Secret That Lived in Git Forever
Section titled “War Story: The Secret That Lived in Git Forever”A team I worked with made a common mistake:
The Incident:
Day 1: New developer commits a Secret with database credentials Day 2: Reviewer catches it, asks to remove Day 3: Developer deletes the file and commits Day 30: Security audit finds the secret in git history
# The secret is still there!git log --all --full-history -- secrets/database.yamlgit show abc123:secrets/database.yaml # There it isThe Damage:
- Credentials had to be rotated immediately
- Database briefly inaccessible during rotation
- Git history couldn’t be easily cleaned (forks, clones, backups)
- Compliance audit failed
The Fix (Painful):
- Rotate all exposed credentials
- BFG Repo Cleaner to rewrite history
- Force push (broke everyone’s clones)
- Notify all fork owners
- Invalidate CI caches
Prevention:
# Pre-commit hook (.pre-commit-config.yaml)repos: - repo: https://github.com/Yelp/detect-secrets rev: v1.4.0 hooks: - id: detect-secrets
# GitHub secret scanning# (enabled by default on public repos)
# Sealed Secrets for all K8s secrets# (never commit plain Secrets)Lesson: The only safe approach is preventing secrets from ever entering Git. All other solutions are damage control.
Secret Rotation
Section titled “Secret Rotation”Managing secrets isn’t just about storage — rotation is critical.
Why Rotate?
Section titled “Why Rotate?”- Credentials may be compromised
- Compliance requirements
- Employee departures
- Limiting exposure time
Rotation with Sealed Secrets
Section titled “Rotation with Sealed Secrets”# Manual rotation process# 1. Create new secretkubectl create secret generic db-creds \ --from-literal=username=admin \ --from-literal=password=NEW_PASSWORD \ --dry-run=client -o yaml > secret.yaml
# 2. Seal itkubeseal --format yaml < secret.yaml > sealed-secret.yaml
# 3. Commit and pushgit add sealed-secret.yamlgit commit -m "Rotate database credentials"git push
# 4. GitOps syncs, pods get new secret# (may need pod restart depending on how secrets are consumed)Rotation with External Secrets
Section titled “Rotation with External Secrets”apiVersion: external-secrets.io/v1beta1kind: ExternalSecretmetadata: name: db-credsspec: refreshInterval: 1h # Automatically fetches new values # ... rest of specRotation happens in the external store. ESO picks up changes automatically.
Rotation Patterns
Section titled “Rotation Patterns”| Pattern | Pros | Cons |
|---|---|---|
| Manual rotation | Full control | Human effort, error-prone |
| Scheduled rotation | Regular, predictable | May rotate unnecessarily |
| Event-driven | Rotate when needed | Needs triggering mechanism |
| Automatic (ESO) | Hands-off | Depends on external store |
Common Mistakes
Section titled “Common Mistakes”| Mistake | Problem | Solution |
|---|---|---|
| Committing plain secrets | In history forever | Use Sealed Secrets, SOPS, or ESO |
| Base64 = encryption | False security | Base64 is encoding, not encryption |
| Not backing up sealing key | Lose all secrets if lost | Secure backup of controller key |
| Same secrets all environments | One compromise = all compromised | Different secrets per environment |
| No rotation process | Stale, potentially leaked secrets | Implement rotation |
| Secrets in ConfigMaps | ”It’s not really a secret” | If sensitive, use Secrets |
Quiz: Check Your Understanding
Section titled “Quiz: Check Your Understanding”Question 1
Section titled “Question 1”Why is base64 encoding insufficient for storing secrets in Git?
Show Answer
Base64 is encoding, not encryption.
# Anyone can decode it instantlyecho "c3VwZXJzZWNyZXQ=" | base64 -d# Output: supersecretBase64:
- Transforms data to printable characters
- No key required to reverse
- Provides zero security
- Required by Kubernetes for Secret data field
Encryption:
- Requires a key to decrypt
- Computationally hard to reverse without key
- Provides actual security
Kubernetes uses base64 because Secret data might be binary. It’s not trying to hide the value.
Question 2
Section titled “Question 2”You have 5 clusters and want to share the same encrypted secrets across all of them. Which approach works best?
Show Answer
SOPS or External Secrets Operator — not Sealed Secrets.
Why not Sealed Secrets?
- Sealed Secrets encrypts with a cluster-specific public key
- Each cluster has its own key pair
- A secret sealed for Cluster A can’t be unsealed in Cluster B
- You’d have to seal 5 times for 5 clusters
SOPS works because:
- Encrypts with a shared key (KMS, PGP)
- Any cluster with KMS access can decrypt
- Same encrypted file works everywhere
External Secrets works because:
- Secret stored once in central manager (Vault, AWS SM)
- Each cluster fetches from same source
- Change once, propagates everywhere
Sealed Secrets is great for:
- Single cluster
- When you want cluster-specific secrets
- Simplicity over sharing
Question 3
Section titled “Question 3”Your application loads secrets at startup and caches them. You rotate a secret using External Secrets. What happens?
Show Answer
The application still has the old secret.
External Secrets Operator:
- Fetches new secret from backend ✓
- Updates Kubernetes Secret ✓
- Application doesn’t know about update ✗
Solutions:
-
Restart pods (simple but disruptive)
Terminal window kubectl rollout restart deployment my-app -
Stakater Reloader (automatic pod restart on Secret change)
metadata:annotations:reloader.stakater.com/auto: "true" -
Watch for Secret changes (app-level)
// Application watches Secret file for changes -
Use Vault Agent (sidecar handles rotation)
- Vault Agent renews secrets
- Writes to shared volume
- App reads from file (can watch for changes)
Best practice: Design applications to handle secret rotation. Don’t assume secrets are static.
Question 4
Section titled “Question 4”How do you recover if you lose the Sealed Secrets controller key?
Show Answer
Short answer: You can’t decrypt existing SealedSecrets.
Recovery options:
-
Restore from backup (if you have one)
Terminal window kubectl apply -f sealed-secrets-key-backup.yamlkubectl rollout restart deployment sealed-secrets-controller -n kube-system -
Re-create all secrets (if you have original values)
- Get plaintext values from application configs, password managers, etc.
- Create new SealedSecrets with new controller key
- Commit and deploy
-
You’re stuck (if no backup, no original values)
- Rotate everything
- This is a disaster scenario
Prevention:
# Backup the keykubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml > sealed-secrets-key-backup.yaml
# Store securely (Vault, encrypted S3, HSM)# Not in the same Git repo!Lesson: Back up the sealing key immediately after installing Sealed Secrets.
Hands-On Exercise: Implement GitOps Secrets
Section titled “Hands-On Exercise: Implement GitOps Secrets”Set up secrets management for a GitOps workflow.
Scenario
Section titled “Scenario”You have a Kubernetes application that needs:
- Database credentials (username, password)
- API key for external service
- TLS certificate
Part 1: Choose Your Approach
Section titled “Part 1: Choose Your Approach”Based on your environment:
## My Environment
- Number of clusters: ___- Existing secret manager: [ ] None [ ] Vault [ ] AWS SM [ ] Other: ___- Team managing secrets: [ ] Same as infra [ ] Security team
## Chosen Approach
[ ] Sealed Secrets[ ] SOPS with ___ (KMS provider)[ ] External Secrets with ___ (backend)
Rationale:_______________________________________________Part 2: Implementation
Section titled “Part 2: Implementation”For Sealed Secrets:
# Install controllerkubectl apply -f ___
# Create and seal database secretkubectl create secret generic db-creds \ --from-literal=username=___ \ --from-literal=password=___ \ --dry-run=client -o yaml | kubeseal --format yaml > ___
# Create and seal API key___
# Create and seal TLS cert___For External Secrets:
# ClusterSecretStore (configure your backend)apiVersion: external-secrets.io/v1beta1kind: ClusterSecretStoremetadata: name: ___spec: provider: ___: # Your provider config
---# ExternalSecret for databaseapiVersion: external-secrets.io/v1beta1kind: ExternalSecretmetadata: name: db-credsspec: secretStoreRef: name: ___ kind: ClusterSecretStore target: name: db-creds data: - secretKey: ___ remoteRef: key: ___Part 3: Verification
Section titled “Part 3: Verification”# Verify secrets are created in clusterkubectl get secrets
# Verify application can access secretskubectl exec -it deploy/my-app -- env | grep DB_Part 4: Document Rotation Process
Section titled “Part 4: Document Rotation Process”## Secret Rotation Runbook
### When to Rotate- [ ] Scheduled: Every ___ days- [ ] On employee departure- [ ] On suspected compromise
### Rotation Steps
1. Generate new credential ```bash ___-
Update in secrets management
Terminal window ___ -
Verify propagation
Terminal window ___ -
Restart affected workloads (if needed)
Terminal window ___ -
Verify application works
Terminal window ___
### Success Criteria
- [ ] Chose approach with documented rationale- [ ] Implemented at least one secret (database or API key)- [ ] Verified secret is accessible in cluster- [ ] Documented rotation process
---
## Key Takeaways
1. **Never commit plaintext secrets**: Git history is forever2. **Three main patterns**: Encrypt in Git, reference external, inject at runtime3. **Sealed Secrets**: Simple, cluster-specific, good for single-cluster4. **SOPS**: Flexible, multi-cluster, needs KMS setup5. **External Secrets Operator**: Central management, best for existing secret stores6. **Always have rotation process**: Secrets are not set-and-forget
---
## Further Reading
**Documentation**:- **Sealed Secrets** — github.com/bitnami-labs/sealed-secrets- **SOPS** — github.com/mozilla/sops- **External Secrets Operator** — external-secrets.io
**Articles**:- **"Managing Kubernetes Secrets"** — Various tech blogs- **"GitOps Secret Management"** — Weaveworks
**Tools**:- **detect-secrets**: Secret detection for pre-commit- **gitleaks**: Audit git repos for secrets- **truffleHog**: Find secrets in git history
---
## Summary
Secrets in GitOps is a solved problem — but you must explicitly solve it.
Choose based on your needs:- **Sealed Secrets**: Simple, GitOps-native, single cluster- **SOPS**: Flexible, multi-cluster, uses existing KMS- **External Secrets**: Central management, existing secret stores
All approaches share the goal: keep plaintext secrets out of Git while maintaining GitOps workflows.
---
## Next Module
Continue to [Module 3.6: Multi-Cluster GitOps](../module-3.6-multi-cluster/) to learn how to manage multiple clusters with GitOps.
---
*"The best secret management is when developers never touch secrets directly."* — Security Wisdom