azureazure-api-managementapim

Best way to do ip-filter policy with X-Forwarded-For


The APIM ip-filter policy doesn't support changing to check X-Forwarded-For instead of request IP. I've tried using the check-header policy, but I get the X-Forwarded-For header with port suffixed <ip>:<port>. I haven't found a way to make the check-header check more advanced to take care of this (removing the port part).

What I've come up with is this, but I don't like it, very bulky and a big if clause.

Can anyone see how this can be solved in a simpler and neater way?

<inbound>
    <base />
    <set-variable name="allowedIPs" value="192.192.192.192" />
    <!-- Determine the IP address to check -->
    <set-variable name="clientIP" value="@(context.Request.Headers.GetValueOrDefault("X-Forwarded-For", context.Request.IpAddress))" />
    <!-- The regular expression ^([\d\.]+) captures only the numeric part of the IP address (i.e., the part before any colon : that would indicate the port). -->
    <set-variable name="cleanClientIP" value="@(System.Text.RegularExpressions.Regex.Match((string)context.Variables["clientIP"], @"^([\d\.]+)").Value)" />
    <!-- Function to check if the IP is allowed -->
    <choose>
        <when condition="@(context.Variables.GetValueOrDefault<string>("allowedIPs").Split(',').Contains(context.Variables.GetValueOrDefault<string>("cleanClientIP")))">
            <!-- REQUEST IS OK, ADD FURTHER LOGIC HERE -->
        </when>
        <otherwise>
            <return-response>
                <set-status code="403" reason="Forbidden" />
                <set-header name="Content-Type" exists-action="override">
                    <value>application/json</value>
                </set-header>
                <set-body>{
"statusCode": 403,
"message": "Not authorized"
}</set-body>
            </return-response>
        </otherwise>
    </choose>
</inbound>

What I mainly want to achieve is:

  1. Early exit (no big if clause)
  2. IPs in a list instead of in a comma separated string

This you get by using check-header or ip-filter. See example below:

<check-header name="header name" failed-check-httpcode="code" failed-check-error-message="message" ignore-case="true | false">
    <value>Value1</value>
    <value>Value2</value>
</check-header>

Solution

  • Simplify the policy by avoiding regular expressions altogether. By splitting the header value using Split(':'), the policy becomes much cleaner and easier to read.

    Simplified APIM policy:`

    <inbound>
        <base />
        
        <!-- Define the list of allowed IPs -->
        <set-variable name="allowedIPs" value="192.192.192.192,203.0.113.0" />
        
        <!-- Extract the client IP, removing the port if present -->
        <set-variable name="cleanClientIP" value="@(context.Request.Headers.GetValueOrDefault("X-Forwarded-For", context.Request.IpAddress).Split(':')[0])" />
        
        <!-- Check if the cleaned client IP is in the list of allowed IPs -->
        <choose>
            <when condition="@(context.Variables["allowedIPs"].Split(',').Contains(context.Variables["cleanClientIP"]))">
                <!-- REQUEST IS OK, ADD FURTHER LOGIC HERE -->
            </when>
            <otherwise>
                <!-- Deny the request if the IP is not allowed -->
                <return-response>
                    <set-status code="403" reason="Forbidden" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json</value>
                    </set-header>
                    <set-body>
                        {
                            "statusCode": 403,
                            "message": "Not authorized"
                        }
                    </set-body>
                </return-response>
            </otherwise>
        </choose>
    </inbound>
    

    Policy extracts 192.168.1.1 from the header and matches it with the allowedIPs list.

    Response:

    {
      "message": "Request successfully processed"
    }
    

    Valid IP with Port:

    Response:

    {
      "message": "Request successfully processed"
    }
    

    Aswell as it works with Multiple IPs in X-Forwarded-For Header also.

    Modified:

    <inbound>
        <base />
        <!-- Rewrite X-Forwarded-For to remove any port information -->
        <set-header 
            name="X-Forwarded-For" 
            exists-action="override" 
            value="@(context.Request.Headers.GetValueOrDefault('X-Forwarded-For', context.Request.IpAddress).Split(':')[0])" />
        
        <!-- Early exit if the IP is not in the allowed list -->
        <check-header 
            name="X-Forwarded-For" 
            failed-check-httpcode="403" 
            failed-check-error-message="Not authorized" 
            ignore-case="false">
            <value>192.192.192.192</value>
            <value>192.192.192.193</value>
            <!-- Additional allowed IPs can be added here -->
        </check-header>
    
        <!-- Further processing can go here -->
    </inbound>
    

    Now, the policy checks if the cleaned IP value matches any of the allowed IPs listed as separate <value> entries. If there's no match, it automatically returns a 403 response with your custom error message.