Saturday, December 27, 2025

Securing Azure API Management with Policies

Azure API Management sits between API consumers and backend services, and policies are the mechanism through which you enforce security controls, shape traffic, and transform messages. A well-composed policy pipeline in APIM produces consistent security posture across all APIs without requiring changes to backend code.

This post covers the four policies that should be in place on any production APIM instance: rate limiting, JWT validation, IP filtering, and response header cleanup.

1. Policy Scopes and Execution Order

Policies in Azure API Management can be applied at four levels:

ScopeApplied to
GlobalEvery API in the instance
ProductAll APIs assigned to a product
APIAll operations within a specific API
OperationA single HTTP operation (e.g., POST /orders)

Policies execute from the outermost scope inward. A global inbound policy runs before an API-level inbound policy, which runs before an operation-level policy. The <base /> element controls where the parent scope's policies execute relative to the current scope. Omitting <base /> prevents parent policies from running — which is occasionally intentional but more often an accidental override that disables security controls silently.

To open the policy editor:

  1. Navigate to API Management > APIs
  2. Select the target API or individual operation
  3. Open the Design tab
  4. Select the pencil icon next to Inbound processingOutbound processing, or Backend

2. Rate Limiting with rate-limit-by-key

The rate-limit-by-key policy restricts how many calls a single consumer can make within a rolling time window. The counter key is a policy expression — typically the subscription key, a JWT claim, or the caller's IP address.

Following is a policy limiting each subscription to 100 calls per 60 seconds:

<inbound>
  <base />
  <rate-limit-by-key
    calls="100"
    renewal-period="60"
    counter-key="@(context.Subscription?.Key ?? string.Empty)"
    increment-condition="@(context.Response.StatusCode >= 200 && context.Response.StatusCode < 300)"
  />
</inbound>

Setting increment-condition to only count successful responses avoids penalising consumers for backend errors outside their control. When the limit is exceeded, APIM returns 429 Too Many Requests. I recommend pairing this with a Retry-After header in the outbound section so clients know when the window resets.

Apply this policy at the Product scope so it covers all APIs within that product, then override at the operation level only for endpoints that legitimately require different limits.

3. JWT Validation

The validate-jwt policy validates a JSON Web Token before the request reaches the backend — checking the signature, expiry, and required claims. For APIs secured with Microsoft Entra ID, this eliminates an entire class of authentication bypass risks that arise when each backend independently validates tokens.

Following is a policy that validates an Entra ID-issued token:

<inbound>
  <base />
  <validate-jwt
    header-name="Authorization"
    failed-validation-httpcode="401"
    failed-validation-error-message="Unauthorised. A valid bearer token is required.">
    <openid-config url="https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration" />
    <required-claims>
      <claim name="aud">
        <value>api://<app-id></value>
      </claim>
    </required-claims>
  </validate-jwt>
</inbound>

The <openid-config> element fetches and caches the provider's signing keys automatically. The <required-claims> block ensures the token was issued specifically for this API's audience, preventing a valid token for one API from being replayed against another. Requests without a valid bearer token receive the configured 401 response and never reach the backend.

4. IP Filtering

The ip-filter policy evaluates the caller's IP address against an allowlist or blocklist. It is most useful for internal or partner-facing APIs that should only accept traffic from known networks — office IP ranges, VPN exit nodes, or peered virtual networks.

Following is an allowlist policy permitting a CIDR range and a specific address:

<inbound>
  <base />
  <ip-filter action="allow">
    <address-range from="10.0.0.0" to="10.0.0.255" />
    <address>203.0.113.42</address>
  </ip-filter>
</inbound>

When API Management is deployed behind an Azure Application Gateway or Azure Front Door, the caller IP seen by APIM is the gateway's address, not the client's original IP. In this scenario, extract the original client IP from the X-Forwarded-For header using a set-variable policy before the ip-filter evaluation.

5. Response Header Cleanup

Backend services frequently return headers that expose internal implementation details — server software versions, framework identifiers, or internal hostnames. These are useful during development but should not reach API consumers in production.

Following is an outbound policy that removes common information-disclosure headers and adds a security response header:

<outbound>
  <base />
  <set-header name="X-Powered-By" exists-action="delete" />
  <set-header name="X-AspNet-Version" exists-action="delete" />
  <set-header name="Server" exists-action="delete" />
  <set-header name="X-Content-Type-Options" exists-action="override">
    <value>nosniff</value>
  </set-header>
  <set-header name="X-Frame-Options" exists-action="override">
    <value>DENY</value>
  </set-header>
</outbound>

Apply this at the Global scope so it covers every API without repeating it per-API. Backend teams can then focus on returning correct data — APIM handles response hygiene uniformly.

Summary

APIM policies provide a central enforcement point for security controls that would otherwise be replicated, inconsistently, across every backend service. Rate limiting at the product scope, JWT validation at the API scope, IP filtering for restricted APIs, and outbound header cleanup at the global scope form a baseline security posture that is both effective and maintainable. Start with these four policies and tighten them at the operation level only where specific endpoints require different behaviour.

No comments: