Suppose a storefront asks our commerce API to create a product. The API receives POST /products/create and a JSON body. What, exactly, should make it believe the request?
“It came from the storefront” leaves too much unanswered. Anyone can put a service name in a header. A secret shared by every internal service proves only that somebody has the secret. A private network proves where the packet travelled, not who intended the operation.
Before going further, we need to disambiguate application. When a known legal entity accesses our services, we register a non-human application for that access and attach its credentials, sessions and relationships to the record. The storefront and commerce API remain services: workloads that make calls. They act within the application's authorization context. For the rest of this piece, application refers to that registered record, not to a storefront, API or generic piece of software.
Our API has to answer four questions, in order. Which service is calling? What exact request did it intend to make? May it make that request for this application and principal? And, if fulfilling the request requires another service, how much of that authority should move with the work?
The order matters. Each answer gives us the evidence required to ask the next question, without pretending that it has answered the next question too.
Knowing the service does not authorize its request.
What are we trying to prove?
The word authentication draws attention to the caller's identity. We need that proof first, but accepting the request requires more.
Suppose we establish that the storefront is a known workload. We now know who is calling. We still do not know what it intended to send. An intermediary could change {"name":"A"} to {"name":"B"} and leave the caller's identity untouched. So the second proof has to bind that identity to the method, target and body we actually received.
A genuine service can still send an intact request that policy forbids. We therefore make a separate decision from the route, application, principal and relationship through which the service is acting. Identity tells us who. Integrity tells us what. Authorization tells us whether it may.
Those questions become four verification gates:
Is the caller a service registered for this environment?
Did that service sign the assertion we received?
Do the method, target and body hash match the request we received?
May this service perform this route's action for this application and principal?
We cannot sensibly check the later gates without the earlier evidence. So we begin with the smallest useful claim: naming the calling workload. We make that claim with a public-key infrastructure.
The certificate names the service
We begin with a public-key infrastructure, or PKI. A certificate authority that we trust issues each participating service a certificate binding its identity to a public key.
Each service receives an ECDSA P-256 key pair. Its certificate carries the public half; the service keeps the private half and uses it to sign. Anyone holding the certificate can verify that signature without gaining the ability to produce another one. Successful verification therefore shows that the caller controls the private key belonging to the identity in the certificate.
We encode the identity as a URI. A production storefront certificate could, for example, name spiffe://inttegro/production/storefront.
The URI gives the identity a structure. It names both the environment and the workload inside our trust domain. A development certificate therefore cannot quietly become a production identity, and two services cannot present themselves as the same workload merely because they run on the same network.
We took that shape from SPIFFE's workload identity model, borrowing its discipline of one URI for one workload in one trust domain. Our certificate issuance and rotation machinery does not constitute a complete SPIFFE deployment.
When a request arrives, our authorization service verifies the certificate chain, validity period, key usage and registered fingerprint. It also checks whether the certificate, its issuer or the service identity has been revoked. If those checks pass, we know that the caller controls the private key assigned to that named workload.
The certificate has now proved its one claim: which service controls the signing key. It cannot tell us which request that service intended to make. We have to bind the identity to the request next.
The assertion names the request
Knowing who holds the key lets us ask the next question: what did that service intend to send? Before the storefront makes the request, it builds a small payload and signs its canonical form. The payload names the service and signing key, the application and principal context, the time and nonce, and three facts about the HTTP request: method, exact request target and SHA-256 body hash.
signed request assertionservice · signing key · workload or subjectapplication · relationshipmethod · target · body digesttimestamp · noncebase64url(payload).base64url(signature)The API does not reinterpret the target. The signed value includes the path and query string it actually received. It does not parse and rebuild the body before checking it. It hashes the bytes it actually received. If a proxy changes the target, if a caller signs one body and sends another, or if one field is added after signing, verification fails.
We reject any input that permits two interpretations: unknown assertion fields, duplicate JSON properties, duplicate header parameters and malformed encodings. A verifier should have only one way to understand the claim it receives.
One changed byte
Take the product request again. The storefront signs:
POST /products/create{"name":"Original body"}POST /products/create{"name":"Changed body"}Both bodies are valid JSON. Both requests can truthfully say they came through the storefront. But their hashes differ, so the signature cannot authorize the received request. The API rejects it before a product handler gets to decide what the new name means.
The changed byte exposes the limit we want. The service cannot reuse this assertion as a login; it can use it only to support the request it signed.
At this point the API knows which workload signed the request and that the request arrived intact. One question remains before the handler may run: is that workload allowed to perform this operation in this context?
The authorization service makes the decision
Certificate and signature verification carry us to policy: may this workload perform this operation in this context? Rather than let every receiving service invent an answer, the API refers the assertion and request facts to our authorization service.
assertion + method + target + body hashThe authorization service first repeats the claims on which policy depends: the registered certificate, signature, freshness and request binding. It then maps the real route to an action—POST /products/create becomes “create product”—and asks whether this caller may perform that action.
The answer depends on more than the route. The application must exist and be active. A service acting through a parent-child application relationship must present the expected relationship. A request made for a person is not treated as though it came from an autonomous workload, and workload-only access exists only for services and routes that explicitly allow it.
If the checks pass, the authorization service creates a short-lived session containing the actor, action, application, principal and the evidence behind the decision. The receiving API now has authority to handle this request. It has not acquired authority to make every request that might follow from it.
That distinction becomes important as soon as fulfilling one request requires another service.
Authority shrinks as it moves
Consider a real chain. A caller sends POST /orders/send_invoice. Handling that request requires the commerce API to call POST /chimes/send. The messaging service, in turn, may call POST /customers/lookup to resolve the recipient.
The first authorization decision covers only the first request. It says that the caller may ask the commerce API to send this invoice. It does not say that the caller may invoke the messaging endpoint directly, nor that the messaging service may read any customer it chooses.
Passing the original assertion down the chain would erase those boundaries. The assertion names the original caller, target and body; each downstream request has a different caller, target and body. Reusing it would either fail its own integrity checks or force us to weaken those checks until the credential became a master key.
- Source requestPOST /orders/send_invoice
- Permitted requestPOST /chimes/send
- Permitted requestPOST /customers/lookup
Only declared edges. Never the original credential.
Instead, the authorization service mints a short-lived bearer session from the approved parent request. The commerce API can redeem it for POST /chimes/send only because policy declares that edge. The messaging service can then reach POST /customers/lookup only through another declared edge.
At every hop, we bind the new request to its own method, target and body hash, verify that the parent session remains active and check the application relationship again. The original authority stays behind. The next service receives a smaller authority for one known step.
Each hop therefore receives less authority than the one before it, expressed for a new request rather than copied from the old one. This leaves an obvious question: could transport security have done all of this by itself?
Why not just use mTLS?
Transport identity stops at the peer on one connection. We use certificates, but we do not treat that peer identity as the authority carried by an application request.
Mutual TLS can prove the peer on one TLS connection. Real systems terminate TLS at load balancers, proxies and gateways. The next service may see a connection made by the intermediary, not the original workload. Even the OAuth mutual-TLS specification leaves secure forwarding of client-certificate metadata beyond a TLS-terminating intermediary to the deployment; see RFC 8705, section 6.5.
A request-bound signature carries the claim across that boundary. It does not depend on a proxy copying an identity header and everyone agreeing that the header is trustworthy. The receiving service can recompute the request hash, and the authorization service can verify the signature against the registered certificate.
HTTP Message Signatures develops related ideas: sign the message components that matter, bind the key context, and limit the claim in time. Our internal assertion protocol does not implement RFC 9421, and we should not describe it as though it does.
Once authorization happens at the application layer—and may happen again at each service boundary—we need to be able to explain each decision after the request has finished.
Record the proof, not the secret
Each hop therefore leaves an audit record. If we cannot later explain why a request passed or failed, we have preserved only the result and lost the decision. We keep the service identity, environment, action, application and relationship context, certificate and issuer fingerprints, signature hash, request binding, timestamp, request and trace identifiers, and the reason it was allowed or denied.
We do not keep the private key. We do not turn the Authorization header into an audit field. We hash the nonce before recording it. Fingerprints and hashes let us correlate a decision without copying the credential that made it possible.
That evidence has already shaped the tests. We exercise the complete caller → API → authorization-service flow; body tampering; stale assertions; unknown routes; revoked keys; and downstream delegation. One regression test asserts something especially important: the original signed assertion must not be reused as downstream authorization.
The same evidence lets us compare the design with systems that are already public. We did not invent workload identity, signed HTTP requests, relationship-based authorization or attenuated authority. The comparison matters at the point where each idea stops—and where another proof must begin.
Prior art
None of the examples below reproduces our system. Each solves a recognisable part of the problem. Their limits matter because a proof that answers one security question should not quietly be promoted into the answer for another.
Read together, these systems show why no new cryptographic primitive would answer the whole problem. We compose several narrower proofs instead. Workload identity names the caller. A request signature binds its intent. Relationship policy decides whether the action is allowed. A short-lived session carries only the authority required for the next hop. Each proof stops before it starts pretending to answer another question.
Why this approach?
If these systems already exist, why not pick one and be done with it? Picking one would leave the other questions unanswered. Our problem sits at the seam between workload identity, request integrity, relationship policy and downstream delegation.
A request moving through Inttegro carries three kinds of context: the workload making the call, the application and principal on whose behalf it acts, and the exact HTTP operation it intends to perform. Losing any one of them changes the meaning of the request. Collapsing them into one credential makes that credential more powerful than the evidence behind it.
We pay for that separation. An online authorization service adds a dependency to the request path, and separate proofs create more machinery than one bearer token. We accepted the cost because the alternative moves complexity into a credential whose authority is harder to see, narrow or revoke.
So we did not choose against SPIRE, DPoP, SpiceDB or Biscuit. We chose to keep the questions they illuminate separate in our own system: who is calling, what did it send, for whom is it acting, is the action allowed now, and what is the smallest authority the next service needs?
A note on extensibility
We want to improve a decision made for today's threat, scale or operating environment without weakening the protocol that exists today. That does not require accepting whatever a caller sends. It requires leaving clear boundaries around the parts we may later replace.
Separating identity, request binding, policy and delegation preserves that room. We could change how certificates are issued, move keys into hardware, add a durable replay ledger, adopt a standard message-signature format, derive route actions from an API contract, or make application relationships more expressive. None of those changes requires us to collapse the other proofs or discard the evidence already attached to a decision.
The same applies to operating trade-offs. Today we ask the authorization service for a current answer when authority moves. If scale or availability later makes that cost unacceptable for a particular class of request, we can introduce a narrowly scoped, attenuated capability or a safe cache at that boundary. We would not have to turn every credential into an offline bearer token to do it.
Strict versioning makes those changes safer rather than harder. The current assertion has one canonical meaning and requires ECDSA P-256. A future version can bind more data or use another signing algorithm without teaching old verifiers to reinterpret old claims. Both versions can coexist while services move deliberately, and an audit record can still say which rules produced its decision.
These examples do not promise a roadmap. Some may never become necessary; better alternatives may appear. The architecture lets us improve identity, integrity, policy and delegation independently as circumstances change, rather than making the first implementation permanent.
Where this leaves us
When one service calls another, it does not present a badge that says “trust me, I am internal”. It makes a narrow claim: this registered workload, carrying this application and principal context, intended this exact request at this time.
The authorization service verifies the claim and decides whether the requested action is allowed. If the work must continue elsewhere, the next service receives narrower, short-lived authority tied to its own request. The original credential stops where its original meaning stops.
Our services authenticate each other through that chain of evidence. Keeping every proof within its boundary prevents them from mistaking identity for permission.