Client certificates together with wildcard Letsencrypt not working with "authentication ambiguously configured" error


(Maciej świć) #1

Hi, I have Caddy as a reverse proxy for a couple internal services and I am using Letsencrypt with wildcard DNS-01. I am now trying to add client certificates but Caddy doesnt want to start with the following config and error:

tls { //Reused config in each site block below. I want all sites to share this wildcard cert and require the same client cert.
    dns namecheap
    wildcard
    clients require /mnt/user/appdata/caddy/conf/ca.crt
}

service1.domain.com {
    import tls
        proxy / https://10.0.1.0:8443 {
        transparent
        websocket
        insecure_skip_verify
    }
}

service2.domain.com {
    import tls
        proxy / https://10.0.1.0:8443 {
        transparent
        websocket
        insecure_skip_verify
    }
}

The error is:

incompatible TLS configurations for the same SNI name (*.domain.com) on the same listener: multiple hosts requiring client authentication ambiguously configured


(Matt Holt) #2

Thanks for the question – is that your full, unmodified Caddyfile, perchance?


(Maciej świć) #3

Thanks for replying, here is the full config

(auth) {
    basicauth / username password
}

(options) {
    gzip
    tls {
        dns namecheap
        wildcard
        clients /mnt/user/appdata/caddy/conf/ca.crt
    }
}

(external) {
    redir 302 {
        if {remote} not_starts_with "10.0.0"
        if {remote} not_starts_with "10.0.1"
        if {remote} not_starts_with "10.0.2"
        if {remote} not_starts_with "10.0.3"
        / https://e-{host}{uri}
    }
}

############################################
# public.domain.com
############################################

public.domain.com, xn--pckwb0bu4h.domain.com {
    import options
    
    root /mnt/user/public/
    browse
}

############################################
# unifi.domain.com
############################################

(unifi) {
    proxy / https://10.0.1.0:8443 {
        transparent
        websocket
        insecure_skip_verify
    }
}

unifi.domain.com {
    import options
    import unifi
}

############################################
# tracer.domain.com
############################################

(tracer) {
    proxy / 10.0.1.0 {
        transparent
        websocket
        insecure_skip_verify
        header_upstream Authorization "Basic cm9vdDpC=="
    }
}

tracer.domain.com, xn--vckubygscb.domain.com {
    import options
    import external
    import tracer
}

e-tracer.domain.com, xn--e--4h4a3c2isdb.domain.com {
    import auth
    import options

    redir 302 {
        if {remote} starts_with "10.0"
        / https://tracer.domain.com{uri}
    }

    import tracer
}

############################################
# openhab.domain.com
############################################

(openhab) {
    proxy / https://10.0.1.2:8443 {
        transparent
        websocket
        insecure_skip_verify
    }
}

openhab.domain.com {
    import options
    import external
    import openhab
}

e-openhab.domain.com {
    import auth
    import options

    redir 302 {
        if {remote} starts_with "10.0"
        / https://openhab.domain.com{uri}
    }

    import openhab
}

############################################
# vpn.domain.com
############################################

(vpn) {
    proxy / https://10.0.1.0:943 {
        transparent
        websocket
        insecure_skip_verify
    }
}

vpn.domain.com {
    import options
    import external
    import vpn
}

e-vpn.domain.com {
    import auth
    import options

    redir 302 {
        if {remote} starts_with "10.0"
        / https://vpn.domain.com{uri}
    }

    import vpn
}

############################################
# plex.domain.com
############################################

(plex) {
    proxy / http://10.0.1.15:32400 {
        transparent
        websocket
        insecure_skip_verify
    }
}

plex.domain.com {
    import options
    import plex
}

############################################
# sonarr.domain.com
############################################

(sonarr) {
    proxy / http://10.0.1.12:8989/ {
        transparent
        websocket
        insecure_skip_verify
    }
}

sonarr.domain.com {
    import options
    import external
    import sonarr
}

e-sonarr.domain.com {
    import auth
    import options

    redir 302 {
        if {remote} starts_with "10.0"
        / https://sonarr.domain.com{uri}
    }

    import sonarr
}

############################################
# radarr.domain.com
############################################

(radarr) {
    proxy / http://10.0.1.11:7878 {
        transparent
        websocket
        insecure_skip_verify
    }
}

radarr.domain.com{
    import options
    import external
    import radarr
}

e-radarr.domain.com {
    import auth
    import options

    redir 302 {
        if {remote} starts_with "10.0"
        / https://radarr.domain.com{uri}
    }

    import radarr
}

############################################
# transmission.domain.com
############################################

(transmission) {
    proxy / http://10.0.1.14:9091 {
        transparent
        websocket
        insecure_skip_verify
    }
}

transmission.domain.com {
    import options
    import external
    import transmission
}

e-transmission.domain.com {
    import auth
    import options

    redir 302 {
        if {remote} starts_with "10.0"
        / https://transmission.domain.com{uri}
    }

    import transmission
}

############################################
# pihole.domain.com
############################################

(pihole) {
    proxy / http://10.0.1.22 {
        transparent
        websocket
        insecure_skip_verify
    }
}

pihole.domain.com {
    import options
    import external
    import pihole
}

e-pihole.domain.com {
    import auth
    import options

    redir 302 {
        if {remote} starts_with "10.0"
        / https://pihole.domain.com{uri}
    }

    import pihole
}

############################################
# backup.domain.com
############################################

(backup) {
    proxy / http://10.0.1.18:5800 {
        transparent
        websocket
        insecure_skip_verify
    }
}

backup.domain.com {
    import options
    import external
    import backup
}

e-backup.domain.com {
    import auth
    import options

    redir 302 {
        if {remote} starts_with "10.0"
        / https://backup.domain.com{uri}
    }

    import backup
}

############################################
# netdata.domain.com
############################################

(netdata) {
    proxy / http://10.0.1.13:19999 {
        transparent
        websocket
        insecure_skip_verify
    }
}

netdata.domain.com {
    import options
    import external
    import netdata
}

e-netdata.domain.com {
    import auth
    import options

    redir 302 {
        if {remote} starts_with "10.0"
        / https://netdata.domain.com{uri}
    }

    import netdata
}

############################################
# radar.domain.com
############################################

(radar) {
    proxy / http://10.0.1.23:8080 {
        transparent
        websocket
        insecure_skip_verify
    }
}

radar.domain.com {
    import options
    import external
    import radar
}

e-radar.domain.com {
    import auth
    import options

    redir 302 {
        if {remote} starts_with "10.0"
        / https://radar.domain.com{uri}
    }

    import radar
}

############################################
# cloud.domain.com
############################################

(cloud) {
    proxy / https://10.0.1.24 {
        transparent
        websocket
        insecure_skip_verify
    }
}

cloud.domain.com {
    import options
    import cloud
}

############################################
# db.domain.com
############################################

(db) {
    proxy / http://10.0.1.26 {
        transparent
        websocket
        insecure_skip_verify
    }
}

db.domain.com {
    import options
    import external
    import db
}

e-db.domain.com {
    import auth
    import options

    redir 302 {
        if {remote} starts_with "10.0"
        / https://db.domain.com{uri}
    }

    import db
}

############################################
# dns.domain.com
############################################

(dns) {
    proxy / http://10.0.1.22/admin {
        without /admin
        transparent
        websocket
        insecure_skip_verify
    }
}

dns.domain.com {
    import options
    import external
    import dns
}

e-dns.domain.com {
    import auth
    import options

    redir 302 {
        if {remote} starts_with "10.0"
        / https://dns.domain.com{uri}
    }

    import dns
}

(Matthew Fay) #4

Replicated with a minimal Caddyfile:

(tls) {
  tls {
    dns cloudflare
    wildcard
    clients testca.pem
  }
}

test1.whitestrake.net {
  import tls
}

test2.whitestrake.net {
  import tls
}
whitestrake at apollo in ~/Projects/test
❯ caddy -version
Caddy 0.11.0 (non-commercial use only)

whitestrake at apollo in ~/Projects/test
❯ env CLOUDFLARE_EMAIL=[snip] CLOUDFLARE_API_KEY=[snip] caddy
Activating privacy features... done.
2018/08/03 09:51:58 incompatible TLS configurations for the same SNI name (*.whitestrake.net) on the same listener: multiple hosts requiring client authentication ambiguously configured

The client auth requirement shouldn’t be ambiguous (it’s identical between hosts) and the SNI name should be different. Perhaps this is an unintended side effect of wildcard changing the site label in order to have multiple sites come under one certificate.


(Matthew Fay) #5

Looks like here’s the source of the error:

Is it failing the check there because each TLS config creates its own x509.NewCertPool() to use as config.ClientCAs, even if the config.ClientCerts list is identical?

Worth opening an issue to discuss further, I’d say - this usage should be supported.


(Maciej świć) #6

Thank you for your time and sorry for the formatting, I am away from a computer until this afternoon. Is there any possible easy workaround in the source that i could recompile myself?

Should i open an issue on Github or do you want to do it?

Regards
Maciej


(Toby Allen) #7

If you could open an issue on github that would be great. If you could submit a PR that would be better!


(Matthew Fay) #8

Possibly, but a caution:

The error serves to prevent a security vulnerability - as the comment on the check explains, if client certificate requirements vary between configs on the same server name, a client could gain access to a protected resource without supplying the correct certificate because Caddy can’t tell which one to enforce.

From a quick assessment, I’d wager that this particular vulnerability doesn’t apply to you if you keep your actual server names separate, because it seems like the wildcard setting is provoking the error, as Caddy thinks they’re all the same server name.

You could try simply removing that check entirely and recompiling. As long as there’s no other logic in Caddy that relies on the assumption that check was passed (I can’t think of any off the top of my head), it should function just fine. Being a security concern, though, I’d probably want to wait until some of the contributors have had a chance to look it over on Github and discuss the implications, just in case; you’re disabling a feature in place to protect you from clients bypassing your authorization, and that responsibility will be on you.


(Maciej świć) #9

So if i have a wildcard certificate for domain.com, dont require a client certificate on public.domain.com but require a client certificate for anything on private.domain.com there should be no issues? Not sure if i understand the definition of resource correctly.

If i required a certificate for private.domain.com/res1 but not for private.domain.com/res2 and remove the check, that would cause a security issue because it is the same fqdn?


(Matthew Fay) #10

Yeah, that’s my understanding. If Caddy must make a decision about which client certificate requirement to enforce at the SNI stage, before the full URL is requested, all Caddy knows at that point is that it’s private.domain.com.

If it had to “guess” (by using the first available, or a “default”, config), it might get it wrong and fail to require the correct certificate, because it only found out after the fact that the request was actually for the other resource at /res2 (which has different security requirements). Hence the check: Make sure the client certificate requirement is the same across the entire SNI name, and we don’t have this problem.

Based on that, as long as you don’t configure two of the same FQDNs (doesn’t look like you have, Caddy just thinks so because wildcard works by pretending the sites are all *.domain.com for TLS purposes) with different client validation requirements (looks like all sites that require client certificates are identical, Caddy doesn’t realise this because each TLS configuration creates its own clientCA pool instead of reusing them), you’ll be fine, security-wise.


Probably worth mentioning that I’m not a security expert or even particularly knowledgeable about the inner mechanics of TLS; I’m just working off the explanation in the source.


(Matt Holt) #11

Were all the “domain.com” examples in your Caddyfile the same domain? I ask for unredacted Caddyfiles because domain names are already public anyway, and what the actual values are can affect the answer and implementation. I know I won’t invest time on fixing the bug unless we can be absolutely sure we understand the problem.


(Maciej świć) #12

Hi Matt, yes, they are all the same domain. I will open an issue on Github within the next day or so. The issue was also confirmed by someone else with a minimal example Caddyfile. Thank you for time!


(Matt Holt) #13

Okay, perfect. Thanks!