Kubernetes Entry Created: 10 Mar 2026 Updated: 10 Mar 2026

Kubernetes Secret

In the previous article we learned how ConfigMaps store non-sensitive configuration data. But what about passwords, API keys, TLS certificates, and database connection strings? Storing these values in a ConfigMap is a bad idea — ConfigMap data is not encrypted, not access-controlled by default, and appears in plain text whenever you run kubectl get configmap -o yaml.

Kubernetes provides a dedicated object for sensitive data called a Secret. Secrets are designed so that container images can be built without bundling sensitive data inside them. This keeps containers portable across environments — the same image runs in development, staging, and production, and only the Secrets differ.

Secrets are exposed to Pods through explicit declarations in Pod manifests and the Kubernetes API. They leverage native OS isolation primitives: Secret volumes are backed by tmpfs (RAM disks), which means Secret data is never written to a node's physical disk.

Core Concepts

How Secrets Differ from ConfigMaps

Secrets and ConfigMaps look similar on the surface — both are key-value stores consumed by Pods. The differences become important when security matters:

FeatureConfigMapSecret
Intended dataNon-sensitive configurationPasswords, tokens, keys, certificates
Data encodingPlain UTF-8 textBase64-encoded (supports binary data)
Volume backingRegular filesystemtmpfs (RAM disk) — never written to node disk
Size limit1 MB1 MB
RBACCluster-wide read by defaultCan be restricted with fine-grained RBAC rules
Encryption at restNot typically encryptedSupports EncryptionConfiguration for etcd encryption

Secret Types

Kubernetes defines several built-in Secret types. The type tells Kubernetes (and human readers) what kind of data the Secret holds:

TypeDescription
OpaqueThe default type. Can hold any arbitrary key-value data.
kubernetes.io/tlsHolds a TLS certificate and private key. Requires tls.crt and tls.key data keys.
kubernetes.io/dockerconfigjsonStores credentials for private container registries.
kubernetes.io/basic-authStores a username and password.
kubernetes.io/ssh-authStores an SSH private key.

The most common types you will use day-to-day are Opaque (for database passwords, API keys, tokens) and kubernetes.io/tls (for HTTPS certificates).

Base64 Encoding — Not Encryption

A common source of confusion: Secret values stored in the data field are base64-encoded, not encrypted. Base64 is a reversible encoding scheme — anyone who can read the Secret can decode the values instantly:

echo "YmlsbGluZ19zdmM=" | base64 --decode
# Output: billing_svc

Base64 exists so that Secrets can hold binary data (like TLS certificates) inside YAML, which is a text format. It provides zero security on its own.

If you want to write values in plain text when creating a Secret manifest, use the stringData field instead of data. Kubernetes will base64-encode them automatically when the Secret is stored:

apiVersion: v1
kind: Secret
metadata:
name: example-stringdata
type: Opaque
stringData:
DB_USER: billing_svc
DB_PASSWORD: "X9k!vLp2@dQm7!"

Security Warning: Default Storage

By default, Kubernetes stores Secrets in plain text in etcd — the cluster's key-value store. This means anyone with cluster administration rights can read every Secret. For production clusters, you should:

  1. Enable encryption at rest using an EncryptionConfiguration with a user-supplied key (often integrated with a cloud KMS).
  2. Use RBAC rules to restrict who can read Secrets.
  3. Consider the Secrets Store CSI Driver to pull secrets directly from your cloud provider's key vault (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault) instead of storing them in etcd at all.

Naming Constraints

Secret and ConfigMap key names must be valid environment variable names. The rules are:

  1. May begin with a dot (.), then a letter or digit.
  2. Followed by letters, digits, dots, dashes (-), or underscores (_).
  3. Dots cannot repeat. Dots cannot be adjacent to dashes or underscores.

Some examples:

Valid Key NameInvalid Key Name
.auth_tokenToken..properties
key.pemauth file.json
config_file_password.txt

When choosing key names, remember that they become file names in volume mounts. A key named key.pem is much clearer than tls-key when configuring applications to read certificate files.

Hands-On: Kubernetes Commands

Creating Secrets Imperatively

The kubectl create secret command supports several sub-commands depending on the Secret type.

Generic (Opaque) Secrets

Use generic to create an Opaque Secret from literals or files:

kubectl create secret generic my-db-secret \
--from-literal=DB_USER=admin \
--from-literal=DB_PASSWORD='s3cr3t!Pass'

You can also load values from files. Each file's name becomes the key, and its content becomes the value:

kubectl create secret generic my-tls-secret \
--from-file=server.crt \
--from-file=server.key

TLS Secrets

Kubernetes provides a dedicated sub-command for TLS Secrets that validates the certificate and key:

kubectl create secret tls my-app-tls \
--cert=server.crt \
--key=server.key

This creates a Secret of type kubernetes.io/tls with keys tls.crt and tls.key.

Docker Registry Secrets

To store credentials for pulling images from a private container registry:

kubectl create secret docker-registry my-registry-secret \
--docker-server=myregistry.azurecr.io \
--docker-username=myuser \
--docker-password='myP@ssw0rd' \
--docker-email=dev@example.com

Viewing Secrets

To list all Secrets in the current Namespace:

kubectl get secrets

To view the metadata (without revealing actual values):

kubectl describe secret my-db-secret

This shows key names and byte sizes but not the values — a deliberate safety measure. To see the base64-encoded values:

kubectl get secret my-db-secret -o yaml

To decode a specific value on the command line:

kubectl get secret my-db-secret -o jsonpath='{.data.DB_PASSWORD}' | base64 --decode

Editing a Secret

kubectl edit secret my-db-secret

This opens the Secret in your editor. Values are shown base64-encoded. Remember to encode any new values before saving:

echo -n "newPassword123" | base64
# Output: bmV3UGFzc3dvcmQxMjM=

Deleting a Secret

kubectl delete secret my-db-secret

Like ConfigMaps, deleting a Secret that is referenced by a running Pod will cause that Pod to fail if it restarts.

Step-by-Step Example

In this walkthrough, we build a realistic scenario: a .NET Billing API that needs database credentials as environment variables and TLS certificates as files. We will create two Secrets and a Deployment that consumes both.

Step 1: Create the Database Credentials Secret

This Opaque Secret holds the database connection details for our Billing API. Save this as billing-api-db-credentials.yaml:

apiVersion: v1
kind: Secret
metadata:
name: billing-api-db-credentials
type: Opaque
data:
DB_HOST: cG9zdGdyZXMuaW50ZXJuYWwubG9jYWw=
DB_PORT: NTQzMg==
DB_NAME: YmlsbGluZ19kYg==
DB_USER: YmlsbGluZ19zdmM=
DB_PASSWORD: WDlrIXZMcDJAZFFtNyE=

Every value in the data field is base64-encoded. Here is what each one decodes to:

KeyBase64 ValueDecoded Value
DB_HOSTcG9zdGdyZXMuaW50ZXJuYWwubG9jYWw=postgres.internal.local
DB_PORTNTQzMg==5432
DB_NAMEYmlsbGluZ19kYg==billing_db
DB_USERYmlsbGluZ19zdmM=billing_svc
DB_PASSWORDWDlrIXZMcDJAZFFtNyE=X9k!vLp2@dQm7!

You can generate base64 values on the command line:

echo -n "postgres.internal.local" | base64
# Output: cG9zdGdyZXMuaW50ZXJuYWwubG9jYWw=

The -n flag is critical — without it, echo appends a newline character, which gets encoded into the value and will cause your application to fail with cryptic connection errors.

Apply the Secret:

kubectl apply -f billing-api-db-credentials.yaml

Step 2: Create the TLS Secret

This Secret stores the HTTPS certificate and private key. In production, your certificates come from a Certificate Authority (like Let's Encrypt). For this example, we define the Secret type as kubernetes.io/tls. Save this as billing-api-tls.yaml:

apiVersion: v1
kind: Secret
metadata:
name: billing-api-tls
type: kubernetes.io/tls
data:
tls.crt: LS0tLS1CRUdJTi... (base64-encoded certificate)
tls.key: LS0tLS1CRUdJTi... (base64-encoded private key)

In real-world usage, you would replace the placeholder values with the actual base64-encoded contents of your certificate and key files. The easiest way to create a TLS Secret is imperatively:

kubectl create secret tls billing-api-tls \
--cert=billing-api.crt \
--key=billing-api.key

Kubernetes validates that tls.crt and tls.key are present and that the certificate and key are properly formatted.

Step 3: Create the Deployment That Consumes Both Secrets

This Deployment injects database credentials as environment variables and mounts TLS certificates as files. Save this as billing-api-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
name: billing-api
labels:
app: billing-api
spec:
replicas: 2
selector:
matchLabels:
app: billing-api
template:
metadata:
labels:
app: billing-api
spec:
containers:
- name: billing-api
image: mcr.microsoft.com/dotnet/aspnet:10.0
ports:
- containerPort: 8080
- containerPort: 8443
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
- name: ConnectionStrings__BillingDb
valueFrom:
secretKeyRef:
name: billing-api-db-credentials
key: DB_PASSWORD
- name: Database__Host
valueFrom:
secretKeyRef:
name: billing-api-db-credentials
key: DB_HOST
- name: Database__Port
valueFrom:
secretKeyRef:
name: billing-api-db-credentials
key: DB_PORT
- name: Database__Name
valueFrom:
secretKeyRef:
name: billing-api-db-credentials
key: DB_NAME
- name: Database__User
valueFrom:
secretKeyRef:
name: billing-api-db-credentials
key: DB_USER
volumeMounts:
- name: tls-certs
mountPath: /app/certs
readOnly: true
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
volumes:
- name: tls-certs
secret:
secretName: billing-api-tls

Apply the Deployment:

kubectl apply -f billing-api-deployment.yaml

Step 4: Understanding the Environment Variable Method

The Deployment uses secretKeyRef to inject individual keys from the Secret as environment variables:

env:
- name: Database__Host # Env var name the .NET app reads
valueFrom:
secretKeyRef:
name: billing-api-db-credentials # Which Secret
key: DB_HOST # Which key in that Secret

This works exactly like configMapKeyRef for ConfigMaps, but references a Secret instead. In .NET, the double underscore (__) in Database__Host maps to the configuration path Database:Host in IConfiguration.

You can also inject all keys at once with envFrom:

envFrom:
- secretRef:
name: billing-api-db-credentials

This creates environment variables DB_HOST, DB_PORT, DB_NAME, DB_USER, and DB_PASSWORD — one for each key in the Secret.

Step 5: Understanding the Volume Mount Method

The TLS certificates are mounted as files using a Secret volume:

volumeMounts:
- name: tls-certs
mountPath: /app/certs # Directory where cert files appear
readOnly: true

volumes:
- name: tls-certs
secret:
secretName: billing-api-tls

After the Pod starts, the container will see:

/app/certs/tls.crt
/app/certs/tls.key

These files are stored on a tmpfs volume (RAM disk). They are never written to the node's physical disk, which reduces the risk of credential leakage even if someone gains access to the node's storage.

Verify the mounted files by exec-ing into a running Pod:

kubectl exec -it deploy/billing-api -- ls -la /app/certs/

Step 6: Verify the Environment Variables

Confirm that Secret values were injected into the container's environment:

kubectl exec -it deploy/billing-api -- printenv | grep -E "Database__|DB_"

You should see the decoded (plain text) values — Kubernetes automatically decodes base64 when injecting Secret data into environment variables or volume files.

Step 7: Private Container Registry Secrets

A special use case for Secrets is storing credentials for private container registries. When your Pod needs to pull an image from a private registry (like Azure Container Registry or a private Docker Hub repo), Kubernetes needs credentials to authenticate.

Create a registry Secret:

kubectl create secret docker-registry acr-pull-secret \
--docker-server=myregistry.azurecr.io \
--docker-username=myuser \
--docker-password='myP@ssw0rd' \
--docker-email=dev@example.com

Then reference it in any Pod that pulls from that registry using spec.imagePullSecrets:

apiVersion: v1
kind: Pod
metadata:
name: private-app
spec:
containers:
- name: private-app
image: myregistry.azurecr.io/billing-api:1.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
imagePullSecrets:
- name: acr-pull-secret

imagePullSecrets tells the kubelet to use the specified Secret when authenticating with the container registry. Without it, the Pod will enter an ImagePullBackOff state because the registry will reject the unauthenticated pull request.

If every Pod in a Namespace uses the same registry, you can attach the Secret to the default ServiceAccount so you do not have to repeat it in every Pod manifest:

kubectl patch serviceaccount default \
-p '{"imagePullSecrets": [{"name": "acr-pull-secret"}]}'

Step 8: Clean Up

Remove all the resources we created:

kubectl delete deployment billing-api
kubectl delete secret billing-api-db-credentials
kubectl delete secret billing-api-tls

Or delete from the manifest files:

kubectl delete -f billing-api-deployment.yaml
kubectl delete -f billing-api-db-credentials.yaml
kubectl delete -f billing-api-tls.yaml

Summary

Secrets provide a Kubernetes-native mechanism for storing and distributing sensitive configuration data. Here is what we covered:

  1. A Secret stores sensitive data (passwords, tokens, certificates) as base64-encoded key-value pairs. It is not the same as encryption — base64 is reversible.
  2. Kubernetes offers several Secret types: Opaque for general data, kubernetes.io/tls for certificates, and kubernetes.io/dockerconfigjson for registry credentials.
  3. You can create Secrets imperatively with kubectl create secret (generic, tls, or docker-registry) or declaratively from a YAML manifest using data (base64) or stringData (plain text).
  4. Secrets are consumed by Pods in two main ways:
  5. Environment variables — using secretKeyRef for individual keys or envFrom with secretRef for all keys.
  6. Volume mounts — mounting the Secret as a directory where each key becomes a file on a tmpfs RAM disk.
  7. Image pull Secrets authenticate with private container registries. They are referenced via spec.imagePullSecrets or attached to the default ServiceAccount.
  8. By default, Secrets are stored in plain text in etcd. For production, enable encryption at rest, apply RBAC restrictions, and consider using the Secrets Store CSI Driver to integrate with external key vaults.
  9. Key names in both Secrets and ConfigMaps must follow strict naming rules — no spaces, no repeated dots, no leading underscores. Choose names that make sense as file paths (e.g., key.pem over tls-key).
Share this lesson: