Istio EnvoyFilter Custom Configuration For Advanced HTTP Header Manipulation
Mastering Istio EnvoyFilter: The Complete Guide to Advanced HTTP Header Manipulation
Let's be honest: if you're running a service mesh in production, you're going to need custom header manipulation. Whether it's adding security headers, transforming legacy API responses, or implementing sophisticated routing logic, the default Istio configuration will only get you so far. That's where EnvoyFilter comes in – and trust me, it's both more powerful and more dangerous than most people realize.
I've spent countless hours debugging mysterious traffic behavior that traced back to poorly configured EnvoyFilters. In this guide, I'll show you not just how to configure these filters, but how to do it safely and effectively in production environments.
Understanding EnvoyFilter: Beyond the Basics
EnvoyFilter is Istio's escape hatch – it gives you direct access to Envoy proxy configuration when Istio's higher-level abstractions aren't enough. Think of it as dropping down to assembly language when your high-level programming language hits its limits.
The key insight most engineers miss is that EnvoyFilter operates at the Envoy proxy level, not the Kubernetes level. This means your filters are applied to actual network traffic flowing through the sidecar proxies, giving you unprecedented control over request and response processing.
Here's what makes EnvoyFilter particularly powerful for header manipulation:
- Real-time processing: Headers are modified as traffic flows through the mesh
- Bidirectional control: You can modify both request and response headers
- Fine-grained targeting: Apply filters to specific workloads, namespaces, or traffic patterns
- Integration with Envoy's rich filter ecosystem: Access to Lua scripting, WASM filters, and more
Core EnvoyFilter Configuration Patterns
Request Header Manipulation
Let's start with the most common use case: adding, modifying, or removing request headers. This is typically needed for authentication, routing, or legacy system integration.
apiVersion: install.istio.io/v1alpha1
kind: EnvoyFilter
metadata:
name: custom-request-headers
namespace: production
spec:
workloadSelector:
labels:
app: api-gateway
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND
listener:
filterChain:
filter:
name: "envoy.filters.network.http_connection_manager"
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inline_code: |
function envoy_on_request(request_handle)
-- Add security headers
request_handle:headers():add("X-Request-ID", request_handle:headers():get(":path") .. "-" .. os.time())
request_handle:headers():add("X-Forwarded-Proto", "https")
-- Remove sensitive headers that might leak information
request_handle:headers():remove("X-Internal-Token")
-- Transform legacy header format
local legacy_auth = request_handle:headers():get("Authorization-Legacy")
if legacy_auth then
request_handle:headers():add("Authorization", "Bearer " .. legacy_auth)
request_handle:headers():remove("Authorization-Legacy")
end
end
This configuration demonstrates several key patterns:
- Workload targeting: We're specifically targeting workloads labeled
app: api-gateway - Context specification:
SIDECAR_INBOUNDmeans this applies to incoming traffic - Lua scripting: Provides maximum flexibility for header manipulation logic
Response Header Security Hardening
Security headers are non-negotiable in production. Here's how to implement comprehensive response header security:
apiVersion: install.istio.io/v1alpha1
kind: EnvoyFilter
metadata:
name: security-headers
namespace: istio-system # Apply to entire mesh
spec:
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND
listener:
filterChain:
filter:
name: "envoy.filters.network.http_connection_manager"
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inline_code: |
function envoy_on_response(response_handle)
-- OWASP recommended security headers
response_handle:headers():add("X-Content-Type-Options", "nosniff")
response_handle:headers():add("X-Frame-Options", "DENY")
response_handle:headers():add("X-XSS-Protection", "1; mode=block")
response_handle:headers():add("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
-- CSP header with environment-specific values
local csp_policy = "default-src 'self'; script-src 'self' 'unsafe-inline' *.googleapis.com"
response_handle:headers():add("Content-Security-Policy", csp_policy)
-- Remove server identification headers
response_handle:headers():remove("Server")
response_handle:headers():remove("X-Powered-By")
-- Add custom tracking headers for monitoring
response_handle:headers():add("X-Response-Time", os.time())
response_handle:headers():add("X-Service-Version", "v2.1.0")
end
Pro tip: Deploy security headers at the istio-system namespace level to apply them mesh-wide, but use specific workload selectors for application-specific modifications.
Advanced Header Manipulation Scenarios
Dynamic Header Generation Based on Request Content
Sometimes you need headers that depend on the request content or external systems. Here's how to implement dynamic header generation:
apiVersion: install.istio.io/v1alpha1
kind: EnvoyFilter
metadata:
name: dynamic-headers
namespace: analytics
spec:
workloadSelector:
labels:
tier: api
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND
listener:
filterChain:
filter:
name: "envoy.filters.network.http_connection_manager"
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inline_code: |
function envoy_on_request(request_handle)
-- Extract user context from JWT
local auth_header = request_handle:headers():get("Authorization")
if auth_header then
-- Simple JWT parsing (in production, use proper JWT library)
local token = string.match(auth_header, "Bearer%s+(.+)")
if token then
local payload = string.match(token, "[^.]+%.([^.]+)%.[^.]+")
if payload then
-- Base64 decode would happen here in production
request_handle:headers():add("X-User-Context", "authenticated")
request_handle:headers():add("X-Request-Priority", "high")
end
end
end
-- Geographic routing based on IP (simplified)
local client_ip = request_handle:headers():get("X-Forwarded-For")
if client_ip then
-- In production, integrate with GeoIP service
if string.match(client_ip, "^192%.168%.") then
request_handle:headers():add("X-Geo-Region", "internal")
else
request_handle:headers():add("X-Geo-Region", "external")
end
end
-- Rate limiting headers
local path = request_handle:headers():get(":path")
if string.match(path, "/api/v1/heavy%-operation") then
request_handle:headers():add("X-Rate-Limit-Tier", "premium")
else
request_handle:headers():add("X-Rate-Limit-Tier", "standard")
end
end
Conditional Header Modification with Environment Awareness
Different environments need different header strategies. Here's how to implement environment-aware header manipulation:
apiVersion: install.istio.io/v1alpha1
kind: EnvoyFilter
metadata:
name: environment-headers
namespace: default
spec:
workloadSelector:
labels:
component: frontend
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND
listener:
filterChain:
filter:
name: "envoy.filters.network.http_connection_manager"
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inline_code: |
function envoy_on_request(request_handle)
-- Read environment from pod metadata (injected as header by ingress)
local env = request_handle:headers():get("X-Environment") or "production"
if env == "development" then
request_handle:headers():add("X-Debug-Mode", "enabled")
request_handle:headers():add("X-Log-Level", "debug")
-- Allow CORS for development
request_handle:headers():add("X-Dev-CORS", "allow-all")
elseif env == "staging" then
request_handle:headers():add("X-Debug-Mode", "limited")
request_handle:headers():add("X-Log-Level", "info")
request_handle:headers():add("X-Staging-Flag", "true")
else
-- Production: security-first approach
request_handle:headers():add("X-Debug-Mode", "disabled")
request_handle:headers():add("X-Log-Level", "warn")
-- Remove any debug headers that might have leaked through
request_handle:headers():remove("X-Debug-Info")
request_handle:headers():remove("X-Internal-State")
end
end
function envoy_on_response(response_handle)
local env = response_handle:headers():get("X-Environment") or "production"
if env ~= "production" then
-- Add development/staging identification
response_handle:headers():add("X-Environment-Info", env)
response_handle:headers():add("X-Debug-Timestamp", os.date("%Y-%m-%d %H:%M:%S"))
end
end
Production-Ready Configuration Patterns
Header Validation and Sanitization
In production, you can't trust incoming headers. Here's how to implement proper validation and sanitization:
apiVersion: install.istio.io/v1alpha1
kind: EnvoyFilter
metadata:
name: header-validation
namespace: security
spec:
configPatches:
- applyTo: HTTP_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter:
name: "envoy.filters.network.http_connection_manager"
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inline_code: |
-- Header validation functions
function is_valid_header_value(value)
-- Reject headers with control characters or excessive length
if not value or string.len(value) > 8192 then
return false
end
-- Check for control characters (ASCII 0-31 except tab)
for i = 1, string.len(value) do
local byte = string.byte(value, i)
if byte < 32 and byte ~= 9 then -- 9 is tab
return false
end
end
return true
end
function sanitize_header_name(name)
-- Convert to lowercase and remove invalid characters
return string.lower(string.gsub(name or "", "[^a-zA-Z0-9%-_]", ""))
end
function envoy_on_request(request_handle)
local headers_to_validate = {
"User-Agent", "X-Forwarded-For", "X-Real-IP",
"Authorization", "Content-Type", "Accept"
}
for _, header_name in ipairs(headers_to_validate) do
local header_value = request_handle:headers():get(header_name)
if header_value and not is_valid_header_value(header_value) then
-- Log the violation (in production, integrate with logging system)
request_handle:headers():add("X-Header-Violation", header_name)
-- Either remove the invalid header or reject the request
request_handle:headers():remove(header_name)
end
end
-- Prevent header injection attacks
local suspicious_patterns = {
"\r\n", "\n", "\r", -- CRLF injection
"<script", "</script>", -- XSS attempts
"javascript:", -- Protocol injection
"data:text/html" -- Data URL injection
}
local user_agent = request_handle:headers():get("User-Agent")
if user_agent then
for _, pattern in ipairs(suspicious_patterns) do
if string.find(string.lower(user_agent), string.lower(pattern)) then
request_handle:headers():add("X-Security-Violation", "suspicious-user-agent")
request_handle:headers():remove("User-Agent")
break
end
end
end
end
Performance-Optimized Header Processing
Header manipulation can impact performance. Here's how to optimize for production workloads:
apiVersion: install.istio.io/v1alpha1
kind: EnvoyFilter
metadata:
name: optimized-headers
namespace: high-traffic
spec:
workloadSelector:
labels:
performance-tier: critical
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND
listener:
filterChain:
filter:
name: "envoy.filters.network.http_connection_manager"
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inline_code: |
-- Precompile patterns and cache common operations
local common_headers = {
security = {
["X-Content-Type-Options"] = "nosniff",
["X-Frame-Options"] = "DENY",
["X-XSS-Protection"] = "1; mode=block"
},
tracking = {
["X-Service"] = "api-service",
["X-Version"] = "2.1.0"
}
}
function envoy_on_request(request_handle)
-- Fast path: only process if specific conditions are met
local content_type = request_handle:headers():get("Content-Type")
local method = request_handle:headers():get(":method")
-- Skip processing for static assets
if content_type and (
string.match(content_type, "image/") or
string.match(content_type, "text/css") or
string.match(content_type, "application/javascript")
) then
return
end
-- Batch header operations
local headers_to_add = {}
local headers_to_remove = {}
-- Conditional logic with minimal string operations
if method == "POST" or method == "PUT" then
headers_to_add["X-Request-Type"] = "mutation"
headers_to_add["X-CSRF-Protection"] = "enabled"
end
-- Apply all header changes in one batch
for name, value in pairs(headers_to_add) do
request_handle:headers():add(name, value)
end
for _, name in ipairs(headers_to_remove) do
request_handle:headers():remove(name)
end
end
function envoy_on_response(response_handle)
-- Use cached header sets for common responses
for name, value in pairs(common_headers.security) do
response_handle:headers():add(name, value)
end
-- Only add tracking headers for successful responses
local status = response_handle:headers():get(":status")
if status and string.match(status, "^2") then
for name, value in pairs(common_headers.tracking) do
response_handle:headers():add(name, value)
end
end
end
Debugging and Troubleshooting EnvoyFilter
Debugging Configuration Issues
EnvoyFilter debugging can be challenging. Here's my systematic approach:
1. Verify Configuration Applied
# Check if the EnvoyFilter is properly applied
kubectl get envoyfilter -A
kubectl describe envoyfilter custom-headers -n production
# Verify the configuration reached the Envoy proxy
kubectl exec -n production deployment/api-gateway -c istio-proxy -- pilot-agent request GET config_dump | jq '.configs[2].dynamic_listeners'
2. Enable Debug Logging
apiVersion: install.istio.io/v1alpha1
kind: EnvoyFilter
metadata:
name: debug-headers
namespace: default
spec:
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND
listener:
filterChain:
filter:
name: "envoy.filters.network.http_connection_manager"
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inline_code: |
function envoy_on_request(request_handle)
-- Debug logging
request_handle:logInfo("Processing request: " .. (request_handle:headers():get(":path") or "unknown"))
-- Add debug headers to trace processing
request_handle:headers():add("X-Debug-Filter-Applied", "true")
request_handle:headers():add("X-Debug-Timestamp", os.time())
-- Log all incoming headers for debugging
request_handle:logInfo("Incoming headers:")
local headers = request_handle:headers()
for key, value in pairs(headers:getAll() or {}) do
request_handle:logInfo(" " .. key .. ": " .. value)
end
end
3. Test with Controlled Traffic
# Send test requests with specific headers
curl -H "X-Test-Header: debug-value" \
-H "Authorization: Bearer test-token" \
http://api-gateway.production.svc.cluster.local/health
# Check Envoy access logs
kubectl logs -n production deployment/api-gateway -c istio-proxy | grep "X-Test-Header"
Common Pitfalls and How to Avoid Them
1. Filter Ordering Issues
Problem: Your EnvoyFilter isn't working because it's applied in the wrong order.
Solution: Use explicit filter ordering and understand the Envoy filter chain:
# Wrong: This might be applied too late in the chain
patch:
operation: INSERT_AFTER # Dangerous!
# Right: Insert before existing filters to ensure early processing
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.lua
2. Performance Impact
Problem: Complex header manipulation causing latency spikes.
Solution: Profile and optimize your Lua code:
inline_code: |
-- Wrong: String concatenation in loops
function envoy_on_request(request_handle)
local result = ""
for i = 1, 1000 do
result = result .. "value" .. i -- Creates many temporary strings
end
end
-- Right: Use table concatenation
function envoy_on_request(request_handle)
local parts = {}
for i = 1, 1000 do
table.insert(parts, "value" .. i)
end
local result = table.concat(parts, "")
end
3. Scope and Targeting Confusion
Problem: EnvoyFilter applying to wrong workloads or not applying at all.
Solution: Be explicit about targeting:
# Wrong: Too broad, affects entire mesh
spec:
configPatches: [...]
# Right: Explicit workload targeting
spec:
workloadSelector:
labels:
app: specific-service
version: v2
configPatches: [...]
Security Considerations
Header Injection Prevention
Always validate and sanitize headers to prevent injection attacks:
inline_code: |
function is_safe_header_value(value)
-- Prevent CRLF injection
if string.find(value, "[\r\n]") then
return false
end
-- Prevent script injection in header values
if string.find(string.lower(value), "<script") then
return false
end
return true
end
function envoy_on_request(request_handle)
local user_header = request_handle:headers():get("X-User-Input")
if user_header and not is_safe_header_value(user_header) then
request_handle:respond({[":status"] = "400"}, "Invalid header value")
return
end
end
Sensitive Information Leakage
Be careful not to expose internal information through headers:
function envoy_on_response(response_handle)
-- Remove internal headers before response
local internal_headers = {
"X-Internal-Service-Id",
"X-Database-Host",
"X-Cache-Key",
"X-Debug-SQL-Query"
}
for _, header in ipairs(internal_headers) do
response_handle:headers():remove(header)
end
end
Real-World Implementation Strategies
Gradual Rollout Strategy
Never deploy EnvoyFilter changes to production all at once. Use canary deployments:
# Phase 1: Test with specific workload
apiVersion: install.istio.io/v1alpha1
kind: EnvoyFilter
metadata:
name: header-manipulation-canary
namespace: production
spec:
workloadSelector:
labels:
app: api-service
version: canary # Only apply to canary version
configPatches: [...]
---
# Phase 2: After validation, expand to stable version
apiVersion: install.istio.io/v1alpha1
kind: EnvoyFilter
metadata:
name: header-manipulation-stable
namespace: production
spec:
workloadSelector:
labels:
app: api-service
version: stable
configPatches: [...]
Monitoring and Observability
Always include monitoring for your header manipulations:
inline_code: |
function envoy_on_request(request_handle)
-- Track header manipulation metrics
local start_time = os.clock()
-- Your header manipulation logic here
request_handle:headers():add("X-Custom-Header", "value")
local processing_time = os.clock() - start_time
-- Add performance tracking
if processing_time > 0.001 then -- 1ms threshold
request_handle:headers():add("X-Header-Processing-Slow", "true")
end
-- Add metrics headers for monitoring systems
request_handle:headers():add("X-Header-Filter-Version", "v2.1.0")
request_handle:headers():add("X-Header-Processing-Time", string.format("%.3f", processing_time))
end
Conclusion
EnvoyFilter is a powerful tool that requires respect and careful handling. While it gives you unprecedented control over HTTP header manipulation in your service mesh, it also introduces complexity and potential failure points.
The key lessons from my experience:
- Start simple: Begin with basic header additions before moving to complex logic
- Test thoroughly: Use canary deployments and comprehensive testing
- Monitor everything: Add observability to track the impact of your filters
- Security first: Always validate and sanitize header values
- Performance matters: Profile your Lua code and optimize for production workloads
Remember, with great power comes great responsibility. Use EnvoyFilter judiciously, document your configurations thoroughly, and always have a rollback plan. Your future self (and your teammates) will thank you.
The patterns and examples in this guide should give you a solid foundation for implementing sophisticated header manipulation in your Istio service mesh. But remember – every production environment is different. Test these patterns in your specific context and adapt them to your needs.
Happy meshing!
Was this article helpful?
Data Infrastructure Engineer
Your data is only as good as the infrastructure it sits on. I specialize in PostgreSQL, Redis, database migrations, backup strategies, and making sure your data survives whatever chaos your application throws at it.
Related Articles
Istio AuthorizationPolicy For Namespace-Level RBAC In Multi-Tenant Kubernetes Clusters
Multi-tenancy in Kubernetes is one of those topics that sounds straightforward until you're three months into a production incident because Team A's micros...
Istio Pilot Discovery XDS Push Errors: Fixing "No Healthy Upstream" In Multi-Cluster Mesh Deployments
Quick reference for debugging `no healthy upstream` errors caused by Pilot xDS push failures in multi-cluster Istio setups. --- You're running a multi-clus...
Istio Gateway Returns 503 Service Unavailable: Debugging VirtualService And DestinationRule Misconfigurations
Nothing ruins your day quite like a 503 Service Unavailable error when you've spent hours configuring your shiny new Istio service mesh. I've been there mo...
Fix Istio Sidecar Injection Not Working
Step-by-step guide to diagnose and fix Istio sidecar proxy not being injected into Kubernetes pods, covering namespace labels, webhook configuration, and annotation overrides.
Istio Installation & Architecture: Your First Service Mesh
Install Istio on Kubernetes, understand the control plane architecture, deploy your first sidecar proxy, and configure namespace injection.
Istio mTLS & Security: Zero-Trust Service Communication
Enable mutual TLS in Istio, configure PeerAuthentication and AuthorizationPolicy, and secure service-to-service communication with zero-trust principles.
More in Istio
View all →Istio Observability: Kiali, Jaeger, and Prometheus Integration
Leverage Istio's built-in observability — Kiali service graph, Jaeger distributed tracing, Prometheus metrics, and Grafana dashboards for your service mesh.
Istio Traffic Management: Routing, Canary, and Circuit Breaking
Configure Istio VirtualServices, DestinationRules, and Gateways for advanced traffic routing, canary deployments, fault injection, and circuit breaking.