Your database password is in a ConfigMap. Your API key is in a git repo. Your cloud credentials are in plaintext environment variables.
This is how most Kubernetes clusters look in the wild. And it’s how breaches happen.
Zero-trust means: don’t trust anything by default. Encrypt secrets at rest. Network-isolate workloads. Control access explicitly. Only then grant it.
This article covers a complete secrets and zero-trust strategy: encryption, network policies, access control, credential rotation, and scanning for leaks. We’ll use real examples from a production-grade setup.
Part 1: Secrets
The Problem: Unencrypted Secrets in etcd
By default, Kubernetes stores secrets in etcd as base64-encoded text. Base64 isn’t encryption: it’s just encoding. Anyone with etcd access reads your passwords:
| |
If your etcd is compromised (stolen backup, network sniffing, insider access), your secrets are exposed.
The fix: Encrypt secrets at rest using SOPS + age.
SOPS: Secrets Operations
SOPS is a tool that encrypts individual secret values in YAML files. It keeps your infrastructure code in git but encrypts sensitive data using your encryption key.
| |
SOPS encrypts it:
| |
Only your age private key can decrypt it. Commit the encrypted YAML to git. Your git repo is now safe.
Setup: SOPS + age
Step 1: Generate an age key
| |
Step 2: Create .sops.yaml
| |
The encrypted_regex: ^(data|stringData)$ tells SOPS to only encrypt the actual secret values, not metadata. This keeps your YAML readable and git-diffable.
Step 3: Create a secret and encrypt it
| |
Step 4: Deploy with Flux CD + SOPS
Flux automatically decrypts SOPS secrets when applying them to the cluster.
| |
Then configure Flux to use it:
| |
Flux decrypts secrets on apply. Encrypted values never touch your CI/CD pipeline unencrypted.
What to Encrypt
Encrypt anything that grants access:
- Database passwords
- API keys (cloud, third-party services)
- SSH keys
- TLS certificates (private keys only)
- OAuth tokens
- Backup credentials
- Registry credentials
Example from a production setup:
| |
Don’t Encrypt
Don’t encrypt non-sensitive data:
- ConfigMaps (configuration, not secrets)
- Namespace labels and annotations
- RBAC resource definitions
- Deployment specs (unless they reference secrets, which is fine)
Encrypt only the stringData/data fields that contain credentials. Keep metadata readable for git diffs.
Part 2: Network
Secrets protect data at rest. Network policies protect data in flight and control who can talk to what.
Default-Deny + Explicit Allow
Zero-trust networking means: block all traffic by default. Allow only what you explicitly need.
Step 1: Apply default-deny to production namespaces
| |
This blocks all incoming traffic to all pods in the production namespace. No exceptions.
Step 2: Add explicit allows for known traffic
| |
Now traffic flow is explicit:
Ingress traffic→ Only from ingress-nginx, kube-system, or same namespace, or monitoring (for metrics)DNS→ Allowed from kube-system (kubelet sends DNS queries)Egress→ Unrestricted by default (you can add egress policies if needed)
Namespace Isolation Pattern
Apply this pattern to every sensitive namespace:
production/
├── network-policies/
│ ├── default-deny-ingress.yaml
│ ├── allow-from-ingress-nginx.yaml
│ ├── allow-from-kube-system.yaml
│ ├── allow-intra-namespace.yaml
│ ├── allow-from-monitoring.yaml
│ └── kustomization.yaml
└── kustomization.yaml (includes network-policies)
Sensitive namespaces that get this treatment:
- production: Your main apps
- databases: If you run databases in K8s
- payments: If you handle payments
- secrets: Vaultwarden, secret stores
- monitoring: Prometheus scrapes everything, but shouldn’t be scraped by random pods
- ai-platform: If running LLMs or proprietary models
Namespaces that don’t need network policies:
- kube-system: Core Kubernetes components (exempt from network policies)
- flux-system: GitOps controller (exempt from network policies)
- dev: Development/testing (can be more permissive)
Debugging Network Policies
Network policies are hard to debug. Tools help:
| |
Part 3: Access Control
Secrets are encrypted. Network is isolated. Now control who can access what.
RBAC: Role-Based Access Control
RBAC answers: what can this service account do?
The concept:
- ServiceAccount: Identity (who)
- Role/ClusterRole: Permissions (what)
- RoleBinding/ClusterRoleBinding: Link them (who can do what)
Example: Monitoring stack needs to read pod metadata
| |
RBAC best practices:
- Least privilege: Only grant what’s needed. Prometheus needs to read pods, not delete them.
- Namespace-scoped where possible: Use Role + RoleBinding instead of ClusterRole + ClusterRoleBinding when scoped to one namespace.
- Service account per workload: Don’t share service accounts across deployments.
- No cluster-admin for apps: cluster-admin is for infrastructure operators, not applications.
Dangerous patterns to avoid:
| |
Pod Security Standards
Pod security controls what a container can do: mount host filesystems, run as root, change kernel parameters.
PodSecurityPolicy (deprecated) → PodSecurityStandard (now)
Apply labels to namespaces:
| |
This enforces:
- Non-root user
- No privileged containers
- No hostPath mounts
- No host networking
- Immutable root filesystem
- Read-only containers
Example deployment that fails restricted policy:
| |
Kubernetes rejects this in a restricted namespace.
Correct deployment:
| |
Part 4: Credential Rotation
Encrypted secrets are good. But if a secret is compromised, you need to rotate it quickly.
Rotation Strategy
Principle: secrets should have expiration dates.
For each type of credential, define a rotation schedule:
| Credential | Rotation | Reason |
|---|---|---|
| Database passwords | 90 days | Regular credential hygiene |
| API keys | 180 days | Less risky than passwords, rotate less often |
| TLS certificates | Auto (cert-manager) | Certificate expiration forces rotation |
| SSH keys | 1 year or on team change | Stable, rotate on personnel changes |
| Cloud access keys | 90 days | High-value targets, rotate frequently |
Manual Rotation Process
For credentials not automated (e.g., database passwords):
| |
Automated Rotation with External Secrets Operator
For cloud credentials and API keys, use External Secrets Operator (ESO) to sync from a secret manager:
| |
This automatically syncs credentials from AWS Secrets Manager every hour. Rotate in AWS, and Kubernetes picks up the change.
Part 5: Scanning for Credential Leaks
Despite your best efforts, credentials slip into code. Git history, logs, old commits.
Tools to Find Leaks
truffleHog: Scans git history for credential patterns
| |
git-secrets: Prevents commits containing secrets
| |
SOPS pre-commit hook: Ensures all secrets are encrypted before commit
| |
Audit Existing Code
| |
Fix found credentials by:
- Rotate the leaked credential immediately
- Move to secrets (encrypt with SOPS)
- Commit the fix with the credential removed
- Scan again to verify removal
Putting It All Together: Zero-Trust Architecture
Here’s how all pieces work together:
┌─────────────────────────────────────────────────────────────────┐
│ Production Namespace │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─ Pod Security Standard: restricted ──────────────────────┐ │
│ │ - Non-root user │ │
│ │ - No privileged containers │ │
│ │ - Read-only root filesystem │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Network Policies: default-deny + explicit allows ──────┐ │
│ │ ✓ Allow from ingress-nginx (external traffic) │ │
│ │ ✓ Allow from monitoring (metrics scraping) │ │
│ │ ✓ Allow intra-namespace (pod-to-pod) │ │
│ │ ✗ Block everything else │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ RBAC: least privilege ───────────────────────────────────┐ │
│ │ app-sa can only: │ │
│ │ - Read configmaps named "app-config" │ │
│ │ - Read secrets named "app-db-password" │ │
│ │ - Cannot list/delete/modify other resources │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Encrypted Secrets ───────────────────────────────────────┐ │
│ │ Secret: app-db-password │ │
│ │ - Stored encrypted in etcd (SOPS + age) │ │
│ │ - Only decrypted when pod requests it │ │
│ │ - Rotated every 90 days │ │
│ │ - Leaked credentials detected by truffleHog │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ Pod: api-server │
│ │ │
│ ├─ Runs as user: 1000 (not root) │
│ ├─ SA: app-sa (RBAC enforced) │
│ ├─ Mount: /etc/app-secrets (from encrypted secret) │
│ ├─ Filesystem: read-only (except /tmp) │
│ │ │
│ └─ Can only: │
│ - Read database password from secret mount │
│ - Connect to allowed pods (network policy) │
│ - Cannot access cluster secrets │
│ - Cannot escalate privileges │
│ │
└─────────────────────────────────────────────────────────────────┘
Traffic in:
Internet
↓
Firewall (pfSense, cloud SG)
↓
LoadBalancer IP
↓
Ingress Controller (ingress-nginx)
↓
NetworkPolicy (allow-from-ingress-nginx)
↓
Service Endpoint
↓
Pod (restricted PSS, non-root, RO filesystem)
All layers enforce trust boundaries. Compromise at one level doesn’t cascade.
Implementation Checklist
Phase 1: Secrets Encryption (Week 1)
- Generate age key and store securely
- Create .sops.yaml in your repo
- Encrypt all existing secrets with SOPS
- Add SOPS decryption key to Flux
- Verify Flux can decrypt and deploy secrets
Phase 2: Network Policies (Week 2)
- Identify sensitive namespaces (production, databases, payments)
- Apply default-deny-ingress to each
- Add explicit allow policies (ingress-nginx, monitoring, kube-system)
- Test connectivity (curl from pods)
- Document which namespaces have policies
Phase 3: RBAC (Week 3)
- Audit existing service accounts and RBAC rules
- Create least-privilege roles for each workload
- Bind roles to service accounts
- Test that apps work with new RBAC (permission errors indicate overly strict)
Phase 4: Pod Security (Week 4)
- Label namespaces with pod-security.kubernetes.io/enforce: restricted
- Update deployments to pass restricted policy (non-root, RO FS, no privileged, no hostPath)
- Test deployments in non-production first
- Roll out to production
Phase 5: Credential Rotation (Ongoing)
- Set up credential rotation schedule (90d for passwords, 180d for API keys)
- Document rotation procedures for each credential type
- Automate where possible (External Secrets Operator for cloud credentials)
- Test rotation in staging before rotating in production
Phase 6: Scanning (Ongoing)
- Install truffleHog and scan existing repo
- Fix any found credentials (rotate, encrypt, commit fix)
- Set up git-secrets pre-commit hook
- Add truffleHog to CI/CD pipeline (fail build if credentials detected)
Common Mistakes
❌ Storing decryption keys in git
SOPS age public key can be in git. The private key must not. Store it in:
- Your laptop only
- Your secret manager (AWS Secrets Manager, HashiCorp Vault)
- Never in git, CI/CD environment variables, or container images
❌ Network policies that break observability
If you restrict all traffic, monitoring can’t scrape metrics. Add explicit allow from monitoring namespace:
| |
❌ Pod security policies too strict
Some apps need to write to disk or bind to privileged ports. Test in dev first:
| |
❌ Forgetting RBAC for stateless jobs
Jobs need ServiceAccounts too:
| |
❌ Rotating a secret but not restarting pods
Pods cache environment variables. Change a secret, and old pods still have old values. You must:
- Update the secret
- Restart the pods (via rollout restart or new deployment)
| |
What’s Next
This covers the core. From here:
Audit Logging
Log who accessed secrets and when:
| |
Secrets Rotation at Scale
Coordinate rotation across multiple environments:
| |
Zero-Trust Across Clusters
If you run multiple clusters:
| |
Summary
You now have:
- Secrets Encryption: SOPS + age, encrypted at rest in etcd and in git
- Network Isolation: Default-deny ingress, explicit allows for known traffic
- Access Control: RBAC enforcing least privilege, Pod Security Standards
- Credential Rotation: Scheduled rotation for all credential types
- Leak Detection: Automated scanning for credentials in code
This is production-grade zero-trust. Attackers need to compromise multiple layers (network, access, secrets encryption, pod security) to break in. Any single compromise doesn’t cascade.
Deploy it in phases. Test in dev first. And remember: security is a practice, not a destination.
Implementing zero-trust on Kubernetes? We take on short-term infrastructure engagements and platform work.