Kubernetes calls it a Secret. When I described one on Azure Kubernetes Service (AKS), kubectl showed only a size: 21 bytes. One command later, I had the value in plain text. No password, no key, nothing to break.
This explainer shows what a Kubernetes Secret really protects, who can read it, and what keeps it safe. Everything below comes from the Microsoft Learn exercise, run on my own cluster.
Key points
- A Secret is encoded, not encrypted. Kubernetes stores Secret values as base64. Base64 has no key: anyone allowed to read the Secret turns it back with one command. Listing Secrets returns their values too.
- Inside the pod it's plain text. The kubelet hands the decoded value to the container. An app can mask it in its own output, but anyone who may run
kubectl execon the pod reads the full value. - RBAC is the lock, not the pod's identity. The pod's own service account could not read a single Secret, and it doesn't need to: the kubelet fetches Secrets for the pods on its node. So whoever may create pods in a namespace can mount any Secret there.
- On AKS the disks are encrypted; each Secret inside etcd only with KMS. Azure Storage always encrypts the data at rest. Encrypting each Secret before it's written to etcd is an optional layer, off by default. The Azure Key Vault provider for the Secrets Store CSI Driver keeps the value out of the cluster altogether.
Encoded, not encrypted
A Secret is a Kubernetes object for sensitive values, like passwords and keys. It works like a ConfigMap, the object for plain settings, but it has its own type and its own permissions. The Microsoft Learn exercise writes one in a YAML manifest, with the values in plain text under stringData:
apiVersion: v1kind: Secretmetadata:name: api-secretstype: OpaquestringData:secret-endpoint: "SecretEndpointValue"secret-access-key: "SecretAccessKey123456"
These are the exercise's demo values, not a real key. A manifest like this is the first leak: it should never go into source control. Kubernetes merges stringData into the data field, and data holds base64.
The module checks a Secret with kubectl describe, which shows the keys and their sizes, not the values:
> kubectl describe secret api-secretsType: OpaqueData====secret-access-key: 21 bytessecret-endpoint: 19 bytes
That looks protected. It isn't. Asking for the raw data field returns the stored value:
> kubectl get secret api-secrets -o jsonpath="{.data.secret-access-key}"U2VjcmV0QWNjZXNzS2V5MTIzNDU2
Base64 is an encoding, not encryption. It turns any bytes into plain letters so they fit into YAML and JSON: every four letters stand for three bytes (U2Vj is Sec, cmV0 is ret, and so on). There is no key. kubectl's go-template output decodes it in the same command:
> kubectl get secret api-secrets -o go-template='{{index .data "secret-access-key" | base64decode}}'SecretAccessKey123456
A Secret is an envelope with a label, not a safe. kubectl describe just doesn't open it for you.
Plain text in the pod
In the exercise, a Deployment runs the app in a pod, and its template maps each key to an environment variable with secretKeyRef:
env:- name: SECRET_ACCESS_KEYvalueFrom:secretKeyRef:name: api-secretskey: secret-access-key
When the container starts, the kubelet, the Kubernetes agent on the node, fetches the Secret and hands the decoded value to the container. Like a ConfigMap read as environment variables, it's a copy taken at the start: a rotated Secret reaches the app only after a rollout.
The exercise's app is careful. Its /secrets endpoint masks the value:
> python cfg.py get /secretsGET /secrets -> 200… "secret_access_key":{…,"value":"***3456","length":21} …
But masking is something the app does. The container itself holds the real value:
> kubectl exec deploy/aks-config-api -- printenv SECRET_ACCESS_KEYSecretAccessKey123456
Inside the pod, a Secret is plain text. Anyone allowed to run kubectl exec on that pod can read it.
Who may read it
Kubernetes decides access with RBAC, role-based access control: which user or identity may get, list or watch which kind of object. list counts as reading: listing Secrets returns their values. kubectl auth can-i asks the cluster that question:
> kubectl auth can-i get secretsyes> kubectl auth can-i get secrets --as system:serviceaccount:default:defaultno
The first answer is mine. My kubeconfig came from az aks get-credentials, on a cluster without Microsoft Entra sign-in; there, that credential has the same effect as cluster admin. The second answer is the pod's: the exercise sets no service account, so its pods run as default, and that identity may not read a single Secret.
It doesn't need to. The pod never asks the API for its Secret; the kubelet does, and the cluster lets each kubelet read only the Secrets of pods bound to its own node. So the pod's identity isn't the lock. The lock is who may read Secrets, and who may get into the pods that use them.
| Who | Reads the value how |
|---|---|
Anyone with get, list or watch on Secrets | kubectl get secret … -o jsonpath + a base64 decode |
Anyone who may exec into the pod | printenv or the mounted file |
| Anyone who may create pods (or Deployments) in the namespace | a new pod that mounts the Secret |
| The pod's own service account (default) | no API access needed: the kubelet delivers it |
Encrypted where?
Kubernetes keeps every object, Secrets included, in etcd, its database. On AKS, etcd runs in the control plane, the part of the cluster that Microsoft runs for you. Is each Secret encrypted there by default? No, only if you turn it on.
- Azure platform encryption: Azure Storage always encrypts the data at rest. That protects the disk, not each Secret.
- KMS encryption: an optional layer that encrypts each Secret before it's written to etcd, with a key in Azure Key Vault (platform-managed or customer-managed keys).
My cluster's security profile showed that layer off, which is the default:
> az aks show -g rg-demo -n aks-demo --query securityProfile -o json{"azureKeyVaultKms": null,"customCaTrustCertificates": null,"defender": null,"imageCleaner": null,"workloadIdentity": null}
Even with KMS on, kubectl still gets the plain value: the API server decrypts it for anyone RBAC lets through. Encryption at rest protects the stored copy, not the API.
The stronger move keeps the value out of the cluster: the Azure Key Vault provider for the Secrets Store CSI Driver, an add-on that mounts secrets from Key Vault into pods. The value lives in Key Vault; when the pod starts, the driver fetches it with a managed identity and mounts it as a file. It never sits in etcd, unless you ask the driver to sync it into a Kubernetes Secret as well. Key Vault can log every read, and with rotation turned on (it's off by default), the driver checks for a new value every two minutes. Inside the pod it's still a plain file, so the rules above still apply.
The permission that reads every Secret
The kubelet hands a Secret to any pod that names it. So whoever can create a pod, or a Deployment, in a namespace can mount any Secret in it, and print it, without ever being allowed to read Secrets. In my run, the app's service account couldn't:
> kubectl auth can-i create pods --as system:serviceaccount:default:defaultno
When you hand out roles, treat create pods like read Secrets.
What happened when I ran it
Tested on October 28, 2026 in my own Azure subscription (AKS 1.36 in westeurope, Azure CLI 2.88.0, kubectl 1.36); everything was deleted afterwards. The cluster had one Standard_D2s_v5 node and no Microsoft Entra integration (local accounts), as in the exercise. Every command above ran in under a second, except the az aks show call (3.1 s). The checks on the default service account exited with code 1, which is how kubectl auth can-i says no. The Secret held the exercise's demo values, so the decoded output on this page is safe to show.
What the video leaves out
- Other Secret types:
kubernetes.io/dockerconfigjsonfor registry credentials andkubernetes.io/tlsfor certificates, andkubectl create secret generic --from-literal. - Azure App Configuration with Key Vault references: its Kubernetes provider resolves the references and generates Kubernetes Secrets, which then live in etcd again.
- Microsoft Entra integration and disabling local accounts, so cluster access goes through Entra ID instead of a cluster-admin certificate.
- Workload identity for the CSI driver instead of the add-on's managed identity, and immutable Secrets.
- Persistent storage, the third part of the module (a separate explainer).



