-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
autohttps: Implement auto_https prefer_wildcard
option
#6146
Conversation
auto_https prefer_wildcard
option
Thanks Francis, this looks appealing. Will wait for one or two people to field-test it. |
This looks amazing and a super useful feature to reduce subdomain discovery through certificate transparency! I would suggest a small addition in order to minimise this even further. E.g. to make Caddy always use wildcards. This could maybe be implemented as another option for this directive, called Say I have two sites setup, for serviceA.domain.tld and serviceB.domain.tld, and use this setting, Caddy would then generate *.domain.tld and use it. If I have hostX.serviceA.domain.tld, hostY.serviceA.domain.tld, hostX.serviceB.domain.tld, hostY.serviceB.domain.tld, then all of those would be covered by the *.*.domain.tld certificate (does ACME even allow such certificates? 🤔). Alternatively, add another directive that allows users to specify a list of wildcard certs that will always be managed regardless of if they're used by sites or not, and then your feature here will be able to use those to avoid generating specific certs. A little less "magic" than the What do you think about this @francislavoie? I guess it might be necessary to give Caddy a list of domains we control in order for this to work when using multiple domains so it doesn't see domain1.tld and domain2.tld and tries to generate *.tld... 😂 |
@abjugard |
Right, unfortunate limitation but understandable. Then I think we can simplify the behaviour and not need to specify which domains are owned, but just let Caddy automatically figure out to make wildcards for the leftmost label. Perhaps we should just wait for this PR to get merged then I can take a crack at a |
I'm actually implementing part of this in CertMagic -- likely will become a "subject transformer" that allows changes to the subject name when managing certs, so, e.g. one can lob off the leftmost label and replace it with |
@francislavoie How would you feel if we/(I?) updated this PR to use the new SubjectTransformer introduced in the linked issue/commit? It should be relatively simple I think, if the global option is set, then set the SubjectTransformer for CertMagic configs to be a 3-line function. |
I'm not sure how that would look 🤔 maybe make a branch off this one if you think it's simple? |
I will try soon! :) |
Actually the SubjectTransformer might be slightly tangential to this, rather than directly related -- the transformer is for, like, "I have a.b.c and b.b.c, but I want you to manage a single wildcard instead of individual certs for each specific domain." Whereas this change is, "I have *.b.c and a.b.c, and I want a.b.c to use the wildcard cert." There's another aspect I want to consider as well, that is some users want just specific domains to be served under a wildcard, while the others shouldn't be. For example, in the config above, if there was also a site, I think I want to give this more thought, even before I implement anything here. This is a needed change though and I think it's a good start. I just don't want to commit to its API/syntax/implementation quite yet until we have a better picture of the bigger picture. |
I think that's already handled by my approach, because |
Ah, okay. Hmm. I might still wait on this until after 2.8 so I can give this a little more thought. We now have a way, in CertMagic, of mapping/transforming one subject name (domain name) to another, for the purposes of cert management. Even if this PR ends up being good as-is, I just want a little more time on it. |
Glad there is a PR for this and I really look forward to it. Thank you for your great work. One big advantage of having separate site blocks is when using Since these labels can be distributed across multiple docker-compose files, there can be some possibilities of misconfiguration somewhere (especially done by multiple people). With the current handle approach, if one of the subdomain is misconfigured, causes the ENTIRE wildcard site block to be removed, bringing down everything. With this PR, the failure would at least be localized (hopefully😊) |
962efa3
to
4baebcc
Compare
Could there not just be a It already seems to be an issue according to this report where an internal wildcard cert is being used instead of the LetsEncrypt one for an explicit site address. Alternatively, you could go the other way around like with Since the certmagic subject transformer feature is available and Caddy 2.8 is released, is there anything that can be done to assist moving this feature forward?
If you can provide a rough outline of what to test, I could put together configs to verify?
|
40d8022
to
c87b450
Compare
I am now wondering if we should make preferring wildcards the default behavior as @abjugard suggested above. In Slack, it was expressed that it would be a breaking change, but I am not sure if there are (m)any(?) use cases that would actually break. A wildcard cert is just as good as a subdomain cert. |
EDIT: Ignore me. I mistook the "prefer" wildcard cert to prefer provisioning an explicit subdomain with a wildcard cert (even if no wildcard was explicitly configured/requested).
Isn't it generally considered better practice to prefer explicit SAN for certs than a wildcard? Some businesses may have compliance requirements related to that expectation? That concern is only relevant if the private key was compromised for the attacker to leverage it before expiry/detection, but that may be sufficient time for the attacker? 🤷♂️ I don't operate at a scale where I'm paranoid about such personally, but I could understand it being a compliance concern elsewhere, which would make it a breaking change for those users? Thus you may want to introduce as opt-in, and switch to opt-out when a breaking change is acceptable? Unless you don't consider such implicit assumptions/trust in software to be a breaking change? You could also take the route of adding a notice (to release notes and pinned issue) for awareness before switching to opt-out, giving any potential users time to become aware of the change landing in future? To be fair though, I think when compliance matters that upgrading to newer releases would have a more strict policy than trusting semantic versioning 😅 So perhaps I'm rambling about a non-issue. |
If Caddy fails to obtain cert for *.example.com then foo.example.com won't have any cert too right? That seems problematic, especially because LetsEncrypt won't give you wildcard cert without DNS-01 challenge. |
The scenario you describe here isn't really a concern as the requirements for procuring a wildcard cert are strictly different from certs for specific subdomains. Caddy (I'm guessing) won't try to get a wildcard cert unless its able to, and if its able to then it will either succeed or fail in which case you've configured it wrong and it should correctly refuse to continue starting up the site in question. |
Wildcard certs require DNS challenges which have to be explicitly configured. If someone is configuring a wildcard into their server, then they probably don't intend to get subdomain certs at all. IMO.
Well, if we default to always preferring the wildcard cert, that changes this PR. I just want to do what is most expected and intuitive, and I feel like setting up wildcard hostnames is juuust explicit and involved enough that a user probably wants to use that cert for the rest of the subdomains. Maybe an inverse of this PR would be useful then, that is, to ignore wildcard certs. |
4f4e13e
to
dda73f4
Compare
This will go out with 2.9 beta 1. Would appreciate some field testing! |
I think I'm missing something about this. When I use a single wildcard domain, everything works as expected and it gets a cert. If I try to define multiple wildcard domains, it fails with multiple.Caddyfile.txt |
Interesting, thanks @coandco, looks like the Caddyfile adapter part is consolidating automation policies too aggressively. I'll try to make a fix. |
The previous config would end requesting a TLS certificate for each individual subdomain and not use the wildcard certificate. This change modifies the labels used on the containers to create host matchers and handlers to do the routing under a single wildcard Caddyfile site. This is a little trickier and more verbose while defining the labels but ends a much cleaner Caddyfile[1][2] and only requires a single certificate. Hopefully this will all be moot once the auto_https prefer_wildcard option is released in `2.9.x`. 1. https://caddyserver.com/docs/caddyfile/patterns#wildcard-certificates 2. https://caddy.community/t/docker-proxy-wildcard-subdomains/22170 3. caddyserver/caddy#6146
Adding to the earlier conversation about excluding some subdomains from using the wildcard domain; here's an example that applies to me: I'd like to use Cloudflare's DNS proxy on the wildcard record, except for one subdomain, which I want to go directly to server's IP. It would be nice if Caddy could exclude that one subdomain from the catch-all. |
Wait, are you saying you want one wildcard to override all subdomain, but another wildcard to not override subdomains? That's uh... wild. Sigh. |
I might be misspeaking here-- I only want one wildcard to cover all subdomains defined. But I would like one specific subdomain to be excluded from the wildcard... To be honest, thinking about it more, I think managing the records manually is easier since it's just one site. Sorry for the confusion. |
Presently the docs have no coverage of this feature? Hope it's ok to document here in the meantime for those interested in it. I wasn't 100% sure of some behaviour, and someone recently asked me about this feature with the ability to exclude specific sites (seems to be a common expectation / requirement), while not that discoverable yet - the new
@rirze you would use To have a single address opt-out of the wildcard preference, you'd then use the other related feature Here's an example {
auto_https prefer_wildcard
}
# Provision a wildcard for your domain (or load an external one via `tls` directive):
# NOTE: Order of site blocks declare is not important for the feature availability.
*.internal {
#tls /srv/certs/example.internal/cert.pem /srv/certs/example.internal/key.pem
abort
}
# Opt-out of preferring wildcard cert (or externally loaded cert):
not-wild.internal {
tls force_automate
respond "Individual SAN cert provisioned"
}
wild.example.internal {
respond "This prefers to use the wildcard cert"
}
# Does not implicitly provision wildcards:
not-wild.localhost {
respond "Caddy needs to match an existing wildcard to skip provisioning a new cert"
} Reference configSelf-isolated # Run Caddy container:
$ docker compose up -d --force-recreate
# Query each address and report back the SANs from the certificate provided:
$ docker compose run --rm --quiet-pull verify-certs
╭───────────────────────────┬──────────────────────────────╮
│ site-addresses │ sans │
├───────────────────────────┼──────────────────────────────┤
│ wild.localhost │ *.localhost │
│ wild.example.test │ example.test, *.example.test │
│ also-wild.example.test │ example.test, *.example.test │
│ not-wild.localhost │ not-wild.localhost │
│ not-wild.example.test │ not-wild.example.test │
│ not-wild.example.internal │ not-wild.example.internal │
╰───────────────────────────┴──────────────────────────────╯ It's mostly verbose for supporting the Additional commentary added below in the config regarding Quick overview:
networks:
default:
name: example-wildcard
ipam:
driver: default
config:
- subnet: "172.16.42.0/24"
volumes:
caddy_data:
name: example-caddy-data
services:
reverse-proxy:
image: caddy:2.9
volumes:
- caddy_data:/data
configs:
- source: caddy-config
target: /etc/caddy/Caddyfile
- source: tls-leaf-cert-ecdsa-example
target: /tls/example.test/cert.pem
- source: tls-leaf-key-ecdsa-example
target: /tls/example.test/key.pem
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3
networks:
default:
ipv4_address: 172.16.42.11
# Docker's embedded DNS will resolve these addresses to
# this Caddy container:
aliases:
- wild.example.test
- also-wild.example.test
- not-wild.example.test
- not-wild.example.internal
# Optional: This service is for conveniently verifying correct cert provisioning:
verify-certs:
init: true
# Scope to a profile (avoids starting this service by default):
profiles:
- cli
# Local build (no need to try pull `image` remotely):
pull_policy: build
image: localhost/verify-certs
build:
dockerfile_inline: |
FROM alpine
RUN apk add --no-cache step-cli jq bash csview ca-certificates
ENTRYPOINT /usr/local/bin/check-certs
# Add to `/etc/hosts` for resolving DNS correctly instead of 127.0.0.1:
# NOTE: curl ignores any `/etc/hosts` entries for the `localhost` TLD.
# Use `curl --connect-to ::172.16.42.11 https://not-wild.localhost` to connect
# to the Caddy container successfully, however curl will also fail to verify
# any cert for an FQDN that relies on a `*.localhost` wildcard SAN.
extra_hosts:
- "wild.localhost=172.16.42.11"
- "not-wild.localhost=172.16.42.11"
volumes:
- caddy_data:/data:ro
configs:
- source: check-certs
target: /usr/local/bin/check-certs
# Make the file executable to run the script via it's shebang:
mode: 755
- source: tls-ca-cert-ecdsa
target: /tls/example.test/ca-root.pem
# The `$$` used below opts-out of the variable interpolation feature from Docker Compose
configs:
check-certs:
content: |
#! /usr/bin/env bash
# Caddy's self-signed root CA (used for `local_certs`):
# Required for successfully verifying chain of trust during the inspect query:
# Effectively copies each cert into /usr/local/share/ca-certificates
# and runs `update-ca-certificates` to install into the system trust store.
step certificate install /data/caddy/pki/authorities/local/root.crt > /dev/null
step certificate install /tls/example.test/ca-root.pem > /dev/null
SITES=(
"wild.localhost"
"wild.example.test"
"also-wild.example.test"
"not-wild.localhost"
"not-wild.example.test"
"not-wild.example.internal"
)
# Store results in TSV for better display formatting:
echo -e 'site-addresses\tsans' > /tmp/results.tsv
for FQDN in "$${SITES[@]}"; do
# Parse JSON response for SANs and format into a string:
DNS_NAMES=$(
step certificate inspect --format json "https://$${FQDN}" \
| jq -c '.extensions.subject_alt_name.dns_names | join(", ")'
)
echo -e "$${FQDN}\t$${DNS_NAMES}" >> /tmp/results.tsv
done
# Render a table from the TSV formatted data:
csview --tsv --style rounded --disable-pager /tmp/results.tsv
caddy-config:
content: |
# Global Settings
{
# Optional: For testing purposes (otherwise defaults to a public CA)
# Have Caddy provision certs locally (self-signed):
local_certs
# If Caddy is already aware of a wildcard certificate for the SAN / FQDN,
# have it prefer to use that instead of provisioning a separate cert:
auto_https prefer_wildcard
}
:1337 {
respond "Hello World!"
}
# This site-block will trigger automatic cert provisioning / renewal
# for this wildcard domain (only required if configuring via Caddyfile):
# NOTE: More specific site-addresses have priority when routing a request to
# a site block, while this wildcard site-block acts as a fallback.
*.localhost {
# Reject fallback traffic received (subdomain did not match elsewhere)
abort
}
# Force a certificate to be automatically provisioned regardless of any
# existing valid provisioned certificates found (eg: a compatible wildcard)
not-wild.localhost, not-wild.example.test {
tls force_automate
respond "Individual SAN certs provisioned"
}
# Although this is a specific site-address, the external cert loaded provides
# a wildcard SAN which Caddy will recognize for wildcard preference elsewhere
wild.example.test {
tls /tls/example.test/cert.pem /tls/example.test/key.pem
reverse_proxy :1337
}
# The first two will use the wildcard certs Caddy has provisioned or loaded,
# The last site-address is provisioned with a new certificate (non-wildcard),
# as no existing certificate has a valid SAN entry match for the FQDN:
wild.localhost, also-wild.example.test, not-wild.example.internal {
reverse_proxy :1337
}
# Optional - Remaining files are an externally provisioned cert example
# Provisioned for `*.example.test` + `example.test` (via `step certificate create`):
tls-leaf-cert-ecdsa-example:
content: |
-----BEGIN CERTIFICATE-----
MIIB0DCCAXagAwIBAgIRANQ/OtiKq5l49rHqweg0tQkwCgYIKoZIzj0EAwIwHDEa
MBgGA1UEAxMRU21hbGxzdGVwIFJvb3QgQ0EwHhcNMjQwMTAxMDAwMDAwWhcNMzQw
MTAxMDAwMDAwWjAZMRcwFQYDVQQDEw5TbWFsbHN0ZXAgTGVhZjBZMBMGByqGSM49
AgEGCCqGSM49AwEHA0IABLgdMSPKbiATi8GVm0rdAtZeBN+XxCBcYkf696fIPEfc
Z3LuU2P1ikmPx7f+UhGqSBK5e7CewfZatoA0F+lzl9WjgZswgZgwDgYDVR0PAQH/
BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQU
FzcbZ2PsKlBSNv8EVcrSmVWPKdwwHwYDVR0jBBgwFoAUpaV6oR6VlDl+0RXY3qXK
I5SizQ8wJwYDVR0RBCAwHoIMZXhhbXBsZS50ZXN0gg4qLmV4YW1wbGUudGVzdDAK
BggqhkjOPQQDAgNIADBFAiBJvyURWSG8gJ2/ykcZ5ipudXe7/D0COd4EP3ks4VxN
aQIhALjQoeJzwLHTqnbOENedTEVdOI/j/0Wr38JhJht0lgdy
-----END CERTIFICATE-----
tls-leaf-key-ecdsa-example:
content: |
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIClhaEYEdVTKSqG7mbMbdKE/uB1wd1ncEcwwe1kULbR+oAoGCCqGSM49
AwEHoUQDQgAEuB0xI8puIBOLwZWbSt0C1l4E35fEIFxiR/r3p8g8R9xncu5TY/WK
SY/Ht/5SEapIErl7sJ7B9lq2gDQX6XOX1Q==
-----END EC PRIVATE KEY-----
tls-ca-cert-ecdsa:
content: |
-----BEGIN CERTIFICATE-----
MIIBfTCCASKgAwIBAgIRANvAFk7fgnv6LBBO8KMJ7BswCgYIKoZIzj0EAwIwHDEa
MBgGA1UEAxMRU21hbGxzdGVwIFJvb3QgQ0EwHhcNMjEwMTAxMDAwMDAwWhcNMzEw
MTAxMDAwMDAwWjAcMRowGAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTBZMBMGByqG
SM49AgEGCCqGSM49AwEHA0IABDaUnm6W6pQTSVcP+rl+QqwwpzrLuDVMXHGvQtun
1si87g04a1462KQAZCUtmnAIJiEowvcXMQaS2dm8Z0qf28GjRTBDMA4GA1UdDwEB
/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBSlpXqhHpWUOX7R
FdjepcojlKLNDzAKBggqhkjOPQQDAgNJADBGAiEA/R5frrFFWY3441N1CncFaMlY
9b/rUJKU6rh0eezXKHwCIQCTZ4D2/HPX06betNj8d/bTG16iAqoqgLUc+6PKc3PV
sA==
-----END CERTIFICATE-----
# This CA key is only needed if provisioning more leafs / intermediates:
tls-ca-key-ecdsa:
content: |
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIFfxEAiTHCjehLSCydAt6v4Qt1MCp0OpW7QIrQuZ7JMeoAoGCCqGSM49
AwEHoUQDQgAENpSebpbqlBNJVw/6uX5CrDCnOsu4NUxcca9C26fWyLzuDThrXjrY
pABkJS2acAgmISjC9xcxBpLZ2bxnSp/bwQ==
-----END EC PRIVATE KEY----- Reproduction for the curl concern for reference (verified with curl in a glibc based Fedora 41 container too FWIW): # Shell into the Alpine container:
$ docker compose run --rm -it --entrypoint /bin/ash verify-certs
# Add both private CA certs into the trust store, then install curl:
$ check-certs && apk add curl
# Explicit FQDNs in a SAN work fine:
$ curl --connect-to ::172.16.42.11 https://not-wild.localhost
Hello World!
# Despite the certificate chain of trust being valid, curl fails to validate this wildcard SAN:
$ curl --connect-to ::172.16.42.11 https://wild.localhost
curl: (60) SSL: no alternative certificate subject name matches target hostname 'wild.localhost'
More details here: https://curl.se/docs/sslcerts.html
# Yet this does not happen for a wildcard for another TLD like `example.test`:
# It would fail if the CA root was not installed into the trust store prior.
$ curl --connect-to ::172.16.42.11 https://wild.example.test
Hello World! UPDATE: The above reference example only checks the SANs by DNS names, if your certificate relies on the Additionally if Caddy has a certificate loaded with an exact match of the site-address, it seems it'll prefer that even when it's externally loaded, which might not be how |
Interesting, I will give this a try! Thanks for the detailed explanation. |
Does this feature work with the
|
Try it. Looks at the output of |
Thanks, I wasn't aware of that command to see more detailed output. It appears to work. The reason I ask is because it wasn't possible (to my knowledge) to use wildcards in conjunction with For completeness, here is my test config and the resulting JSON configuration printed.
results in this correct config from
|
Upon further inspection, it doesn't appear to play well enough with
Note that
Note that the
As a result, I've observed that even with |
Closes #5447
This implements a new
auto_https prefer_wildcard
option, which drops automation policies for non-wildcard domains when there's already a wildcard in another policy.This allows users to flatten their config, and instead of using a pattern like https://caddyserver.com/docs/caddyfile/patterns#wildcard-certificates they can instead do something like:
This would only produce a single wildcard certificate, and no individual certificate for
foo.example.com
since it's already covered by the wildcard.This also allows specifying multiple arguments to
auto_https
, so you can do the following to set multiple Automatic HTTPS options. Previously only one could be set at a time, which was generally fine because there wasn't actually any usecase where it would be useful:I've only manually (visually) tested with a few simple usecases. Unfortunately we have a big lack of tests for the Automatic HTTPS logic because it manipulates config at runtime. I probably need help with testing this to make sure it doesn't have weird side effects. Thankfully, this should be safe/backwards compatible as long as users don't enable this option. We could call it experimental for now.