CAMPUX Cloud Bootcamp Advanced Build · Zero-Trust ← All labs
Advanced Build · Expert
~5–6 hrs · Costs real money (AKS)
Azure CLI & kubectl · torn down at the end
Security · Zero Trust

A workload with no secrets to steal.

The fastest way to leak a credential is to have one. In this build you run a real application on AKS that holds no password, no connection string, no key of its own — it proves who it is to Azure with a federated token, reads what it needs from Key Vault just-in-time, and sits inside a namespace that denies every packet it did not explicitly invite. This is what "zero trust" means once you stop saying it and start deploying it.

Fig. 1 · Zero-trust AKS: no static secret
Resource group · rg-lab-zttokenread · least privilegeAKS workloadfederated ServiceAccountFederated identitytrusts the podKey Vaultone secret it may readevery static secret is a liability with a timer
● Screen walkthrough Not yet recorded · ~12 min
Reel · 00:00 / 12:00

Federate a ServiceAccount to a Managed Identity, watch a pod read Key Vault with no secret, then break a connection with a network policy on purpose.

Placeholder — the page below stands alone until the reel lands
Why

Every static secret is a liability with a timer on it

A Kubernetes Secret feels like security, but it is just base64 sitting in etcd, copied into pods, and printed into anyone's terminal who runs the wrong kubectl. The moment it exists, it can leak; once leaked, rotating it is a scramble across every place it was pasted. Workload Identity removes the object entirely: the pod presents a short-lived token that Azure trusts because you federated its identity in advance, and Azure hands back exactly the access you granted — no long-lived credential ever touches the cluster.

You will enable that on AKS, tie a Kubernetes ServiceAccount to an Azure Managed Identity, and watch a pod read a Key Vault secret with nothing but its own identity. Then you will close the other half of zero trust — the network — by defaulting the namespace to deny all and allowing only the one path the app actually needs.

Identity you can federate; secrets you can only hope to rotate.

This one costs money — read before you start

Unlike the free-tier labs, an AKS cluster runs real nodes and bills by the hour (roughly $3–6 for a short sitting on a small cluster). Do it in one sitting and run the teardown at the end — the final step deletes everything. Set a budget alert first if you're cost-nervous; the cost-guardrails lab shows how.

Before you begin — one-time setup

You need a paid or free-trial Azure subscription, the Azure CLI (az) with the aks-preview features enabled, and kubectl. New to the tools? The Set up your machine page covers installs and az login; everything here also runs in Azure Cloud Shell (Bash), which has az and kubectl ready. This build assumes you have met AKS and kubectl before — if not, that's the prerequisite, not this page.

Setup

A cluster that can issue tokens

Create a resource group and a small AKS cluster with two capabilities most clusters ship without: an OIDC issuer (so Kubernetes can mint verifiable tokens) and Workload Identity (so Azure will accept them). Capture the issuer URL and your tenant id — you will need both to federate.

# Windows/Git Bash: stop it mangling resource-id arguments (harmless on macOS/Linux)
export MSYS_NO_PATHCONV=1

RG="campux-zt-rg"
az group create -n "$RG" -l eastus

az aks create -g "$RG" -n campux-zt-aks \
  --node-count 2 --node-vm-size Standard_B2s \
  --enable-oidc-issuer --enable-workload-identity \
  --network-plugin azure --network-policy azure \
  --generate-ssh-keys

az aks get-credentials -g "$RG" -n campux-zt-aks
kubectl create namespace production

# capture the two values federation needs
export OIDC_ISSUER=$(az aks show -g "$RG" -n campux-zt-aks --query "oidcIssuerProfile.issuerUrl" -o tsv)
export TENANT_ID=$(az account show --query tenantId -o tsv)
echo "issuer: $OIDC_ISSUER"
Checkpoint kubectl get nodes shows two Ready nodes, and $OIDC_ISSUER holds a URL. The --network-policy azure flag matters — it installs the policy engine you'll rely on in the last step, and it cannot be added after the cluster exists.
Step 1

An identity, and a secret only it may read

Create a user-assigned Managed Identity — the Azure-side identity the pod will borrow — and a Key Vault holding one secret. Grant the identity read access to that secret and nothing else. This is least privilege written as configuration.

az identity create -g "$RG" -n app-workload-id
export CLIENT_ID=$(az identity show -g "$RG" -n app-workload-id --query clientId -o tsv)

az keyvault create -g "$RG" -n kv-campux-zt --enable-rbac-authorization false
az keyvault secret set --vault-name kv-campux-zt --name db-password --value "SecureP@ssword123"

# the identity may read secrets — get/list only
az keyvault set-policy -n kv-campux-zt --spn "$CLIENT_ID" --secret-permissions get list
Checkpoint The vault holds db-password, and the identity's clientId is in $CLIENT_ID. Nothing yet connects the two worlds — the pod cannot use this identity until you federate it, which is the next step and the crux of the whole build.
Step 2

Federate the ServiceAccount

This is the join. A Kubernetes ServiceAccount, annotated with the identity's client id, is what pods run as. A federated credential tells Azure: "trust tokens this specific issuer mints for this specific ServiceAccount, and treat them as this Managed Identity." Subject and issuer must match exactly, or Azure rejects the token — most first-attempt failures are a typo here.

kubectl apply -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  namespace: production
  annotations:
    azure.workload.identity/client-id: "$CLIENT_ID"
EOF

az identity federated-credential create \
  --name app-fed-credential \
  --identity-name app-workload-id \
  --resource-group "$RG" \
  --issuer "$OIDC_ISSUER" \
  --subject "system:serviceaccount:production:app-sa"
Checkpoint The federated credential exists (az identity federated-credential list --identity-name app-workload-id -g "$RG" -o table) and its subject reads system:serviceaccount:production:app-sa. That string is a contract: change the namespace or ServiceAccount name later and you must re-federate.
Step 3

Deploy the workload — watch it read a secret it never stored

Deploy a pod that runs as app-sa and carries the azure.workload.identity/use label. The mutating webhook injects the token path and environment; your code (or, here, the Key Vault CSI/SDK) exchanges that token for access and pulls the secret at runtime. No Secret object, no env-var password.

kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: secure-app
  namespace: production
spec:
  replicas: 1
  selector:
    matchLabels: { app: secure-app }
  template:
    metadata:
      labels:
        app: secure-app
        azure.workload.identity/use: "true"
    spec:
      serviceAccountName: app-sa
      containers:
      - name: app
        image: mcr.microsoft.com/azure-cli:latest
        command: ["/bin/sh","-c","az login --identity && az keyvault secret show --vault-name kv-campux-zt --name db-password --query value -o tsv && sleep 3600"]
EOF

# the secret value should appear in the logs — fetched with no stored credential
kubectl logs -n production deploy/secure-app
Checkpoint The pod logs print the secret value, retrieved live from Key Vault by an identity the pod proved at runtime. Nowhere in the cluster is that password stored. Delete the pod and it takes no credential with it — there was never one to take.
Step 4

Default-deny the network, then allow exactly one path

Identity is half of zero trust; the network is the other half. By default Kubernetes lets every pod talk to every pod — the flat network an attacker loves. Apply a default-deny policy to the namespace, then a single explicit allow for the one connection the app legitimately needs. Everything unlisted is now refused.

# deny all ingress and egress in the namespace, then allow frontend -> backend:3000 only
kubectl apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: production
spec:
  podSelector:
    matchLabels: { app: backend }
  ingress:
  - from:
    - podSelector:
        matchLabels: { app: frontend }
    ports:
    - port: 3000
EOF

Prove it. Stand up a throwaway pod with no allowed path and try to reach the backend — it should hang and fail, not connect.

kubectl run probe -n production --image=busybox --restart=Never -- \
  sh -c "wget -T 5 -qO- backend:3000 || echo BLOCKED"
kubectl logs -n production probe   # -> BLOCKED (timed out, as designed)
Checkpoint The probe reports BLOCKED. An unlisted pod cannot reach the backend, while the sanctioned frontend→backend path stays open. You've turned "trust nothing by default" from a slogan into an enforced policy — the exact control auditors ask to see.
Down

Tear it down

The cluster bills until it's gone. One resource-group delete removes the AKS nodes, the identity, and the vault together. Do this now, not tomorrow.

az group delete -n campux-zt-rg --yes --no-wait
az group exists -n campux-zt-rg      # -> false, once the async delete completes
Checkpoint The group is deleting. Key Vault has soft-delete on by default — if you plan to reuse the name kv-campux-zt soon, purge it with az keyvault purge -n kv-campux-zt, otherwise the name is reserved for the retention window.
End

What you can now honestly claim

You ran a workload on AKS with zero static secrets, federating a Kubernetes ServiceAccount to an Azure Managed Identity so a pod read Key Vault with nothing but a short-lived token, and you enforced default-deny network policies with a single explicit allow. That is "implemented zero-trust workload identity and network segmentation on Kubernetes" — a senior line on any cloud-security posting — done, not described. The pattern transfers straight to GitHub Actions federating into Azure, to app-to-database access, and to any place you're tempted to paste a key: the durable lesson is that the safest secret is the one that never existed.

Footnotes
  1. The demo container here logs the secret to prove retrieval — a teaching move, never a production one. In a real app the SDK reads the secret into memory and it is never printed. Treat a secret in a log as an incident, even in a lab.
  2. Key Vault is used in its access-policy mode for brevity (--enable-rbac-authorization false). The RBAC model — a Key Vault Secrets User role assignment on the identity — is the modern default and worth doing next; it's the same idea expressed as Azure RBAC rather than a vault-local policy.
  3. Network policy is only enforced if the cluster has an engine for it, which is why the cluster was created with --network-policy azure. On a cluster without one, the same manifests apply cleanly and do nothing — a silent failure worth knowing about.