Copy header into new header iff it is set

1. Caddy version: v2.6.2

2. How I installed, and run Caddy:

Docker swarm deploy of this service:

  caddy:
    image: caddy:2-alpine
    ports:
      - target: 443
        published: 443
        mode: host
      - target: 80
        published: 80
        mode: host
    volumes:
      - type: bind
        source: /srv
        target: /srv
        read_only: true
      - type: bind
        source: /srv/sys/caddy
        target: /data/caddy
    configs:
      - source: Caddyfile
        target: /etc/caddy/Caddyfile
      - source: index.html
        target: /www/index.html
      - source: index.mjs
        target: /www/index.mjs
      - source: deergrove.png
        target: /www/deergrove.png
      - source: index.css
        target: /www/index.css
      - source: browser.html
        target: /browser.html
    extra_hosts:
      - host.docker.internal:host-gateway

a. System environment:

Raspberry Pi 4, Ubuntu 22.04.1, Linux 5.15.0-1024-raspi, docker 20.10.23

b. Command:

docker --context deergrove stack deploy -c docker-compose.yaml --prune $(basename $(pwd))

c. Service/unit/compose file:

version: "3.8"
services:
  coredns:
    image: coredns/coredns
    networks:
      - hostnet
    configs:
      - source: Corefile
        target: /Corefile

  caddy:
    image: caddy:2-alpine
    ports:
      - target: 443
        published: 443
        mode: host
      - target: 80
        published: 80
        mode: host
    volumes:
      - type: bind
        source: /srv
        target: /srv
        read_only: true
      - type: bind
        source: /srv/sys/caddy
        target: /data/caddy
    configs:
      - source: Caddyfile
        target: /etc/caddy/Caddyfile
      - source: index.html
        target: /www/index.html
      - source: index.mjs
        target: /www/index.mjs
      - source: deergrove.png
        target: /www/deergrove.png
      - source: index.css
        target: /www/index.css
      - source: browser.html
        target: /browser.html
    extra_hosts:
      - host.docker.internal:host-gateway

  authelia:
    image: authelia/authelia
    environment:
      AUTHELIA_JWT_SECRET_FILE: /run/secrets/jwt.secret
      AUTHELIA_SESSION_SECRET_FILE: /run/secrets/session.secret
      AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: /run/secrets/storage.secret
    secrets:
      - jwt.secret
      - session.secret
      - storage.secret
      - users.yaml
    configs:
      - source: authelia.yaml
        target: /config/configuration.yml
    volumes:
      - type: bind
        source: /srv/sys/authelia
        target: /srv/sys/authelia

  forgejo:
    image: codeberg.org/forgejo/forgejo:1.18-rootless
    secrets:
      - source: forgejo.ini
        target: /etc/gitea/app.ini
        uid: "1000"
        gid: "1000"
        mode: 0400
    volumes:
      - type: bind
        source: /srv/sys/forgejo
        target: /data
      - type: bind
        source: /etc/timezone
        target: /etc/timezone
        read_only: true
      - type: bind
        source: /etc/localtime
        target: /etc/localtime
        read_only: true

  geneweb:
    image: ravermeister/geneweb
    volumes:
      - type: bind
        source: /srv/sys/geneweb/etc
        target: /usr/local/share/geneweb/etc
      - type: bind
        source: /srv/sys/geneweb/share/data
        target: /usr/local/share/geneweb/share/data
      - type: bind
        source: /srv/sys/geneweb/log
        target: /usr/local/share/geneweb/log

  webdav:
    image: micromata/dave
    volumes:
      - type: bind
        source: /srv
        target: /data
    configs:
      - source: dave.yaml
        target: /config/config.yaml
    user: "911:911"

  ddns:
    image: qmcgaw/ddns-updater
    dns:
      - 1.1.1.1
    volumes:
      - type: bind
        source: /srv/sys/ddns-updater
        target: /updater/data


configs:
  dave.yaml:
    file: dave.yaml
    name: dave.yaml-v3
  Corefile:
    file: Corefile
    name: Corefile-v4
  Caddyfile:
    file: Caddyfile
    name: Caddyfile-v98
  index.html:
    file: www/index.html
    name: index.html-v36
  index.mjs:
    file: www/index.mjs
    name: index.mjs-v1
  index.css:
    file: www/index.css
    name: index.css-v1
  browser.html:
    file: www/browser.html
    name: browser.html-v3
  deergrove.png:
    file: www/deergrove.png
    name: deergrove.png-v1
  authelia.yaml:
    file: authelia.yaml
    name: authelia.yaml-v6

secrets:
  passwd:
    file: secrets/passwd
    name: passwd-v2
  simpleauth.key:
    file: secrets/simpleauth.key
    name: simpleauth.key-v1
  tunnel:
    file: secrets/tunnel
    name: tunnel-v1
  known_hosts:
    file: secrets/known_hosts
    name: known_hosts-v1
  forgejo.ini:
    file: secrets/forgejo.ini
    name: forgejo.ini-v1
  jwt.secret:
    file: secrets/jwt.secret
    name: jwt.secret-v1
  storage.secret:
    file: secrets/storage.secret
    name: storage.secret-v1
  session.secret:
    file: secrets/session.secret
    name: session.secret-v1
  users.yaml:
    file: secrets/users.yaml
    name: users.yaml-v2

networks:
  hostnet:
    external: true
    name: host

d. My complete Caddy config:

{
        email neale@woozle.org
}

(authelia) {
        uri /api/verify?rd=https://auth.woozle.org/
        copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
}

(restricted-access) {
        @noauth header !Authorization
        route {
                forward_auth @noauth authelia:9091 {
                        import authelia
                }
                forward_auth authelia:9091 {
                        import authelia
                        header_up Proxy-Authorization {header.authorization}
                }
        }
}

auth.woozle.org {
        reverse_proxy authelia:9091
}

git.woozle.org {
        reverse_proxy forgejo:3000
}

drive.woozle.org {
        import restricted-access

        @nondav {
                method HEAD GET
        }
        # route overrides built-in ordering
        route {
                file_server @nondav {
                        root /srv/
                        browse /browser.html
                }
                reverse_proxy webdav:8000
        }
}

media.woozle.org {
        reverse_proxy jellyfin:8096
}

ancestry.woozle.org {
        reverse_proxy geneweb:2317
}

##
## handle sends original path
## handle_path truncates path
##

(deergrove) {
        handle_path /ddns/* {
                import restricted-access
                reverse_proxy ddns:8000
        }

        handle_path /octoprint/* {
                import restricted-access
                reverse_proxy {
                        to 192.168.86.20:80
                        header_up X-Script-Name "/octoprint"
                }
        }

        handle /webcam/* {
                # Octoprint doesn't properly prefix webcam URLs
                import restricted-access
                reverse_proxy {
                        to 192.168.86.20:80
                }
        }

        handle_path /public/* {
                file_server {
                        root /srv/storage/public
                }
        }

        handle {
                import restricted-access
                file_server {
                        root /www
                }
        }
}

deergrove.woozle.org {
        import deergrove
}

sweetums.lan {
        tls internal
        import deergrove
}

3. The problem I’m having:

Authelia needs the Authorization header passed in as Proxy-Authorization, but only if an Authorization header was sent by the client.

I’m trying to make that happen without modifying Caddy’s source code.

What I have right now is getting close, but appears to be running requests with no Authorization header set, through both forward_auth sections. This means every client request results in Caddy sending two requests to Authelia, which makes Authelia complain that the Proxy-Authorization header of the second request is incorrectly formatted. That’s true: {headers.authorization} is an empty string.

4. Error messages and/or full log output:

This is the result of running two curl commands:

curl -b /tmp/jar -c /tmp/jar  https://auth.woozle.org/api/firstfactor --data-raw '{"username":"neale","password":"'$password'"}'
curl -b /tmp/jar -c /tmp/jar -v https://deergrove.woozle.org/
homelab_caddy.1.s7dc6ketwosz@sweetums    | {"level":"debug","ts":1675702891.2546768,"logger":"events","msg":"event","name":"tls_get_certificate","id":"2dfddc06-f5e8-40cd-bbd2-35d7a1868b07","origin":"tls","data":{"client_hello":{"CipherSuites":[4866,4867,4865,49196,49200,159,52393,52392,52394,49195,49199,158,49188,49192,107,49187,49191,103,49162,49172,57,49161,49171,51,157,156,61,60,53,47,255],"ServerName":"auth.woozle.org","SupportedCurves":[29,23,30,25,24,256,257,258,259,260],"SupportedPoints":"AAEC","SignatureSchemes":[1027,1283,1539,2055,2056,2057,2058,2059,2052,2053,2054,1025,1281,1537,771,769,770,1026,1282,1538],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[772,771],"Conn":{}}}}
homelab_caddy.1.s7dc6ketwosz@sweetums    | {"level":"debug","ts":1675702891.2548246,"logger":"tls.handshake","msg":"choosing certificate","identifier":"auth.woozle.org","num_choices":1}
homelab_caddy.1.s7dc6ketwosz@sweetums    | {"level":"debug","ts":1675702891.2548718,"logger":"tls.handshake","msg":"default certificate selection results","identifier":"auth.woozle.org","subjects":["auth.woozle.org"],"managed":true,"issuer_key":"acme-v02.api.letsencrypt.org-directory","hash":"e3fa1ab50a261ccf2aae92d4df5e65b75723c0e271e8da3b00f1f90a69efd115"}
homelab_caddy.1.s7dc6ketwosz@sweetums    | {"level":"debug","ts":1675702891.2548974,"logger":"tls.handshake","msg":"matched certificate in cache","remote_ip":"172.18.0.1","remote_port":"55414","subjects":["auth.woozle.org"],"managed":true,"expiration":1683429570,"hash":"e3fa1ab50a261ccf2aae92d4df5e65b75723c0e271e8da3b00f1f90a69efd115"}
homelab_caddy.1.s7dc6ketwosz@sweetums    | {"level":"debug","ts":1675702891.2662523,"logger":"http.handlers.reverse_proxy","msg":"selected upstream","dial":"authelia:9091","total_upstreams":1}
homelab_caddy.1.s7dc6ketwosz@sweetums    | {"level":"debug","ts":1675702892.2769196,"logger":"http.handlers.reverse_proxy","msg":"upstream roundtrip","upstream":"authelia:9091","duration":1.010491726,"request":{"remote_ip":"172.18.0.1","remote_port":"55414","proto":"HTTP/2.0","method":"POST","host":"auth.woozle.org","uri":"/api/firstfactor","headers":{"X-Forwarded-For":["172.18.0.1"],"X-Forwarded-Proto":["https"],"X-Forwarded-Host":["auth.woozle.org"],"User-Agent":["curl/7.81.0"],"Accept":["*/*"],"Cookie":[],"Content-Length":["62"],"Content-Type":["application/x-www-form-urlencoded"]},"tls":{"resumed":false,"version":772,"cipher_suite":4867,"proto":"h2","server_name":"auth.woozle.org"}},"headers":{"Content-Type":["application/json; charset=utf-8"],"X-Content-Type-Options":["nosniff"],"X-Frame-Options":["SAMEORIGIN"],"Set-Cookie":[],"X-Xss-Protection":["1; mode=block"],"Pragma":["no-cache"],"Cache-Control":["no-store"],"Content-Security-Policy":["default-src 'none';"],"Date":["Mon, 06 Feb 2023 17:01:31 GMT"],"Content-Length":["15"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Permissions-Policy":["interest-cohort=()"]},"status":200}
homelab_caddy.1.s7dc6ketwosz@sweetums    | {"level":"debug","ts":1675702892.5105677,"logger":"events","msg":"event","name":"tls_get_certificate","id":"ffe78729-0dcf-4ea0-be48-b173b2f08e75","origin":"tls","data":{"client_hello":{"CipherSuites":[4866,4867,4865,49196,49200,159,52393,52392,52394,49195,49199,158,49188,49192,107,49187,49191,103,49162,49172,57,49161,49171,51,157,156,61,60,53,47,255],"ServerName":"deergrove.woozle.org","SupportedCurves":[29,23,30,25,24,256,257,258,259,260],"SupportedPoints":"AAEC","SignatureSchemes":[1027,1283,1539,2055,2056,2057,2058,2059,2052,2053,2054,1025,1281,1537,771,769,770,1026,1282,1538],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[772,771],"Conn":{}}}}
homelab_caddy.1.s7dc6ketwosz@sweetums    | {"level":"debug","ts":1675702892.5106957,"logger":"tls.handshake","msg":"choosing certificate","identifier":"deergrove.woozle.org","num_choices":1}
homelab_caddy.1.s7dc6ketwosz@sweetums    | {"level":"debug","ts":1675702892.5107353,"logger":"tls.handshake","msg":"default certificate selection results","identifier":"deergrove.woozle.org","subjects":["deergrove.woozle.org"],"managed":true,"issuer_key":"acme-v02.api.letsencrypt.org-directory","hash":"37b6cc293a72b89fc417bfe5e5868e2e5f5ea93bee17c25341785d373694a028"}
homelab_caddy.1.s7dc6ketwosz@sweetums    | {"level":"debug","ts":1675702892.5107617,"logger":"tls.handshake","msg":"matched certificate in cache","remote_ip":"172.18.0.1","remote_port":"55420","subjects":["deergrove.woozle.org"],"managed":true,"expiration":1680724900,"hash":"37b6cc293a72b89fc417bfe5e5868e2e5f5ea93bee17c25341785d373694a028"}
homelab_caddy.1.s7dc6ketwosz@sweetums    | {"level":"debug","ts":1675702892.5253625,"logger":"http.handlers.reverse_proxy","msg":"selected upstream","dial":"authelia:9091","total_upstreams":1}
homelab_caddy.1.s7dc6ketwosz@sweetums    | {"level":"debug","ts":1675702892.5300813,"logger":"http.handlers.reverse_proxy","msg":"upstream roundtrip","upstream":"authelia:9091","duration":0.00454495,"request":{"remote_ip":"172.18.0.1","remote_port":"55420","proto":"HTTP/2.0","method":"GET","host":"deergrove.woozle.org","uri":"/api/verify?rd=https://auth.woozle.org/","headers":{"Accept":["*/*"],"X-Forwarded-Method":["GET"],"Cookie":[],"X-Forwarded-For":["172.18.0.1"],"X-Forwarded-Proto":["https"],"X-Forwarded-Host":["deergrove.woozle.org"],"User-Agent":["curl/7.81.0"],"X-Forwarded-Uri":["/"]},"tls":{"resumed":false,"version":772,"cipher_suite":4867,"proto":"h2","server_name":"deergrove.woozle.org"}},"headers":{"Referrer-Policy":["strict-origin-when-cross-origin"],"Permissions-Policy":["interest-cohort=()"],"X-Frame-Options":["SAMEORIGIN"],"X-Xss-Protection":["1; mode=block"],"Remote-User":["neale"],"Remote-Groups":["admins,deergrove,fam"],"Remote-Name":["Neale Pickett"],"Date":["Mon, 06 Feb 2023 17:01:31 GMT"],"Content-Length":["0"],"X-Content-Type-Options":["nosniff"],"Remote-Email":["neale@woozle.org"],"Set-Cookie":[]},"status":200}
homelab_caddy.1.s7dc6ketwosz@sweetums    | {"level":"debug","ts":1675702892.5301678,"logger":"http.handlers.reverse_proxy","msg":"handling response","handler":0}
homelab_caddy.1.s7dc6ketwosz@sweetums    | {"level":"debug","ts":1675702892.5303807,"logger":"http.handlers.reverse_proxy","msg":"selected upstream","dial":"authelia:9091","total_upstreams":1}
homelab_caddy.1.s7dc6ketwosz@sweetums    | {"level":"debug","ts":1675702892.5347753,"logger":"http.handlers.reverse_proxy","msg":"upstream roundtrip","upstream":"authelia:9091","duration":0.0042059,"request":{"remote_ip":"172.18.0.1","remote_port":"55420","proto":"HTTP/2.0","method":"GET","host":"deergrove.woozle.org","uri":"/api/verify?rd=https://auth.woozle.org/","headers":{"Accept":["*/*"],"Remote-Groups":["admins,deergrove,fam"],"X-Forwarded-Method":["GET"],"User-Agent":["curl/7.81.0"],"X-Forwarded-Proto":["https"],"X-Forwarded-For":["172.18.0.1"],"Proxy-Authorization":[],"Cookie":[],"X-Forwarded-Host":["deergrove.woozle.org"],"Remote-User":["neale"],"Remote-Email":["neale@woozle.org"],"Remote-Name":["Neale Pickett"],"X-Forwarded-Uri":["/"]},"tls":{"resumed":false,"version":772,"cipher_suite":4867,"proto":"h2","server_name":"deergrove.woozle.org"}},"headers":{"Www-Authenticate":["Basic realm=\"Authentication required\""],"Date":["Mon, 06 Feb 2023 17:01:31 GMT"],"Content-Type":["text/plain; charset=utf-8"],"Content-Length":["16"]},"status":401}

5. What I already tried:

I have tried various methods involving matchers, but always get a fall-through to the second forward_auth. I believe what I need is a header_up that is conditional on a matcher, but I don’t know how to do this.

6. Links to relevant resources:

handle is what I was missing:

(restricted-access) {
  @noauth header !Authorization
  handle @noauth {
    forward_auth authelia:9091 {
      import authelia
    }
  }
  handle {
    forward_auth authelia:9091 {
      import authelia
      header_up Proxy-Authorization {header.authorization}
    }
  }
}

This does the following:

  1. If there is no Authorization header, make @noauth true
  2. If @noauth, forward auth to authelia
  3. Otherwise, forward auth to authelia, setting the Proxy-Authorization header

handle does the if/else logic.

1 Like

Right, that makes sense. Your config in your top-post would run forward_auth twice. Handle makes it mutually exclusive, and only runs one.

I will say though, handle is mutually exclusive for all directives at the same “level”. So if you add other handle for things to run after forward_auth, then it won’t run those afterwards.

Just something to keep in mind if you further expand your config later.

And wouldn’t you know it, I just ran into this exact problem.

This import restricted-access works better, but it still requires being at the same level as other handle directives:

(restricted-access) {
  handle {
    @noauth header !Authorization
    handle @noauth {
      forward_auth authelia:9091 {
        import authelia
      }
    }
    handle {
      forward_auth authelia:9091 {
        import authelia
        header_up Proxy-Authorization {header.authorization}
      }
    }
  }
}

The other way to do it is to not use handle at all and instead have the negated version of the matcher for your other forward_auth, like this:

@noAuth header !Authorization
forward_auth @noAuth authelia:9091 {
	import authelia
}

@hasAuth not header !Authorization
forward_auth @hasAuth authelia:9091 {
	import authelia
	header_up Proxy-Authorization {header.Authorization}
}

Because they are exactly the inverse of eachother with not, they will never run at the same time.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.