Learning Kubernetes - Episode 21.2 - Secret

Learning Kubernetes - Episode 21.2 - Secret

In this episode, we'll explore Kubernetes Secrets — the native mechanism for storing and consuming sensitive data like passwords, API keys, and TLS certificates. You'll learn how Secrets work, their types, security limitations, and how to use them safely in production.

Arman Dwi Pangestu
Arman Dwi PangestuMarch 27, 2026
0 views
10 min read

Introduction

Note

If you want to read the previous episode, you can click the Episode 21.1 thumbnail below

Episode 21.1Episode 21.1

In the previous episode, we deep-dived into PersistentVolume and PersistentVolumeClaim for durable storage. In this episode (21.2), we'll focus on Secrets — Kubernetes' native object for managing sensitive data such as database passwords, API tokens, TLS certificates, and SSH keys.

Note: Here I'll be using a Kubernetes Cluster installed through K3s.

Most real-world applications need credentials of some kind. Without a proper mechanism, developers end up hardcoding secrets in container images or environment variable lists, creating security liabilities. Kubernetes Secrets give you a structured, API-driven way to separate sensitive data from application code and configuration.

What Is a Secret?

A Secret is a Kubernetes API object that stores a small amount of sensitive data in key-value pairs. Secrets are stored in etcd and can be consumed by Pods as environment variables, files via volumes, or used by the kubelet to pull private container images.

Think of Secrets like a locked safe that only authorized people can open. The application doesn't carry its own password vault — it requests access to the vault at runtime.

Key characteristics of Secrets:

  • Namespace-scoped - A Secret belongs to one Namespace
  • Base64-encoded - Values are base64-encoded (not encrypted by default)
  • Multiple types - Generic, TLS, Docker registry credentials, service account tokens
  • Multiple consumption patterns - Environment variables, volume mounts, image pull secrets
  • RBAC-controlled - Access governed by Kubernetes RBAC
  • Encryption at rest - Optional, must be configured explicitly via EncryptionConfiguration

Warning

Kubernetes Secrets are base64-encoded by default, not encrypted. Anyone with kubectl get secret access or read access to etcd can decode them trivially. Always enable encryption at rest and use strict RBAC in production.

Secret vs ConfigMap

Both Secrets and ConfigMaps store key-value data. The difference is intent and handling:

FeatureSecretConfigMap
Intended forSensitive dataNon-sensitive config
Storage formatBase64-encodedPlain text
Encryption at restSupported (opt-in)Not typically encrypted
Mounted as tmpfsYes (in-memory)No
RBAC granularitysecrets resourceconfigmaps resource
Visible in kubectl describe podValues hiddenValues shown

Use ConfigMaps for application configuration. Use Secrets for anything that would be harmful if leaked: passwords, tokens, certificates, private keys.

Secret Types

Kubernetes supports several built-in Secret types, each designed for a specific use case.

Opaque (Generic)

The default type for arbitrary user-defined data.

Kubernetessecret-opaque.yml
apiVersion: v1
kind: Secret
metadata:
    name: app-credentials
    namespace: default
type: Opaque
data:
    username: YWRtaW4=        # base64 of "admin"
    password: c2VjcmV0cGFzcw== # base64 of "secretpass"

Or use stringData to provide values in plain text (Kubernetes encodes them automatically):

Kubernetessecret-opaque-stringdata.yml
apiVersion: v1
kind: Secret
metadata:
    name: app-credentials
type: Opaque
stringData:
    username: admin
    password: secretpass

Tip

Prefer stringData in your manifests — it's more readable and avoids manual base64 encoding mistakes. Kubernetes will automatically convert the values to base64 when storing them.

kubernetes.io/tls

Used for TLS certificates and private keys. Consumed by Ingress controllers and other TLS-aware components.

Kubernetessecret-tls.yml
apiVersion: v1
kind: Secret
metadata:
    name: tls-secret
    namespace: default
type: kubernetes.io/tls
data:
    tls.crt: <base64-encoded-cert>
    tls.key: <base64-encoded-private-key>

Creating TLS secrets from files is more common:

KubernetesCreate TLS Secret from Files
sudo kubectl create secret tls tls-secret \
    --cert=path/to/tls.crt \
    --key=path/to/tls.key

kubernetes.io/dockerconfigjson

Used for authenticating to private container image registries.

KubernetesCreate Docker Registry Secret
sudo kubectl create secret docker-registry regcred \
    --docker-server=registry.example.com \
    --docker-username=myuser \
    --docker-password=mypassword \
    --docker-email=me@example.com

Reference it in a Pod spec:

Kubernetespod-with-imagepullsecret.yml
apiVersion: v1
kind: Pod
metadata:
    name: private-image-pod
spec:
    imagePullSecrets:
        - name: regcred
    containers:
        - name: app
          image: registry.example.com/myapp:latest

kubernetes.io/service-account-token

Automatically created by Kubernetes for ServiceAccounts. Used by Pods to authenticate to the API server. Modern clusters use projected service account tokens instead.

Summary of Built-in Secret Types

TypeUse Case
OpaqueGeneric key-value secrets
kubernetes.io/tlsTLS certificates and keys
kubernetes.io/dockerconfigjsonRegistry authentication
kubernetes.io/basic-authUsername/password basic auth
kubernetes.io/ssh-authSSH private keys
kubernetes.io/service-account-tokenServiceAccount tokens
bootstrap.kubernetes.io/tokenNode bootstrap tokens

Creating Secrets

Using kubectl (Imperative)

# From literal values
sudo kubectl create secret generic db-credentials \
    --from-literal=username=admin \
    --from-literal=password=supersecret
 
# From files
sudo kubectl create secret generic app-certs \
    --from-file=ca.crt \
    --from-file=server.crt \
    --from-file=server.key

Using YAML Manifests (Declarative)

Kubernetesdb-credentials.yml
apiVersion: v1
kind: Secret
metadata:
    name: db-credentials
    namespace: production
    labels:
        app: myapp
        env: production
type: Opaque
stringData:
    DB_HOST: postgres.production.svc.cluster.local
    DB_PORT: "5432"
    DB_NAME: myappdb
    DB_USER: appuser
    DB_PASSWORD: Sup3rS3cur3P@ssw0rd!

Caution

Never commit Secret manifests with real values to version control. Your git history is permanent. Use sealed-secrets, External Secrets Operator, or Vault to manage secrets as code safely. More on this in the best practices section.

Consuming Secrets in Pods

Secrets can be consumed in two ways: environment variables or volume mounts. Each has distinct trade-offs.

As Environment Variables

The simplest pattern — directly inject secret values as container environment variables.

Inject a Specific Key

Kubernetespod-env-from-secret.yml
apiVersion: v1
kind: Pod
metadata:
    name: app-pod
spec:
    containers:
        - name: app
          image: myapp:latest
          env:
              - name: DB_PASSWORD
                valueFrom:
                    secretKeyRef:
                        name: db-credentials
                        key: DB_PASSWORD
              - name: DB_USER
                valueFrom:
                    secretKeyRef:
                        name: db-credentials
                        key: DB_USER

Inject All Keys from a Secret

Kubernetespod-envfrom-secret.yml
apiVersion: v1
kind: Pod
metadata:
    name: app-pod
spec:
    containers:
        - name: app
          image: myapp:latest
          envFrom:
              - secretRef:
                    name: db-credentials

This mounts every key in db-credentials as an environment variable. The key names become the variable names.

Warning

Environment variables are visible in process listings (/proc/<pid>/environ) and may be logged by third-party libraries or crash reports. For maximum security, prefer volume mounts for sensitive credentials.

As Volume Mounts (Files)

Secrets mounted as volumes are stored on tmpfs (in-memory filesystem), which is safer than disk. Each key becomes a file inside the mount directory.

Kubernetespod-secret-volume.yml
apiVersion: v1
kind: Pod
metadata:
    name: app-pod
spec:
    containers:
        - name: app
          image: myapp:latest
          volumeMounts:
              - name: credentials
                mountPath: /etc/secrets
                readOnly: true
    volumes:
        - name: credentials
          secret:
              secretName: db-credentials
              defaultMode: 0400  # owner read-only

Inside the container, the files will be:

bash
/etc/secrets/DB_HOST
/etc/secrets/DB_PORT
/etc/secrets/DB_NAME
/etc/secrets/DB_USER
/etc/secrets/DB_PASSWORD

Mount Specific Keys Only

Kubernetespod-secret-volume-items.yml
volumes:
    - name: credentials
      secret:
          secretName: db-credentials
          items:
              - key: DB_PASSWORD
                path: password.txt
                mode: 0400
              - key: DB_USER
                path: username.txt
                mode: 0444

This mounts only the specified keys, with custom file names and permissions.

Comparing Env Vars vs Volume Mounts

AspectEnvironment VariableVolume Mount
Visibility in processcat /proc/self/environNot exposed
StorageMemory (env)tmpfs (in-memory)
Dynamic updatesRequires Pod restartUpdated without restart
File permissionsN/AConfigurable (mode)
Audit trailHarder to auditEasier (file access logging)
Best forSimple apps, legacy compatProduction, security-sensitive

Practical Implementation

Example 1: Database Application with Secret

A typical pattern: inject database credentials securely into a web application.

apiVersion: v1
kind: Secret
metadata:
    name: postgres-credentials
    namespace: production
type: Opaque
stringData:
    POSTGRES_USER: appuser
    POSTGRES_PASSWORD: Sup3rS3cur3P@ss!
    POSTGRES_DB: myappdb

Example 2: TLS Termination with Ingress

Kubernetesingress-tls.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
    name: app-ingress
    namespace: production
    annotations:
        nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
    tls:
        - hosts:
              - myapp.example.com
          secretName: tls-secret
    rules:
        - host: myapp.example.com
          http:
              paths:
                  - path: /
                    pathType: Prefix
                    backend:
                        service:
                            name: web-app
                            port:
                                number: 8080

Example 3: Private Registry Pull Secret

Kubernetesserviceaccount-with-pull-secret.yml
apiVersion: v1
kind: ServiceAccount
metadata:
    name: app-service-account
    namespace: production
imagePullSecrets:
    - name: regcred

Attaching the pull secret to the ServiceAccount means all Pods using this ServiceAccount automatically get access to the private registry — no need to specify imagePullSecrets on every Pod.

Example 4: Updating a Secret

KubernetesUpdate Secret Value
# Method 1: Edit directly
sudo kubectl edit secret db-credentials -n production
 
# Method 2: Apply updated manifest
sudo kubectl apply -f db-credentials.yml
 
# Method 3: Patch a specific key (base64 encode first)
NEW_PASS=$(echo -n "newpassword" | base64)
sudo kubectl patch secret db-credentials \
    -p "{\"data\":{\"DB_PASSWORD\":\"${NEW_PASS}\"}}"

Note

Pods consuming Secrets as volume mounts will see the updated secret within ~1 minute (kubelet sync period) without restart. Pods consuming Secrets as environment variables require a restart to pick up the new values.

Secret Encryption at Rest

By default, Kubernetes stores Secrets in etcd as base64-encoded plain text. Anyone with etcd access can read them. To enable true encryption at rest, configure an EncryptionConfiguration.

Kubernetesencryption-config.yml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
    - resources:
          - secrets
      providers:
          - aescbc:
                keys:
                    - name: key1
                      secret: <base64-encoded-32-byte-key>
          - identity: {}

Apply this by passing --encryption-provider-config flag to the API server. After enabling, encrypt existing secrets:

KubernetesEncrypt Existing Secrets
# Re-write all existing secrets to trigger encryption
sudo kubectl get secrets --all-namespaces -o json | \
    sudo kubectl replace -f -

Important

Managed Kubernetes services (GKE, EKS, AKS) typically provide encryption at rest for Secrets by default using their cloud KMS. Always verify this is enabled in your cloud provider's documentation.

Common Mistakes and Pitfalls

Mistake 1: Storing Secrets in Git

Problem: Committing Secret YAML files with real values to version control. The values persist in git history even after deletion.

Solution: Use one of these approaches:

  • Sealed Secrets (Bitnami) — encrypt secrets client-side, safe to commit
  • External Secrets Operator — sync secrets from Vault, AWS SSM, GCP Secret Manager
  • SOPS — encrypt YAML/JSON files using age or KMS
KubernetesNever Do This
# DO NOT commit files containing real secrets
git add db-credentials.yml   # ❌
git commit -m "add database secret"

Mistake 2: Overly Permissive RBAC

Problem: Granting broad get secrets access exposes all secrets in the Namespace.

Solution: Follow least privilege. Grant access only to specific secrets:

Kubernetesrbac-secret-specific.yml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
    name: db-secret-reader
    namespace: production
rules:
    - apiGroups: [""]
      resources: ["secrets"]
      resourceNames: ["db-credentials"]  # specific secret only
      verbs: ["get"]

Mistake 3: Using Environment Variables for Highly Sensitive Data

Problem: Environment variables are visible to all processes in the container and may be logged.

Solution: Mount sensitive data as volume files with restrictive permissions:

Kubernetesyml
# Less secure: env var (visible in /proc)
env:
    - name: PRIVATE_KEY
      valueFrom:
          secretKeyRef:
              name: app-secret
              key: private_key
 
# More secure: volume mount (tmpfs, file permissions)
volumeMounts:
    - name: app-secret
      mountPath: /run/secrets
      readOnly: true

Mistake 4: Not Setting File Permissions

Problem: Secret files mounted in volumes are readable by all users in the container.

Solution: Set restrictive defaultMode and per-item mode:

Kubernetesyml
volumes:
    - name: credentials
      secret:
          secretName: db-credentials
          defaultMode: 0400  # owner read-only

Mistake 5: Not Rotating Secrets

Problem: Static, long-lived credentials are a liability. A leaked credential remains valid indefinitely.

Solution: Implement secret rotation. Use short-lived tokens where possible (e.g., Vault dynamic secrets, AWS IAM roles for service accounts).

Best Practices

Use External Secret Management in Production

Don't manage secrets natively in Kubernetes at scale. Use an external secrets backend:

Kubernetesexternal-secret.yml
# External Secrets Operator syncing from AWS SSM
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
    name: db-credentials
    namespace: production
spec:
    refreshInterval: 1h
    secretStoreRef:
        name: aws-secretsmanager
        kind: ClusterSecretStore
    target:
        name: db-credentials
        creationPolicy: Owner
    data:
        - secretKey: DB_PASSWORD
          remoteRef:
              key: /production/myapp/db-password

Enable Encryption at Rest

Always enable EncryptionConfiguration pointing to a KMS provider (not just aescbc with a static key) in production.

Apply Strict RBAC

Restrict who can list and get secrets:

Kubernetesrestrict-secret-access.yml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
    name: secret-reader-restricted
rules:
    - apiGroups: [""]
      resources: ["secrets"]
      verbs: ["get"]
      # No "list" verb — prevents bulk enumeration

Tip

Removing the list verb prevents users from enumerating all Secret names in a Namespace — a useful defense-in-depth measure even if they can get specific secrets by name.

Mount Secrets as Read-Only

Always add readOnly: true to Secret volume mounts:

Kubernetesyml
volumeMounts:
    - name: credentials
      mountPath: /etc/secrets
      readOnly: true

Audit Secret Access

Enable Kubernetes audit logging with a policy that captures get and watch operations on Secrets:

Kubernetesaudit-policy.yml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
    - level: Metadata
      resources:
          - group: ""
            resources: ["secrets"]

Use Immutable Secrets

Mark Secrets as immutable to prevent accidental modification and reduce API server load (Kubernetes stops watching for changes):

Kubernetesyml
apiVersion: v1
kind: Secret
metadata:
    name: app-credentials
type: Opaque
immutable: true
stringData:
    API_KEY: my-api-key-value

When NOT to Use Kubernetes Secrets

Kubernetes Secrets are convenient but not always the right tool:

  • Large binary payloads — Secrets have a 1 MiB size limit. Use object storage (S3, GCS) for large files.
  • Secrets shared across many clusters — Managing the same secret per-cluster is operationally expensive. Use a centralized vault (HashiCorp Vault, AWS Secrets Manager) as the single source of truth.
  • Secrets requiring dynamic generation — If credentials need to be generated on-demand per-pod (e.g., short-lived database credentials), Vault's dynamic secrets are purpose-built for this.
  • Compliance-regulated environments — Some compliance frameworks (PCI-DSS, HIPAA) require dedicated secrets management hardware or services. Kubernetes Secrets alone may not satisfy the audit requirements.

Note

Kubernetes Secrets are fine for simple deployments and development. For production at scale, always layer them with proper encryption, external secrets management, and strict RBAC.

Conclusion

In episode 21.2, we've covered Kubernetes Secrets comprehensively — from what they are and their types, to how to create and consume them, to securing them properly in production.

Key takeaways:

  • Secrets store sensitive data as base64-encoded key-value pairs in etcd
  • Base64 is encoding, not encryption — always enable encryption at rest
  • Common Secret types: Opaque, kubernetes.io/tls, kubernetes.io/dockerconfigjson
  • Secrets can be consumed as environment variables (simpler) or volume mounts (more secure)
  • Volume-mounted secrets are stored on tmpfs (in-memory), more secure than env vars
  • Volume mounts update dynamically (~1 min); env vars require Pod restart
  • Never commit Secret manifests with real values to version control
  • Use RBAC to restrict who can access which secrets — avoid broad list permissions
  • Use External Secrets Operator, Sealed Secrets, or HashiCorp Vault for production-grade secrets management
  • Set immutable: true for secrets that don't change to reduce API server overhead
  • Enable Kubernetes audit logging to track Secret access

Proper secrets management is one of the most security-critical aspects of running Kubernetes in production. Getting it right from the start saves painful security incidents later.

Are you getting a clearer picture of how Kubernetes manages sensitive data? Keep the momentum going and look forward to the next episode!

Note

If you want to continue to the next episode, you can click the Episode 22 thumbnail below

Episode 22Episode 22

Related Posts