CORS on F5 BIG-IP

⚠️ Security Warning: Using Access-Control-Allow-Origin: * allows any website to access your resources. Always specify exact origins in production.
About iRules: F5 BIG-IP uses iRules (TCL-based scripting) to customize traffic management. These rules can handle CORS headers at the load balancer level before requests reach your backend servers.

Method 1: Simple Single Origin with Credentials (Recommended)

For most use cases with authentication/cookies, specify a single trusted origin:

when HTTP_REQUEST priority 200 {
    unset -nocomplain cors_origin

    # Validate against single trusted origin
    if { [HTTP::header Origin] equals "https://example.com" } {
        if { ([HTTP::method] equals "OPTIONS") and
             ([HTTP::header exists "Access-Control-Request-Method"]) } {
            # Handle preflight request
            HTTP::respond 200 "Access-Control-Allow-Origin" "https://example.com" \
                              "Access-Control-Allow-Methods" "GET, POST, PUT, DELETE" \
                              "Access-Control-Allow-Headers" "Content-Type, Authorization" \
                              "Access-Control-Allow-Credentials" "true" \
                              "Access-Control-Max-Age" "86400" \
                              "Vary" "Origin"
            return
        } else {
            set cors_origin "https://example.com"
        }
    }
}

when HTTP_RESPONSE {
    if { [info exists cors_origin] } {
        HTTP::header insert "Access-Control-Allow-Origin" $cors_origin
        HTTP::header insert "Access-Control-Allow-Credentials" "true"
        HTTP::header insert "Vary" "Origin"
    }
}
    

Note: When using credentials, you must not use wildcards (*) for either Access-Control-Allow-Origin or Access-Control-Allow-Headers. Specific values must be provided.

Method 2: Multiple Trusted Origins with Credentials

To allow multiple specific origins from the same domain family:

when HTTP_REQUEST priority 200 {
    unset -nocomplain cors_origin

    # Validate against multiple trusted origins
    if { [HTTP::header Origin] ends_with ".example.com" or
         [HTTP::header Origin] equals "https://example.com" } {
        if { ([HTTP::method] equals "OPTIONS") and
             ([HTTP::header exists "Access-Control-Request-Method"]) } {
            # Handle preflight - echo back the validated origin
            HTTP::respond 200 "Access-Control-Allow-Origin" [HTTP::header "Origin"] \
                              "Access-Control-Allow-Methods" "GET, POST, PUT, DELETE" \
                              "Access-Control-Allow-Headers" "Content-Type, Authorization, X-Requested-With" \
                              "Access-Control-Allow-Credentials" "true" \
                              "Access-Control-Max-Age" "86400" \
                              "Vary" "Origin"
            return
        } else {
            set cors_origin [HTTP::header "Origin"]
        }
    }
}

when HTTP_RESPONSE {
    if { [info exists cors_origin] } {
        HTTP::header insert "Access-Control-Allow-Origin" $cors_origin
        HTTP::header insert "Access-Control-Allow-Credentials" "true"
        HTTP::header insert "Vary" "Origin"
    }
}
    

Method 3: Explicit Origin Whitelist

For strict control with a defined list of allowed origins:

when HTTP_REQUEST priority 200 {
    unset -nocomplain cors_origin

    # Define allowed origins
    set allowed_origins {
        "https://app.example.com"
        "https://dashboard.example.com"
        "https://mobile.example.com"
    }

    set origin [HTTP::header Origin]
    if { [lsearch -exact $allowed_origins $origin] >= 0 } {
        if { ([HTTP::method] equals "OPTIONS") and
             ([HTTP::header exists "Access-Control-Request-Method"]) } {
            HTTP::respond 200 "Access-Control-Allow-Origin" $origin \
                              "Access-Control-Allow-Methods" "GET, POST, PUT, DELETE" \
                              "Access-Control-Allow-Headers" "Content-Type, Authorization" \
                              "Access-Control-Allow-Credentials" "true" \
                              "Access-Control-Max-Age" "86400" \
                              "Vary" "Origin"
            return
        } else {
            set cors_origin $origin
        }
    }
}

when HTTP_RESPONSE {
    if { [info exists cors_origin] } {
        HTTP::header insert "Access-Control-Allow-Origin" $cors_origin
        HTTP::header insert "Access-Control-Allow-Credentials" "true"
        HTTP::header insert "Vary" "Origin"
    }
}
    

Method 4: Public API (No Credentials)

Only use for completely public resources without authentication. This allows any website to access your API:

when HTTP_REQUEST {
    if { ([HTTP::method] equals "OPTIONS") and
         ([HTTP::header exists "Access-Control-Request-Method"]) } {
        # Handle preflight for public API
        HTTP::respond 200 "Access-Control-Allow-Origin" "*" \
                          "Access-Control-Allow-Methods" "GET, POST, OPTIONS" \
                          "Access-Control-Allow-Headers" "Content-Type" \
                          "Access-Control-Max-Age" "86400"
        return
    }
}

when HTTP_RESPONSE {
    # Add CORS headers to all responses
    HTTP::header insert "Access-Control-Allow-Origin" "*"
}
    

Important: When using wildcard (*) for Access-Control-Allow-Origin, you cannot include Access-Control-Allow-Credentials: true. This is a CORS specification requirement.

Method 5: Path-Specific CORS (Advanced)

Apply different CORS policies based on URI paths:

when HTTP_REQUEST priority 200 {
    unset -nocomplain cors_origin
    set uri [HTTP::uri]

    # Public API - no credentials
    if { $uri starts_with "/api/public" } {
        if { [HTTP::method] equals "OPTIONS" } {
            HTTP::respond 200 "Access-Control-Allow-Origin" "*" \
                              "Access-Control-Allow-Methods" "GET, POST" \
                              "Access-Control-Allow-Headers" "Content-Type"
            return
        }
        set cors_origin "*"
    # Private API - with credentials
    } elseif { $uri starts_with "/api/private" } {
        if { [HTTP::header Origin] ends_with ".example.com" } {
            if { [HTTP::method] equals "OPTIONS" } {
                HTTP::respond 200 "Access-Control-Allow-Origin" [HTTP::header "Origin"] \
                                  "Access-Control-Allow-Methods" "GET, POST, PUT, DELETE" \
                                  "Access-Control-Allow-Headers" "Content-Type, Authorization" \
                                  "Access-Control-Allow-Credentials" "true" \
                                  "Vary" "Origin"
                return
            }
            set cors_origin [HTTP::header "Origin"]
            set cors_credentials "true"
        }
    }
}

when HTTP_RESPONSE {
    if { [info exists cors_origin] } {
        HTTP::header insert "Access-Control-Allow-Origin" $cors_origin
        if { [info exists cors_credentials] } {
            HTTP::header insert "Access-Control-Allow-Credentials" $cors_credentials
            HTTP::header insert "Vary" "Origin"
        }
    }
}
    

Understanding the Two-Event Pattern

F5 iRules typically handle CORS using two events:

This pattern ensures CORS headers are added consistently while allowing the backend to process non-preflight requests normally.

Important Security Considerations

⚠️ Security Best Practices:
  • Always validate origins - never blindly echo the Origin header without validation
  • When using credentials (Access-Control-Allow-Credentials: true):
    • MUST NOT use wildcard (*) for Access-Control-Allow-Origin
    • MUST NOT use wildcard (*) for Access-Control-Allow-Headers
    • Must specify exact origins and header names
  • Include Vary: Origin header when origin varies to prevent cache poisoning
  • Use pattern matching (e.g., ends_with, equals) rather than wildcards for domain validation
  • Set appropriate Access-Control-Max-Age to reduce preflight requests (86400 = 24 hours)

Applying iRules in BIG-IP

To apply these iRules:

  1. Navigate to Local Traffic > iRules > iRule List
  2. Click Create and give your iRule a name
  3. Paste the iRule code into the Definition field
  4. Click Finished
  5. Go to Local Traffic > Virtual Servers
  6. Select your virtual server
  7. Go to the Resources tab
  8. Under iRules, click Manage
  9. Move your iRule from Available to Enabled
  10. Click Finished

Testing Your CORS Configuration

For comprehensive testing instructions including curl commands, browser DevTools usage, and troubleshooting common CORS errors, see the CORS Testing Guide.

Additional Resources

Who’s behind this

Monsur Hossain and Michael Hausenblas

Contribute

The content on this site stays fresh thanks to help from users like you! If you have suggestions or would like to contribute, fork us on GitHub.

Buy the book

Save 39% on CORS in Action with promotional code hossainco at manning.com/hossain