Docs: the on-demand TLS "ask" example for Caddy doesn't validate anything (and it took down our entire domain's HTTPS for 30+ hours) #29079
T0mThomson
started this conversation in
Coder
Replies: 1 comment
|
Hey! I'm sorry you ran into this and this was very odd wording on our end! I'm glad you were able to get this resolved. I'm working on a fix to our docs now. Regarding #17395, there is another flag
It would! I'd be happy to test it myself and see if we can extend our docs to mention this. |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Summary
The Caddy reverse-proxy tutorial (https://coder.com/docs/tutorials/reverse-proxy-caddy) includes this global block:
with an explicit warning: "DO NOT CHANGE the ask http://example.com line! Doing so will result in your certs potentially not being generated."
This line looks like a security control but isn't one.
example.com(andexample.org, seen in some doc revisions) is a real, live, unrelated third-party domain that returns HTTP 200 for any GET request. Since Caddy's on-demand TLS treats any 2xx response from theaskendpoint as "approved," this configuration approves certificate issuance for literally any hostname an attacker points at the server. It provides zero validation while looking like it does.What happened on our end
We followed this tutorial to expose Coder workspace apps via
*.dv.example.comwith on-demand TLS, exactly as documented. Within days, a scanner (fabricated hostnames included the literal string "notexists", suggesting an automated probe for this exact class of vulnerability β see caddyserver/caddy#6188 for the general pattern) sent TLS handshakes for many fake hostnames matching Coder's{port}--{agent}--{workspace}--{owner}wildcard naming convention. Because theaskcheck always returned 200, Caddy dutifully tried to obtain a real Let's Encrypt certificate for each fake hostname.This exhausted Let's Encrypt's account-wide rate limit (50 certificates per registered domain per 168h) for our entire base domain β not just the Coder wildcard. Every other service on the same domain (unrelated apps, mail, other subdomains) lost the ability to renew or obtain certificates for roughly 30 hours across several retry cycles, since the rate limit is scoped to the registrable domain, not the specific site block.
Root cause
Caddy's own docs are explicit that on-demand TLS is "insecure unless you also configure the on_demand_tls global option to mitigate abuse" (https://caddyserver.com/docs/caddyfile/directives/tls). The Coder tutorial provides no real mitigation β the example.com placeholder is functionally equivalent to having no
askcheck at all, while the "DO NOT CHANGE" warning actively discourages users from fixing it.What we built instead
A small standalone Node.js service that Caddy's
askdirective points to, which:{app}--{agent}--{workspace}--{owner}.wildcard-domain, per https://coder.com/docs/admin/setup#wildcard-access-url).GET /api/v2/workspaces?q=owner:{owner} name:{workspace}with aCoder-Session-Tokento confirm the workspace genuinely exists before approving certificate issuance.This works, but required reverse-engineering the naming convention and building custom infrastructure that arguably should be a first-class, documented Coder feature.
Side note: we also hit
CODER_MAX_TOKEN_LIFETIMEbeing hard-capped at 168h regardless of the configured value (relevant to anyone building a similar long-lived service-account integration β see #17395), which forced us to add a token-rotation timer as well.Ask
example.com/example.orgplaceholder with either a real working example, or an explicit warning that it must be replaced with a real validation endpoint before production use.GET /api/v2/ok-to-issue-cert?domain=...), so self-hosters can point Caddy'saskdirectly at Coder without building a custom validator and managing a separate API token.Happy to share our validator script if useful as a reference/starting point.
All reactions