Reverse Proxy & mTLS
Reverse Proxy & mTLS Configuration
Production PHP applications typically run behind reverse proxies, load balancers, or CDNs. Cbox includes built-in support for:
- Reverse Proxy Headers: Cloudflare, HAProxy, Traefik, Nginx, Fastly, AWS ALB/ELB
- VPN/Tunnel: Tailscale, Cloudflare Tunnel, WireGuard
- mTLS: Mutual TLS client certificate authentication for zero-trust networks
Quick Start
Basic Proxy Configuration
# docker-compose.yml
services:
app:
image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.4-bookworm
environment:
# Trust private network ranges (Docker, Kubernetes)
NGINX_TRUSTED_PROXIES: "10.0.0.0/8 172.16.0.0/12 192.168.0.0/16"
That's it! Cbox will now:
- Extract the real client IP from
X-Forwarded-Forheader - Pass proxy headers to PHP (
$_SERVER['HTTP_X_FORWARDED_FOR'], etc.) - Enable Laravel
TrustProxiesand Symfonytrusted_proxiesto work correctly
Provider-Specific Configuration
Cloudflare
Cloudflare uses CF-Connecting-IP for the real client IP:
services:
app:
image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.4-bookworm
environment:
# Cloudflare IP ranges (updated January 2025)
NGINX_TRUSTED_PROXIES: >-
173.245.48.0/20
103.21.244.0/22
103.22.200.0/22
103.31.4.0/22
141.101.64.0/18
108.162.192.0/18
190.93.240.0/20
188.114.96.0/20
197.234.240.0/22
198.41.128.0/17
162.158.0.0/15
104.16.0.0/13
104.24.0.0/14
172.64.0.0/13
131.0.72.0/22
NGINX_REAL_IP_HEADER: CF-Connecting-IP
Laravel Configuration (app/Http/Middleware/TrustProxies.php):
protected $proxies = '*'; // Trust all proxies (Cloudflare IPs change)
protected $headers = Request::HEADER_X_FORWARDED_FOR;
Cloudflare Tunnel (cloudflared)
For Cloudflare Tunnel, trust private networks since the tunnel runs locally:
services:
app:
image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.4-bookworm
environment:
NGINX_TRUSTED_PROXIES: "172.16.0.0/12"
NGINX_REAL_IP_HEADER: CF-Connecting-IP
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel run
environment:
TUNNEL_TOKEN: ${CLOUDFLARE_TUNNEL_TOKEN}
Traefik
services:
app:
image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.4-bookworm
environment:
# Trust Traefik container network
NGINX_TRUSTED_PROXIES: "172.16.0.0/12"
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.services.app.loadbalancer.server.port=80"
traefik:
image: traefik:v3.0
command:
- "--providers.docker=true"
- "--entrypoints.web.address=:80"
ports:
- "80:80"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
HAProxy
services:
app:
image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.4-bookworm
environment:
NGINX_TRUSTED_PROXIES: "172.16.0.0/12"
# HAProxy typically uses X-Forwarded-For (default)
HAProxy Configuration (haproxy.cfg):
frontend http-in
bind *:80
option forwardfor
default_backend app
backend app
server app1 app:80 check
Tailscale
Tailscale uses the 100.64.0.0/10 CGNAT range:
services:
app:
image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.4-bookworm
environment:
NGINX_TRUSTED_PROXIES: "100.64.0.0/10"
network_mode: service:tailscale
tailscale:
image: tailscale/tailscale:latest
environment:
TS_AUTHKEY: ${TAILSCALE_AUTHKEY}
TS_STATE_DIR: /var/lib/tailscale
volumes:
- tailscale-state:/var/lib/tailscale
volumes:
tailscale-state:
Nginx (External Load Balancer)
services:
app:
image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.4-bookworm
environment:
NGINX_TRUSTED_PROXIES: "172.16.0.0/12"
Upstream Nginx Configuration:
upstream app {
server app:80;
}
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Fastly
services:
app:
image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.4-bookworm
environment:
# Fastly publishes IP ranges at https://api.fastly.com/public-ip-list
NGINX_TRUSTED_PROXIES: "23.235.32.0/20 43.249.72.0/22 ..."
NGINX_REAL_IP_HEADER: Fastly-Client-IP
AWS ALB/ELB
services:
app:
image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.4-bookworm
environment:
# Trust VPC CIDR range
NGINX_TRUSTED_PROXIES: "10.0.0.0/8"
Kubernetes (Ingress)
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: app
image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.4-bookworm
env:
- name: NGINX_TRUSTED_PROXIES
value: "10.0.0.0/8" # Pod network CIDR
Environment Variables Reference
| Variable | Default | Description |
|---|---|---|
NGINX_TRUSTED_PROXIES |
(empty) | Space-separated list of trusted proxy IPs/CIDRs |
NGINX_REAL_IP_HEADER |
X-Forwarded-For |
Header containing the real client IP |
NGINX_REAL_IP_RECURSIVE |
on |
Enable recursive IP extraction (recommended) |
Common Proxy IP Ranges
| Provider | IP Ranges |
|---|---|
| Docker networks | 172.16.0.0/12 |
| Private networks | 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 |
| Tailscale | 100.64.0.0/10 |
| Cloudflare | See cloudflare.com/ips |
| Fastly | See api.fastly.com/public-ip-list |
What Gets Forwarded to PHP
When NGINX_TRUSTED_PROXIES is configured, the following are available in PHP:
// Real client IP (after proxy extraction)
$_SERVER['REMOTE_ADDR']; // Real client IP, not proxy IP
// Proxy headers (always forwarded)
$_SERVER['HTTP_X_FORWARDED_FOR']; // Full proxy chain
$_SERVER['HTTP_X_FORWARDED_PROTO']; // http or https
$_SERVER['HTTP_X_FORWARDED_HOST']; // Original host
$_SERVER['HTTP_X_FORWARDED_PORT']; // Original port
$_SERVER['HTTP_X_REAL_IP']; // Real client IP
mTLS (Mutual TLS) Client Authentication
mTLS adds client certificate verification for zero-trust security:
- Service Mesh: Istio, Linkerd, Consul Connect
- Zero-Trust Networks: BeyondCorp, Tailscale with certificates
- API Authentication: Machine-to-machine communication
- Enterprise Security: PCI-DSS, HIPAA compliance
Basic mTLS Setup
services:
app:
image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.4-bookworm
environment:
SSL_MODE: "on"
MTLS_ENABLED: "true"
MTLS_CLIENT_CA_FILE: /etc/ssl/certs/client-ca.crt
MTLS_VERIFY_CLIENT: "optional" # or "on" for required
MTLS_VERIFY_DEPTH: "2"
volumes:
- ./certs/client-ca.crt:/etc/ssl/certs/client-ca.crt:ro
- ./certs/server.crt:/etc/ssl/certs/cbox-selfsigned.crt:ro
- ./certs/server.key:/etc/ssl/private/cbox-selfsigned.key:ro
mTLS Environment Variables
| Variable | Default | Description |
|---|---|---|
MTLS_ENABLED |
false |
Enable mTLS client certificate verification |
MTLS_CLIENT_CA_FILE |
/etc/ssl/certs/client-ca.crt |
CA certificate for client verification |
MTLS_VERIFY_CLIENT |
optional |
optional, on (required), or optional_no_ca |
MTLS_VERIFY_DEPTH |
2 |
Maximum certificate chain depth |
Client Certificate Info in PHP
When mTLS is enabled, client certificate information is available in PHP:
// Check if client presented a valid certificate
if ($_SERVER['SSL_CLIENT_VERIFY'] === 'SUCCESS') {
$clientDN = $_SERVER['SSL_CLIENT_S_DN']; // Subject DN
$issuerDN = $_SERVER['SSL_CLIENT_I_DN']; // Issuer DN
$serial = $_SERVER['SSL_CLIENT_SERIAL']; // Certificate serial
$fingerprint = $_SERVER['SSL_CLIENT_FINGERPRINT']; // SHA1 fingerprint
// Extract CN from DN
if (preg_match('/CN=([^,]+)/', $clientDN, $matches)) {
$clientCN = $matches[1]; // e.g., "service-a.internal"
}
}
Generate Test Certificates
For development/testing:
# Create CA
openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 365 \
-out ca.crt -subj "/CN=Test CA"
# Create server certificate
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -subj "/CN=localhost"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 365 -sha256
# Create client certificate
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr -subj "/CN=test-client"
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out client.crt -days 365 -sha256
# Test with curl
curl --cert client.crt --key client.key --cacert ca.crt https://localhost/api
Service Mesh Integration (Istio)
Istio injects mTLS automatically via sidecar:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
sidecar.istio.io/inject: "true"
spec:
template:
spec:
containers:
- name: app
image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.4-bookworm
env:
# Istio handles TLS termination
- name: NGINX_TRUSTED_PROXIES
value: "127.0.0.6" # Envoy sidecar
Framework Integration
Laravel TrustProxies
Edit app/Http/Middleware/TrustProxies.php:
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
// Trust all proxies (Cbox handles IP validation at Nginx level)
protected $proxies = '*';
// Standard proxy headers
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO;
}
Symfony trusted_proxies
Edit config/packages/framework.yaml:
# config/packages/framework.yaml
framework:
trusted_proxies: '%env(TRUSTED_PROXIES)%'
trusted_headers:
- 'x-forwarded-for'
- 'x-forwarded-host'
- 'x-forwarded-proto'
- 'x-forwarded-port'
# docker-compose.yml
services:
app:
environment:
TRUSTED_PROXIES: "REMOTE_ADDR" # Trust immediate proxy (Cbox validated)
Security Considerations
Only Trust Necessary Proxies
# ✅ GOOD: Trust specific proxy network
NGINX_TRUSTED_PROXIES: "172.20.0.0/16"
# ⚠️ RISKY: Trust all private networks
NGINX_TRUSTED_PROXIES: "10.0.0.0/8 172.16.0.0/12 192.168.0.0/16"
# ❌ BAD: Trust everything (IP spoofing risk)
NGINX_TRUSTED_PROXIES: "0.0.0.0/0"
Keep CDN IP Lists Updated
CDN IP ranges change. Automate updates:
#!/bin/bash
# update-cloudflare-ips.sh
curl -s https://www.cloudflare.com/ips-v4 | tr '\n' ' ' > /etc/nginx/cloudflare-ips.conf
nginx -s reload
mTLS Best Practices
- Rotate certificates regularly (90 days recommended)
- Use separate CA for clients (not the server CA)
- Validate CN/SAN in application (don't just trust any valid cert)
- Set appropriate verify depth (2-3 for typical hierarchies)
- Use
oninstead ofoptionalfor strict security
Troubleshooting
Client IP Not Detected
# Check Nginx real_ip configuration
docker exec app cat /etc/nginx/conf.d/default.conf | grep -A5 "set_real_ip"
# Expected output:
# set_real_ip_from 172.16.0.0/12;
# real_ip_header X-Forwarded-For;
# real_ip_recursive on;
PHP Not Receiving Headers
// Debug: Print all proxy-related headers
print_r(array_filter($_SERVER, function($key) {
return strpos($key, 'FORWARD') !== false ||
strpos($key, 'REAL') !== false ||
strpos($key, 'SSL_CLIENT') !== false;
}, ARRAY_FILTER_USE_KEY));
mTLS Certificate Rejected
# Verify certificate chain
openssl verify -CAfile ca.crt client.crt
# Check certificate details
openssl x509 -in client.crt -noout -text
# Test connection
openssl s_client -connect localhost:443 -cert client.crt -key client.key -CAfile ca.crt
SSL_CLIENT_VERIFY is NONE
This means mTLS is not enabled or client didn't present certificate:
# Check Nginx SSL config
docker exec app cat /etc/nginx/conf.d/default.conf | grep ssl_client
# Expected when MTLS_ENABLED=true:
# ssl_client_certificate /etc/ssl/certs/client-ca.crt;
# ssl_verify_client optional;
Common Combinations
Production with Cloudflare + mTLS API
services:
app:
image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.4-bookworm
environment:
# Cloudflare for public traffic
NGINX_TRUSTED_PROXIES: "173.245.48.0/20 103.21.244.0/22 ..."
NGINX_REAL_IP_HEADER: CF-Connecting-IP
# mTLS for internal API
SSL_MODE: "on"
MTLS_ENABLED: "true"
MTLS_VERIFY_CLIENT: "optional" # Required on /api/* routes
volumes:
- ./certs:/etc/ssl/certs:ro
Kubernetes with Istio
services:
app:
image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.4-bookworm
environment:
# Trust Istio sidecar
NGINX_TRUSTED_PROXIES: "127.0.0.6"
# Istio handles mTLS
Development with Tailscale
services:
app:
image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.4-bookworm
environment:
NGINX_TRUSTED_PROXIES: "100.64.0.0/10"
network_mode: service:tailscale