Limit surface access by agent identity

Add an Identity element to a surface to derive a stable DID for each agent, then write an OPA policy that gates requests based on that identity.

This guide adds an Identity element to a surface so the gateway derives a stable DID for each agent that calls or answers through it, and a surface policy that gates requests by that identity. For background on how identity derivation works, see Agent identity and DIDs.

Without an Identity element, every caller is anonymous to policy evaluation. Rules that reference input.agent.did always evaluate to false, and audit logs carry no stable agent identifier across sessions.

Use this guide when:

  • You need to identify callers or upstream agents by a stable identifier that survives credential rotation.
  • You want OPA policies to allow or deny requests by agent DID.
  • Audit logs and metrics must be attributable to a specific agent across sessions.
  • You need to capture the identity of your managed agent or the upstream it calls, for audit or downstream trust chains.

You do not need an Identity element if your surface only needs to authenticate callers. Authentication alone is sufficient when you have no requirement for per-agent attribution or DID-based policy.

Surface target caller context MCP

Prerequisites

  • An existing surface with at least an Access Point and a Managed Agent configured. See Restrict surface access with API key authentication if you need to create one first.
  • Dashboard access with permission to create and edit surfaces and policies.
  • For From mTLS certificate mode: a client-leaf certificate already stored under Secrets → Certificates with kind set to Client leaf.
  • For From API Key mode: an active API key already stored under Secrets → API Keys.
  • For From JWT Claims mode: a Caller Context element using JWT Bearer authentication on the same surface. To add one, follow Restrict surface access with API key authentication and select JWT Bearer as the authentication method instead of API key.

Steps

Choose an identity slot

The Identity element can sit in three positions on the surface canvas. Each position maps to a different edge in the request–response pipeline:

SlotEdgeWhat it identifies
Inbound callerCaller → Managed Agent (request leg)Who is calling your surface from outside.
Protected agentManaged Agent → Caller (response leg)Who your managed agent is: resolved from the managed agent’s response.
External agentExternal Target → Managed Agent (response leg)Who the upstream target is that your managed agent called.

Transit Points support a fourth per-upstream slot. See Apply policy to outbound calls for Transit Point configuration.

Most operators start with the Inbound caller slot: derive the caller’s DID so policies and audit logs record who made each request.

Add the Identity element to the canvas

  1. Open Surfaces in the dashboard and select the surface to configure.
  2. In the left palette, find the Identity element under Enhancements.
  3. Drag Identity onto the canvas and drop it on the edge that matches the slot you chose in Step 1.
    • Inbound caller: drop on the arrow between the Access Point and the Managed Agent node.
    • Protected agent: drop on the return arrow from the Managed Agent back toward the Access Point.
    • External agent: drop on the arrow between the External Target and the Managed Agent node.
  4. Select the Identity node. A context banner at the top of the configuration panel confirms which slot the node occupies and what it identifies.

Configure the extraction type

Select an extraction type from the Identity Extraction Type dropdown. The type determines how the gateway derives the agent’s DID.

TypeDerives the DID fromPrerequisite
From PayloadNamed fields in the request or response body.Agents that include identity metadata in their payloads.
From API KeyA stored API key identifier.An active API key under Secrets → API Keys.
From mTLS certificateA stored client-leaf certificate.A certificate under Secrets → Certificates with kind set to Client leaf.
From JWT ClaimsA claim in the validated bearer token.A Caller Context element using JWT Bearer auth on the same surface.
Static DIDA fixed value you configure.None — use for stub agents and integration tests only.

From Payload (identity metadata)

Derives the DID from named fields in the request or response body. The same field values always produce the same DID.

  1. Select From Payload (identity metadata).

  2. Select Configure Fields… to open the field editor.

  3. In the field editor, paste a sample payload that your agent sends. The editor extracts a JSON schema from it.

  4. Mark the fields that uniquely identify the agent by enabling "x-identity": true on each one. Only marked fields contribute to the DID hash. All other fields remain available to policy evaluation but do not affect the DID.

    Example: an agent distinguished by LLM provider and model name.

    {
      "type": "object",
      "properties": {
        "agentIdentity": {
          "type": "object",
          "properties": {
            "llmInfo": {
              "type": "object",
              "properties": {
                "provider": { "type": "string", "x-identity": true },
                "model":    { "type": "string", "x-identity": true }
              }
            }
          }
        }
      }
    }
  5. Optionally enable Strip raw identity metadata after injection. When on, the raw _meta.[field] block is removed from the forwarded request after the gateway has injected the signed credential. This prevents the raw identity block from reaching the managed agent.

  6. Close the editor.

The meta_field defaults to agentIdentity on the request leg and serverIdentity on the response leg. These match the A2A, AP2, and MCP extension conventions. Change the field name only if your agents use a different key.

For background on how the hash is computed, see Agent identity and DIDs.


From API Key

Derives the DID from a stored API key identifier. The derived DID is stable across key rotation. See Agent identity and DIDs for how this works.

  1. Select From API Key.
  2. In the API Key dropdown, select an active API key. If the list is empty, create one under Secrets → API Keys first.

From mTLS certificate

Derives the DID from a stored client-leaf certificate. As with API key mode, the DID is stable across certificate rotation. See Agent identity and DIDs for how this works.

  1. Select From mTLS certificate.
  2. In the Client Certificate dropdown, select a certificate. Only active certificates with kind Client leaf appear in this list. If the list is empty, add a certificate under Secrets → Certificates and set its kind to Client leaf.

From JWT Claims

Derives the DID from a claim in the caller’s validated bearer token. One stable DID is minted per distinct claim value. See Agent identity and DIDs for the full derivation model.

  1. Select From JWT Claims.
  2. In JWT Claim, enter the claim name that identifies the agent. The default value is oid, the Microsoft Entra Agent ID object identifier.
  3. In Namespace Claims, enter any additional claims to fold into the identity hash, separated by commas. For example, entering iss, tid prevents the same oid value across different tenants from resolving to the same DID. All listed claims must be present in the token or the request fails with a 422 response.

Static DID

Always presents the same fixed DID for all requests through this slot. Useful for stub agents and integration tests.

  1. Select Static DID.
  2. In the Static DID field, enter the DID, for example did:web:example.com:agents:my-stub.

Create a surface policy that uses the derived identity

Once the Identity element is configured, the derived DID is available in OPA policy context as input.agent.did. Use a surface-scope (surface.policy) policy to gate requests based on that value.

  1. In the sidebar, select Policies.
  2. Select the Agent Surfaces tab.
  3. Select Define Agent Surface Policy.
  4. Fill in the form:
    • Name: for example, require-known-agent.
    • Type: select Agent Surfaces.
  5. In Policy Content (Rego), write the policy rule.

Deny anonymous callers (any DID is sufficient)

The simplest check: the request must have produced a DID. Callers that carry no identity extension or credential are denied.

package surface.policy

default allow = false

allow if {
  input.agent.did
}

Require a specific agent DID

Restrict access to one or more known agents by DID.

package surface.policy

default allow = false

allowed_dids := {
  "did:webvh:gateway.example.com:channel:9fd06096-0188-4979-aa99-8171cde7090a",
  "did:webvh:gateway.example.com:channel:3b2c71de-a482-4c22-b9f6-e90d3fa01abc"
}

allow if {
  allowed_dids[input.agent.did]
}

Combine identity with caller authentication

When the surface also has a Caller Context element, require both a valid credential and a resolved DID.

package surface.policy

default allow = false

allow if {
  input.source_auth.subject
  input.agent.did
}

Gate on JWT claim values directly

For surfaces using From JWT Claims, the full token claim set is also available on input.source_auth.claims.

package surface.policy

default allow = false

allow if {
  input.source_auth.claims.tid == "<expected-tenant-id>"
  input.agent.did
}

Select Save. The policy appears in the list with status Enabled.

Attach the policy to the surface

A policy definition has no effect until it is attached to a surface using the Policy canvas element.

  1. Open Surfaces and select the surface you added the Identity element to.
  2. In the left palette, find Policy under the Security & Policy category.
  3. Drag Policy onto the canvas and drop it on the arrow leaving the Access Point to evaluate the rule on every inbound request.
  4. Select the Policy element to open its config panel. In the Policy Definition dropdown, select the policy definition you created in Step 4. Only enabled Agent Surfaces definitions appear.
  5. Select the Save changes button (the blue disk icon in the toolbar) or press Cmd+S (macOS) / Ctrl+S (Windows/Linux).

Confirm

Replace <GATEWAY_HOST> and <CHANNEL_ROUTE> with the values shown in the Access Point panel under Channel Route.

Test 1: request without identity returns 403

curl -k -X POST "https://<GATEWAY_HOST><CHANNEL_ROUTE>" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"message/send","params":{}}'

The -k flag disables TLS certificate verification. Use this for local testing only. Remove it in production.

Expected: 403 Forbidden. The policy ran, input.agent.did was absent, and the default false rule applied.

Test 2: request with identity metadata returns 200

curl -k -X POST "https://<GATEWAY_HOST><CHANNEL_ROUTE>" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "message/send",
    "params": {
      "message": { "role": "user", "parts": [{"text": "hello"}] }
    },
    "extensions": ["https://fabric.affinidi.io/extensions/agent-identity/v1"],
    "metadata": {
      "https://fabric.affinidi.io/extensions/agent-identity/v1": {
        "agentIdentity": {
          "provider": "openai",
          "model": "gpt-4"
        }
      }
    }
  }'

The -k flag disables TLS certificate verification. Use this for local testing only. Remove it in production.

Expected: 200 OK. The gateway extracted the provider and model fields, derived a DID, and the policy allowed the request.

To confirm the DID was derived, open Monitoring → Logs and filter for managed_identity_resolve events on this surface. Each event carries the derived DID and the resolution result (ok_new for first derivation, ok_cached for a reused DID).

Troubleshooting

SymptomLikely causeFix
Every request returns 403 even with identity fields in the payload.The Identity element has no extraction type selected, or the field names in the schema do not match the actual request payload.Open the Identity node, confirm an extraction type is selected, and check that the meta_field value and x-identity field paths match what your agent sends.
The Identity node has a red error badge after selecting From JWT Claims.No Caller Context element using JWT Bearer is on this surface.Add a Caller Context element and set Authentication Method to JWT Bearer before saving.
The Client Certificate dropdown is empty in From mTLS certificate mode.No active certificate with kind Client leaf exists in the secrets store.Add or activate a certificate under Secrets → Certificates and set its Kind to Client leaf.
input.agent.did is absent during policy evaluation even though the Identity element is configured.The request did not carry the credential or extension the configured mode expects.Confirm the inbound request includes the payload extension (payload mode), the correct API key header (API key mode), or a valid client certificate (mTLS mode).
Derived DIDs change after a gateway restart.TG_IDENTITY_HASH_PEPPER is not set. An ephemeral pepper is generated at startup, so DIDs derived without it do not survive restarts.Set TG_IDENTITY_HASH_PEPPER to a stable hex string of at least 32 bytes on every gateway node.

Next steps