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:
- 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.
- Breaks on IPv6 addresses - The getfield command fails on IPv6 host headers like [2001:db8::1]:8080.
- No way to make exceptions - Redirects everything unconditionally, which breaks ACME challenges, health checks, and webhooks that need HTTP.
- Everything is hardcoded - No configuration parameters means editing the core logic for any change.
- Zero visibility - No logging makes troubleshooting difficult.
- 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
- 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
- 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
- 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:
- Copy the iRule
- Search/replace httpsredirect_ with your own unique prefix
- 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
- Check the iRule is attached to the HTTP virtual server (not HTTPS)
- Verify redirect_enabled is set to 1
- Set logging to "debug" and check /var/log/ltm for details
- Look for "DEPLOYMENT ERROR" messages if accidentally attached to HTTPS virtual server
Exemptions Not Working
- Confirm exemption_processing is set to 1 (disabled by default for security)
- Verify path patterns match exactly (use wildcards like * where needed)
- Check logs for "Exemption matched" messages
IPv6 Redirect Issues
- Ensure clients are sending proper IPv6 host headers with brackets
- Enable debug logging to see host header processing
- Verify IPv6 is enabled on the virtual server
Host Validation Rejecting Valid Requests
- Check valid_hosts list includes all legitimate domains
- Use "*" to disable validation during troubleshooting
- Remember validation is case-sensitive
Getting Help
- Documentation: Review inline comments in the iRule for detailed explanations
- Community Support: F5 DevCentral
- Bug Reports: Open a GitHub issue
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
}
3 Comments
- Tony_Marfil
Employee
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:
- Separate iRules - Create modified versions with different global variable values
- Runtime conditions - Add logic in HTTP_REQUEST to check the virtual server name or other criteria
- 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:
- X-XSS-Protection should not be used anymore: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-XSS-Protection
- Global Variables in iRules can have a severe performance impact and should no be used: Getting Started with iRules: Variables | DevCentral
- HTTP::remove and HTTP::insert should be used, HTTP::replace replaces only the first occurrence of a header.
- The HSTS header has no effect on none secure connections
- You should check the host header before redirection: https://my.f5.com/s/article/K000141035
- I would prefer an iRule that do one thing properly - redirection and not mix it with inserting security headers in responses.
- Tony_Marfil
Employee
Thumbs up. Excellent feedback. I will incorporate all of the improvements you suggested in the next release. Thanks!