omarcevi.dev / workshops / deploy-agents / 4 · Secure

Step 4 · Secure

8 min

Send all outbound calls of the agent through Agent Gateway. Use a policy to block the compensation system. The code of the agent does not change.

checkpoint branch step-4-secure

How it works

Identity and Agent Gateway. On the left, the agent on Agent Runtime has its own Agent Identity. When the agent calls the HRIS and the ticketing system, it acts as itself with an ID token for its own principal. When the agent calls Google Calendar, it acts for the employee with the OAuth token of the employee. Auth Manager keeps that token. In the middle, Agent Gateway does these checks on each outbound call, in this sequence. 1 Agent Registry identifies the destination. 2 IAP examines the IAM access policy for the identity of the agent: allow the HRIS, the ticketing system and Google APIs, and deny the compensation system. 3 An optional semantic policy and Model Armor examine the content. On the right, the gateway allows the calls to the HRIS, the ticketing system and Google APIs. The compensation MCP server gets the error 403 Egress request is not authorized, and the gateway records this error in its log.
The identity of the agent when it calls a tool, and the checks that Agent Gateway does on each call.

Two types of authority. When the agent calls the HRIS, it acts as itself with an ID token for its own Agent Identity, and Cloud Run checks roles/run.invoker. For external APIs, the same procedure is 2-legged OAuth (client credentials). When the agent adds leave to your calendar, it acts for you, and you must give your consent one time (3-legged OAuth). Agent Identity Auth Manager controls the consent procedure and keeps the tokens in storage that Google manages. When the tool runs, Auth Manager gives your token to ADK.

Agent Gateway checks each outbound call in this sequence:

  1. Agent Registry identifies the destination. The gateway refuses a destination that is not in the registry, unless a policy names that destination.
  2. IAM access policy. IAP examines the policy for the identity of the agent. The policy tells which agents can reach which MCP server, endpoint or tool. Its conditions can use tool names or annotations, for example read_only_hint. A deny rule has priority over an allow rule.
  3. Content checks (optional). A semantic governance policy (Preview) compares each tool call that the model proposes with a rule in plain language. An example rule is “never get the compensation of a person”. Model Armor examines MCP tool calls for prompt injection and sensitive data.

Default deny. After you bind the agent to the gateway, the gateway refuses all calls that the policy does not allow. This includes the calls of the agent to Gemini, Sessions, Memory Bank and telemetry. For this reason, setup.sh registered a google-apis endpoint, and the policy allows it.

Egress and ingress. This lab uses an egress gateway (Agent-to-Anywhere), which controls the calls that the agent makes. An ingress gateway (Client-to-Agent) is in front of the agent. It examines the query and streamQuery requests with Model Armor or a semantic policy. An ingress gateway must be in the same project and region as the agent. You can bind one agent to the two types of gateway.

Who owns the policy. The security team or the platform team writes the access policy. The agent team registers its tools and asks for access. The policy file in this repo is short:

infra/gateway/egress-policy.rules.jsonthe deny rule
1{2  "description": "No agent in this project may call the compensation system.",3  "effect": "DENY",4  "principals": ["principalSet://agents.global.proj-NUMBER…/aiplatform/projects/NUMBER"],5  "operation": {"permissions": ["iap.googleapis.com/resources.egressViaIAP"]},6  "conditions": {"iap.googleapis.com": {"expression":7    "destination.agent_registry.resource_type == 'MCP_SERVER' && destination.agent_registry.mcp_server.name == 'projects/PROJECT_ID/…/mcpServers/…'"}}8}
The allow rules name the principal of this agent. The deny rule applies to all agents in the project. secure.sh writes the names from Agent Registry into the rules.

Run

Make sure that the gateway is ready. Setup started to build it in the background in step 1:

tail -1 .setup-gateway.log      # should say GATEWAY READY

Apply the access policy. Then bind the agent to the gateway (a new deployment, approximately 4 minutes):

./scripts/secure.sh
agents-cli deploy --project $PROJECT_ID --update-only --agent-identity --no-confirm-project \
  --agent-gateway-egress=projects/$PROJECT_ID/locations/europe-west1/agentGateways/workplace-gw \
  --update-env-vars GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false

The gateway examines the TLS traffic, thus the certificate-bound tokens of the agent cannot go through it without a change. The setting GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false makes the agent use unbound tokens for Google APIs. The gateway documentation requires this setting.

Ask the same salary question as in step 2. Then make sure that the HRIS still works:

agents-cli run --url "$AGENT_URL" --mode adk "What is the salary band for an L5 in Istanbul?"
agents-cli run --url "$AGENT_URL" --mode adk "How many leave days do I have?"

Read the denial in the log of the agent and in the log of the gateway:

gcloud logging read 'labels."event.name"="workplace.tool_blocked"' --freshness=15m --limit=3 \
  --format='value(timestamp,jsonPayload.tool,jsonPayload.reason)'
gcloud logging read 'resource.type="networkservices.googleapis.com/Gateway" AND jsonPayload.authzPolicyInfo.result="DENIED"' \
  --freshness=15m --limit=3 --format='value(timestamp,httpRequest.requestUrl,jsonPayload.mcpInfo.method)'

Expected result

[hr_policy_agent]: I'm sorry, but I can't access the compensation system right now, so I can't look up that salary band. Your People Partner can share the published bands. [leave_agent]: You have 14 available leave days this year (2 more are pending approval). 2026-10-27T11:42:10Z get_salary_band gateway_denied 2026-10-27T11:42:09Z https://compensation-mcp-…run.app/mcp

The code and the tools are the same as before. The answer changed because of a policy. The audit callback of the agent recorded gateway_denied, and the gateway recorded its own decision.

Check

./scripts/check.sh 4
✔ get_salary_band was refused: I'm sorry, but I can't access the compensation system… ✔ HRIS still answers (get_leave_balance) ✔ logged in Cloud Logging: get_salary_band gateway_denied ==> Step 4 checks passed.

No Agent Gateway in your project? Use the fallback

If .setup-gateway.log says GATEWAY FAILED, or if Agent Gateway is not available, block the compensation server with Cloud Run IAM. The code of the agent continues to call the server, but IAM refuses the call. The script shows the differences between this fallback and the gateway.

./scripts/fallback_step4.sh
agents-cli run --url "$AGENT_URL" --mode adk "What is the salary band for an L5 in Istanbul?"
./scripts/check.sh 4             # the log reason is permission_denied instead

IAM applies the removal of a grant in one to three minutes. If the agent still gives the salary band, wait one minute. Then ask again.

Optional: the employee's own Calendar (3-legged OAuth)

This is a demonstration for the instructor, because it needs an OAuth client and a consent screen. The procedure:

  1. Create an OAuth client (type Web application) in APIs & Services → Credentials. Enable the Google Calendar API.
  2. Create the Auth Manager provider:
    gcloud agent-identity auth-providers create calendar-3lo --location=europe-west1 \
      --three-legged-oauth-client-id=CLIENT_ID --three-legged-oauth-client-secret=CLIENT_SECRET \
      --three-legged-oauth-authorization-url=https://accounts.google.com/o/oauth2/v2/auth \
      --three-legged-oauth-token-url=https://oauth2.googleapis.com/token
  3. Add the redirect URL of the provider (…/authProviders/calendar-3lo/oauthcallback on agentidentitycredentials.googleapis.com) to the OAuth client.
  4. Give the agent permission to use the provider: gcloud agent-identity auth-providers add-iam-policy-binding calendar-3lo --location=europe-west1 --role=roles/agentidentity.user --member="$AGENT_PRINCIPAL"
  5. Set CALENDAR_AUTH_PROVIDER=projects/$PROJECT_ID/locations/europe-west1/authProviders/calendar-3lo in .env. Deploy the agent again. Now leave_agent has the tool create_calendar_event. At the first call, a client asks the employee for consent. This client must handle the adk_request_credential event of ADK. Then Auth Manager keeps the token.
If this fails
All questions fail immediately after you bind the agent (Reasoning Engine Execution failed)
The access policy is not active yet. A policy change gets to the gateway in one to three minutes. Wait for the change. Then ask again. Until then, the gateway log shows DENIED for …aiplatform.mtls.googleapis.com.
Calls to Google APIs through the gateway return 401
The agent still uses certificate-bound tokens. Deploy the agent again with --update-env-vars GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false.
The gateway shows DENIED for all calls, minutes after you apply the policy
The policy binding must use the project number as its target (secure.sh does this). The names in the conditions must use the project ID, as gcloud agent-registry … list shows them. The gateway log shows numbers, which can cause confusion.
498 errors
The agent uses a Google API hostname that is not in Agent Registry. Find the hostname in the gateway log (jsonPayload.enforcedGatewaySecurityPolicy.hostname). Add it to infra/gateway/platform-endpoints.txt. Then run ./scripts/setup.sh --only registry.
The agent still gives the salary band
The deployment that binds the agent to the gateway did not finish yet. Or you use the fallback, and IAM did not apply the change yet (one to three minutes).
Remove the block
For the fallback, run ./scripts/fallback_step4.sh --undo. For the gateway, remove the deny rule. Then run ./scripts/secure.sh again.