Skip to main content
When you configure server-side API keys on your AI Gateway, your endpoint becomes publicly accessible—anyone with the URL can make requests. This guide shows how to add an authorization layer using secrets and Traffic Policy to protect your gateway.

The problem

Without authorization, a gateway configured with server-side API keys is open to abuse:
on_http_request:
  - type: ai-gateway
    config:
      providers:
        - id: openai
          api_keys:
            - value: ${secrets.get('openai', 'api-key')}
Anyone who discovers your gateway URL can make unlimited requests using your API keys. You need to add your own authorization layer.

Solution: API key authorization

Require clients to provide an access token that you control. Generate your own tokens, store them in ngrok Vaults & Secrets, and validate them in your Traffic Policy before processing requests.
1

Create a secret for your access token

Create a vault and secret to store your access token:
# Create a vault for gateway secrets
ngrok api vaults create --name "gateway-auth"

# Create a secret with your access token (use a long, random string)
ngrok api secrets create \
  --name "access-token" \
  --value "your-secret-access-token-here" \
  --vault-id "vault_xxxxxxxxxxxxx"
Generate a secure access token using: openssl rand -base64 32
2

Add authorization to your Traffic Policy

Use an expression to compare the Authorization header against your secret and reject unauthorized requests:
on_http_request:
  # Reject unauthorized requests
  - expressions:
      - req.headers['authorization'][0] != 'Bearer ' + secrets.get('gateway-auth', 'access-token')
    actions:
      - type: custom-response
        config:
          status_code: 401
          headers:
            content-type: application/json
          body: '{"error": {"message": "Invalid or missing authorization token", "type": "authentication_error"}}'

  # Process authorized requests through the gateway
  - actions:
      - type: ai-gateway
        config:
          providers:
            - id: openai
              api_keys:
                - value: ${secrets.get('openai', 'api-key')}
            - id: anthropic
              api_keys:
                - value: ${secrets.get('anthropic', 'api-key')}
3

Update your client configuration

Clients must now include the access token in their requests:
import OpenAI from 'openai';

const client = new OpenAI({
  baseURL: "https://your-gateway.ngrok.app/v1",
  apiKey: "your-secret-access-token-here"  // Your gateway access token
});

const response = await client.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: "Hello!" }]
});
Or with curl:
curl https://your-gateway.ngrok.app/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-secret-access-token-here" \
  -d '{
    "model": "gpt-4o",
    "messages": [{"role": "user", "content": "Hello!"}]
  }'

Multiple access tokens

Support multiple clients with different access tokens. Use set-vars to define the list of valid tokens, then check membership with in:
on_http_request:
  # Define valid authorization headers
  - actions:
      - type: set-vars
        config:
          vars:
            - valid_auth_headers:
                - ${'Bearer ' + secrets.get('gateway-auth', 'client-a-token')}
                - ${'Bearer ' + secrets.get('gateway-auth', 'client-b-token')}
                - ${'Bearer ' + secrets.get('gateway-auth', 'client-c-token')}

  # Reject if token is not in the valid list
  - expressions:
      - "!(req.headers['authorization'][0] in vars.valid_auth_headers)"
    actions:
      - type: custom-response
        config:
          status_code: 401
          headers:
            content-type: application/json
          body: '{"error": {"message": "Invalid or missing authorization token", "type": "authentication_error"}}'

  - actions:
      - type: ai-gateway
        config:
          providers:
            - id: openai
              api_keys:
                - value: ${secrets.get('openai', 'api-key')}
This approach makes it easy to add or remove client tokens by updating the list.

Using X-API-Key header

If you prefer using an X-API-Key header instead of Authorization: Bearer:
on_http_request:
  - expressions:
      - req.headers['x-api-key'][0] != secrets.get('gateway-auth', 'access-token')
    actions:
      - type: custom-response
        config:
          status_code: 401
          headers:
            content-type: application/json
          body: '{"error": {"message": "Invalid or missing API key", "type": "authentication_error"}}'

  - actions:
      - type: ai-gateway
        config:
          providers:
            - id: openai
              api_keys:
                - value: ${secrets.get('openai', 'api-key')}
Configure the OpenAI SDK to use a custom header:
import OpenAI from 'openai';

const client = new OpenAI({
  baseURL: "https://your-gateway.ngrok.app/v1",
  apiKey: "not-used",  // Required but not used
  defaultHeaders: {
    "X-API-Key": "your-secret-access-token-here"
  }
});

Adding rate limiting

Combine authorization with rate limiting for additional protection:
on_http_request:
  # Reject unauthorized requests
  - expressions:
      - req.headers['authorization'][0] != 'Bearer ' + secrets.get('gateway-auth', 'access-token')
    actions:
      - type: custom-response
        config:
          status_code: 401
          headers:
            content-type: application/json
          body: '{"error": {"message": "Invalid or missing authorization token", "type": "authentication_error"}}'

  # Apply rate limiting to authorized requests
  - actions:
      - type: rate-limit
        config:
          name: ai-gateway-limit
          algorithm: sliding_window
          capacity: 100
          rate: 100/min
          bucket_key:
            - req.headers['authorization']

  # Process through AI Gateway
  - actions:
      - type: ai-gateway
        config:
          providers:
            - id: openai
              api_keys:
                - value: ${secrets.get('openai', 'api-key')}

Combining with IP restrictions

For additional security, restrict access to specific IP addresses using CIDR notation:
on_http_request:
  # Restrict to specific IPs (CIDR notation required)
  - actions:
      - type: restrict-ips
        config:
          enforce: true
          allow:
            - 203.0.113.0/24    # Allow entire subnet
            - 198.51.100.50/32  # Allow single IP

  # Reject unauthorized requests
  - expressions:
      - req.headers['authorization'][0] != 'Bearer ' + secrets.get('gateway-auth', 'access-token')
    actions:
      - type: custom-response
        config:
          status_code: 401
          headers:
            content-type: application/json
          body: '{"error": {"message": "Invalid or missing authorization token", "type": "authentication_error"}}'

  - actions:
      - type: ai-gateway
        config:
          providers:
            - id: openai
              api_keys:
                - value: ${secrets.get('openai', 'api-key')}

Alternative: JWT validation

For more complex authentication scenarios, use the jwt-validation action to validate JWTs from your identity provider:
on_http_request:
  # Validate JWT from your identity provider
  - actions:
      - type: jwt-validation
        config:
          issuer:
            allow_list:
              - value: https://your-auth-provider.com/
          audience:
            allow_list:
              - value: your-ai-gateway
          http:
            tokens:
              - type: access_token
                method: header
                name: Authorization
                prefix: "Bearer "
          jws:
            allowed_algorithms:
              - RS256
            keys:
              sources:
                additional_jkus:
                  - https://your-auth-provider.com/.well-known/jwks.json

  # Process authorized requests
  - actions:
      - type: ai-gateway
        config:
          providers:
            - id: openai
              api_keys:
                - value: ${secrets.get('openai', 'api-key')}

Rotating access tokens

To rotate your access token without downtime:
  1. Add the new token to your secrets
  2. Update your Traffic Policy to accept both tokens temporarily
  3. Update clients to use the new token
  4. Remove the old token from your policy and secrets
# Temporary policy accepting both tokens during rotation
- actions:
    - type: set-vars
      config:
        vars:
          - valid_auth_headers:
              - ${'Bearer ' + secrets.get('gateway-auth', 'old-token')}
              - ${'Bearer ' + secrets.get('gateway-auth', 'new-token')}

- expressions:
    - "!(req.headers['authorization'][0] in vars.valid_auth_headers)"
  actions:
    - type: custom-response
      config:
        status_code: 401
        headers:
          content-type: application/json
        body: '{"error": {"message": "Invalid or missing authorization token", "type": "authentication_error"}}'

Best practices

  1. Use strong tokens - Generate access tokens with at least 32 bytes of entropy
  2. Never share tokens - Issue different tokens to different clients
  3. Rotate regularly - Change access tokens periodically
  4. Monitor usage - Use observability to detect unusual patterns
  5. Layer defenses - Combine authorization with rate limiting and IP restrictions
  6. Use HTTPS - Always use HTTPS endpoints (ngrok handles this automatically)

Next steps