mirror of
https://github.com/juanfont/headscale.git
synced 2026-01-11 20:00:28 +01:00
tailscale cert + serve tracking #1000
Open
opened 2025-12-29 02:27:16 +01:00 by adam
·
29 comments
No Branch/Tag Specified
main
update_flake_lock_action
gh-pages
kradalby/release-v0.27.2
dependabot/go_modules/golang.org/x/crypto-0.45.0
dependabot/go_modules/github.com/opencontainers/runc-1.3.3
copilot/investigate-headscale-issue-2788
copilot/investigate-visibility-issue-2788
copilot/investigate-issue-2833
copilot/debug-issue-2846
copilot/fix-issue-2847
dependabot/go_modules/github.com/go-viper/mapstructure/v2-2.4.0
dependabot/go_modules/github.com/docker/docker-28.3.3incompatible
kradalby/cli-experiement3
doc/0.26.1
doc/0.25.1
doc/0.25.0
doc/0.24.3
doc/0.24.2
doc/0.24.1
doc/0.24.0
kradalby/build-docker-on-pr
topic/docu-versioning
topic/docker-kos
juanfont/fix-crash-node-id
juanfont/better-disclaimer
update-contributors
topic/prettier
revert-1893-add-test-stage-to-docs
add-test-stage-to-docs
remove-node-check-interval
fix-empty-prefix
fix-ephemeral-reusable
bug_report-debuginfo
autogroups
logs-to-stderr
revert-1414-topic/fix_unix_socket
rename-machine-node
port-embedded-derp-tests-v2
port-derp-tests
duplicate-word-linter
update-tailscale-1.36
warn-against-apache
ko-fi-link
more-acl-tests
fix-typo-standalone
parallel-nolint
tparallel-fix
rerouting
ssh-changelog-docs
oidc-cleanup
web-auth-flow-tests
kradalby-gh-runner
fix-proto-lint
remove-funding-links
go-1.19
enable-1.30-in-tests
0.16.x
cosmetic-changes-integration
tmp-fix-integration-docker
fix-integration-docker
configurable-update-interval
show-nodes-online
hs2021
acl-syntax-fixes
ts2021-implementation
fix-spurious-updates
unstable-integration-tests
mandatory-stun
embedded-derp
prtemplate-fix
v0.28.0-beta.1
v0.27.2-rc.1
v0.27.1
v0.27.0
v0.27.0-beta.2
v0.27.0-beta.1
v0.26.1
v0.26.0
v0.26.0-beta.2
v0.26.0-beta.1
v0.25.1
v0.25.0
v0.25.0-beta.2
v0.24.3
v0.25.0-beta.1
v0.24.2
v0.24.1
v0.24.0
v0.24.0-beta.2
v0.24.0-beta.1
v0.23.0
v0.23.0-rc.1
v0.23.0-beta.5
v0.23.0-beta.4
v0.23.0-beta3
v0.23.0-beta2
v0.23.0-beta1
v0.23.0-alpha12
v0.23.0-alpha11
v0.23.0-alpha10
v0.23.0-alpha9
v0.23.0-alpha8
v0.23.0-alpha7
v0.23.0-alpha6
v0.23.0-alpha5
v0.23.0-alpha4
v0.23.0-alpha4-docker-ko-test9
v0.23.0-alpha4-docker-ko-test8
v0.23.0-alpha4-docker-ko-test7
v0.23.0-alpha4-docker-ko-test6
v0.23.0-alpha4-docker-ko-test5
v0.23.0-alpha-docker-release-test-debug2
v0.23.0-alpha-docker-release-test-debug
v0.23.0-alpha4-docker-ko-test4
v0.23.0-alpha4-docker-ko-test3
v0.23.0-alpha4-docker-ko-test2
v0.23.0-alpha4-docker-ko-test
v0.23.0-alpha3
v0.23.0-alpha2
v0.23.0-alpha1
v0.22.3
v0.22.2
v0.23.0-alpha-docker-release-test
v0.22.1
v0.22.0
v0.22.0-alpha3
v0.22.0-alpha2
v0.22.0-alpha1
v0.22.0-nfpmtest
v0.21.0
v0.20.0
v0.19.0
v0.19.0-beta2
v0.19.0-beta1
v0.18.0
v0.18.0-beta4
v0.18.0-beta3
v0.18.0-beta2
v0.18.0-beta1
v0.17.1
v0.17.0
v0.17.0-beta5
v0.17.0-beta4
v0.17.0-beta3
v0.17.0-beta2
v0.17.0-beta1
v0.17.0-alpha4
v0.17.0-alpha3
v0.17.0-alpha2
v0.17.0-alpha1
v0.16.4
v0.16.3
v0.16.2
v0.16.1
v0.16.0
v0.16.0-beta7
v0.16.0-beta6
v0.16.0-beta5
v0.16.0-beta4
v0.16.0-beta3
v0.16.0-beta2
v0.16.0-beta1
v0.15.0
v0.15.0-beta6
v0.15.0-beta5
v0.15.0-beta4
v0.15.0-beta3
v0.15.0-beta2
v0.15.0-beta1
v0.14.0
v0.14.0-beta2
v0.14.0-beta1
v0.13.0
v0.13.0-beta3
v0.13.0-beta2
v0.13.0-beta1
upstream/v0.12.4
v0.12.4
v0.12.3
v0.12.2
v0.12.2-beta1
v0.12.1
v0.12.0-beta2
v0.12.0-beta1
v0.11.0
v0.10.8
v0.10.7
v0.10.6
v0.10.5
v0.10.4
v0.10.3
v0.10.2
v0.10.1
v0.10.0
v0.9.3
v0.9.2
v0.9.1
v0.9.0
v0.8.1
v0.8.0
v0.7.1
v0.7.0
v0.6.1
v0.6.0
v0.5.2
v0.5.1
v0.5.0
v0.4.0
v0.3.6
v0.3.5
v0.3.4
v0.3.3
v0.3.2
v0.3.1
v0.3.0
v0.2.2
v0.2.1
v0.2.0
v0.1.1
v0.1.0
Labels
Clear labels
CLI
DERP
DNS
Nix
OIDC
SSH
bug
database
documentation
duplicate
enhancement
faq
good first issue
grants
help wanted
might-come
needs design doc
needs investigation
no-stale-bot
out of scope
performance
policy 📝
pull-request
question
regression
routes
stale
tags
tailscale-feature-gap
well described ❤️
wontfix
Mirrored from GitHub Pull Request
Milestone
No items
No Milestone
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: starred/headscale#1000
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @kradalby on GitHub (Apr 14, 2025).
Background
There are some long standing feature requests for
tailscale certandtailscale serveto work with HTTPS.From a discussion with @erisa, we talked about the steps for doing this and how it currently works, all should be possible, but will require a few building blocks.
This issue is intended to track the effort and to note down what is needed (while fresh in memory), to ultimately support a couple of things, but there are no ETA for this as we have a bunch of other things to do first.
Most of these features depend on each other, so it looks like it will be a pretty "linear" effort. There is a couple of side quests which likely will improve other things on the way.
Details / observations
It looks like HTTPS in
tailscale serveis directly dependent ontailscale certto work (as we need the ACME facilities for the certificate).tailscale certrequires headscale to implement/machine/set-dns, which in terms requires headscale to automatically create TXT ACME records "somewhere" for the given domain.headscale needs to be able to set DNS for the
base_domainzone on behalf of clients. This can likely be done withlibdnsside quest: headscale currently has a ACME/letsencrypt implementation that dates back to the origin of the project. It only supports HTTP challenge (which is fine since the server must be public anyways). This can be replaced and simplified with something like
certmagic(or the lower levelacmez) to support DNS Challenge (and reuse the other config).This side quest could potentially be leveraged to do something like a "embedded funnel", where funnel requests can be set up to go to the headscale instance and it can serve requests on behalf of clients. Please note that this is very hypothetical and might be unfeasible. There are more parts here to discover like having to run a Tailscale client inside the Headscale process. This will also not work with reverse proxies, leaving the users to decide if they want this feature.
TODO
base_domainTXT recordsDNSConfig.CertDomainstailscale certtailscale serve@liorsl commented on GitHub (Apr 29, 2025):
Wouldn't it be better to have another process running that does the reverse proxy into the tailscale network?
@kradalby commented on GitHub (Apr 30, 2025):
Depends on what you consider better. The goal would be to provide something simple that works and not provide a complicated multi process setup.
@Pamplemousse commented on GitHub (Jul 7, 2025):
From the original post
and the documentation about DNS challenge
, the proposed solution would only allow to create certificates for "valid" FQDN (
my.vpn.example.com), but not for "made-up" domains (my.vpn). Because the domain needs to be reachable from the public DNS system, and not rely onMagicDNSresolution.Not that it's a hard requirement on my end, but I found it convenient to be able to refer to my machines and services through a explicitly custom domain (I'm gonna miss accessing
grafana.potato)...@Pamplemousse commented on GitHub (Jul 7, 2025):
Also,
seems non trivial: as far as I can tell, it implies that
headscaleusers either have a local DNS server, or use a DNS provider that permit to set DNS records via API.In either cases, I wonder how
headscalewould be able to implement such a feature without making too many assumptions about the underlying system 🤔@1fexd commented on GitHub (Jul 18, 2025):
You'd probably have to implement some sort of adapter that delegates setting the TXT record to an external program (some providers have CLIs) or by sending a (generic) API call to the user's provider
@kotx commented on GitHub (Jul 18, 2025):
Caddy's certmagic uses https://github.com/libdns/libdns for DNS-01 challenges. Perhaps this could be used?
@1fexd commented on GitHub (Jul 19, 2025):
I've implemented a PoC using libdns and their Hetzner package in #2696
@rbollampally commented on GitHub (Jul 21, 2025):
Implementing setting of DNS records in hundreds, if not thousands, of DNS providers is not ideal.
I think the best approach for headscale would be to implement a local DNS server which can respond to base_domain and controlplane domain DNS queries.
This https://github.com/siilike/certbot-dns-standalone should solve majority of the above requirements.
The rest of #2137 could also be solved by this PR: https://github.com/juanfont/headscale/pull/2312
From the admin perspective, one only has to now point the NS records for base_domain to be same as controlplane.
@stinovlas commented on GitHub (Jul 21, 2025):
Hi! I'm new to headscale, but I'd like to point out that implementing yet another DNS server is not ideal, especially for people that already host their own DNS server (which is fairly easy with software like
knot).Did you consider implementing RFC 2136? This is an open standard supported by multiple DNS server implementations.
@rbollampally commented on GitHub (Jul 22, 2025):
You make a good point I completely forgot the selfhost DNS crowd. I have no simple solution. Not every DNS host is RFC 2136 complaint.
@kradalby commented on GitHub (Jul 23, 2025):
Note that I've added a milestone to this, this is to try to provide some sort of idea for people when we think we will work on it, it might change, but at least its the idea.
It would also be meaningful to rework the old (not been touch since initiation) TLS config we have built in at the same time.
For now it is great to get input, which should make the implementation more informed.
This is true, some sort of library like the one mentioned from Caddy (I think libdns) and there are a couple of other candidates.
I feel that make sense to include, even if we need to sidestep the library for that one.
Only having had a quick look, I think there are parts here that are useful, but there is way to much that is too Hertzner specific for us to be able to maintain, so hopefully we can find some library that can handle the configuration of the individual provider in a more generic way.
@tuarrep commented on GitHub (Aug 5, 2025):
I stumbled upon this quite popular lib which, IMO, fits the needs of this issue
https://github.com/go-acme/lego
@kotx commented on GitHub (Aug 6, 2025):
The client uses the /machine/set-dns endpoint, so an ACME library would likely not work, we need a generic DNS library.
@tuarrep commented on GitHub (Aug 7, 2025):
Sorry if don’t get it, but why can’t we use ACME DNS challenge? Is that not what’s required to obtain a certificate?
This is what I do (and how I find this library) to workaround this lack of feature in Headscale.
I have a Nginx proxy in my telnet, which obtains cert with this lib (through Nginx-ui package) using ACME DNS challenge. Even though the domain name resolve a private IP address.
BTW, if the need is only putting a TXT record in a DNS zone, lego supports this for a wide variety of DNS providers.
@mys721tx commented on GitHub (Aug 7, 2025):
So far my set up is to have certbot using one of the DNS plugin to use DNS challenge. As long as Letsencrypt can access the DNS record, there is no need for the tailnet member to be publicly accessible.
If the number of DNS providers is a problem, we should have a standard API and let plugin to handle the DNS update.
@kotx commented on GitHub (Aug 7, 2025):
I believe the Tailscale client performs ACME, not the coordination server- the client uses the coordination server to set the DNS record for ACME challenges, so it's a lower-level API. I've only glanced at the client code, so let me know if I'm wrong!
@tuarrep commented on GitHub (Aug 13, 2025):
You’re right.
The challenge is generated by client.
But I’m confident we can use Lego as DNS library as there is a whole lot of providers and dns methods for ACME DNS challenge are built in (setting value, waiting for propagation, clean up) and exposed as an abstraction.
0012e20e52/challenge/dns01/dns_challenge.go (L74-L76)0012e20e52/challenge/dns01/dns_challenge.go (L161)@rbollampally commented on GitHub (Aug 13, 2025):
Actually, after getting #2312 to work, I realised that cert-bot is not needed. All I had to do was create a shell script interacting with AWS CLI/Route53 to update my DNS records. So, if Lego solves DNS management, along with #2312 should solve #2137
A few gotchas while getting this to work if anyone wants to try this:
@nom3ad commented on GitHub (Aug 15, 2025):
@rbollampally
Author of #2312 here.
I occasionally rebase that work on top of the latest release, as I am using my private build of headscale to get the
servefeature on my tailnets. Since that PR was a POC draft, I never cared to push my updates once it was closed.I didn't revert #2503, but I think I got the root cause resolved in my last update. I just rebased it on top of main branch force pushed to my PR branch
BTW, In my Cloudflare setup, I never had to put sleep commands to get the DNS record correct. Not sure why you faced that issue. Previously, a route53 setup I tested also worked fine.
Not sure what caused the old record to serve from your Rotue53.
AFAIK, Letsencrypt does full recursive resolution by its own to avoid caching. That means it should directly query the authoritative nameservers (Route53). And I believe Tailscale client would only initiate DNS01 challenge verification after set-dns call is completed, which ensure that route53 is already updated by the script.
@rbollampally commented on GitHub (Aug 15, 2025):
@nom3ad Thanks for the update. I tried debugging the issues caused by #2503 and got a point that I determined the issue was preventing me from reaching the code you wrote. So, I just reverted it to get going. I'm not a GO programmer. So, don't know what would be the fix.
I again had to do a lot of debugging to figure out that the Route53 record setting was causing the issues. I'm running it directly on an EC2 with role attached to it; which could probably be leading to super fast AWS API. The TTL is not even that long. Maybe we should try and poll letsencrypt for second time within headscale, just in case?
@nom3ad commented on GitHub (Aug 15, 2025):
Yeah, I agree that polling would be a good idea just to make sure records are propagated. I've seen ACME clients implementing polling before initiating DNS01 challenge verification.
IMO, it would have been nice if Tailscale was actually doing it. If not, headscale could do the same.
I'd have to look into logs in my setup to see if I am actually having DNS issues during certificate renewals.
BTW,
Last time I checked, I couldn't find a cleanup api call implementation in Tailscale client code. I was looking for something like
unset-dns. That would have been taking care of stale records.I don't have any better ideas on how to properly do the cleanup - other than some hacks like differed cleanup script execution.
In my setup, I just leave them as they any way get updated on renewals.
@xkxx commented on GitHub (Aug 26, 2025):
Hey all,
I'm trying to deploy tsidp which requires HTTPS serve. As such, I'm pretty invested in the progress of this feature. Are you taking contributors in pushing this over the finish line by any chance? Thanks!
@mattdale77 commented on GitHub (Oct 21, 2025):
I'm just registering my interest so I can get https working with TSDProxy
@almereyda commented on GitHub (Oct 28, 2025):
Today upstream presented a possible follow up to this story:
I don't know how much Headscale would be or is affected by this.
@evilhamsterman commented on GitHub (Nov 11, 2025):
I have a suggestion. Don't deal with the DNS in the Headscale controller at all, it adds a lot of complexity and maintenance.
I propose that you define a webhook. Then the community can create interfaces for whatever DNS provider and API they want with any language they want.
If you want to provide some webhook provders for common DNS like Route53, Azure, a Zone file, etc. they could be maintained separately as well.
@timka commented on GitHub (Nov 26, 2025):
Let's assume I have a headscale instance running serving hosts in
ovl1.example.com. Now only the 100.100.100.100 MagicDNS resolver knows about the existence of myovl1.example.comsubdomain.Now imagine I create the
ovl1.example.comzone (NS record) and configure my DNS server (coredns) to use its ACME plugin (coredns-acme) to work with Let's Encrypt DNS01 challenge for my headscale subdomain.Will that make the 100.100.100.100 resolver use my authoritative server for names in my headscale subdomain and break MagicDNS?
UPD: fixed example domain
@Giggitybyte commented on GitHub (Nov 26, 2025):
@timka I use Caddy with a DNS provider plugin for my domain registrar. Caddy is not accessible from the internet, only through headscale/tailscale (no public DNS entries, only MagicDNS A records), and it's able to get Let's Encrypt certs for my private MagicDNS domains using a DNS01 challenge. There's no need to complicate things with your own DNS server in the middle.
However, with the setup you described, you'd need set
override_local_dnstotrueand to add your DNS server tonameservers.globalin the config, and your DNS server would need to be accessible over the internet or at the very least hosted on a tailscale node. Though I'm not sure it'll work smoothly, as I've never needed to set things up like that.I recommend using Caddy with a DNS provider plugin to get certificates over trying to mess with DNS.
@timka commented on GitHub (Nov 27, 2025):
@Giggitybyte Running a DNS server has been usual practice for ages. I already do that and don't see it as complication. So that would be just adding another subdomain to my coredns configuration, plus the ACME plugin.
AFAIU, in your setup Caddy plugin talks to your DNS provider's API which creates public TXT records for Let's Encrypt. And from your message I implicitly assume that your DNS provider's server (e.g. Hetzner, Route53, whatever) is authoritative only for your 2nd level domain (
example.comin my case). That is there's no NS record for your MagicDNS subdomain. Is that correct?@Giggitybyte commented on GitHub (Nov 27, 2025):
@timka Pretty much, yeah. My domain registrar uses Cloudflare as the DNS provider for my domain name,
example.casa, and I have no public DNS records at all forexample.casa.The Caddy plugin creates and deletes public TXT records through my domain registrar's API to complete the DNS01 challenge. All of the A records for my
example.casasubdomains are published with MagicDNS, and accessible only through headscale.