F5 HTTPS Redirect 2025

An updated HTTP to HTTPS redirect iRule for F5 BIG-IP that fixes the issues with the legacy redirect solution.

The Problem with Legacy Redirect iRules

F5's old _sys_https_redirect iRule is simple and gets the job done, but it's starting to show its age:

  1. Always sends HTTP 302 redirects - This changes POST requests to GET, breaking things like form submissions. We need HTTP 308 to preserve the request method.
  2. Breaks on IPv6 addresses - The getfield command fails on IPv6 host headers like [2001:db8::1]:8080.
  3. No way to make exceptions - Redirects everything unconditionally, which breaks ACME challenges, health checks, and webhooks that need HTTP.
  4. Everything is hardcoded - No configuration parameters means editing the core logic for any change.
  5. Zero visibility - No logging makes troubleshooting difficult.
  6. No port flexibility - Can't handle non-standard port mappings like 8080→8443.

 

The Solution: HTTPS Redirect 2025

Here's how the new iRule tackles these issues:

Key Features

  • HTTP 308 Permanent Redirects - Preserves request methods (POST, PUT, DELETE)
  • Full IPv6 Support - Correctly handles IPv6 addresses in brackets
  • Configurable Exemptions - Skip redirects for ACME challenges, health checks, and webhooks (disabled by default for security)
  • Flexible Port Mapping - Define custom HTTP→HTTPS port mappings
  • Host Header Validation - Optional domain allowlist for security
  • Three-Tier Logging - Choose between none, standard, or debug logging
  • Performance Optimized - Static variables prevent CMP demotion
  • Variable Namespace Protection - Prefixed variables prevent conflicts with other iRules
  • Deployment Error Detection - Automatically detects and logs misconfiguration on HTTPS virtual servers
  • Standardized Headers - RFC-compliant cache control and connection headers prevent caching issues

How It Works

Proper HTTP 308 Redirects

set static::httpsredirect_redirect_code 308
HTTP::respond $static::httpsredirect_redirect_code Location $redirect_location

HTTP 308 tells browsers "permanently moved, preserve the request method" - exactly what we need.

Robust IPv6 Handling

# Process IPv6 addresses in brackets (e.g., [2001:db8::1]:8080)
if {[string match {\[*\]*} $raw_host]} {
    # Extract IPv6 address from brackets
    set bracket_end [string first "\]" $raw_host]
    if {$bracket_end > 0} {
        set ipv6_addr [string range $raw_host 1 [expr {$bracket_end - 1}]]
        # Set host to IPv6 with brackets preserved
        set host "\[$ipv6_addr\]"
    }
}

Inline IPv6 processing correctly handles bracketed addresses and port extraction.

Smart Exemptions (Opt-in for Security)

# Exemptions are disabled by default for security
set static::httpsredirect_exemption_processing 0  # Change to 1 to enable

set static::httpsredirect_exemption_paths {
    "/.well-known/acme-challenge/*"
    "/health"
    "/status"
    "/ping"
    "/api/webhook/*"
}

Define paths that bypass redirects for Let's Encrypt, monitoring, and webhooks. Note: Exemptions are disabled by default for enhanced security. Enable only if needed.

Flexible Port Mapping

array set static::httpsredirect_port_mapping {
    80    443
    8080  8443
    8888  9443
    8000  8443
    3000  3443
}

Map any HTTP port to its HTTPS equivalent.

Host Validation Security

set static::httpsredirect_valid_hosts {
    "mysite.com"
    "www.mysite.com"
    "api.mysite.com"
}

Optional valid hosts list for additional security. iRule responds with a 403 Forbidden HTTP code for hosts not in the valid hosts list. Protects against Redirection via Arbitrary Host Header.

Standardized Redirect Headers

set static::httpsredirect_cache_control "no-cache, no-store, must-revalidate"
set static::httpsredirect_connection "close"

RFC-compliant headers prevent caching issues and ensure clean redirects.

 

Deployment

The iRule is designed for simplicity - deploy to your HTTP virtual server and you're done.

Installation

  1. Review Configuration - Edit the RULE_INIT section to customize:
    • Enable exemptions if needed (disabled by default for security)
    • Exemption paths for your environment
    • Port mappings if using non-standard ports
    • Host validation domains (or leave as "*" for all hosts)
    • Logging level for your needs
  2. Deploy to HTTP Virtual Server ONLY - Attach the iRule to your HTTP virtual server (typically port 80)
    • Important: Do NOT attach to HTTPS virtual servers - the iRule will detect this error and log warnings
  3. Verify Operation - Test redirects and confirm exemptions work as expected (if enabled)

That's it! The iRule handles all HTTP→HTTPS redirects with enhanced security by default.

Configuration Options

All configuration is centralized in the RULE_INIT section:

# Core redirect settings
set static::httpsredirect_redirect_enabled 1
set static::httpsredirect_redirect_code 308
set static::httpsredirect_default_https_port 443

# Logging: "none", "standard", or "debug"
set static::httpsredirect_log_level "standard"

# Exemptions (disabled by default for security)
set static::httpsredirect_exemption_processing 0  # Change to 1 to enable
set static::httpsredirect_exemption_paths {
    "/.well-known/acme-challenge/*"
    "/health"
}

# Host validation (use "*" to accept all)
set static::httpsredirect_valid_hosts {
    "*"
}

# Standardized headers
set static::httpsredirect_cache_control "no-cache, no-store, must-revalidate"
set static::httpsredirect_connection "close"

Logging Levels

Three logging levels to choose from:

  • "none" - No operational logging, errors only
  • "standard" - Log redirects and exemption matches (recommended)
  • "debug" - Verbose logging including host processing and port mapping

Example standard logging output:

F5_HTTPS_Redirect_2025: Redirecting to https://www.example.com/login with code 308
F5_HTTPS_Redirect_2025: Exemption matched '/.well-known/acme-challenge/*' for /.well-known/acme-challenge/token123 - allowing passthrough
F5_HTTPS_Redirect_2025: DEPLOYMENT ERROR - iRule attached to HTTPS virtual server!

 

Performance Considerations

  • Static Variables - Using static:: prevents CMP demotion
  • Early Returns - Exempted paths exit immediately
  • Inline Processing - Efficient host header processing without procedure overhead
  • Selective Logging - Production systems can disable logging entirely

The iRule runs efficiently on modern BIG-IP hardware.

 

Variable Namespace Protection

The iRule uses prefixed variables (static::httpsredirect_*) to prevent conflicts in multi-iRule environments.

Why This Matters

Static variables in F5 iRules are global across the entire BIG-IP system. Without proper namespacing, iRules can accidentally overwrite each other's variables.

How We Protect Against This

All variables use the httpsredirect_ prefix:

  • static::httpsredirect_redirect_code instead of static::redirect_code
  • static::httpsredirect_port_mapping instead of static::port_mapping

This ensures the iRule plays nicely with other iRules in your environment.

Creating Your Own Version

To create a customized version with different settings:

  1. Copy the iRule
  2. Search/replace httpsredirect_ with your own unique prefix
  3. Deploy alongside the original without conflicts

 

Compatibility and Requirements

  • BIG-IP Version: Tested on 17.1.0+ but compatible with all currently supported versions
  • License: Standard LTM license (no additional modules required)
  • Virtual Server: HTTP virtual server with standard HTTP profile
  • SSL: Not required on the HTTP virtual server (HTTPS traffic handled separately)

 

Troubleshooting

Redirects Not Working

  1. Check the iRule is attached to the HTTP virtual server (not HTTPS)
  2. Verify redirect_enabled is set to 1
  3. Set logging to "debug" and check /var/log/ltm for details
  4. Look for "DEPLOYMENT ERROR" messages if accidentally attached to HTTPS virtual server

Exemptions Not Working

  1. Confirm exemption_processing is set to 1 (disabled by default for security)
  2. Verify path patterns match exactly (use wildcards like * where needed)
  3. Check logs for "Exemption matched" messages

IPv6 Redirect Issues

  1. Ensure clients are sending proper IPv6 host headers with brackets
  2. Enable debug logging to see host header processing
  3. Verify IPv6 is enabled on the virtual server

Host Validation Rejecting Valid Requests

  1. Check valid_hosts list includes all legitimate domains
  2. Use "*" to disable validation during troubleshooting
  3. Remember validation is case-sensitive

 

Getting Help

 

Full F5 HTTPS Redirect 2025 iRule below is the latest version as of Sept 4th, 2025. See the GitHub repo for the latest version.

# ============================================================================
# F5 HTTPS Redirect 2025 v0.3.0
# ============================================================================
# HTTP to HTTPS redirect iRule with configurable exemptions.
# Optimized for performance with static variables to prevent CMP demotion.
# Enhanced with port mapping array for flexible HTTP->HTTPS port redirects.
# 
# DEPLOYMENT STRATEGY:
# - Deploy to HTTP virtual server (port 80) ONLY for redirect functionality
# - Clean HTTP 308 redirects (RFC 7538) with standardized headers
#
# Context-aware processing with minimal performance impact.
# Supports ACME challenges, health checks, webhooks, and custom exemptions.
#
# CONFIGURATION:
# All configuration options are in the RULE_INIT section immediately below.
# Modify exemption paths, port mappings, and debug settings as needed.
# ============================================================================

when RULE_INIT {
    # ========================================================================
    # SYSTEM INITIALIZATION
    # ========================================================================
    # IMPORTANT: Variable Collision Prevention
    # static:: variables are shared across ALL iRules on the F5 system!
    # The prefix 'httpsredirect_' prevents conflicts with other iRules.
    # To create a new iRule: search/replace 'httpsredirect_' with your unique prefix.
    # ========================================================================
    set static::httpsredirect_IRULE_VERSION "0.3.0"
    set static::httpsredirect_IRULE_NAME "F5_HTTPS_Redirect_2025"
    
    # ========================================================================
    # DEPLOYMENT CONFIGURATION
    # ========================================================================
    
    # Feature toggles - Enable/disable functionality independently
    set static::httpsredirect_redirect_enabled 1
    # Exemption processing: 0 = disabled (redirect all paths), 1 = enabled (allow exempted paths)
    # To enable exemptions: change 0 to 1 and configure exemption_paths below
    set static::httpsredirect_exemption_processing 0
    # Logging levels: "none", "standard", "debug"
    # none = No operational logging, errors only
    # standard = Key events (redirects, exemptions, initialization)  
    # debug = Verbose details (host processing, IPv6 parsing, context)
    set static::httpsredirect_log_level "standard"
    
    # Redirect configuration (only used when redirect_enabled = 1)
    set static::httpsredirect_redirect_code 308
    set static::httpsredirect_default_https_port 443
    
    # Standardized redirect headers (RFC 7231 HTTP semantics, RFC 7234 HTTP caching)
    set static::httpsredirect_cache_control "no-cache, no-store, must-revalidate"
    set static::httpsredirect_connection "close"
    
    # HTTP to HTTPS port mapping
    # Maps source HTTP ports to destination HTTPS ports
    # If HTTP port is not in this mapping, uses default_https_port
    array set static::httpsredirect_port_mapping {
        80    443
        8080  8443
        8888  9443
        8000  8443
        3000  3443
    }
    
    # Exemption paths (only processed when exemption_processing = 1)
    # These paths will NOT be redirected and will pass through to backend pool
    # Default: exemption_processing is disabled, all paths redirect to HTTPS
    set static::httpsredirect_exemption_paths {
        "/.well-known/acme-challenge/*"
        "/health"
        "/status" 
        "/ping"
        "/api/webhook/*"
    }
    
    # Host header validation (optional security enhancement)
    # Default: "*" = accept any host (current behavior)
    # Security: Replace "*" with specific hosts to enable validation
    # Example: {"mysite.com" "www.mysite.com" "api.mysite.com"}
    set static::httpsredirect_valid_hosts {
        "*"
    }
    
    # ========================================================================
    # END OF USER CONFIGURATION SECTION
    # ========================================================================
    # All user-configurable settings are defined above this point.
    # ========================================================================
    
    # ========================================================================
    # CONFIGURATION VALIDATION AND RUNTIME SETUP
    # ========================================================================
    
    # Validate logging level configuration
    set valid_log_levels [list "none" "standard" "debug"]
    if {[lsearch $valid_log_levels $static::httpsredirect_log_level] == -1} {
        log local0.error "$static::httpsredirect_IRULE_NAME: ERROR - Invalid log_level \
            '$static::httpsredirect_log_level'. Must be: none, standard, or debug. \
            Using 'standard'."
        set static::httpsredirect_log_level "standard"
    }
    
    # Helper variables for log level checking (must be static for HTTP_REQUEST access)
    set static::httpsredirect_log_standard [expr {
        $static::httpsredirect_log_level eq "standard" || 
        $static::httpsredirect_log_level eq "debug"
    }]
    set static::httpsredirect_log_debug [expr {$static::httpsredirect_log_level eq "debug"}]
    
    # Validate host configuration during RULE_INIT
    if {[llength $static::httpsredirect_valid_hosts] == 0} {
        log local0.error "$static::httpsredirect_IRULE_NAME: ERROR - \
            valid_hosts array is empty! Using wildcard mode."
        set static::httpsredirect_valid_hosts {
        "*"
    }
        set static::httpsredirect_host_validation_active 0
    } elseif {[lsearch $static::httpsredirect_valid_hosts "*"] != -1} {
        if {[llength $static::httpsredirect_valid_hosts] > 1} {
            log local0.error "$static::httpsredirect_IRULE_NAME: ERROR - \
                valid_hosts contains '*' mixed with specific hosts! \
                Using wildcard mode only."
            set static::httpsredirect_valid_hosts {
        "*"
    }
        }
        set static::httpsredirect_host_validation_active 0
    } else {
        set static::httpsredirect_host_validation_active 1
        if {$static::httpsredirect_log_standard} {
            log local0.info "$static::httpsredirect_IRULE_NAME: \
                Host validation enabled for [llength $static::httpsredirect_valid_hosts] hosts"
        }
    }
    
    # Log initialization (show for standard and debug levels)
    if {$static::httpsredirect_log_standard} {
        log local0. "$static::httpsredirect_IRULE_NAME v$static::httpsredirect_IRULE_VERSION: \
            Initialized - redirect_enabled=$static::httpsredirect_redirect_enabled, \
            host_validation=$static::httpsredirect_host_validation_active, \
            log_level=$static::httpsredirect_log_level, \
            port_mappings=[array size static::httpsredirect_port_mapping]"
    }
}

when HTTP_REQUEST {
    # ========================================================================
    # DEPLOYMENT ERROR DETECTION
    # ========================================================================
    # This iRule is designed for HTTP virtual servers ONLY (port 80)
    # If deployed on HTTPS virtual server, log error and exit
    # ========================================================================
    
    if {[PROFILE::exists clientssl]} {
        # ERROR: iRule deployed on HTTPS virtual server!
        log local0.error "$static::httpsredirect_IRULE_NAME v$static::httpsredirect_IRULE_VERSION: \
            DEPLOYMENT ERROR - iRule attached to HTTPS virtual server! \
            This iRule should only be attached to HTTP (port 80) virtual servers. \
            Remove from HTTPS virtual server immediately."
        return
    }
    
    # Get local port for redirect mapping
    set local_port [TCP::local_port]
    
    # ========================================================================
    # REDIRECT PROCESSING
    # ========================================================================
    
    # Check if redirect functionality is disabled
    if {!$static::httpsredirect_redirect_enabled} {
        if {$static::httpsredirect_log_standard} {
            log local0. "$static::httpsredirect_IRULE_NAME v$static::httpsredirect_IRULE_VERSION: \
                Redirect disabled, passing through [HTTP::uri]"
        }
        return
    }
    
    # ========================================================================
    # EXEMPTION PROCESSING
    # ========================================================================
    
    # Get URI for exemption checking
    set uri [HTTP::uri]
    
    # Process exemptions if enabled
    set is_exempt 0
    if {$static::httpsredirect_exemption_processing} {
        foreach pattern $static::httpsredirect_exemption_paths {
            if {[string match $pattern $uri]} {
                set is_exempt 1
                if {$static::httpsredirect_log_standard} {
                    log local0. "$static::httpsredirect_IRULE_NAME v$static::httpsredirect_IRULE_VERSION: \
                        Exemption matched '$pattern' for $uri - allowing passthrough"
                }
                break
            }
        }
    }
    
    # If exempt, allow request to pass through to backend pool
    if {$is_exempt} {
        return
    }
    
    # ========================================================================
    # HOST HEADER PROCESSING FOR REDIRECT
    # ========================================================================
    
    # Extract host header for validation and redirect URL construction
    set raw_host [HTTP::host]
    
    # Host header validation (if enabled) - validate BEFORE cleaning
    if {$static::httpsredirect_host_validation_active} {
        # Extract hostname without port for validation
        set validation_host $raw_host
        if {[string match {\[*\]*} $raw_host]} {
            # IPv6 format - extract just the address part
            set bracket_end [string first "\]" $raw_host]
            if {$bracket_end > 0} {
                set validation_host [string range $raw_host 1 [expr {$bracket_end - 1}]]
            }
        } else {
            # Regular hostname - remove port if present
            set colon_pos [string first ":" $raw_host]
            if {$colon_pos > -1} {
                set validation_host [string range $raw_host 0 [expr {$colon_pos - 1}]]
            }
        }
        
        if {[lsearch $static::httpsredirect_valid_hosts $validation_host] == -1} {
            if {$static::httpsredirect_log_standard} {
                log local0. "$static::httpsredirect_IRULE_NAME v$static::httpsredirect_IRULE_VERSION: \
                    Invalid host '$validation_host' - rejecting request"
            }
            HTTP::respond 403 content "Forbidden" "Content-Type" "text/plain"
            return
        }
    }
    
    # ========================================================================
    # HOST HEADER PROCESSING
    # ========================================================================
    # Process IPv6 addresses and remove ports for clean redirect URLs
    # ========================================================================
    
    # Process IPv6 addresses in brackets (e.g., [2001:db8::1]:8080)
    if {[string match {\[*\]*} $raw_host]} {
        if {$static::httpsredirect_log_debug} {
            log local0. "$static::httpsredirect_IRULE_NAME v$static::httpsredirect_IRULE_VERSION: \
                IPv6 pattern detected in host: '$raw_host'"
        }
        
        # Extract IPv6 address from brackets
        set bracket_end [string first "\]" $raw_host]
        if {$bracket_end > 0} {
            set ipv6_addr [string range $raw_host 1 [expr {$bracket_end - 1}]]
            
            if {$static::httpsredirect_log_debug} {
                log local0. "$static::httpsredirect_IRULE_NAME v$static::httpsredirect_IRULE_VERSION: \
                    Extracted IPv6 address: '$ipv6_addr'"
            }
            
            # Check for port after closing bracket
            if {[string first ":" $raw_host [expr {$bracket_end + 1}]] > -1} {
                # Has port, but we'll strip it for redirect
                if {$static::httpsredirect_log_debug} {
                    set port_start [expr {$bracket_end + 2}]
                    set orig_port [string range $raw_host $port_start end]
                    log local0. "$static::httpsredirect_IRULE_NAME v$static::httpsredirect_IRULE_VERSION: \
                        Found port '$orig_port', returning IPv6 host without port for redirect"
                }
            }
            
            # Set host to IPv6 with brackets preserved
            set host "\[$ipv6_addr\]"
        } else {
            # Malformed IPv6, use as-is
            if {$static::httpsredirect_log_debug} {
                log local0. "$static::httpsredirect_IRULE_NAME v$static::httpsredirect_IRULE_VERSION: \
                    IPv6 bracket parsing failed, using original host"
            }
            set host $raw_host
        }
    } else {
        # Handle regular hostnames and IPv4 addresses
        # Remove port if present (we'll use our configured HTTPS port)
        if {$static::httpsredirect_log_debug} {
            log local0. "$static::httpsredirect_IRULE_NAME v$static::httpsredirect_IRULE_VERSION: \
                Processing regular hostname: '$raw_host'"
        }
        
        set colon_pos [string first ":" $raw_host]
        if {$colon_pos > -1} {
            set host [string range $raw_host 0 [expr {$colon_pos - 1}]]
            if {$static::httpsredirect_log_debug} {
                log local0. "$static::httpsredirect_IRULE_NAME v$static::httpsredirect_IRULE_VERSION: \
                    Host after port removal: '$host'"
            }
        } else {
            set host $raw_host
        }
    }
    
    # Log the transformation if debug enabled and host changed
    if {$static::httpsredirect_log_debug && ($raw_host ne $host)} {
        log local0. "$static::httpsredirect_IRULE_NAME v$static::httpsredirect_IRULE_VERSION: \
            Processed host from '$raw_host' to '$host'"
    }
    
    # ========================================================================
    # REDIRECT PORT DETERMINATION
    # ========================================================================
    
    # Determine target HTTPS port based on current HTTP port
    if {[info exists static::httpsredirect_port_mapping($local_port)]} {
        set target_https_port $static::httpsredirect_port_mapping($local_port)
        if {$static::httpsredirect_log_debug} {
            log local0. "$static::httpsredirect_IRULE_NAME v$static::httpsredirect_IRULE_VERSION: \
                Using port mapping: $local_port -> $target_https_port"
        }
    } else {
        set target_https_port $static::httpsredirect_default_https_port
        if {$static::httpsredirect_log_debug} {
            log local0. "$static::httpsredirect_IRULE_NAME v$static::httpsredirect_IRULE_VERSION: \
                No port mapping for $local_port, using default: $target_https_port"
        }
    }
    
    # ========================================================================
    # REDIRECT URL CONSTRUCTION AND RESPONSE
    # ========================================================================
    
    # Construct the HTTPS URL
    if {$target_https_port != 443} {
        set redirect_location "https://${host}:${target_https_port}${uri}"
    } else {
        set redirect_location "https://${host}${uri}"
    }
    
    # Log the redirect (Standard level - key operational event)
    if {$static::httpsredirect_log_standard} {
        log local0. "$static::httpsredirect_IRULE_NAME v$static::httpsredirect_IRULE_VERSION: \
            Redirecting to $redirect_location with code $static::httpsredirect_redirect_code"
    }
    
    # Send HTTP 308 redirect response (RFC 7538) with standardized headers
    HTTP::respond $static::httpsredirect_redirect_code \
        Location $redirect_location \
        Connection $static::httpsredirect_connection \
        Cache-Control $static::httpsredirect_cache_control
}

 

Updated Sep 04, 2025
Version 2.0

3 Comments

  • Global Variable Scope and System-Wide Policy

    My colleague and iRule expert code reviewer, Joseph Martin, pointed out an important implication for using RULE_INIT to set global variables. Variables defined in RULE_INIT are system-wide global, meaning they apply to all virtual servers with any iRule that references the same global variable names.

    How Global Variables Work

    When you set configuration values like ::redirect_code, ::security_headers_enabled, or ::port_mapping in RULE_INIT, these settings apply to every virtual server where this iRule is attached. This provides:

    • Consistent redirect behavior across all applications
    • Single configuration point for security headers
    • Uniform port mapping rules

    Considerations for Variable Customization

    Since RULE_INIT variables share the global namespace across all iRules and virtual servers, any iRule that sets a variable with the same name will override the previous value system-wide.

    If you need different configurations for specific virtual servers (such as selective debug logging, custom port mappings, or unique security headers), consider these approaches:

    1. Separate iRules - Create modified versions with different global variable values
    2. Runtime conditions - Add logic in HTTP_REQUEST to check the virtual server name or other criteria
    3. Local variable overrides - Use local variables within events to override global settings for specific requests

    The global scope ensures consistent policy enforcement while allowing customization through standard iRule techniques when needed.

  • @Tony_Marfil: Thank you for sharing this peace of code. I think it is a good idea to write and publish a battle tested iRule for redirection.

    I do not use the _sys_https_redirect iRule due to the issues described above and I have my own iRule that enforces security headers and valid hostnames.

    In my opinion the iRules above has some issues:

    • Tony_Marfil's avatar
      Tony_Marfil
      Icon for Employee rankEmployee

      Thumbs up. Excellent feedback. I will incorporate all of the improvements you suggested in the next release. Thanks!