Stage 5 · Platform
GitOps Operations
Secrets in GitOps
Encrypting Kubernetes secrets with SOPS, Sealed Secrets, External Secrets Operator, and KMS.
The Secrets Challenge
GitOps requires storing desired state in Git. But Kubernetes Secrets are base64-encoded, not encrypted. Storing them in Git exposes credentials to anyone with repository access. The solution is to encrypt secrets before committing, or to store them externally and reference them from Git.
SOPS Encryption
# .sops.yaml
creation_rules:
- path_regex: secrets/production/.*
kms: "arn:aws:kms:us-east-1:123456789012:key/abc-123"
age: "age1xxxxxxxxxxxxxxxxxxxxx"
- path_regex: secrets/staging/.*
kms: "arn:aws:kms:us-east-1:123456789012:key/def-456"
# Encrypted Kubernetes secret (created with sops -e)
apiVersion: v1
kind: Secret
metadata:
name: myapp-secrets
namespace: production
type: Opaque
data:
database-url: ENC[AES256_GCM,data:encrypted...,type:str]
api-key: ENC[AES256_GCM,data:encrypted...,type:str]
sops:
kms:
- arn: arn:aws:kms:us-east-1:123456789012:key/abc-123
created_at: "2024-01-15T10:00:00Z"
enc: "encrypted-key..."
lastmodified: "2024-01-15T10:00:00Z"
version: 3.8.0SOPS encrypts values in YAML files while keeping keys readable. The encrypted file is safe to commit to Git. The GitOps controller decrypts at runtime using the configured KMS key. Install the SOPS Kustomize plugin for Flux integration.
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: myapp
namespace: flux-system
spec:
interval: 5m
path: ./overlays/production
sourceRef:
kind: GitRepository
name: myapp
decryption:
provider: sops
secretRef:
name: sops-age
---
apiVersion: v1
kind: Secret
metadata:
name: sops-age
namespace: flux-system
type: Opaque
stringData:
age.agekey: |
# SOPS age private key
-----BEGIN AGE PRIVATE KEY-----
...
-----END AGE PRIVATE KEY-----Flux integrates with SOPS natively. The decryption section tells Flux to decrypt secrets using the specified provider and key. The age key is stored as a Kubernetes Secret in the flux-system namespace.
Sealed Secrets
# Create a SealedSecret from a regular Secret
kubectl create secret generic myapp-secrets \
--from-literal=database-url='postgres://...' \
--from-literal=api-key='sk-...' \
--dry-run=client -o yaml | \
kubeseal --format yaml > sealed-secret.yaml
# The encrypted SealedSecret (safe to commit)
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: myapp-secrets
namespace: production
spec:
encryptedData:
database-url: AgBy3i4OJSWK+PiTySYZZA9rO43...
api-key: AgBTdVZz4r8+ZzHJ4Q5x4R5N...
template:
metadata:
name: myapp-secrets
namespace: productionkubeseal encrypts the Secret using the Sealed Secrets controller's public key. Only the controller in the cluster can decrypt it. The encrypted SealedSecret is safe to commit to Git. The controller creates the actual Secret at runtime.
External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets-manager
namespace: production
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: external-secrets
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: myapp-secrets
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: myapp-secrets
creationPolicy: Owner
data:
- secretKey: database-url
remoteRef:
key: production/myapp/database-url
- secretKey: api-key
remoteRef:
key: production/myapp/api-keyExternal Secrets Operator syncs secrets from external providers (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault) into Kubernetes Secrets. The ExternalSecret resource defines which secrets to sync. Secrets are refreshed automatically.
Vault Integration
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: production
spec:
provider:
vault:
server: "https://vault.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "external-secrets"
serviceAccountRef:
name: external-secrets
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: myapp-vault-secrets
namespace: production
spec:
refreshInterval: 5m
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: myapp-secrets
dataFrom:
- extract:
key: production/myappVault provides dynamic secrets with automatic rotation. The dataFrom field extracts all key-value pairs from a Vault path. The service account authenticates with Vault via Kubernetes auth method.
Secrets Best Practices
- Never store plaintext secrets in Git — use SOPS, Sealed Secrets, or External Secrets.
- Use OIDC for CI/CD credentials — avoid long-lived tokens stored as secrets.
- Rotate secrets regularly — set expiration dates and automate rotation.
- Audit secret access — log every secret read and write.
- Use separate secrets per environment — staging secrets should not work in production.
External Secrets Operator integrates with AWS Secrets Manager, GCP Secret Manager, and Azure Key Vault. It is the most production-ready solution for teams already using cloud secret stores.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.