Repeated Server Header

1. The problem I’m having:

I am trying to replace the Server response header with a custom value. The problem is that with the following configuration the Server header is being sent twice, once with the value I set in the Caddy configuration and once with the value set by the upstream service.

What am I doing wrong, should I use > to defer writing the header?

Thanks in advance :pray:

2. Error messages and/or full log output:

3. Caddy version:

v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=

4. How I installed and ran Caddy:

Using Docker and Docker Compose.

a. Service/unit/compose file:

services:
  caddy:
    build:
      context: .
      dockerfile: Dockerfile
    image: example/caddy
    container_name: caddy
    hostname: caddy
    restart: unless-stopped
    environment:
      - TLS__EMAIL=${TLS__EMAIL}
      - TLS__CLOUDFLARE_API_TOKEN=${TLS__CLOUDFLARE_API_TOKEN}
      - DOMAINS__001=${DOMAINS__001}
      - TZ=${GLOBAL__TIMEZONE}
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./site:/srv
      - ${GLOBAL__VOLUME_DIR}/data:/data
      - ${GLOBAL__VOLUME_DIR}/config:/config
      - ${GLOBAL__VOLUME_DIR}/logs:/logs
    networks:
      - default
      - reverse-proxy
    ports:
      - 80:80
      - 443:443
      - 443:443/udp

networks:
  default:
    name: caddy
  reverse-proxy:
    name: reverse-proxy
    external: true

b. My complete Caddy config:

# Global Configuration
## Global options block. Entirely optional, HTTPS is ON by default.
{
	### TLS configuration
	email {$TLS__EMAIL}

	### Enable debug mode
	debug
}

# Snippets
## Snippets are reusable configuration blocks that can be included in multiple sites.
(security) {
	header {
		### Replace Server header
		Server "Example Server"

		### Disable FLoC tracking
		Permissions-Policy "interest-cohort=()"

		### Enable HSTS
		Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

		### Upgrade insecure requests to HTTPS
		Content-Security-Policy "upgrade-insecure-requests"

		### Disable clients from sniffing the media type
		X-Content-Type-Options "nosniff"
		X-XSS-Protection "1; mode=block"
		Referrer-Policy "strict-origin-when-cross-origin"

		### Clickjacking protection
		?X-Frame-Options "SAMEORIGIN"
	}
}

(compression) {
	### Enable compression
	encode zstd gzip
}

(log) {
	log {args[0]} {
		hostnames {args[0]}
		level INFO
		output file /logs/{args[0]}.log {
			roll_size 3MiB
			roll_keep 5
			roll_keep_for 48h
		}
		format json
	}
}

(service) {
	import log {args[0]}

	@{args[1]} host {args[0]}

	handle @{args[1]} {
		import security
		import compression

		reverse_proxy {args[1]}:{args[2]}
	}
}

# Sites
## Configuration for defining sites.
*.{$DOMAINS__001}, {$DOMAINS__001} {
	tls {$TLS__EMAIL} {
		dns cloudflare {$TLS__CLOUDFLARE_API_TOKEN}
		resolvers 1.1.1.1 8.8.8.8 8.8.4.4
	}

	import service wordpress.{$DOMAINS__001} wordpress 80

	handle {
		abort
	}
}

That’s normal and expected, as it shows you that the request hit both servers.

You can override headers from the upstream by using header_down inside the reverse_proxy directive. It works the same way header directive does, but it applies specifically to headers going toward the downstream (client).

2 Likes

Thank you, @matt. Understood.

I have a couple of additional questions.

Would using the > (defer) option with the header directive be equivalent to using header_down inside the reverse_proxy directive? Making the change I see that it overwrites the header Server from the upstream server with the one I specify and the client only gets one.

# Snippets
## Snippets are reusable configuration blocks that can be included in multiple sites.
(security) {
	header {
		>Server "Example Server"

		...
	}
}

On the other hand, could this header Server be set globally without having to specify it in each reverse_proxy/header_down directive?

Thanks again!

Yes, you can instead defer in the header directive to apply the changes after the reverse proxy sets the header from the upstream.

Caddy doesn’t really support “global” configuration (there are “global options” but those mainly pertain to things that don’t fit in individual site blocks).

2 Likes

Great!

And the last question, is there any difference to consider between using defer in the header directive and using the reverse_proxy/header_down directive for this purpose?

Thanks again!

header with defer overwrites pretty much any changes to the header while handling the request, whereas header_down in the reverse proxy affects only the proxied headers.

2 Likes

Hello again, @matt .

Sorry for asking another question. Going back to the use of the header directive, I see from the documentation that it says the following:

Based on that documentation, shouldn’t the header set by the upstream service be overridden? You said no, that the expected behavior is to keep both headers (in this case the Server header), but why? Because the directive is evaluated immediately?

Thanks again.

It’s overwritten at the time the operation is evaluated, which is earlier in the handler chain than the proxy, unless it’s deferred to the very end.

3 Likes

Hello, @matt .

Great, got it!

I’ve been doing some more testing with the header directive these days and, going back to the original example, isn’t defer supposed to be enabled when using the ? prefix to set a default value for a header if it doesn’t exist?

...
(security) {
	header {
		### Replace Server header
		Server "Example Server"

		...

		### Clickjacking protection
		?X-Frame-Options "SAMEORIGIN"
	}
}
...

In this example, by setting a default value for the X-Frame-Options header, the defer should be activated and, therefore, the Server header should overwrite all other set values since operations on the headers are deferred until the time when the response is written to the client. But that’s not really the case. The above example causes two Server headers to be sent to the client, one with the value Example Server and one with the value set by the upstream service (as in the original question).

Instead, this works if I use - or > in any other header, for example:

...
(security) {
	header {
		### Replace Server header
		Server "Example Server"

		...

		### Clickjacking protection
		-X-Frame-Options "SAMEORIGIN"
	}
}
...

This results in the Server header value being set to Example Server overwriting the one that might be set by the upstream service.

Is there a bug with this or is the documentation not correct?

Thanks in advance :pray:

Hmm, you’re right, that doesn’t seem to be setting deferred: true in the JSON.

I found in the commit history that the deferred logic was removed because:

It removes the need to specify the header as “deferred” because it is
already implicitely deferred by the use of the require handler. This
should be less confusing to the user.

I wonder if we need to revisit this.

@borjapazr Would you mind opening an issue and link to this thread? Doesn’t have to be super detailed since you’ve done all that here. Just so we don’t forget about this.

1 Like

In that case, you should use multiple header directives, not just one big one. So like:

header Server "Example Server"
header ?X-Frame-Options "SAMEORIGIN"

Or something like that, so that they have their modes separated.

2 Likes

That’s probably not a bad idea anyway.