Working With Token Lifetime and Audience
Goal
You check for yourself how a Pod's identity is issued and what lifetime and audience it has, and apply the setting that does not give a token at all when it is not needed.
Why it matters
When a container is breached, the first thing an attacker looks for is the token at /var/run/secrets/kubernetes.io/serviceaccount. So this module's key questions are two — does this Pod really need a token, and when does that token expire. The approach before 1.24 had bad answers to both questions. Creating an account automatically produced a token with no expiry, and Pods had it mounted regardless of whether they needed it. Now the token a Pod receives has a lifetime and is renewed by the kubelet, and you can even turn it off entirely with automountServiceAccountToken: false. On top of that, with audience, the token also carries "where it will be used," preventing a token issued to give to Vault from being reused elsewhere. This structure leads directly to cloud workload identity and becomes a design that eliminates static keys. Finally, do not forget: creating an identity does not create permissions. Permissions are still decided by RBAC.
Steps
- Create the namespace
sa-laband in it create the ServiceAccountpayments. This account must not have an automatically created Secret attached. And in/root/ops/sa/out/sa-note.txt, write in one line why secrets are not created automatically these days (it must include the background of the change to short-lived tokens after 1.24, and expressions such as expiry and lifetime). - In
sa-lab, create the Podpayments-apiwithspec.serviceAccountNameset topayments. With default settings, a projected volume holding the service account token is attached automatically. - Create the ServiceAccount
lockedwithautomountServiceAccountToken: false. Then create the Podhardenedwithspec.serviceAccountNameaslockedandspec.automountServiceAccountTokenasfalse. This Pod must have no token volume attached at all. - Issue a token for the
paymentsaccount with a lifetime of 1 hour (3600seconds) and the audiencevault, and save the JSON decoded from the payload of that JWT to/root/ops/sa/out/token.json. This JSON must havesubequal tosystem:serviceaccount:sa-lab:payments, a non-emptyaud, a difference betweenexpandiatof about 3600 seconds, and akubernetes.ioclaim. - In
sa-lab, create the Podprojected-demo. Setspec.serviceAccountNametopayments, and put aserviceAccountTokensource in a projected volume, specified asaudience: vault,expirationSeconds: 3600, andpath: vault-token. The first container must mount this volume at the path/var/run/secrets/vault. - In
sa-lab, create the Secretpayments-legacy-token. Thetypeiskubernetes.io/service-account-token, and the annotationkubernetes.io/service-account.nameispayments. And in/root/ops/sa/out/legacy-note.txt, write in at least 60 bytes, about two sentences, why this approach is not recommended (it must include that there is no expiry and the burden of revocation and rotation). - Give the
paymentsaccount only minimal permissions. Insa-lab, it must be able togetandlistConfigMaps, and must not be able to read Secrets or delete ConfigMaps. Connect the permissions with a RoleBinding whose subject ispayments. - Create
/root/ops/sa/out/sa-audit.json.podsis an array and must exactly match the actual number of Pods insa-lab. Each element has three fields,name,service_account, andautomount. Forpayments-api,service_accountmust bepayments; forhardened,automountmust befalse; and there must be no Pod whoseservice_accountisdefault. Finally, write 2 or more improvement recommendations in therecommendationsarray.
Reference
- Issue a token with
kubectl create token <어카운트> -n sa-lab --duration=3600s --audience=vault(where the placeholder is the account). A JWT has the structure헤더.페이로드.서명(header.payload.signature), so you only need to cut off the second piece and decode it. base64url must be padded to a multiple of 4 in length, so it is convenient to usepython3and itsbase64.urlsafe_b64decode. - You can check a Pod's volume structure with
kubectl get pod <이름> -n sa-lab -o jsonpath='{.spec.volumes}'(where the placeholder is the Pod name). The name of an automatically injected token volume starts withkube-api-access-. - For the Pod list in step 8, it is safer to build it from
kubectl get pods -n sa-lab -o jsonwithjq. For a Pod whoseautomountfield is not stated explicitly, write the default (true). - Common mistake 1: turning it off only on the Pod in step 3 and not setting it on the account. This lab checks both places.
- Common mistake 2: having the Pods in steps 5 and 3 use the
defaultaccount. You get caught in the step 8 audit. Specify a dedicated account for each Pod. - Common mistake 3: saving the token string itself to the file in step 4. What you save is the decoded payload JSON.
- The lab Pod comes up fresh for each lab, so the cluster state created in the earlier lab does not remain. Create the namespace and accounts yourself within this lab. This is exactly why operational procedures must be left in runbooks and manifests rather than in memory.
Create a dedicated service account
Create the namespace sa-lab and in it create the ServiceAccount payments. This account must not have an automatically created Secret attached. And in /root/ops/sa/out/sa-note.txt, write in one line why secrets are not created automatically these days (it must include the background of the change to short-lived tokens after 1.24, and expressions such as expiry and lifetime).
In current versions, creating an account does not bring a secret along. Sum up in one line why it changed that way.
Specify a dedicated account for a Pod
In sa-lab, create the Pod payments-api with spec.serviceAccountName set to payments. With default settings, a projected volume holding the service account token is attached automatically.
Check the name of the field that specifies the account in the Pod spec. Once you specify it, the token volume is attached automatically.
Turn off the automatic token mount
Create the ServiceAccount locked with automountServiceAccountToken: false. Then create the Pod hardened with spec.serviceAccountName as locked and spec.automountServiceAccountToken as false. This Pod must have no token volume attached at all.
You can use the same field on both the Pod and the account. If you turned it off, no token may remain in the volume list.
Issue a short-lived token and take apart the payload
Issue a token for the payments account with a lifetime of 1 hour (3600 seconds) and the audience vault, and save the JSON decoded from the payload of that JWT to /root/ops/sa/out/token.json. This JSON must have sub equal to system:serviceaccount:sa-lab:payments, a non-empty aud, a difference between exp and iat of about 3600 seconds, and a kubernetes.io claim.
A JWT is three pieces separated by dots, and the middle one is the payload. It is base64url, so you have to fix the padding for it to decode. Check the lifetime from the difference between the two timestamps.
Use a projected token with a specified audience
In sa-lab, create the Pod projected-demo. Set spec.serviceAccountName to payments, and put a serviceAccountToken source in a projected volume, specified as audience: vault, expirationSeconds: 3600, and path: vault-token. The first container must mount this volume at the path /var/run/secrets/vault.
If you stamp an audience into a token, it is valid only at that audience. Match the file name and the mount path exactly.
Create a legacy long-lived token Secret and sort out the risk
In sa-lab, create the Secret payments-legacy-token. The type is kubernetes.io/service-account-token, and the annotation kubernetes.io/service-account.name is payments. And in /root/ops/sa/out/legacy-note.txt, write in at least 60 bytes, about two sentences, why this approach is not recommended (it must include that there is no expiry and the burden of revocation and rotation).
The controller fills in the token only if the specific type and annotation are present. The real problem with this approach is not convenience but that there is no expiry.
Give the account only minimal permissions
Give the payments account only minimal permissions. In sa-lab, it must be able to get and list ConfigMaps, and must not be able to read Secrets or delete ConfigMaps. Connect the permissions with a RoleBinding whose subject is payments.
Creating an account and granting permissions are separate. Open only what is needed and verify by checking that the rest is blocked.
Audit how accounts are used
Create /root/ops/sa/out/sa-audit.json. pods is an array and must exactly match the actual number of Pods in sa-lab. Each element has three fields, name, service_account, and automount. For payments-api, service_account must be payments; for hardened, automount must be false; and there must be no Pod whose service_account is default. Finally, write 2 or more improvement recommendations in the recommendations array.
The Pod count must exactly match the real cluster. There must not be even one Pod that uses the default account.