How can I merge Set-Cookie headers from separate reverse_proxy calls?

1. The problem I’m having:

I am using the forward_auth directive to check user authentication state before using reverse_proxy on the main app. Depending on the user’s current state, our authentication server may return a Set-Cookie header to update or reset the user’s authentication related cookies.

In that case, we want to merge that Set-Cookie header with the one received from our reverse_proxy. I have this partially working using the Caddyfile below, however if the authentication server doesn’t return a Set-Cookie header, the end result is that Set-Cookie gets sets to the raw placeholder name (i.e: {http.reverse_proxy.header.Set-Cookie}).

2. Error messages and/or full log output:

No errors, and the logs aren’t super useful since Set-Cookie is redacted. When curling the request, I receive the following headers:

Set-Cookie: mock-cookie-upstream=mock-value; Path=/; Domain=oauth2-proxy-mock
Set-Cookie: {http.reverse_proxy.header.Set-Cookie}

The first Set-Cookie header is from my upstream service. The second one is what my caddy server is setting.

3. Caddy version:

caddy:2.9 docker image

4. How I installed and ran Caddy:

I am using a Caddyfile and running caddy run through Docker.

a. System environment:

MacOS/Docker

b. Command:

`docker run` (`caddy run` under the hood)

c. Service/unit/compose file:

services:
  caddy-proxy:
    build:
      context: ./caddy-proxy
      dockerfile: Dockerfile
    container_name: caddy-proxy
    ports:
      - "8080:8080"

d. My complete Caddy config:

:8080 {
	    forward_auth http://localhost:8081 {
                uri /oauth2/auth
                copy_headers X-Access-Token Set-Cookie
        }
        reverse_proxy http://localhost:8082 {
                header_up -Set-Cookie
		        header_down +Set-Cookie {rp.header.Set-Cookie}
        }
}

Other things I’ve tried:

  1. Changing to header_down +Set-Cookie {rp.header.Set-Cookie:""} – It doesn’t pick up the Set-Cookie value at all in this case.
  2. Removing my header_down directive and replacing it with header +Set-Cookie {rp.header.Set-Cookie:""}

5. Links to relevant resources:

Edit: More context

There might be a better way to handle this, but here’s a quick example I put together.

Caddyfile

{
	http_port 8080
}

:8080 {
	## Authenticate first
	forward_auth http://127.0.0.1:8081 {
		uri /oauth2/auth
		copy_headers {
			## Copy X-Access-Token from the response as-is
			X-Access-Token
			## Preserve Set-Cookie from the AUTH site as X-Access-Cookie request header
			Set-Cookie>X-Access-Cookie
		}
	}
	
	## If we got an X-Access-Cookie from the AUTH site, pass it along
	@authCookie header X-Access-Cookie *
	handle @authCookie {
		header +Set-Cookie {header.X-Access-Cookie}
	}

	## Regular reverse proxy business
	reverse_proxy http://127.0.0.1:8082
}

## Auth Site
:8081 {
	header {
		X-Access-Token TOKEN8081
		Set-Cookie cookie8081=value8081
	}
	respond 200
}

## Main Site
:8082 {
	header /gimmecookie* Set-Cookie cookie8082=value8082
	respond "Main Site Alive!"
}

Test results:

## Auth site (8081) sets a cookie, Main site (8082) doesn’t

$ curl http://localhost:8080 -I
HTTP/1.1 200 OK
Content-Length: 16
Content-Type: text/plain; charset=utf-8
Date: Fri, 11 Apr 2025 19:44:57 GMT
Server: Caddy
Server: Caddy
Set-Cookie: cookie8081=value8081

## Both Auth site (8081) and Main site (8082) set cookies

$ curl http://localhost:8080/gimmecookie -I
HTTP/1.1 200 OK
Content-Length: 16
Content-Type: text/plain; charset=utf-8
Date: Fri, 11 Apr 2025 19:46:26 GMT
Server: Caddy
Server: Caddy
Set-Cookie: cookie8081=value8081
Set-Cookie: cookie8082=value8082
2 Likes

And obviously, if your Auth site sets no cookie:

## Auth Site
:8081 {
	header {
		X-Access-Token TOKEN8081
		# Set-Cookie cookie8081=value8081
	}
	respond 200
}

Test results:

## Main site (8082) sets a cookie, Auth site (8081) doesn’t

$ curl http://localhost:8080/gimmecookie -I
HTTP/1.1 200 OK
Content-Length: 16
Content-Type: text/plain; charset=utf-8
Date: Fri, 11 Apr 2025 19:59:13 GMT
Server: Caddy
Server: Caddy
Set-Cookie: cookie8082=value8082

## Both Auth site (8081) and Main site (8082) set no cookies

$ curl http://localhost:8080 -I
HTTP/1.1 200 OK
Content-Length: 16
Content-Type: text/plain; charset=utf-8
Date: Fri, 11 Apr 2025 19:59:48 GMT
Server: Caddy
Server: Caddy
2 Likes

And one last thing - just to make sure no one’s spoofing your cookies: instead of using something like X-Access-Cookie as your temporary header name, go with something more random, like X-74A3A1D4-71AC-7C27-BCBB-8221E2930CA9.

It’s just for internal use anyway, never exposed to the client, so a random value helps avoid any accidental or intentional overlap.

1 Like

Thank you for the responses, your approach seems to be working :slight_smile:

I actually was typing up my concern around this as you messaged haha.

Would it be reasonable to use the request_header directive to strip the header on incoming requests before calling forward_auth? My thought is that way no one can pass in their own value so if its defined it could only come from the forward_auth call, and would mean we could use a more human-readable name.

This is sort of what I’m thinking:

:8080 {
        request_header -X-Access-Cookie
	## Authenticate first
	forward_auth http://127.0.0.1:8081 {
		uri /oauth2/auth
		copy_headers {
			## Copy X-Access-Token from the response as-is
			X-Access-Token
			## Preserve Set-Cookie from the AUTH site as X-Access-Cookie request header
			Set-Cookie>X-Access-Cookie
		}
	}
	
	## If we got an X-Access-Cookie from the AUTH site, pass it along
	@authCookie header X-Access-Cookie *
	handle @authCookie {
		header +Set-Cookie {header.X-Access-Cookie}
	}

	## Regular reverse proxy business
	reverse_proxy http://127.0.0.1:8082
}
1 Like

You definitely can do that, but you’ll want to watch out for directive order in the Caddyfile - forward_auth has higher priority than request_header, so if you’re not careful, you might strip your own header before it gets passed along :slight_smile:

To avoid that, wrap everything in a route block. That way, things are processed in the order you specify.

:8080 {

	route {
		request_header -X-Access-Cookie
		
		## Authenticate first
		forward_auth http://127.0.0.1:8081 {
			uri /oauth2/auth
			copy_headers {
				## Copy X-Access-Token from the response as-is
				X-Access-Token
				## Preserve Set-Cookie from the AUTH site as X-Access-Cookie request header
				Set-Cookie>X-Access-Cookie
			}
		}
		
		## If we got an X-Access-Cookie from the AUTH site, pass it along
		@authCookie header X-Access-Cookie *
		header @authCookie +Set-Cookie {header.X-Access-Cookie}
	}

	## Regular reverse proxy business
	reverse_proxy http://127.0.0.1:8082
}

That’s totally fine too. I’d still lean toward something a bit random, like X-Access-Cookie-wkQE5, just to avoid any accidental collisions. But if you’re sure X-Access-Cookie won’t be used anywhere else in your setup, it’s probably not a big deal. Up to you!

You know what, here’s a completely simplified version - no extra headers, just Set-Cookie:

:8080 {

	route {
		request_header -Set-Cookie

		forward_auth http://127.0.0.1:8081 {
			uri /oauth2/auth
			copy_headers X-Access-Token Set-Cookie
		}
		
		@authCookie header Set-Cookie *
		header @authCookie +Set-Cookie {header.Set-Cookie}
	}

	reverse_proxy http://127.0.0.1:8082
}

Since Set-Cookie isn’t a request header (it should never come from the client), we can just strip it right away and skip the whole renaming thing. And with route, everything stays nice and simple.

1 Like

Thank you again for the continued replies. I think that makes sense. Would this effectively be equivalent to what you have there?

:8080 {
        
	request_header -Set-Cookie
	handle {

		forward_auth http://127.0.0.1:8081 {
			uri /oauth2/auth
			copy_headers X-Access-Token Set-Cookie
		}
		
		@authCookie header Set-Cookie *
		header @authCookie +Set-Cookie {header.Set-Cookie}
	}

	reverse_proxy http://127.0.0.1:8082
}

I’m wondering because the Caddyfile I would like to apply this to is kind of complex and I would need to do a fair amount of research before making the switch from handle->route.

This seems like it is working for me, and I think that makes sense based on the order of directives you linked; Just hoping for a sanity check haha.

It depends on the directive order.

In terms of priority, the directives in your code will be ordered like this:

  1. request_header
  2. handle
  3. reverse_proxy

Which follows the flow of your code. But inside the handle block, they’ll get re-ordered differently than what you have:

  1. header
  2. forward_auth

So, when it comes to route vs handle, “directives within a route are not re-ordered, giving you more control if needed”.

TLDR: no, your code would not work the way you want, sorry.

You can also check out the global order option if you want to change the default directive order and prefer not to use route.

So, this would be your code:

{
	http_port 8080
	order forward_auth before header
}

:8080 {
        
	request_header -Set-Cookie
	handle {

		forward_auth http://127.0.0.1:8081 {
			uri /oauth2/auth
			copy_headers X-Access-Token Set-Cookie
		}
		
		@authCookie header Set-Cookie *
		header @authCookie +Set-Cookie {header.Set-Cookie}
	}

	reverse_proxy http://127.0.0.1:8082
}

Just make sure that changing the global order of forward_auth to come before header doesn’t end up breaking something else in your config.

1 Like

I see what you mean, sorry for being a little dense. Thank you for being patient with me.

1 Like

No worries. Check my previous post, I’ve updated it with a few more details.