Caddyfile with mTLS and Authelia fallback

1. The problem I’m having:

I want to use caddy with mTLS and an authelia fallback for users who are not given certificates. I have a Caddyfile which works but due to things not running in order, i am unsure if it is actually secure.

This is the Caddyfile snippet i use:

(setAuthLevel) {
	vars authEmail {args[0]}
	vars authGroups {args[1]}
	@detect expression {vars.authGroups} == "detect"
	handle @detect {
		map {vars.authEmail} {authGroup} {
			email@example.com owner
			default unknown
		}
		vars authGroups {authGroup}
	}

	map {vars.authGroups} {authLevel} {
		~(^|.*,)owner($|,.*) 1
		~(^|.*,)admin($|,.*) 10
		~(^|.*,)user($|,.*) 100
		default 1000
	}
}

(authenticated) {
	tls {
		protocols tls1.3
		client_auth {
			mode verify_if_given
			trust_pool file /data/mtls/root.crt
		}
	}

	route {
		@has_email expression {http.request.tls.client.san.emails.0} != null

		route @has_email {
			import setAuthLevel {http.request.tls.client.san.emails.0} detect
		}

		@invalid_cert expression {http.request.tls.client.subject} == null || {vars.authGroups} == "unknown"

		reverse_proxy @invalid_cert authelia:9091 {
			method GET
			rewrite /api/authz/forward-auth

			header_up X-Forwarded-Method {method}
			header_up X-Forwarded-Uri {uri}

			@good status 2xx
			handle_response @good {
				import setAuthLevel {rp.header.Remote-Email} {rp.header.Remote-Groups}
			}
		}

		log_append user_email {vars.authEmail}
		log_append user_level {authLevel}

		@authorised expression int({authLevel}) <= int({args[0]})
		route @authorised {
			{block}
		}

		error 403
	}
}

Initially, it was not working but when i changed all the handle directives to route, it started working. I think this is because of the order difference between the 2 directives. I also could not figure out a way to get the groups from ldap for the users with certificates but since only a few users have certificates, i opted to put it in the Caddyfile.

This is how i use the snippet:

domain.example.com {
	log
	import authenticated 10 {
		reverse_proxy http://192.168.1.17:8080
	}
}

And i generated the certificates with these scripts
createRoot.sh:

#! /bin/sh

openssl req -x509 -newkey rsa:4096 -keyout root.key -out root.crt -subj "/CN=example" -days 3650

createClient.sh:

#! /bin/sh

if [ "$#" -lt 2 ]; then
  echo "Missing parameters. Run the script with './createClient.sh cert_name email_address'"
  exit 1
fi

mkdir -p "./$1"
openssl req -newkey rsa:4096 -keyout "./$1/client.key" -subj "/CN=example" -addext "subjectAltName = email:$2" -out "./$1/client.csr"
openssl x509 -req -in "./$1/client.csr" -CA root.crt -CAkey root.key -CAcreateserial -copy_extensions copy -extensions san -out "./$1/client.crt" -days 730
openssl pkcs12 -export -in "./$1/client.crt" -inkey "./$1/client.key" -certfile root.crt -name "$1" -out "./$1/client.p12"

2. Error messages and/or full log output:

There is no error. I am just wanting to check if authelia can be bypassed due to the order of operations.

3. Caddy version:

v2.10.2

4. How I installed and ran Caddy:

a. System environment:

Running in Docker

b. Command:

docker compose up -d

c. Service/unit/compose file:

services:
  caddy:
    image: caddy:2
    user: 1000:1000
    ports:
      - '80:80'
      - '443:443'
    env_file: ./caddy/vars.env
    volumes:
      - ./caddy/conf:/etc/caddy
      - ./caddy/data:/data
      - ./caddy/config:/config
    restart: unless-stopped

d. My complete Caddy config:

Provided in section 1

5. Links to relevant resources:

I looked at this page but since everything is inside a route, i am hoping it runs in order

No, it’s because handle is mutually exclusive, which means if you have one handle in the same nesting level which runs, another at the same nesting level will NOT run. Using route, all that match will run.

I don’t think I can help with that, I don’t know how LDAP is plugged into your setup. The key would be enabling debug logs and checking what the proxy request returns etc.

1 Like

Interesting. Using route should be the correct way to do this then.

For LDAP, i found the docs on a GraphQL api which i could use to get the groups for a user who authenticates with certificates (lldap/docs/scripting.md at main · lldap/lldap · GitHub). I don’t have enough users for it to be worth looking in to right now but i may try it in the future.

I am happy with the way this works and have not found a way to bypass authentication. Thanks for taking a look.