Secrets and Sensitive Data in Kubernetes: Secure Management with Secrets, Vault, and Sealed-Secrets
Introduction: Why Native Kubernetes Secrets Are Insecure by Default
Kubernetes Secrets are the first thing a DevOps engineer reaches for when passing a database password or API key into a Pod. However, most teams underestimate the risks: by default, secrets are stored in etcd in Base64 format — this is not encryption, it's merely encoding. Anyone who gains access to an etcd backup or to a Secret object via kubectl can immediately read the data in plain text.
Real-world threats include:
- etcd compromise — an attacker with access to an etcd snapshot obtains all cluster secrets.
- Excessive RBAC permissions — developers accidentally gain
get secretsrights in production namespaces. - Secrets in Git — a manifest with encoded data is committed to a repository.
- No rotation — the same password lives for years.
In 2026, secrets management in Kubernetes is not optional — it's a mandatory component of a secure infrastructure. Let's explore the approaches and best practices.
Overview of Secrets Management Approaches
1. Native Kubernetes Secrets
A built-in mechanism that is easy to use but requires additional protection:
- Enable encryption at rest via
EncryptionConfigurationon the API server. - Restrict RBAC: minimal permission sets at the namespace level.
- Enable Audit Logging to track access.
2. HashiCorp Vault
A fully-featured secrets store with dynamic credentials, a lease mechanism, and detailed auditing. It integrates with Kubernetes via Agent Injector (sidecar) or CSI Provider. Best suited for large teams with high security requirements.
3. Sealed Secrets (Bitnami)
A Kubernetes controller that allows you to safely store encrypted secrets directly in Git. The secret is encrypted with the cluster's public key and can only be decrypted by the cluster itself. Ideal for a GitOps workflow.
4. External Secrets Operator
An operator that synchronizes secrets from external stores (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, Azure Key Vault) into native Kubernetes Secrets. Enables centralized secrets management outside the cluster.
Hands-On: Setting Up Sealed Secrets in a Cluster
Sealed Secrets is the optimal choice for teams practicing GitOps. Let's walk through step-by-step installation and usage.
Step 1: Install the Controller
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets\nhelm repo update\nhelm install sealed-secrets sealed-secrets/sealed-secrets \\\n --namespace kube-system \\\n --set fullnameOverride=sealed-secrets-controllerStep 2: Install the kubeseal CLI
# Linux\nwget https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.26.0/kubeseal-0.26.0-linux-amd64.tar.gz\ntar xvf kubeseal-0.26.0-linux-amd64.tar.gz\nsudo mv kubeseal /usr/local/bin/Step 3: Create and Encrypt a Secret
First, create a regular Secret manifest, then encrypt it with kubeseal:
kubectl create secret generic db-credentials \\\n --from-literal=DB_PASSWORD=supersecret123 \\\n --from-literal=DB_USER=appuser \\\n --namespace=production \\\n --dry-run=client -o yaml | \\\n kubeseal --controller-name=sealed-secrets-controller \\\n --controller-namespace=kube-system \\\n --format yaml > sealed-db-credentials.yamlThe resulting manifest sealed-db-credentials.yaml looks like this:
apiVersion: bitnami.com/v1alpha1\nkind: SealedSecret\nmetadata:\n name: db-credentials\n namespace: production\nspec:\n encryptedData:\n DB_PASSWORD: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq...\n DB_USER: AgAKIBDLs8yHFUVCG+p1k2M3N4...\n template:\n metadata:\n name: db-credentials\n namespace: production\n type: OpaqueThis file is safe to commit to Git — without access to the cluster's private key, decrypting it is impossible. Apply the manifest:
kubectl apply -f sealed-db-credentials.yamlThe controller will automatically create a native Secret in the production namespace.
Integrating HashiCorp Vault with Kubernetes
Agent Injector
Vault Agent Injector works as a mutating admission webhook: when a Pod is created with the required annotations, a sidecar container is automatically injected into it, which authenticates to Vault and mounts secrets into the Pod's filesystem.
Example Pod annotations:
apiVersion: v1\nkind: Pod\nmetadata:\n name: go-app\n namespace: production\n annotations:\n vault.hashicorp.com/agent-inject: "true"\n vault.hashicorp.com/role: "go-app-role"\n vault.hashicorp.com/agent-inject-secret-config.env: "secret/data/production/go-app"\n vault.hashicorp.com/agent-inject-template-config.env: |\n {{- with secret "secret/data/production/go-app" -}}\n export DB_PASSWORD={{ .Data.data.db_password }}\n export API_KEY={{ .Data.data.api_key }}\n {{- end }}\nspec:\n serviceAccountName: go-app-sa\n containers:\n - name: go-app\n image: myregistry/go-app:latestCSI Provider
Vault CSI Provider mounts secrets as regular files via the Kubernetes Secrets Store CSI Driver — without a sidecar container, reducing overhead. It is well-suited for cases where the application already reads configuration from files.
apiVersion: secrets-store.csi.x-k8s.io/v1\nkind: SecretProviderClass\nmetadata:\n name: vault-db-credentials\n namespace: production\nspec:\n provider: vault\n parameters:\n vaultAddress: "https://vault.example.com"\n roleName: "go-app-role"\n objects: |\n - objectName: "db_password"\n secretPath: "secret/data/production/go-app"\n secretKey: "db_password"Configuring Kubernetes Auth in Vault
# Enable Kubernetes auth\nvault auth enable kubernetes\n\n# Configure cluster connection\nvault write auth/kubernetes/config \\\n kubernetes_host="https://kubernetes.default.svc" \\\n kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \\\n token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token\n\n# Create a role for the application\nvault write auth/kubernetes/role/go-app-role \\\n bound_service_account_names=go-app-sa \\\n bound_service_account_namespaces=production \\\n policies=go-app-policy \\\n ttl=1hSecrets Management in the CI/CD Pipeline
GitHub Actions
In GitHub Actions, secrets are stored in repository or organization settings and passed into workflows via environment variables. Never print secrets to logs.
name: Deploy to Kubernetes\non:\n push:\n branches: [main]\njobs:\n deploy:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - name: Configure kubectl\n uses: azure/k8s-set-context@v3\n with:\n kubeconfig: ${{ secrets.KUBECONFIG }}\n - name: Deploy application\n env:\n REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}\n run: |\n echo "$REGISTRY_TOKEN" | docker login registry.example.com -u ci --password-stdin\n kubectl apply -f k8s/GitLab CI
In GitLab CI, secrets are defined as Protected and Masked variables in the project settings. For HashiCorp Vault integration, GitLab supports a native JWT mechanism:
deploy:production:\n stage: deploy\n id_tokens:\n VAULT_ID_TOKEN:\n aud: https://vault.example.com\n secrets:\n DB_PASSWORD:\n vault: production/go-app/db_password@secret\n file: false\n script:\n - kubectl create secret generic db-credentials \\\n --from-literal=DB_PASSWORD="$DB_PASSWORD" \\\n --namespace=production \\\n --dry-run=client -o yaml | kubectl apply -f -This approach allows secrets to be retrieved directly from Vault without storing them in GitLab, which aligns with the principle of least trust.
Secret Rotation Without Downtime
Secret rotation is a critically important process that is often overlooked until the first incident. Let's look at a strategy for Go and PHP applications.
Double-Write Strategy
- A new version of the secret is created in Vault, while the old one remains active.
- The application is restarted with the new secret (rolling update in Kubernetes).
- After a successful deployment, the old version is marked as deprecated.
Go Application: Hot Configuration Reload
Go applications can use the Vault SDK for dynamic credential updates without a restart:
// Simplified lease monitoring example\nfunc renewSecret(client *vault.Client, secret *vault.Secret) {\n renewer, _ := client.NewLifetimeWatcher(&vault.LifetimeWatcherInput{\n Secret: secret,\n Increment: 3600,\n })\n go renewer.Start()\n defer renewer.Stop()\n for {\n select {\n case renewal := <-renewer.RenewCh():\n log.Printf("Secret renewed: %v", renewal.Secret.LeaseDuration)\n case err := <-renewer.DoneCh():\n log.Printf("Renewal failed, fetching new secret: %v", err)\n // Fetch new secret\n return\n }\n }\n}PHP Application: Rotation via Kubernetes Rolling Update
PHP applications typically do not maintain long-lived connections, so a standard Kubernetes rolling update handles the task without downtime:
apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: php-app\n namespace: production\nspec:\n replicas: 3\n strategy:\n type: RollingUpdate\n rollingUpdate:\n maxUnavailable: 0\n maxSurge: 1\n template:\n spec:\n containers:\n - name: php-app\n image: myregistry/php-app:latest\n envFrom:\n - secretRef:\n name: db-credentialsWhen rotating a secret, simply update the Secret object and run kubectl rollout restart deployment/php-app -n production.
Auditing and Monitoring Secret Access
Security without monitoring is an illusion. Configure the following mechanisms:
Kubernetes Audit Logging
Enable auditing at the API server level, capturing requests to the secrets resource:
apiVersion: audit.k8s.io/v1\nkind: Policy\nrules:\n- level: Metadata\n resources:\n - group: ""\n resources: ["secrets"]\n verbs: ["get", "list", "watch"]\n- level: RequestResponse\n resources:\n - group: ""\n resources: ["secrets"]\n verbs: ["create", "update", "patch", "delete"]Vault Audit Devices
In HashiCorp Vault, enable a file or syslog audit device:
vault audit enable file file_path=/var/log/vault/audit.logAll secret accesses are recorded with the client, timestamp, and operation result. Integrate logs with ELK Stack or Grafana Loki for centralized analysis.
Alerting
Set up alerts for anomalous events: bulk listing of secrets, access from unexpected namespaces, access attempts with revoked tokens.
Security Checklist for a Production Cluster
- Encryption at rest — enable
EncryptionConfigurationfor etcd with an AES-CBC or KMS provider. - RBAC minimalism — principle of least privilege: roles scoped to required namespaces and resources only, no wildcards.
- No plaintext secrets in Git — use Sealed Secrets or External Secrets Operator.
- Audit logs enabled — at minimum Metadata level for secrets resources.
- Rotation configured — automatic rotation via Vault leases or an external scheduler.
- Network Policy — restrict network access to Vault and other secrets stores.
- ServiceAccount tokens with limited TTL — use Bound Service Account Tokens (not static ones).
- Disable default ServiceAccount token mounting — set
automountServiceAccountToken: falsein Deployments where the token is not needed. - Image scanning — scan Docker images for hardcoded secrets (truffleHog, gitleaks in CI/CD).
- Anomaly monitoring — alerts for unusual behavior when accessing secrets.
Conclusion
Secure secrets management in Kubernetes is a multi-layered challenge that requires choosing the right tools for the specific context. For small teams using a GitOps approach, Sealed Secrets addresses most risks. For enterprise infrastructure with compliance requirements, HashiCorp Vault provides full control: dynamic credentials, detailed auditing, and centralized policy management. External Secrets Operator flexibly bridges Kubernetes with cloud-based secrets stores.
Regardless of the tool chosen, the security foundation rests on three pillars: least privilege, auditing all access, and regular rotation. Implement these practices today — and your cluster will be ready for the security demands of 2026.
Technologies
Tags
Ruslan Ismailov
Senior Web / Backend Developer. Senior web/backend developer with 9 years of experience. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservices, CI/CD. More about me →