TLS Client Authetication for External IPs only

1. Caddy version (caddy version):

devel (The one with TLS client auth yesterday: httpcaddyfile: Add client_auth options to tls directive (#3335) · caddyserver/caddy@1dfb114 · GitHub)

2. How I run Caddy:

a. System environment:

Docker on Debian Buster

b. Command:

docker-compose up

c. Service/unit/compose file:

version: "3.2"

networks:
  default:
    external:
      name: proxy

services:
  caddy:
    container_name: caddy
    image: caddy:latest
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ../volumes/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - ../volumes/caddy/cloudflare-origin-pull-ca.pem:/etc/caddy/cloudflare-origin-pull-ca.pem:ro
      - ../volumes/caddy/config:/config
      - ../volumes/caddy/data:/data
      - ../volumes/caddy/logs:/logs
      # php-fpm roots
      - ../volumes/caddy/sites:/sites
      - ../volumes/nextcloud/html:/php-fpm-root/nextcloud
      - ../volumes/nextcloud/apps:/php-fpm-root/nextcloud/custom_apps
      # Testing
      - ../volumes/caddy/caddy:/usr/bin/caddy
    logging:
      driver: "json-file"
      options:
        max-size: "200k"
        max-file: "10"

d. My complete Caddyfile or JSON config:

# /etc/caddy/Caddyfile

# Global config
{
    # Let's Encrypt
    email email@domain.com
    
    # No admin
    admin off
}

(webconf) {
    # Add zstd and gzip compression to requests
    encode zstd gzip
    
    # Remove headers (leading "-")
    header {
        -x-powered-by
    }
}

# Add common headers for non-reverse_proxy sites
(non_reverse_proxy_headers) {
    header {
        Strict-Transport-Security "max-age=31536000;"
        X-Content-Type-Options    "nosniff"
        X-XSS-Protection          "1; mode=block"
        X-Frame-Options           "SAMEORIGIN"
        Referrer-Policy           "strict-origin-when-cross-origin"
        Content-Security-Policy   "frame-src 'self'; frame-ancestors 'self'; object-src 'none';"
    }
    
    @cache_css_js {
        path_regexp cache_css_js \.(?:css|js)$
    }
    @cache_media {
        path_regexp cache_media \.(?:jpg|jpeg|gif|png|bmp|ico|swf|xml|ogg|m4a|mp3)$
    }
    # 1 week
    header @cache_css_js Cache-Control max-age=604800
    # 2 weeks
    header @cache_media Cache-Control max-age=1209600
}

# Only allow connections from Cloudflare (and internal)
# Additionally sets the correct user IP address
(cloudflare) {
    # This isn't doing anything yet
    @internal {
        remote_ip 192.168.0.0/16
    }
    # This works!
    tls {
        client_auth {
            mode require_and_verify
            trusted_ca_cert_file /etc/caddy/cloudflare-origin-pull-ca.pem
        }
    }
    # I assume this works, haven't tested it yet
    request_header remote-addr {http.request.header.CF-Connecting-IP}
}

apps.example.com {
    root * /sites/apps.example.com/public/
    file_server
    php_fastcgi php:9000
    log {
	    output file /logs/apps.example.com/access.log
	    format single_field common_log
    }
    import cloudflare
    import non_reverse_proxy_headers
    import webconf
}

3. The problem I’m having:

The TLS client auth thing seems to be working! I can only connect to my server going through Cloudflare, and I didn’t have to set up a ton of (occasionally updating) whitelist URLs to only allow Cloudflare connections. Very cool! I’m excited for 2.1 to come out!

The next step is that I want to allow internal IPs to bypass this. If the remote_ip is internal, don’t do the TLS client auth and the request_header remote-addr replacement. I didn’t know if there was a way to only apply configuration options to certain matchers (I think that’s the right terminology)? It seemed like the matchers were only used for doing actual routing/rewriting/responds.

4. Error messages and/or full log output:

(There are no errors/log output)

5. What I already tried:

The above works for forcing all connections to go through Cloudflare, but obviously that @internal matcher isn’t doing anything at the moment. I was considering doing some sort of handler setup, where internal IPs would be handled the same exact way as the external Cloudflare IPs (just copy the config to both handlers), but without the TLS stuff. I think that would work, but I’m not too jazzed about duplicating config code, especially when I’d also have to do it for my 11 other sites I have set up in the Caddyfile. I’d prefer to just add an import cloudflare to all the sites, if possible.

I’m all ears for any other solutions though! I like the TLS client auth so much though, I might just remove all of my internal IPs from my network DNS resolver. Only cons to that are ISP data caps and speeds.

6. Links to relevant resources:

7. Extra Credit

Is there a location for submitting suggestions? I think it would be amazing to have snippets that take arguments. I think in code, so whenever I find myself duplicating code, I abstract it out and move it to a method. I have 12 sites configured, half of them are just copy pastes with the site name changed. If i could just import php_site apps.example.com, that’d be very cool.

This sounds more like a feature request to me. The TLS handshake happens before you can use request matchers.

This would probably be pretty easy with GitHub - mholt/conncept: Project Conncept: A layer 4 app for Caddy that multiplexes raw TCP/UDP streams though.

1 Like

So the TLS client authentication is a part of the TLS handshake process? Because what I was thinking was the TLS handshake happens, then it validates the client certificate. Between those steps I wanted to have a sort of “if” condition, where external IPs have the extra step of actually validating the client certificate. I was under the impression that the handshake happens when determining which Caddy site config to use, then the client authentication was a configuration directive inside the Caddy site config (and thus performed afterwards).

Also just noticed this: caddyfile: Add support for args on imports by francislavoie · Pull Request #3423 · caddyserver/caddy · GitHub, that solves my extra credit question. Very cool! Config file is much smaller now.

1 Like

Honestly, I’d probably just run two Caddy servers.

Make one only internally-available. This one serves your internal clients. Split DNS resolves its domain names to its internal IP address. This is the server that actually hosts your sites, so to speak.

The other is only externally-available. It has client auth and proxies to your internal Caddy.

Put their certificate storage on an NFS share and they will share certs and the external Caddy can solve challenges for the internal one if necessary.

Yeah, it’s adding a literal extra layer of complexity. But the overhead is minimal, the configuration is quite straightforward, and the effort required to implement would probably be quite minor.

Otherwise, look into Conncept.

Ah, that’s a good idea! Didn’t think about using 2 caddy instances. I will look into that, thank you.

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