Proxy Headers #
HTTP headers act as carriers of important metadata facilitating communication between the client browser, the reverse proxy, and your backend application servers. Correct header manipulation configuration at the proxy level is a fundamental aspect of securing data flows, forwarding real user identity, managing cross-origin permissions (CORS), and hiding internal server technology traces. Caddy provides a very flexible and secure two-way header modification mechanism to ensure your backends receive the trusted information they need.
The Header Pipeline Mechanism: header_up vs header_down
#
When Caddy acts as a reverse proxy, it manages a two-way data pipeline. Header modification can be done in both phases of this data journey:
flowchart TD
subgraph RequestFlow ["Request (Client to Backend)"]
direction LR
Client1["Client"] -->|"Request"| Caddy1["Caddy Proxy"]
Caddy1 -->|"header_up"| Backend1["Backend"]
end
subgraph ResponseFlow ["Response (Backend to Client)"]
direction LR
Backend2["Backend"] -->|"header_down"| Caddy2["Caddy Proxy"]
Caddy2 -->|"Response"| Client2["Client"]
endheader_up: Used to modify headers on the request object sent by the client before Caddy forwards it to the backend server. Typically used to inject the client’s real IP, reset theHostheader so the backend recognizes it, or remove sensitive credentials.header_down: Used to modify headers on the response object returned by the backend server before Caddy sends it back to the client browser. Typically used to strip backend technology markers, inject browser security policies, or insert caching headers.
Here’s an example of the basic syntax implementation in the Caddyfile:
# Two-way modification example
example.com {
reverse_proxy localhost:3000 {
# Modify the request to the backend
header_up X-Custom-Request-Header "Upstream-Data"
# Modify the response to the client
header_down X-Custom-Response-Header "Downstream-Data"
}
}
Forwarding Client Identity (Standard Headers) #
By default, when a backend server receives a request from a reverse proxy, it sees the source IP address of the connection coming from Caddy’s IP (usually 127.0.0.1 or a local network IP), not the user’s real internet IP. This condition prevents the backend from logging user IPs correctly, complicates geolocation tracking, and breaks the backend’s internal security block logic.
To fix this, Caddy automatically injects several industry-standard headers toward the backend when you enable the reverse_proxy directive:
X-Forwarded-For: Stores the list of the real client IP along with all other proxies the request passed through. Caddy fills this value with the real client IP automatically.X-Forwarded-Proto: Identifies the protocol scheme (HTTP or HTTPS) used by the client browser when communicating with Caddy. This is crucial for the backend to detect whether the request is secure or needs to be redirected to HTTPS.X-Forwarded-Host: Contains the original domain name (Hostheader) requested by the client browser on the internet.
If you want to add popular non-standard headers like X-Real-IP, you can add them explicitly using header_up:
# Manual full identity forwarding configuration
example.com {
reverse_proxy localhost:3000 {
# Fill the client's real IP without the port into the X-Real-IP header
header_up X-Real-IP {remote_host}
# Fill the backend destination host
header_up Host {upstream_hostport}
}
}
IP Spoofing and the trusted_proxies Configuration
#
The X-Forwarded-For header is very vulnerable to IP manipulation attacks (IP spoofing) if your reverse proxy isn’t strictly configured. Because this header is a plain HTTP text header, an attacker can manually inject an X-Forwarded-For: 8.8.8.8 header containing a fake IP when sending a request to your Caddy server.
The IP Spoofing Danger Scenario #
If Caddy blindly trusts those externally-sent headers, it appends the fake IP to the list of IPs forwarded to the backend. If your backend bases its access security (like an admin panel whitelist) on the first index of X-Forwarded-For, attackers can easily bypass your security system.
To prevent this security hole, Caddy provides the global trusted_proxies configuration option:
flowchart TD
Request["Client Sends Request\n(IP: 203.0.113.1)\nX-Forwarded-For: 1.2.3.4 (Fake)"] --> Caddy{"Caddy Proxy"}
Caddy -->|Evaluate Source IP| TrustedCheck{"Is IP 203.0.113.1\nin trusted_proxies?"}
TrustedCheck -- No (Regular User) --> StripIP["Ignore the fake input header\nX-Forwarded-For forwarded to the backend as:\n203.0.113.1"]
TrustedCheck -- Yes (Cloudflare/CDN) --> TrustIP["Accept the input header\nX-Forwarded-For forwarded to the backend as:\n1.2.3.4, 203.0.113.1"]
StripIP --> Backend["Backend Server"]
TrustIP --> BackendYou must define trusted proxy IP addresses (like Cloudflare IPs or your cloud’s internal Load Balancer IPs) in the Caddyfile global options block so Caddy knows which IPs are allowed to forward client-sent headers:
# Caddyfile global options block
{
servers {
# Register Cloudflare IP subnets or your internal load balancer
# Example: private VPC subnet 10.0.0.0/16
trusted_proxies static 10.0.0.0/16
}
}
# Site block
example.com {
reverse_proxy localhost:3000
}
With this configuration, if there’s a direct connection from the outside internet (whose IP is outside the 10.0.0.0/16 subnet), Caddy automatically ignores and removes the fake X-Forwarded-For header data sent by that client, replacing it with the real IP detected at the TCP socket connection level.
Dynamic Header Values (Placeholders) #
Caddy ships with a very rich placeholder system for inserting dynamic request values into the headers you manipulate. These values are filled automatically by Caddy at runtime for every request.
Here’s a table of the most frequently used placeholders along with example output values:
| Placeholder Name | Value Explanation | Example Output |
|---|---|---|
{remote_host} | The client’s real IP address (without the port number) | 192.168.1.50 |
{remote_port} | The client’s outgoing connection port number | 53210 |
{remote_ip} | Synonym for the client’s real IP address | 192.168.1.50 |
{scheme} | The protocol used by the client | https or http |
{host} | The domain name requested by the client (without port) | api.example.com |
{hostport} | The domain name with the port (if non-standard) | api.example.com:8443 |
{upstream_hostport} | The backend destination address selected by Caddy | 10.0.0.22:3000 |
{method} | The HTTP method used by the request | GET, POST, DELETE |
{uri} | The full URI path including the query string | /users/search?q=caddy |
{path} | The clean URI path without the query string | /users/search |
{query} | The search query parameters (without the question mark) | q=caddy&limit=10 |
{http.request.uuid} | The unique UUID v4 tracking ID for this request | 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d |
{header.HEADER_NAME} | Reads the value of a specific client request header | {header.User-Agent} |
Securing Responses: Hiding Technology Traces #
A good backend server must not leak specific details about the technology, programming language, or framework it uses. Information like X-Powered-By: PHP/8.3.2 or Server: Microsoft-IIS/10.0 makes it easy for internet attackers to look up vulnerability lists (CVEs) related to that technology for targeted attacks.
With Caddy, you can remove all these marker headers centrally at the proxy level using the - symbol before the header name:
# Backend metadata leak protection
example.com {
reverse_proxy localhost:3000 {
# Remove sensitive headers from the backend response before sending to the client
header_down -X-Powered-By
header_down -X-AspNet-Version
header_down -Server
}
}
Injecting Centralized Security Headers #
Besides removal, you can also force the addition of industry-standard security headers on all responses leaving your Caddy proxy so client browsers tighten their security:
# Injecting security headers
example.com {
reverse_proxy localhost:3000 {
# Protect against Clickjacking
header_down X-Frame-Options "SAMEORIGIN"
# MIME type protection (MIME sniffing prevention)
header_down X-Content-Type-Options "nosniff"
# HSTS - Force the browser to use HTTPS for 1 year
header_down Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Referrer Policy
header_down Referrer-Policy "strict-origin-when-cross-origin"
# Remove server info
header_down -Server
}
}
Implementing Content Security Policy (CSP) #
To mitigate Cross-Site Scripting (XSS) attacks and data injection, you can inject an advanced CSP header centrally through Caddy. This header tells the client browser where JavaScript files, CSS styles, and images may be loaded from:
# Example of centralized CSP addition
example.com {
reverse_proxy localhost:3000 {
# Deny all assets except from the same origin and trusted subdomains
header_down Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://apis.google.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com;"
}
}
Managing CORS (Cross-Origin Resource Sharing) Centrally #
When you build a separated architecture application (for example, a React Frontend running on app.example.com and an API Backend running on api.example.com), the client browser blocks API requests because of the Same-Origin Policy.
You must allow that access by configuring CORS headers. Writing CORS logic inside the backend code is often cumbersome and inconsistent. You can hand this task over to the Caddy reverse proxy level:
sequenceDiagram
autonumber
participant Browser as Client Browser
participant Caddy as Caddy Proxy
participant Backend as App Backend
Note over Browser: Preflight Request (OPTIONS)
Browser->>Caddy: OPTIONS /api/data (Origin: app.example.com)
Note over Caddy: Caddy matches the @preflight matcher<br/>and answers directly without the backend
Caddy-->>Browser: 204 No Content (Access-Control-Allow-Origin: app.example.com)
Note over Browser: Real Request (GET/POST)
Browser->>Caddy: GET /api/data (Origin: app.example.com)
Caddy->>Backend: Forward GET /api/data
Backend-->>Caddy: 200 OK (JSON Data)
Note over Caddy: Caddy injects CORS header_down
Caddy-->>Browser: 200 OK (Access-Control-Allow-Origin: app.example.com)Here’s the Caddyfile configuration to handle the CORS handshake centrally:
# Securing and configuring CORS in Caddy
api.example.com {
# Define a matcher for the OPTIONS HTTP method (Preflight)
@cors_preflight method OPTIONS
# Handle the Preflight request directly in Caddy without burdening the backend
handle @cors_preflight {
header Access-Control-Allow-Origin "https://app.example.com"
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept"
header Access-Control-Allow-Credentials "true"
header Access-Control-Max-Age "86400"
respond "" 204
}
# Forward normal requests (GET/POST) to the backend and insert CORS headers
reverse_proxy localhost:8080 {
header_down Access-Control-Allow-Origin "https://app.example.com"
header_down Access-Control-Allow-Credentials "true"
}
}
Request Tracking (Request Tracking & Distributed Tracing) #
In distributed system architectures, one client request to the main gateway can trigger a chain of calls to various backend microservices. If a failure or slow response happens midway, it’s very hard to find out which request in the backend logs relates to that user complaint.
You can implement Distributed Tracing using a unique tracking ID (Correlation ID) in Caddy. Caddy has the {http.request.uuid} placeholder, which generates a unique UUID for each incoming request. You inject this UUID toward the backend and return it to the client:
# Log trace distribution
example.com {
reverse_proxy localhost:3000 {
# Inject a unique Request ID into the backend request
header_up X-Request-ID {http.request.uuid}
# Insert the same Request ID into the client response
# Useful for users when reporting errors to customer service
header_down X-Request-ID {http.request.uuid}
}
log {
format json
# The request UUID is automatically recorded in the Caddy log
}
}
At your backend application level (for example, using the Winston logger middleware in Node.js or Logback in Java), you only need to read the X-Request-ID header value from the incoming request and include it in every application log line. If a client reports an error including the X-Request-ID header they received, your developer team just greps that ID across the whole log cluster to analyze the full journey of that request from start to finish.
Authentication and User Metadata Forwarding #
To simplify backend code, you can move the user authentication validation process (like HTTP basic auth, API token validation, or JWT authentication) to the Caddy layer using Caddy authentication plugin modules (like the JWT plugin).
After Caddy successfully verifies the user’s token, Caddy can forward the user’s identity details to the backend through custom headers and remove the raw credentials so the backend doesn’t need to re-validate:
# Authentication metadata forwarding
secure-api.example.com {
# (Assume we're using the Caddy JWT plugin here)
# jwt_auth {
# secret "jwt-secret-key"
# }
reverse_proxy localhost:8080 {
# Forward user metadata extracted from JWT claims
header_up X-User-ID {http.auth.user.id}
header_up X-User-Email {http.auth.user.email}
header_up X-User-Role {http.auth.user.role}
# DON'T forward the raw Authorization token to the backend for security
header_up -Authorization
}
}
This scenario keeps your backend clean, secure, and only receiving legitimately validated requests from the main Caddy gateway.
Handling Large Headers and Cookies #
In some corporate-scale web applications using centralized identity management systems (like Active Directory, Keycloak, or Okta), user session cookie sizes can bloat very large.
If a cookie header exceeds the default buffer size limit on the proxy, the connection is dropped with a 400 Bad Request or 431 Request Header Fields Too Large error. Caddy lets you adjust the header read size limit at the server level before forwarding to the backend:
# Adjusting the header buffer size limit
{
# Global server configuration
servers {
# Raise the client request header read limit to 16KB (Default: 1MB for modern Caddy, but customizable)
max_header_bytes 16384
}
}
example.com {
reverse_proxy localhost:3000 {
# Set the http transport read buffer to 8KB to accommodate large backend responses
transport http {
read_buffer_size 8kb
}
}
}
Summary #
- Two-Way Modification: Use
header_upto modify request metadata to the backend andheader_downto modify response data to the client browser.- Preventing IP Spoofing: Register trusted external proxy IPs in the global
trusted_proxiesoption so attackers can’t fake their IP addresses via theX-Forwarded-Forheader.- Extra Security: Remove informative backend headers (
-Server,-X-Powered-By) and inject HSTS, CSP, and CORS centrally at the Caddy proxy.- System Tracing: Inject
X-Request-IDusing the dynamic{http.request.uuid}placeholder to both the backend and the client to facilitate distributed log tracing.- Clean Authentication: Caddy can process authentication at the gateway and then forward trusted user data via custom headers to the backend without sending raw sensitive tokens.