This episode secures the gateway from the entry side: request authentication via API keys and JWT/OIDC, per-route and per-tool-invocation access authorization, plus transport security with TLS, mTLS, and encrypted connections to providers.

In episode 11 you learned to store credentials safely: provider API keys go into a secret manager, are rotated regularly, and get only least-privilege access. That secures the credential side, but there's one layer left: the gateway's own front door. Incoming requests must be able to prove who sent them, which routes they may access, which tools they may call, and all conversation between the client, gateway, and provider must travel over encrypted paths.
Episode 12 brings together three pillars of perimeter security: authentication of incoming requests (API keys, JWT, OIDC), authorization of route and tool invocation access, and transport security plus provider connections.
Before a request may enter the route engine, 9router must know who sent it. This is called authentication — identity verification. Without it, anyone can fire at your model endpoints and drain your budget, or use the gateway as a free public proxy.
9router places authentication at the gateway level, before routing. That means all routes inherit the same protection, and you don't need to write auth logic in every service. Every config change is tested first with 9router validate. The base configuration in 9router.yaml:
auth:
api_keys:
- name: mobile-app
key_env: API_KEY_MOBILE
- name: internal-service
key_env: API_KEY_INTERNAL
jwks_url: https://idp.example.com/.well-known/jwks.json
required_claims:
- sub
- scopeThe two most common mechanisms: API keys for machine-to-machine, and JWT (JSON Web Token) for end-user-held identities. Both can be active at the same time.
An API key is a secret token the client sends via a header. It suits internal services, mobile apps, and CI/CD pipelines — places where no "human user" logs in. The key configuration point: keys are never hardcoded in config files, but read from the environment, exactly the pattern you learned in episode 11.
The client sends the key via the Authorization header:
curl https://gw.example.com/v1/chat \
-H "Authorization: Bearer ${API_KEY_MOBILE}" \
-d '{"messages": [{"role": "user", "content": "Halo"}]}'In 9router, the received key is matched against the list in auth.api_keys. Each key gets its own consumer identity, which will later be used for rate limiting and audit — two topics covered in the next episodes. If the key is unknown, the gateway answers 401 Unauthorized before the request touches any provider.
9router auth list
9router auth verify --key api_key_mobileThe verification runs at the outermost layer, so a leaked key can be revoked per consumer without stopping the gateway — just remove it from the list and rotate a replacement key. Never put keys in URLs or bodies; the header is the right place.
For applications serving humans, API keys don't fit — there's no way to map a key to a user's role. The solution is a JWT issued by an Identity Provider (IdP) through OpenID Connect (OIDC). The token contains claims like sub (who the user is), scope (what they may do), and exp (when it expires).
9router validates the token by fetching JWKS from the IdP, checking the signature and validity window, then extracting claims for use in the authorization layer:
auth:
jwks_url: https://idp.example.com/.well-known/jwks.json
issuer: https://idp.example.com
audiences:
- gateway-prod
claims_to_metadata:
- source: sub
target: request.consumer_id
- source: scope
target: request.scopesAfter verification, the sub and scope claims attach to the request as metadata. That's the fuel for route and tool authorization in the next section. Validation failures (expired token, wrong issuer, mismatched signature) return 401 or 403 depending on the case.
Info
For service-to-service inside the cluster that doesn't use an IdP, consider mTLS or service account tokens — don't fall back to no authentication just because everyone "knows each other". The inner perimeter matters as much as the outer one.
Authentication proves identity; authorization decides what that identity may do. In 9router the two are separated so policy can be expressed per route and per tool. A user may call the chat route, but not the route that executes payment tools.
Route authorization is declared as part of the route definition:
routes:
- name: chat-general
match:
intent: general_chat
model: gpt-4o-mini
provider: openai-prod
access:
require_scope: chat:read
- name: tools-execute
match:
intent: execute_action
model: gpt-4o
provider: openai-prod
access:
require_scope: tools:writeMeanwhile, tool authorization is separated so a single route with many tools still has granular control. Tool invocation only executes if the consumer meets the requirements:
tools:
- name: create-ticket
access:
require_scope: tickets:write
execute: false
- name: read-orders
access:
require_scope: orders:read
execute: trueIf a request passes authentication but doesn't meet require_scope, the gateway rejects it with 403 Forbidden and records the attempt. This pattern prevents one leaked credential from opening every tool at once.
Two authorization styles are commonly used together: RBAC (Role-Based Access Control) maps roles to scopes, while ABAC (Attribute-Based Access Control) decides based on request attributes — origin region, department, time, or user level. RBAC answers "who you are, what your position is", ABAC answers "does this context allow it or not".
auth:
roles:
- name: support-agent
scopes: [chat:read, tickets:write]
- name: analyst
scopes: [chat:read, orders:read]
abac_rules:
- name: block-offshore-tools
condition: request.region != eu
action: deny
applies_to: tools-executeABAC rules are evaluated after RBAC. So even though a support-agent has the tickets:write scope, the block-offshore-tools rule can deny execution when the request comes from outside Europe — an exact example you'll dig into when discussing data residency in episode 14.
The last layer is transport security: make sure data doesn't travel in readable form along the way. The basic rule is TLS everywhere. 9router only serves HTTPS requests, and for the strictest environments, mTLS also forces the client to present a certificate — two-way mutual verification.
tls:
min_version: "1.3"
cert_file: /etc/9router/tls/cert.pem
key_file: /etc/9router/tls/key.pem
mtls:
enabled: true
ca_file: /etc/9router/tls/ca.pemConnections to LLM providers must also be locked down. The API keys you store in the secret manager are sent only over TLS, provider base URLs must be https, and for internal networks use VPC peering or private links so model traffic never crosses the public internet:
providers:
- name: openai-prod
type: openai
base_url: https://api.openai.com/v1
api_key_env: OPENAI_API_KEY
verify: true
- name: azure-private
type: azure_openai
base_url: https://dev-gw.azure.net/v1
api_key_env: AZURE_OPENAI_KEY
private_endpoint: trueverify: true ensures the provider's server certificate is verified, and private_endpoint asserts the connection only uses a private path — combine this with the policies from episode 6 (blocked intents) and episode 11 (least privilege), and the security chain is closed from client to model.
Episode 12 closes the gateway perimeter with three layers: authentication via API keys for machine-to-machine and JWT/OIDC for human identities, granular authorization per route and per tool invocation with RBAC plus ABAC, and transport security in the form of TLS, mTLS, and encrypted provider connections. Now every request must have a proven identity, proper permissions, and a secure path.
Key takeaways:
Your gateway now has a locked door. In episode 13 we face its dark side: Safeguards Against Malicious Requests — detecting unsafe prompts and adversarial inputs, enforcing prohibited-content and PII policies, plus circuit breakers and rate limits to prevent provider abuse. See you there!