Skip to content

Forwarded Identity Signing

AuthProxy can attach an HMAC-SHA256 proof to the identity headers it sends to a backend. Signing is opt-in per flow. An installation with no ForwardedIdentity blocks keeps its existing unsigned forwarding behavior.

Signed service backends and signed tenant-list endpoints require HTTPS and a configured client certificate outside AuthProxy’s explicit LocalDevelopment profile. Configure Backend TLS before enabling signing in a deployed environment. Signing protects the forwarded identity proof; it does not replace transport authentication or backend ingress restrictions. Installations that do not configure signing retain their existing transport behavior.

Use an Arc receiver with the v1 contract (Arc 22.168.0 or a compatible newer version). Deploy AuthProxy signing before requiring proofs at the receiver. AuthProxy does not require an Arc package upgrade to produce this wire format.

Use WithSignedForwardingTo(backend) to pair the AuthProxy signer and Arc verifier without hand-writing environment configuration. Run mode generates a fresh secret per backend; publishing requires an explicitly provisioned secret parameter. The pairing also configures resource-based selectable-tenant endpoints on that backend.

Set Cratis:AuthProxy:Services:<name>:ForwardedIdentity:

{
"Audience": "orders-api",
"KeyId": "orders-2026-01"
}

Supply the third property, SecretBase64, through your secret provider, for example the environment configuration key Cratis__AuthProxy__Services__orders__ForwardedIdentity__SecretBase64. Generate a fresh cryptographically random 32-byte secret and standard-base64 encode it. Do not put credentials in committed configuration. The published Arc test-vector key is refused at startup.

Property Requirement
Audience Nonblank, 1–128 printable ASCII characters; must exactly match the receiver’s audience.
KeyId Case-sensitive [A-Za-z0-9_-]{1,64}; must identify a key provisioned at the receiver.
SecretBase64 Strict standard base64 encoding exactly 32 random bytes, with no whitespace.

A partial or invalid block stops startup and identifies the configuration path, without printing the secret. There is no global signing key or fallback key. Each backend audience must have distinct key material. Service aliases may share material only with identical audience, key ID and normalized backend destination (scheme, host, port, credentials and exact path). Missing or ambiguous destinations, including query/fragment-bearing base URLs and DNS-discovered replica routing, cannot establish alias ownership. Sharing material across audiences, under different key IDs or across destinations is refused. Startup identifies only the two configuration paths. Do not reuse OIDC, Data Protection or on-behalf assertion keys.

The service block covers:

  • Normal proxied identity-bearing requests to that service’s backend and frontend.
  • Its bearer-route forwards, using the service selected by the bearer route, not a caller-supplied service marker.
  • AuthProxy’s direct /.cratis/me identity-details calls to that service.
  • Sign-in callbacks to that service. Each delivery attempt gets a fresh proof; a configured callback that cannot be signed is not sent. Unsigned services keep their existing callback behavior in mixed deployments.

A service whose frontend and backend verify proofs must provision the matching audience and key at both receivers, or be split into separately configured services. Anonymous requests without principal or on-behalf identity material remain unsigned; tenant or service selectors alone do not create an identity.

On-behalf grant checks and session notifications retain their per-call attestations, bound to purpose, method, target and body. They do not carry the v1 header envelope and are reported as attested, not envelope-signed. Their signed service’s backend requires HTTPS outside LocalDevelopment, and their backend hosts receive the configured client certificate.

Invitation staging/exchange, link exchange, and sign-in notifications are reported for transport only. Their body identity is not covered by the v1 envelope; invitation and sign-in notifications retain their flow-specific attestations.

When any forwarded-identity signing is configured, callback backend URLs, resolved client-credential verification endpoints, invitation staging/exchange, link exchange, sign-in notification, capability verification, and tenant verification URLs must use HTTPS outside LocalDevelopment. A client-credential VerificationPath may be an absolute HTTP/HTTPS URL or backend-relative; validation checks the same resolved endpoint the verifier calls, not just the backend URL. Leading-slash paths, including the default /.cratis/client-credentials/verify, retain the backend’s scheme and authority and resolve at its origin root. An empty or relative backend URL produces a validation failure when the endpoint needs a backend, rather than a URI parsing exception.

Client-credential and capability verifier destinations, queued callback destinations, resolved tenant-verification URLs, and discovered OIDC token endpoints are checked against the current configuration before a client is created. Queued callbacks also use the current signing settings on every attempt. LocalDevelopment retains the existing HTTP allowance. Unsigned deployments retain their existing transport behavior.

Capability/tenant verifiers and identity providers do not extend the backend client-certificate host set. They receive the backend client identity only if their host is independently configured as a backend hop; see Backend TLS.

A service block does not sign TenantsEndpoint calls. Configure ForwardedIdentity on the selection resolver’s Options:

{
"Strategy": "Selection",
"Options": {
"TenantsEndpoint": "https://tenants.example.test/tenants",
"ForwardedIdentity": {
"Audience": "tenant-directory",
"KeyId": "tenants-2026-01"
}
}
}

Supply Cratis__AuthProxy__TenantResolutions__0__Options__ForwardedIdentity__SecretBase64 from the secret provider, adjusting the array index to the selection resolver’s position. If this block is absent, the tenant flow remains unsigned even when service flows are signed. This matters when a tenant endpoint also requires signed identity.

The tenant flow may reuse its own service’s key only with an identical audience and a TenantsEndpoint inside that service’s configured backend origin and base-path boundary. Scheme, host and port must match; path comparison is case-sensitive and segment-boundary aware. A frontend URL does not establish backend ownership. Selection options have no explicit service reference, so deployments that cannot establish this relationship from their URLs must use a separate tenant-endpoint key.

At the Arc receiver, enable both Cratis:Arc:TrustForwardedIdentityHeaders and Cratis:Arc:ForwardedIdentity:Enabled. Configure Cratis:Arc:ForwardedIdentity:Audience and matching ForwardedIdentity:Keys entries with KeyId and SecretBase64. Provision secrets through the receiver’s secret provider too. An Arc verifier may accept several keys during rotation; AuthProxy emits one active key per flow.

  1. Provision the receiving audience and new key without yet requiring proofs from unsigned producers.
  2. Enable AuthProxy signing for every applicable producer, including /.cratis/me and the independently configured tenant endpoint. Confirm each receiving host verifies real requests.
  3. Enable the receiver’s signed-only verification. There is no unsigned fallback in that mode. Missing, expired, replayed, malformed or invalid proofs cannot establish forwarded identity.
  4. During rotation, add the replacement key at receivers first, switch each producer’s KeyId and secret, wait out the verifier’s acceptance/replay window, and then remove the old key.

Keep clocks synchronized. Arc accepts timestamps up to 30 seconds in the future and 60 seconds old; each outbound attempt gets a fresh 256-bit nonce. Replay tracking is process-local, not a cluster-wide replay guarantee. Do not record proofs, nonces or shared secrets in request logs.

AuthProxy strips caller-supplied x-cratis-forwarded-identity-signature values and known forwarded-identity context headers, including older draft names, treating underscore spellings of protected names as their hyphenated equivalents. After identity injection, path rewrites and destination construction, it emits exactly one:

v1.<key-id>.<unix-seconds>.<base64url-nonce>.<base64url-hmac>

The proof binds the audience, key ID, timestamp, nonce, uppercase HTTP method, final escaped path and the sorted covered header set. Percent-triplet hex in the path is uppercased without decoding. The query string and body are not covered.

Covered headers are every x-ms-client-principal* and x-cratis-on-behalf* header, token client ID and scope, and both modern and legacy tenant and service headers. Values retain their exact printable ASCII transport representation after trimming only outer SP/HTAB. Internal commas, base64 padding and percent escapes are not normalized. At most 64 covered names and 32,768 canonical UTF-8 bytes are accepted. Repeated covered values, missing required principal fields, malformed paths and remaining controls or non-ASCII values are refused.

Arc also covers a custom Tenancy.HttpHeader when configured. AuthProxy signs only its fixed modern/legacy tenant headers; a receiver configured to consume another tenant header is not compatible with this producer configuration.

A configured identity-bearing request that cannot be signed is not sent unsigned. Proxy and bearer flows use their existing generic forwarding-failure response. Identity-details signing refusal is an indeterminate verification result and is refused even under BestEffort. Tenant signing refusal uses the existing unavailable result; it does not create an empty tenant list or confirm a tenant. Signed back-channel HTTP clients do not follow redirects, so proofs cannot be replayed automatically to another destination. Legacy unsigned clients retain their previous behavior.

Startup diagnostics list each proxied, bearer, identity-details, tenant-list and sign-in callback flow as signed or not signed, with only service, audience and key ID. On-behalf calls are listed as attested, not envelope-signed. Invitation staging/exchange, link exchange and sign-in notifications have transport-only rows. Check these rows when rolling out: signing a normal request does not prove that the tenant-list flow is signed. Runtime refusals log bounded reason codes, never principal JSON, header values, secrets, canonical input, nonces or MACs.

Signing is not tenant membership, authorization, a replacement for on-behalf assertions, or body integrity. Preserve TLS, backend ingress restrictions and existing authorization checks. Anyone able to verify a shared-key proof can also forge one; constrain access to each service’s signing material.