• English icon
  • German icon
Erklärvideo

Kubernetes-Secrets auf AKS: base64, RBAC und Azure Key Vault

· Reihe: Container auf Azure

azureakskubernetessecretssecuritykubectl

Das Video ist auf Englisch.

Kubernetes nennt es ein Secret. Als ich eines auf Azure Kubernetes Service (AKS) mit kubectl describe angesehen habe, zeigte es nur eine Größe: 21 Bytes. Einen Befehl später hatte ich den Wert im Klartext. Kein Passwort, kein Schlüssel, nichts zu knacken.

Dieser Beitrag zeigt, was ein Kubernetes-Secret wirklich schützt, wer es lesen kann und was es sicher macht. Alles stammt aus der Übung des Microsoft-Learn-Moduls, ausgeführt in meinem eigenen Cluster.

Das Wichtigste

  • Ein Secret ist kodiert, nicht verschlüsselt. Kubernetes speichert die Werte eines Secrets als Base64. Base64 hat keinen Schlüssel: Wer das Secret lesen darf, dreht es mit einem Befehl zurück. Auch das Auflisten von Secrets liefert ihre Werte.
  • Im Pod steht es im Klartext. Das Kubelet übergibt den dekodierten Wert an den Container. Eine App kann ihn in ihrer eigenen Ausgabe maskieren, aber wer kubectl exec auf dem Pod ausführen darf, liest den vollen Wert.
  • RBAC ist das Schloss, nicht die Identität des Pods. Der eigene Service Account des Pods durfte kein einziges Secret lesen, und das muss er auch nicht: Das Kubelet holt die Secrets für die Pods auf seinem Node. Deshalb kann jeder, der in einem Namespace Pods anlegen darf, jedes Secret dort einhängen.
  • Auf AKS sind die Festplatten verschlüsselt, jedes einzelne Secret in etcd nur mit KMS. Azure Storage verschlüsselt ruhende Daten immer. Jedes Secret vor dem Schreiben in etcd zu verschlüsseln ist eine optionale Schicht, standardmäßig aus. Der Azure-Key-Vault-Provider für den Secrets Store CSI Driver hält den Wert ganz aus dem Cluster heraus.

Kodiert, nicht verschlüsselt

Ein Secret ist ein Kubernetes-Objekt für sensible Werte wie Passwörter und Schlüssel. Es funktioniert wie eine ConfigMap, das Objekt für normale Einstellungen, hat aber einen eigenen Typ und eigene Berechtigungen. Die Übung im Microsoft-Learn-Modul schreibt eines in ein YAML-Manifest, mit den Werten im Klartext unter stringData:

apiVersion: v1
kind: Secret
metadata:
name: api-secrets
type: Opaque
stringData:
secret-endpoint: "SecretEndpointValue"
secret-access-key: "SecretAccessKey123456"

Das sind die Demo-Werte der Übung, kein echter Schlüssel. Ein solches Manifest ist das erste Leck: Es gehört nie in die Versionskontrolle. Kubernetes übernimmt stringData in das Feld data, und data enthält Base64.

Das Modul prüft ein Secret mit kubectl describe. Das zeigt die Schlüssel und ihre Größe, nicht die Werte:

> kubectl describe secret api-secrets
Type: Opaque
Data
====
secret-access-key: 21 bytes
secret-endpoint: 19 bytes

Das sieht geschützt aus. Ist es nicht. Fragt man nach dem rohen Feld data, kommt der gespeicherte Wert zurück:

> kubectl get secret api-secrets -o jsonpath="{.data.secret-access-key}"
U2VjcmV0QWNjZXNzS2V5MTIzNDU2

Base64 ist eine Kodierung, keine Verschlüsselung. Es macht aus beliebigen Bytes einfache Buchstaben, damit sie in YAML und JSON passen: Je vier Buchstaben stehen für drei Bytes (U2Vj ist Sec, cmV0 ist ret und so weiter). Einen Schlüssel gibt es nicht. Die Go-Template-Ausgabe von kubectl dekodiert den Wert im selben Befehl:

> kubectl get secret api-secrets -o go-template='{{index .data "secret-access-key" | base64decode}}'
SecretAccessKey123456

Ein Secret ist ein beschrifteter Umschlag, kein Tresor. kubectl describe öffnet ihn nur nicht für Sie.

Klartext im Pod

In der Übung startet ein Deployment die App in einem Pod. Sein Template ordnet jeden Schlüssel mit secretKeyRef einer Umgebungsvariablen zu:

env:
- name: SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: api-secrets
key: secret-access-key

Wenn der Container startet, holt das Kubelet, der Kubernetes-Agent auf dem Node, das Secret und übergibt den dekodierten Wert an den Container. Wie bei einer ConfigMap, die als Umgebungsvariablen gelesen wird, ist das eine Kopie vom Start: Ein rotiertes Secret erreicht die App erst nach einem Rollout.

Die App der Übung geht vorsichtig damit um. Ihr Endpunkt /secrets maskiert den Wert:

> python cfg.py get /secrets
GET /secrets -> 200
… "secret_access_key":{…,"value":"***3456","length":21} …

Aber das Maskieren macht die App. Der Container selbst hält den echten Wert:

> kubectl exec deploy/aks-config-api -- printenv SECRET_ACCESS_KEY
SecretAccessKey123456

Im Pod ist ein Secret Klartext. Wer kubectl exec auf diesem Pod ausführen darf, kann es lesen.

Wer es lesen darf

Kubernetes regelt den Zugriff mit RBAC, der rollenbasierten Zugriffssteuerung: welcher Benutzer oder welche Identität welche Art von Objekt mit get, list oder watch abfragen darf. list zählt als Lesen: Das Auflisten von Secrets liefert ihre Werte. kubectl auth can-i stellt dem Cluster genau diese Frage:

> kubectl auth can-i get secrets
yes
> kubectl auth can-i get secrets --as system:serviceaccount:default:default
no

Die erste Antwort gilt mir. Meine kubeconfig kam von az aks get-credentials, auf einem Cluster ohne Anmeldung über Microsoft Entra; dort wirkt diese Anmeldeinformation wie ein Cluster-Admin. Die zweite Antwort gilt dem Pod: Die Übung setzt keinen Service Account, also laufen ihre Pods als default, und diese Identität darf kein einziges Secret lesen.

Das muss sie auch nicht. Der Pod fragt die API nie nach seinem Secret; das Kubelet tut es, und der Cluster lässt jedes Kubelet nur die Secrets der Pods auf seinem eigenen Node lesen. Die Identität des Pods ist also nicht das Schloss. Das Schloss ist, wer Secrets lesen darf und wer in die Pods kommt, die sie nutzen.

WerLiest den Wert wie
Wer get, list oder watch auf Secrets hatkubectl get secret … -o jsonpath + Base64-Dekodierung
Wer exec in den Pod ausführen darfprintenv oder die eingehängte Datei
Wer im Namespace Pods (oder Deployments) anlegen darfein neuer Pod, der das Secret einhängt
Der eigene Service Account des Pods (default)braucht keinen API-Zugriff: Das Kubelet liefert es

Wo verschlüsselt?

Kubernetes speichert jedes Objekt, auch Secrets, in etcd, seiner Datenbank. Auf AKS läuft etcd in der Control Plane, dem Teil des Clusters, den Microsoft für Sie betreibt. Ist dort jedes Secret standardmäßig verschlüsselt? Nein, nur wenn Sie es einschalten.

  • Azure-Plattformverschlüsselung: Azure Storage verschlüsselt ruhende Daten immer. Das schützt die Festplatte, nicht jedes einzelne Secret.
  • KMS-Verschlüsselung: eine optionale Schicht, die jedes Secret vor dem Schreiben in etcd verschlüsselt, mit einem Schlüssel in Azure Key Vault (von der Plattform verwaltete oder eigene Schlüssel).

Das Sicherheitsprofil meines Clusters zeigte diese Schicht ausgeschaltet, wie es der Standard ist:

> az aks show -g rg-demo -n aks-demo --query securityProfile -o json
{
"azureKeyVaultKms": null,
"customCaTrustCertificates": null,
"defender": null,
"imageCleaner": null,
"workloadIdentity": null
}

Auch mit KMS bekommt kubectl den Klartext: Der API-Server entschlüsselt ihn für jeden, den RBAC durchlässt. Verschlüsselung im Ruhezustand schützt die gespeicherte Kopie, nicht die API.

Der stärkere Schritt hält den Wert aus dem Cluster heraus: der Azure-Key-Vault-Provider für den Secrets Store CSI Driver, ein Add-on, das Secrets aus Key Vault in Pods einhängt. Der Wert liegt in Key Vault; beim Start des Pods holt der Treiber ihn mit einer verwalteten Identität und hängt ihn als Datei ein. In etcd liegt er nie, es sei denn, Sie lassen den Treiber zusätzlich ein Kubernetes-Secret daraus erzeugen. Key Vault kann jeden Lesezugriff protokollieren, und mit eingeschalteter Rotation (standardmäßig aus) prüft der Treiber alle zwei Minuten auf einen neuen Wert. Im Pod ist es weiterhin eine Klartextdatei, die Regeln oben gelten also weiter.

Die Berechtigung, die jedes Secret liest

Das Kubelet übergibt ein Secret an jeden Pod, der es nennt. Wer in einem Namespace einen Pod oder ein Deployment anlegen darf, kann also jedes Secret dort einhängen und ausgeben, ohne je Secrets lesen zu dürfen. In meinem Lauf durfte der Service Account der App das nicht:

> kubectl auth can-i create pods --as system:serviceaccount:default:default
no

Wenn Sie Rollen vergeben: Behandeln Sie „Pods anlegen“ wie „Secrets lesen“.

Was beim Ausführen passiert ist

Getestet am 28. Oktober 2026 in meinem eigenen Azure-Abonnement (AKS 1.36 in westeurope, Azure CLI 2.88.0, kubectl 1.36); danach wurde alles gelöscht. Der Cluster hatte einen Node vom Typ Standard_D2s_v5 und keine Microsoft-Entra-Integration (lokale Konten), wie in der Übung. Jeder Befehl oben lief in unter einer Sekunde, außer az aks show (3,1 s). Die Prüfungen für den Service Account default endeten mit Exit-Code 1, so sagt kubectl auth can-i „nein“. Das Secret enthielt die Demo-Werte der Übung, die dekodierte Ausgabe auf dieser Seite ist also unbedenklich.

Was das Video weglässt

  • Weitere Secret-Typen: kubernetes.io/dockerconfigjson für Registry-Zugangsdaten und kubernetes.io/tls für Zertifikate, dazu kubectl create secret generic --from-literal.
  • Azure App Configuration mit Key-Vault-Verweisen: Ihr Kubernetes-Provider löst die Verweise auf und erzeugt Kubernetes-Secrets, die dann wieder in etcd liegen.
  • Microsoft-Entra-Integration und das Abschalten lokaler Konten, damit der Zugriff auf den Cluster über Entra ID läuft statt über ein Cluster-Admin-Zertifikat.
  • Workload Identity für den CSI-Treiber statt der verwalteten Identität des Add-ons, und unveränderliche Secrets.
  • Persistenter Speicher, der dritte Teil des Moduls (ein eigener Beitrag).

Weiterführende Links

Video-Transkript (Englisch)

Kubernetes calls it a Secret. I described one on Azure Kubernetes Service, and it showed only a size: twenty-one bytes. One command later, I had the value, in plain text. No password, no key, nothing to break.

In this video, you'll see who can really read a Kubernetes Secret, and what keeps it safe. At the end: one permission that reads every Secret in a namespace, without permission to read Secrets.

So what does a Secret actually store? A Secret is a Kubernetes object for sensitive values, like passwords and keys. It works like a ConfigMap, the object for plain settings, but with its own type and its own permissions. The Microsoft Learn module's exercise writes one in a YAML manifest, called api-secrets: two keys, an endpoint and an access key, in plain text, under stringData. I applied it, then asked kubectl, the Kubernetes command line, to describe the Secret, the way the module checks it. Two keys, and only their sizes: twenty-one bytes and nineteen bytes. No values. The value went in as plain text. What did Kubernetes store? The same value, in base64. I asked kubectl for the raw data field of that one key, and nothing else. A string of letters and digits. Base64 is an encoding, not encryption. It turns any bytes into plain letters, so they fit into YAML and JSON. There's no key: anyone can turn it back. So I decoded it, with kubectl's go-template output, a small template language, and its built-in base64decode function. Secret access key, one two three four five six. The whole value. A Secret is an envelope with a label, not a safe. kubectl describe just doesn't open it for you.

Then what does the pod that uses it see? In the exercise, a deployment runs the app in a pod, the unit that wraps your container. Its template maps each key to an environment variable, with secretKeyRef: the Secret's name, and the key inside it. When the container starts, the kubelet, the Kubernetes agent on the node, fetches the Secret and hands the decoded value to the container. Just like a ConfigMap, it's a copy taken at the start. The ConfigMap video showed what that means when the value changes. The exercise's app is careful with it. With a small script of mine, I called its secrets endpoint over the public address. Masked. Three stars, the last four characters, and the length: twenty-one. But masking is something the app does. So I asked the container itself, with kubectl exec and printenv. Secret access key, one two three four five six. The full value, in plain text. Kubernetes hands the real value to every process in the container. So anyone allowed to run kubectl exec on that pod can read it. Inside the pod, a Secret is plain text. Getting into a pod is as good as reading its Secrets. This series runs one Azure service at a time, for real. Subscribe to follow the series.

So who is allowed to read a Secret? Kubernetes decides that with RBAC, role-based access control: which user or identity may get, list or watch which kind of object. And list counts as reading: listing Secrets returns their values too. kubectl auth can-i asks the cluster that question for you. First, for me. Yes. My login file came from az aks get-credentials, the usual way to connect. On a cluster without Microsoft Entra sign-in, that file is a cluster admin: allowed to do everything. Pods have an identity too: a service account. The exercise sets none, so its pods run as default. The pod uses this Secret all the time. Can its own service account read it? No. I asked again with --as, which checks the question for another identity: the pod's service account. A plain no, exit code one. That identity can't 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 on 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.

So is it at least encrypted where it's stored? Kubernetes keeps every object, Secrets included, in etcd, its database. On Azure Kubernetes Service, etcd runs in the control plane, the part of the cluster that Microsoft runs for you. On AKS, is each Secret encrypted inside etcd by default? No, only if you turn it on. The disks under etcd are always encrypted by Azure Storage. But that protects the disk, not each Secret. But encrypting each Secret before it's written to etcd is an extra layer: KMS, the Key Management Service plugin, with a key in Azure Key Vault. I asked AKS for my cluster's security profile, the place where that layer would show up. azureKeyVaultKms: null. The extra layer was off, as it is by default. And even with it 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. So how do you keep the value out of the cluster altogether? With 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, an Azure identity with no password to store, and mounts it as a file. It never sits in etcd, unless you ask the driver to copy it into a Kubernetes Secret as well. Key Vault can log every read. And with rotation turned on, the driver checks for a new value every two minutes. Inside the pod, it's still a plain file, so the rules from before still apply. Base64 hides nothing. RBAC decides who reads it, KMS protects the stored copy, and Key Vault keeps it out of the cluster.

To sum it up: a Secret is base64, and one command decodes it. In the pod, it's plain text for anyone who can exec in. RBAC decides who reads Secrets. Encryption inside etcd is an extra on AKS, and Key Vault keeps the value out of the cluster.

And the permission that reads every Secret in a namespace, without permission to read Secrets? Creating pods. The kubelet hands a Secret to any pod that names it. So whoever can create a pod, or a deployment, in a namespace, a folder of objects in the cluster, can mount any Secret in it, and print it. So I checked the app's service account one last time: may it create pods in the namespace? No. Good. When you hand out roles, treat create pods like read Secrets.

The safest secret is one you never store: the video on screen swaps a connection string for a managed identity. If this was useful, subscribe for more practical .NET and Azure.

Brauchen Sie das in Ihrer eigenen Azure-Umgebung?

Ich entwickle, migriere und betreibe .NET-Plattformen auf Azure, mit Azure DevOps und Terraform. Wenn Ihr Team das sauber aufsetzen möchte, sehen Sie sich mein Angebot an.

Zu den Leistungen