Passwordless Workload Authentication: Closing the NHI Gap
In workload-identity literature — SPIFFE, AWS IRSA, GKE Workload Identity, Azure Workload Identity — "passwordless" never means "no credential material on the wire." Credentials always exist: a projected JWT, an instance identity document, an SVID. What passwordless means is no pre-shared, human-distributed, long-lived secret. The credential is minted and rotated by the platform, not provisioned to the workload by a human or a CI/CD pipeline.
For Kubernetes operators, AWS Lambdas, Azure Container Apps, and GCP Cloud Run services, that definition matters. These workloads run in environments that already have an identity — the kubelet projects a service-account JWT, the EC2 instance metadata service signs an instance identity document, the GCE metadata server issues identity tokens. The question is not how to create more credentials, but how to verify the ones the platform already gives you.
What the platform actually mints
Before any of this is useful, it helps to be concrete about what each platform hands a workload, because the verification logic differs in the details even though the shape is identical.
- Kubernetes. The kubelet projects a service-account JWT into the pod filesystem, bound to a requested audience and rotated well before expiry. The issuer is the cluster's own OIDC issuer, and the subject takes the form
system:serviceaccount:<namespace>:<name>. Pod name and UID travel in akubernetes.ioclaim, which is what makes per-workload binding possible rather than per-service-account. - AWS. For EKS, IRSA exposes the same projected token against a cluster-specific OIDC provider. Outside Kubernetes, the instance metadata service signs an instance identity document describing the instance, its account, and its region.
- Azure. Managed Identity tokens come from the instance metadata endpoint, in v1 (
sts.windows.net) or v2 (login.microsoftonline.com) form. The claims worth binding on aretid(tenant),oid(the identity's object ID), andxms_mirid, which carries the full ARM resource ID — that last one is how you bind to a specific resource group rather than the whole subscription. - GCP. The metadata server issues identity tokens from
accounts.google.comwith a nestedgoogle.compute_engineclaim containing project ID, instance ID, and zone.
The common thread is that none of these are secrets you store. They are short-lived assertions the platform will happily mint again in a few minutes, which is precisely why leaking one matters far less than leaking a static API key.
How verification works
A verifier's job is narrow and worth stating precisely, because most of the security lives in steps people skip.
- Signature against a pinned key set. Fetch the issuer's JWKS and verify the token was signed by a key it publishes. Pinning the issuer, not just trusting any valid JWT, is what stops a token minted by an unrelated cluster from being accepted.
- Issuer and audience. The issuer must match the binding exactly. The audience must match a value you chose — never a default. An audience check is the difference between "this token was minted for us" and "this token was minted for something else and replayed at us."
- Expiry, not-before, and bounded skew. These tokens are short-lived by design, so a generous clock-skew allowance quietly extends their life. Keep the leeway tight enough that it does not become the real TTL.
- Expected claims. Match the platform-specific claims against the binding: namespace and service account for Kubernetes, tenant and resource ID for Azure, project and zone for GCP.
- Template match, then registration. Only once the evidence checks out does a workload template decide whether this identity is allowed to exist, what scopes it gets, and whether the per-template cap has room.
Skip step two and you have built something that accepts any well-formed token from a trusted issuer, which is a surprisingly common failure and an entirely silent one.
The four gaps we closed
CoreLink's non-human identity (NHI) subsystem shipped with Kubernetes and AWS workload attestation working end-to-end. Admin creates an NHI once, binds it to a trusted issuer and expected claims, and the workload hits /api/v1/nhi-agent/connect with its platform-minted JWT. No pre-shared secret. No bootstrap token. Just evidence the platform can verify against a pinned JWKS.
Four gaps remained between that and a complete cross-cloud story. We closed them over the last month:
1. JIT NHI auto-registration via workload templates
Even with attestation working, admins had to pre-create an NHI record and configure its binding before the workload could connect. That's one remaining manual step.
Workload templates close it. An admin defines a template like "any pod in namespace prod with service account myapp-* auto-registers as an NHI with scopes secrets:read and sessions:read." The first attested Connect from a matching workload auto-provisions the NHI. Claim matchers support equals, prefix, and glob patterns. Caps are enforced via MaxNHIs, and default scopes flow to the new NHI's metadata.
2. Azure and GCP attestation at parity
Azure Managed Identity tokens (v1 sts.windows.net, v2 login.microsoftonline.com) and GCP identity tokens (accounts.google.com) needed dedicated verifiers. Each carries platform-specific claims — Azure's tid, oid, xms_mirid; GCP's nested google.compute_engine.{project_id, instance_id, zone}. The verifiers extract these into the binding's expected-claims map so a workload template can match on "resource group X in tenant Y" or "instance in project Z."
3. Multi-cluster SPIFFE trust-bundle federation
Accepting external SVIDs from peer clusters needed JWKS plumbing, peer management, and a middleware routing layer that distinguishes own-domain from federated-domain SPIFFE URIs at mTLS. Federation peers are managed via a new admin UI; each peer has an immutable trust domain and a JWKS that's refreshed from a bundle endpoint (manual refresh for now, automatic refresh is on the follow-up list).
4. RFC 8693 inbound token exchange
The platform could already issue JWT-SVIDs to exchange outbound against AWS STS, Azure workload federation, and GCP workload pool. What it couldn't do was accept an inbound OIDC token from a peer identity platform and return a native CoreLink session. A new POST /oauth/token grant type implements RFC 8693 for SPIFFE subject tokens: external JWT → resolve peer by trust domain → verify signature + issuer + audience against the peer's JWKS → assert subject stability between preview and verification (anti-forgery) → match claims against a workload template → auto-register the NHI → narrow scopes → issue platform session.
What this unlocks
For a Kubernetes operator, the full deploy-to-first-request flow is now:
- Platform admin defines a workload template once (one namespace + service-account pattern).
- Operator creates a pod with the matching service account.
- Kubelet projects a SA token at
/var/run/secrets/kubernetes.io/serviceaccount/token— rotated automatically, never human-distributed. - Pod POSTs the token to CoreLink's Connect endpoint. CoreLink fetches the cluster's JWKS, verifies the JWT against the bound issuer and audience, matches the claims against the template, auto-registers the NHI, returns a short-lived
X-NHI-Session. - Every subsequent API call uses the session header. No credential material in the pod's environment.
For AWS, Azure, and GCP workloads, the only thing that changes is which attestation verifier runs. The rest — template match, NHI auto-registration, session — is identical.
Failure modes worth designing for
Attestation removes the static credential, but it introduces its own set of things that go wrong quietly. These are the ones worth building tests around before you rely on any of it.
- Audience confusion. If a workload requests a token with a default or broadly-scoped audience, that same token is accepted by anything else trusting the issuer. Require an explicit audience per binding and reject tokens that do not carry it.
- Key rotation versus availability. JWKS endpoints rotate keys. Cache too aggressively and you reject valid tokens after a rotation; cache too little and the issuer's availability becomes yours. Cache with a bounded TTL and re-fetch on unknown key ID rather than on every request.
- Replay inside the validity window. A token valid for several minutes can be presented more than once by anyone who obtains it. Short TTLs shrink the window but do not close it — nonce-based replay protection is what closes it, and until that ships the window is real and should be understood rather than assumed away.
- Templates that match more than intended. A glob such as
myapp-*matchesmyapp-debugand anything else a future engineer names that way. Caps bound the damage, but the more useful discipline is writing the narrowest matcher that satisfies the deploy and revisiting it when namespaces change. - Trust domain collisions in federation. Two peers asserting the same trust domain is an ambiguity with no safe resolution at request time, which is why a peer's trust domain is immutable once created. Getting it wrong means creating a new peer, not editing the old one.
- Losing the audit trail. Auto-registration means identities appear without a human action to review. That is the point, but it also means the record of which attestation created a given NHI is the only account of why it exists — worth treating as durable data rather than a log line.
What's next
Near-term: automatic SPIFFE bundle-endpoint refresh, nonce-based replay protection on Connect, and a policy language for claim matchers richer than equals/prefix/glob. Longer-term: WebAssembly-based custom attestation verifiers so customers can plug in platforms we haven't built native support for.
If you're running a cloud workload today with a bootstrap token or an environment variable full of secret material, the fix is on the other side of the workload template + matching attestation verifier. The passwordless part isn't a rewrite; it's a deploy config change.