⚠️ Disclaimer
This article assumes you're already somewhat familiar with Kubernetes concepts (Pods, ServiceAccounts) and the basics of JSON Web Tokens (JWTs).
It was a Tuesday.
Nothing special - just your average day as a platform engineer. My team's notifications were mercifully quiet, and I thought, "Perfect, I can finally clean up that old Helm chart that's been bothering me."
I opened the repo of the underlying image written in Go to double-check the config before merging. As I scrolled through the config file, something caught my eye:
log.Println("SA Token:", token)
Wait. What?
A debug statement. Still in production code. Logging an actual Kubernetes ServiceAccount token. Not cool...
I paused. My heart rate didn't. Curious but mostly horrified, I grabbed the token and decoded the payload in my shell:
{
"iss": "https://kubernetes.default.svc.cluster.local",
"kubernetes.io/serviceaccount/namespace": "payments",
"kubernetes.io/serviceaccount/secret.name": "payments-token-6gh49",
"kubernetes.io/serviceaccount/service-account.name": "payments-sa",
"kubernetes.io/serviceaccount/service-account.uid": "f9a2c144-11b3-4eb0-9f30-3c2a5063e2e7",
"aud": "https://kubernetes.default.svc.cluster.local",
"sub": "system:serviceaccount:payments:payments-sa",
"exp": 1788201600, // Sat, 01 Aug 2026 00:00:00 GMT
"iat": 1756665600 // Fri, 01 Aug 2025 00:00:00 GMT
}
Default audience claim. A 1-year expiry.
This "bad boy" wasn't just a dev leftover - it was a high-privilege token with zero constraints floating around in plaintext logs!
What This Article Covers
In this post, I'll guide you through:
- The inner workings of Vault authentication with JWT and Kubernetes methods
- What Kubernetes ServiceAccounts and their tokens are, and how they’re (mis)used
- How projected ServiceAccount tokens fix many of the hidden dangers of older token behavior
- Why you should start adopting token projection and Vault integration today
We'll cover real-world use cases, implementation tips, and common pitfalls - so you don't end up like I did, staring at a:
log.Println("SA token:", token)
...and wondering how close you just came to a security incident.
Why This Matters
To really understand why that log statement gave me chills, we need to unpack a few core concepts:
- What is a JWT?
- How do Kubernetes ServiceAccounts and their tokens work?
- And what role do these tokens play in authenticating to systems like Vault?
Let's start with the fundamentals.
What Is a JWT?
If you've been around authentication systems long enough, you've probably seen one of these beasts:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
This is a JSON Web Token (short: JWT).
It's a compact, URL-safe format for representing claims between two parties. They're used everywhere: web apps, APIs, and yes — inside your Kubernetes cluster.
A JWT consists of three parts:
- Header – declares the algorithm used to sign the token (e.g. RS256)
- Payload – contains the claims (who you are, what you're allowed to do, etc.)
- Signature – a cryptographic seal that verifies the payload hasn't been tampered with
Claims are the heart of a JWT — key-value pairs that describe who the token refers to and what it can be used for.
They can be:
- Standard claims defined by the spec (e.g.,
iss
,sub
,exp
,aud
) - Custom claims added by the issuer for domain-specific needs
Closer Look at aud
The audience (aud
) claim tells who the token is meant for. Think of it as the intended recipient.
Example: Imagine a Coldplay concert ticket. It says valid for Stadium X on 01-09-2025. You can't take the same ticket and use it at Stadium Y — they'll reject it (...trust me, I tried).
A JWT works the same way:
- If the token has
"aud": "https://kubernetes.default.svc"
, then only the Kubernetes API server should accept it. - If some other service receives that token, the
aud
won't match and the token must be rejected.
Without this check, a token could be misused anywhere that trusts the signing key. With aud
, it's scoped to the right system.
Kubernetes and ServiceAccounts
Kubernetes is an open-source platform that orchestrates containers at scale. At its heart is the Pod — the smallest deployable unit.
But every pod needs an identity. That's where ServiceAccounts come in.
ServiceAccounts 101
- Every Pod references a ServiceAccount (default if none is set), but a token is only mounted if enabled
- Kubernetes mounts the identity at:
/var/run/secrets/kubernetes.io/serviceaccount/token
- That token is a JWT, signed by the Kubernetes control plane
- It lets the pod authenticate with the API server — and sometimes even external systems like Vault
The Catch
Until recently, these tokens came with dangerous defaults:
- Long-lived (often valid for a year)
- Previous to Kubernetes v1.24, there was no default audience set (https://kubernetes.default.svc)
- Automatically mounted into every pod, even if unused
Enter Vault: The Gatekeeper of Secrets
HashiCorp Vault is your cluster’s paranoid librarian:
it stores API keys, certs, passwords — and only hands them out when it's sure you should have them.
How? Authentication methods.
Vault Authentication Methods
- Username & password
- AppRole
- LDAP
- Kubernetes
- JWT
Let's zoom into the last two.
Kubernetes Auth Method
- Pod sends its mounted ServiceAccount token to Vault
- Vault validates it against the Kubernetes API
- If valid, Vault maps it to a policy
This is simple and works well when Vault runs inside the cluster.
JWT Auth Method
- Vault verifies the JWT itself (signature, claims, expiration)
- No need for Kubernetes API access
- More portable
Rule of thumb:
- Use Kubernetes if Vault runs inside your cluster and simplicity matters
- Use JWT if you want portability, stronger boundaries, and flexibility
Projected Tokens: Because It's 2025
Old tokens were static and long-lived. Projected tokens fix this mess.
Instead of mounting a one-year token into every pod, Kubernetes can now generate short-lived, audience-bound tokens on demand.
What You Get
- Short TTL (e.g. 10 minutes)
- Audience restrictions (
aud: vault
) - Automatic rotation by
kubelet
- No automatic mounting into pods
Example Pod with Projected Token
apiVersion: v1
kind: Pod
metadata:
name: projected-token-test-pod
namespace: demo
spec:
serviceAccountName: projected-auth-sa
containers:
- name: projected-auth-test
image: demo/vault-curl:latest
command: ["sleep", "3600"]
volumeMounts:
- name: token
mountPath: /var/run/secrets/projected
readOnly: true
volumes:
- name: token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 600
audience: vault
Why Vault Loves This
Vault's JWT auth method is tailor-made for projected tokens:
- It parses and verifies the JWT signature (via a configured PEM key or JWKS endpoint)
- Validates all claims (
aud
,sub
,exp
,iss
) locally - Issues secrets only if every check passes
Minimal dependencies. Strong claim validation. Secure, verifiable checks.
Back to the Log
Imagine you stumble upon this in a Go app:
log.Println("Auth Token:", token)
- Old world: a one-year, cluster-wide token with no audience. A time bomb.
- New world: a 10-minute token, scoped to Vault, rotating automatically.
It's still bad to log tokens — but at least it's not catastrophic.
Try It Yourself: Vault + K8s AuthN Lab
I've built a hands-on demo repo where you can test this locally with KIND (Kubernetes in Docker) and Vault Helm charts:
👉 GitHub: VincentvonBueren/erfa-projected-sa-token
What's Inside
- KIND cluster with Vault
- Both Kubernetes and JWT auth methods enabled
- Vault policies and roles
- Four demo pods:
- Kubernetes auth method
- JWT with static token
- JWT with projected token
- JWT with wrong audience (failure demo)
Final Drop 🎤
If your pods still run with default, long-lived tokens:
you’re one debug log away from giving away the keys to your cluster.
Projected tokens aren't optional. They're essential.
Adopt them today — and stop shipping security disasters.