When one service calls another inside your network, the callee has to decide whether to answer. In many systems that decision rests on a shared API key in an environment variable, or on nothing more than the caller's network location. Both assumptions break the moment an attacker gets code running anywhere inside the perimeter.
This post explains how services can prove their identity to each other properly: what mutual TLS does, what workload identity adds, and why short-lived credentials are the part that makes it manageable.
Why shared static API keys fail
A static key shared between services is easy to set up, which is why it is everywhere. Its problems only show up later.
- It identifies nobody. Every holder presents the same string. The callee cannot tell which service called, so it cannot authorise or audit per caller.
- It is a bearer secret. Whoever has the string can use it. It is sent on every request, so it appears in proxies, logs, traces and crash dumps.
- It does not expire. A key leaked last year works today.
- Rotation is a coordinated outage. Every caller and the callee must change at the same moment, so in practice nobody rotates.
- It spreads. Keys end up in repositories, CI variables, chat messages and laptops.
Trusting the network instead is worse. An IP address or a header such as X-Service-Name is a claim, not a proof. Anything that can reach the port can make the claim.
Good service authentication has three properties. The caller proves possession of a secret without revealing it. The identity is specific to one workload. And the credential expires soon enough that theft has a short useful life.
Mutual TLS
In ordinary TLS, only the server presents a certificate, and the client checks it. In mutual TLS (mTLS) the client presents one too. During the handshake each side proves that it holds the private key matching its certificate, and each checks that the other's certificate chains to an authority it trusts.
Three things follow:
- The private key never crosses the wire. Possession is proved by a signature.
- Both ends are authenticated before any application data is sent.
- The channel is encrypted, so the identity is bound to the connection and cannot be lifted from it and replayed.
mTLS answers "who is on the other end of this connection?". It does not answer "may they call this endpoint?". Authorisation is a separate step that uses the identity from the certificate.
Workload identity
A certificate needs a name in it. For people that is an email address; for a web server it is a DNS name. For workloads neither fits well. A pod's IP address changes on every restart, and a hostname says where something runs, not what it is.
SPIFFE (Secure Production Identity Framework for Everyone) defines a naming scheme for this. A SPIFFE ID is a URI:
spiffe://prod.example.com/ns/payments/sa/billing-apiThe first part is the trust domain. The path identifies the workload in whatever structure the organisation chooses. The ID is carried in the URI subject alternative name of an X.509 certificate, called an X.509-SVID, or in a JWT-SVID for cases where a token is more practical than a client certificate.
How a workload gets its identity
A new problem appears: how does a workload obtain its first credential without being handed a secret? This is the bootstrap, or "secret zero", problem. The answer is attestation. The platform vouches for the workload based on facts it can observe, such as the Kubernetes service account a pod runs as, the node it is on, or a cloud instance identity document. An issuer checks those facts and signs a certificate. No long-lived secret is ever placed in the workload's configuration.
Choose a stable unit of identity. A name tied to something that changes on every restart breaks rules silently; a namespace and deployment, or a service account, survives rescheduling.
Short-lived certificates
Certificate revocation is hard to operate. Revocation lists and online status checks have to be distributed and consulted, and many clients do not check them reliably.
Short lifetimes avoid most of the problem. If a certificate is valid for hours or minutes and renewed automatically, a stolen one expires on its own, and "revoking" a workload means declining to renew. The cost moves to automation: issuance and rotation must be automatic, and services must reload certificates without a restart.
The same reasoning applies to people reaching servers. See short-lived SSH certificates.
Tokens as an alternative
Not every hop can do mTLS. A load balancer may terminate TLS, or the caller may be a managed service that cannot present a client certificate. Signed tokens fill the gap.
The caller obtains a short-lived JWT for a specific audience, using the OAuth client credentials grant or a platform-issued token, and sends it in the Authorization header. The callee verifies the signature against the issuer's published keys and checks issuer, audience and expiry.
Tokens are bearer credentials, so keep them short and always check the audience. A token for service A must not be accepted by service B. The two approaches combine well: mTLS to authenticate the connection, and a token to carry the identity of the original caller across several hops.
Authorisation and rollout
Authentication without authorisation gives you encrypted traffic between services that all trust each other. Add rules that say which identities may call which service, default to deny, and log refusals.
A rollout order that avoids outages:
- Inventory which services call which.
- Issue identities to every workload and turn on mTLS in a permissive mode that accepts both plain and mutual connections.
- Watch for callers that are still unauthenticated.
- Switch to strict mode one service at a time.
- Add authorisation rules, in report-only mode first.
- Remove the old shared keys. Skipping this step leaves the weak path open.
How AuthFI does it
Each AuthFI project has its own certificate authority for machine-to-machine trust, alongside separate keys for tokens, SAML assertions and SSH certificates. Each key has one purpose, and one project's keys sign nothing for another. Signing happens inside the key service, and no application is handed a private key. See why every secret key lives in one place.
For token-based calls, Zero-Code checks a program's access token on the node, before the request reaches the service: it must be a valid access token, issued for that API. A token issued for another API is refused. Rules on a route say which roles and groups may call it, with no change to the service. Outbound rules limit where each workload may connect, keyed to the workload and not to an address range.
AI agents are treated as a kind of workload with their own identity: each one holds its own key and is assigned an identifier in SPIFFE form. See what is AI agent identity.
Key takeaways
- A shared static key identifies no one, never expires and is rarely rotated. Network location is not identity either.
- mTLS proves both ends of a connection by possession of a private key that is never sent.
- Workload identity gives each service a stable name, issued through attestation so that no bootstrap secret is needed.
- Short-lived, automatically renewed credentials replace most of the need for revocation.
- Authenticate first, then authorise per identity, and remove the old keys when you are done.



