Loading learning content...
Kubernetes Secrets are the built-in mechanism for managing sensitive data in Kubernetes clusters. Understanding their capabilities, limitations, and the ecosystem of tools that extend them is essential for any engineer working with containerized applications.
Kubernetes provides native secrets as a first-class resource type, enabling workloads to access sensitive data without embedding credentials in container images or source code. However, native secrets come with significant security caveats that have spawned an ecosystem of tools designed to address their shortcomings.
By the end of this page, you will understand Kubernetes native secrets, their security model, encryption at rest configuration, external secrets operators for integrating with cloud secret managers, sealed secrets for GitOps workflows, and production security best practices for secrets in Kubernetes.
Kubernetes Secrets are API objects that store sensitive data such as passwords, OAuth tokens, SSH keys, and TLS certificates. They separate sensitive configuration from container images and pod specifications.
Secret Types:
Kubernetes defines several built-in secret types, each with specific schema requirements:
| Type | Purpose | Required Keys |
|---|---|---|
| Opaque | Arbitrary user-defined data | None (any keys) |
| kubernetes.io/service-account-token | Service account tokens | Auto-generated |
| kubernetes.io/dockerconfigjson | Docker registry credentials | .dockerconfigjson |
| kubernetes.io/basic-auth | Basic authentication | username, password |
| kubernetes.io/ssh-auth | SSH authentication | ssh-privatekey |
| kubernetes.io/tls | TLS certificates | tls.crt, tls.key |
| bootstrap.kubernetes.io/token | Bootstrap tokens | token-id, token-secret |
1234567891011121314151617181920212223242526272829303132
# Create a generic/opaque secret from literalskubectl create secret generic db-credentials \ --from-literal=username=admin \ --from-literal=password='MyStr0ngP@ss!' \ --namespace=production # Create from fileskubectl create secret generic api-keys \ --from-file=api-key=./api-key.txt \ --from-file=api-secret=./api-secret.txt # Create TLS secret from certificate fileskubectl create secret tls tls-cert \ --cert=./tls.crt \ --key=./tls.key \ --namespace=production # Create Docker registry secretkubectl create secret docker-registry regcred \ --docker-server=myregistry.azurecr.io \ --docker-username=myuser \ --docker-password=mypassword \ --docker-email=user@example.com # View secret (base64 encoded)kubectl get secret db-credentials -o yaml # Decode a specific valuekubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 --decode # List all secretskubectl get secrets -n productionBase64 encoding is NOT a security measure—it's simply a text encoding format. Anyone with access to the Secret manifest can decode the values instantly. Native Kubernetes secrets are stored in etcd in plaintext (by default) and are only as secure as your cluster's etcd access controls.
Pods can consume secrets in two ways: as environment variables or as volume mounts. Each approach has trade-offs in terms of security, update behavior, and application compatibility.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
apiVersion: v1kind: Podmetadata: name: myappspec: containers: - name: app image: myapp:latest # Method 1: Individual keys as env vars env: - name: DB_USERNAME valueFrom: secretKeyRef: name: db-credentials key: username - name: DB_PASSWORD valueFrom: secretKeyRef: name: db-credentials key: password optional: false # Pod fails if secret doesn't exist # Method 2: All keys as env vars (prefix optional) envFrom: - secretRef: name: api-keys prefix: API_ # Results in API_api-key, API_api-secret ---# Deployment with secret referencesapiVersion: apps/v1kind: Deploymentmetadata: name: myappspec: replicas: 3 template: spec: containers: - name: app image: myapp:latest env: - name: DATABASE_URL valueFrom: secretKeyRef: name: db-credentials key: connection-string # Environment variables are NOT updated when secret changes # Pods must be restarted to pick up new valuesIf you need secrets to update without pod restarts (e.g., credential rotation), use volume mounts. Implement file watching in your application using inotify or polling. Remember that updates may take up to kubelet sync period (default 1 minute) plus cache propagation delay.
Kubernetes native secrets have significant security limitations that are often misunderstood. Understanding these limitations is critical for designing secure systems.
Critical Limitation: etcd Storage
By default, Kubernetes stores secrets in etcd without encryption. This means:
RBAC Risks:
Kubernetes RBAC for secrets operates at the resource level, not the secret content level:
123456789101112131415161718192021222324252627282930313233343536373839404142
# DANGEROUS: Grants access to ALL secrets in namespaceapiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: secret-reader namespace: productionrules: - apiGroups: [""] resources: ["secrets"] verbs: ["get", "list"] # Can read ANY secret ---# BETTER: Restrict to specific secrets by nameapiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: app-secret-reader namespace: productionrules: - apiGroups: [""] resources: ["secrets"] resourceNames: ["myapp-secrets", "myapp-tls"] # Only these secrets verbs: ["get"] # Note: 'list' cannot be restricted by resourceNames ---# DANGEROUS: Pod creation = secret access# Anyone who can create pods can mount any secret they can 'get'apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: pod-creatorrules: - apiGroups: [""] resources: ["pods"] verbs: ["create", "delete"] # This user can create a pod that mounts secrets they have 'get' access to # The pod itself runs with the secret exposed ---# Even with ValidatingWebhook, privilege escalation is complex# Pod service accounts can often access more secrets than intendedAny user who can create pods AND get secrets can effectively read secret contents—even without list permission. They create a pod that mounts the secret and echos it to logs or exfiltrates it. RBAC alone cannot fully protect secrets from users with pod creation rights.
Kubernetes supports encrypting Secret resources at rest in etcd. This is a critical security configuration for production clusters that should be enabled from day one.
Encryption Configuration:
The API server reads encryption configuration from a file specified by --encryption-provider-config. This file defines which resources to encrypt and which providers to use.
1234567891011121314151617181920212223242526272829303132333435363738
# /etc/kubernetes/encryption/config.yamlapiVersion: apiserver.config.k8s.io/v1kind: EncryptionConfigurationresources: - resources: - secrets - configmaps # Optional: encrypt configmaps too providers: # Provider order matters: first provider is used for encryption # All providers are tried for decryption (for rotation) # Option 1: AES-GCM (recommended for most cases) - aescbc: keys: - name: key1 secret: <base64-encoded-32-byte-key> # Generate: head -c 32 /dev/urandom | base64 # Option 2: Secretbox (modern, high-performance) - secretbox: keys: - name: key1 secret: <base64-encoded-32-byte-key> # Option 3: KMS provider (recommended for production) - kms: apiVersion: v2 name: aws-kms endpoint: unix:///var/run/kmsplugin/socket.sock cachesize: 1000 timeout: 3s # Identity provider allows reading old unencrypted secrets # Remove after all secrets are re-encrypted - identity: {} # API server flag:# --encryption-provider-config=/etc/kubernetes/encryption/config.yamlAfter Enabling Encryption:
Existing secrets remain unencrypted until re-created. Force re-encryption of all secrets:
12345678910111213
# Re-encrypt all secrets in all namespaceskubectl get secrets --all-namespaces -o json | \ kubectl replace -f - # Verify encryption is working# Check etcd directly (requires etcdctl and certificates)ETCDCTL_API=3 etcdctl get /registry/secrets/default/my-secret \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key # Encrypted output should start with 'k8s:enc:aescbc:v1:key1:'# Unencrypted output shows plaintext JSONPlan for encryption key rotation before enabling encryption. Add new keys at the beginning of the providers list (for encryption), keep old keys (for decryption), re-encrypt all secrets, then remove old keys. Loss of encryption keys means permanent loss of secrets—back up keys securely.
The External Secrets Operator (ESO) bridges Kubernetes with external secret management systems like HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, and GCP Secret Manager. It synchronizes secrets from external stores into native Kubernetes secrets.
Key Concepts:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
# Install ESO via Helm# helm repo add external-secrets https://charts.external-secrets.io# helm install external-secrets external-secrets/external-secrets -n external-secrets --create-namespace ---# ClusterSecretStore for AWS Secrets ManagerapiVersion: external-secrets.io/v1beta1kind: ClusterSecretStoremetadata: name: aws-secrets-managerspec: provider: aws: service: SecretsManager region: us-east-1 auth: # Option 1: IAM Roles for Service Accounts (IRSA) - recommended jwt: serviceAccountRef: name: external-secrets-sa namespace: external-secrets # Option 2: Static credentials (not recommended) # secretRef: # accessKeyIDSecretRef: # name: aws-credentials # key: access-key-id # secretAccessKeySecretRef: # name: aws-credentials # key: secret-access-key ---# ExternalSecret that syncs from AWS Secrets ManagerapiVersion: external-secrets.io/v1beta1kind: ExternalSecretmetadata: name: db-credentials namespace: productionspec: refreshInterval: 1h # How often to sync secretStoreRef: name: aws-secrets-manager kind: ClusterSecretStore target: name: db-credentials # Name of K8s secret to create creationPolicy: Owner # Delete K8s secret when ExternalSecret is deleted template: type: Opaque data: # Template the secret data DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@db.example.com/production" data: # Map external secret keys to K8s secret keys - secretKey: username remoteRef: key: prod/myapp/database # AWS Secrets Manager secret name property: username # JSON key within the secret - secretKey: password remoteRef: key: prod/myapp/database property: password # Or fetch entire secret as JSON dataFrom: - extract: key: prod/myapp/api-keysSet refreshInterval based on your rotation cadence. Too short increases API calls and costs; too long means delayed credential updates. For secrets with automatic rotation, set refresh slightly shorter than rotation period to ensure timely updates.
Sealed Secrets by Bitnami solves the GitOps secrets problem: how do you store encrypted secrets in Git that can only be decrypted by the cluster? Sealed Secrets uses asymmetric encryption—secrets are encrypted with a public key and can only be decrypted by the controller with the private key.
How It Works:
kubeseal CLI with the public key1234567891011121314151617181920212223242526
# Install Sealed Secrets controllerhelm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secretshelm install sealed-secrets sealed-secrets/sealed-secrets -n kube-system # Install kubeseal CLI# macOS: brew install kubeseal# Linux: wget https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/kubeseal-0.24.0-linux-amd64.tar.gz # Fetch the public key (for offline sealing)kubeseal --fetch-cert \ --controller-name=sealed-secrets \ --controller-namespace=kube-system \ > sealed-secrets-pub.pem # Create a regular secret, then seal itkubectl create secret generic db-credentials \ --from-literal=username=admin \ --from-literal=password='MyS3cret!' \ --dry-run=client -o yaml | \kubeseal \ --controller-name=sealed-secrets \ --controller-namespace=kube-system \ --format yaml > db-credentials-sealed.yaml # The sealed secret can be committed to Gitcat db-credentials-sealed.yamlIf you lose the controller's private key, ALL sealed secrets become permanently undecryptable. Back up the sealing key immediately after installation: kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml > sealing-key-backup.yaml. Store this backup securely (not in Git!).
Securing secrets in Kubernetes requires a defense-in-depth approach combining multiple techniques. Here are battle-tested practices for production environments:
123456789101112131415161718192021222324
# Kubernetes audit policy for secretsapiVersion: audit.k8s.io/v1kind: Policyrules: # Log secret access at RequestResponse level (includes response body) - level: RequestResponse resources: - group: "" resources: ["secrets"] verbs: ["get", "list", "watch"] # Log secret mutations - level: RequestResponse resources: - group: "" resources: ["secrets"] verbs: ["create", "update", "patch", "delete"] # Don't log reads of configmaps (reduce noise) - level: None resources: - group: "" resources: ["configmaps"] verbs: ["get", "list", "watch"]Best practice: Store secrets in an external manager (Vault, AWS Secrets Manager), sync to Kubernetes via External Secrets Operator, enable encryption at rest in etcd, use IRSA/Workload Identity for operator authentication, and mount secrets as volumes with auto-rotation. This provides defense in depth with minimal secrets exposure.
Kubernetes secrets provide essential but limited secrets management for container orchestration. Let's consolidate the key concepts:
What's Next:
Having explored individual secrets management tools, the final page helps you choose the right solution for your organization. You'll learn decision frameworks, comparison matrices, and architectural patterns for selecting and combining these tools based on your specific requirements.
You now have a comprehensive understanding of Kubernetes secrets, their security limitations, and the ecosystem of tools that enhance their security. This knowledge enables you to design secure secrets architectures for containerized workloads.