Skip to content

adrianosela/tsdmg

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tailscale Domain Mgmt. Gateway (tsdmg)

Go Report Card Documentation GitHub issues license

A tsnet based service for managing custom domains in your Tailnet, along with libraries to enable your Tailscale nodes to manage DNS records, and retrieve public (Let's Encrypt) TLS certificates at runtime.

Why Do I Need This?

Running a tsdmg service in your Tailnet enables several use-cases not possible out-of-the-box with Tailscale:

  • Custom domains for your Tailscale nodes e.g. <node>.yourdomain.com
  • Allow Tailscale nodes to retrieve public (Let's Encrypt) TLS certificates for custom domains
  • Allow Tailscale nodes to manage your domains/subdomains arbitrarily

My motivation to build this is that I wanted all my internal services and web applications (not on the Internet, accesible only via Tailscale) to be reachable via my custom domain (<node>.services.adrianosela.com), and I wanted them to serve TLS/HTTPS using public (Let's Encrypt) certificates.

Really I wish I could just delegate the services.adrianosela.com zone to Tailscale, and have them do this for me (but it's not possible as of Feb 2026).

How Does it Work?

Essentially:

  • Using Tailscale ACLs, you define which Tailscale sources (nodes, users, groups) can manage which subdomains (e.g. node "webapp" can manage "webapp.yourdomain.com")
  • You provision the tsdmg service with credentials for your DNS provider (e.g. Cloudflare, Google, GoDaddy, etc...)
  • Your Tailscale nodes can request domains to be created/updated/deleted against the tsdmg service via HTTP
  • The tsdmg service will use incoming requests' Tailscale identity to authenticate and authorize (based on Tailscale ACLs) domain management requests

Service Usage

Run the tsdmg service as shown in ./cmd/server/main.go:

tsdmg, err := service.New(ctx, tsClient, dnsProvider)
if err != nil {
    log.Fatalf("failed to initialize tsdmg service: %v", err)
}

if err = tsdmg.ServeHTTP(ln); err != nil {
    log.Fatalf("failed to serve HTTP over tsnet listener: %v", err)
}

The dns.Provider interface is implemented for all major DNS providers by https://github.com/libdns e.g.:

Running the Server with Docker

The tsdmg server is available as a Docker image:

docker run ghcr.io/adrianosela/tsdmg -h

Example: Running with Cloudflare

docker run -it ghcr.io/adrianosela/tsdmg \
  -domain=yourdomain.com \
  -dns-provider=cloudflare \
  -cloudflare-api-token=$TSDMG_CLOUDFLARE_API_TOKEN \
  -ts-authkey=$TSDMG_TS_AUTHKEY

See the Makefile tsdmg-docker target for a complete working example.

Configuration Options

Flag Description Required Default
-addr Address to listen on No :80
-hostname Hostname to use for Tailscale machine No tsdmg
-ts-authkey Tailscale auth key Yes -
-dns-provider Which DNS provider to use (one of aws, gcp, azure, cloudflare, godaddy) Yes -
-domain Domain management allowlist (repeatable) No -
-node-reg-domain Domain in which to create A/AAAA records for registering Tailscale nodes (repeatable, must be subset of -domain if both set) No -
-cloudflare-api-token Cloudflare API Token If dns-provider=cloudflare -
-aws-access-key-id AWS Access Key ID If dns-provider=aws (or use profile) -
-aws-secret-access-key AWS Secret Access Key If dns-provider=aws (or use profile) -
-aws-profile AWS Profile If dns-provider=aws (or use keys) -
-gcp-project Google Cloud Project ID If dns-provider=gcp -
-gcp-svc-acct-json Google Cloud Service Account JSON If dns-provider=gcp -
-azure-subscription-id Azure Subscription ID If dns-provider=azure -
-azure-resource-group-name Azure Resource Group Name If dns-provider=azure -
-azure-client-id Azure Client ID If dns-provider=azure -
-azure-client-secret Azure Client Secret If dns-provider=azure -
-azure-tenant-id Azure Tenant ID If dns-provider=azure -
-godaddy-api-token GoDaddy API Token If dns-provider=godaddy -

Client Usage

This package includes a tsdmg client capable of requesting, caching, and refreshing public (Let's Encrypt) TLS certificates, by leveraging a tsdmg server.

serverURL := "http://tsdmg" // my server's node name is tsdmg

opts := []tsdmg.Option{
	// My laptop is already running the Tailscale desktop
	// client, so the tsdmg server is already reachable by
	// node-name i.e. http://tsdmg. Not setting this option
	// will attempt to initialize a new Tailscale node.
	tsdmg.WithSkipTailscaleNode(true),
}

client, err := tsdmg.NewClient(ctx, serverURL, opts...)
if err != nil {
	log.Fatalf("failed to initialize client: %v", err)
}
defer client.Close()

created, err := client.CreateRecords(ctx, recordsToCreate...)
// check error

deleted, err := client.DeleteRecords(ctx, recordsToDelete...)
// check error

Certificate Manager Usage

With an initialized tsdmg.Client client:

NOTE: import "github.com/adrianosela/tsdmg/pkg/tsautocert"

certCommonName := "macbook.tsdmg.net"

opts := []tsautocert.Option{
	// Cache certificates in the filesystem to
	// avoid hitting the Let's Encrypt rate limit.
	tsautocert.WithCertificateCache(autocert.DirCache("./certcache")),
}

certManager, err := tsautocert.NewCertificateManager(
	ctx,
	client,
	certCommonName,
	opts...,
)
if err != nil {
	log.Fatalf("failed to initialize certificate manager: %v", err)
}
defer certManager.Close()

if err := certManager.WaitForInitialCert(ctx); err != nil {
	log.Fatalf("failed to wait for initial certificate: %v", err)
}

ln, err := net.Listen("tcp", ":443")
if err != nil {
	log.Fatalf("failed to start tcp listener: %v", err)
}

// Configure TLS listener to get certificate using tsdmg client.
ln = tls.NewListener(ln, &tls.Config{GetCertificate: certManager.GetCertificate})
defer ln.Close()

err = http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello World!"))
}))
if err != nil {
	log.Fatalf("failed to serve HTTP: %v", err)
}

Tailscale ACLs Example

To allow ANY node to retrieve TLS certificates for <node>.<your-custom-domain> (e.g. your-macbook.yourdomain.com), you can add a grant in your ACL as follows:

The ${node} will be replaced with the tsdmg client node's name by the tsdmg server prior to evaluation.

	"grants": [
		{
			"src": ["*"],
			"dst": ["*"],
			"ip":  ["*"],

			"app": {
				"tsdmg.net/dns/v1": [
					{
						"TXT": ["_acme-challenge.${node}.yourdomain.com"],
					},
				],
			},
		},
	],

Say you also want your client to have the ability to create A and AAAA records (e.g. for its own tailnet private IP or any other IP):

	"grants": [
		{
			"src": ["*"],
			"dst": ["*"],
			"ip":  ["*"],

			"app": {
				"tsdmg.net/dns/v1": [
					{
						"TXT":  ["_acme-challenge.${node}.yourdomain.com"],
						"A":    ["${node}.yourdomain.com"],
						"AAAA": ["${node}.yourdomain.com"],
					},
				],
			},
		},
	],

TODOs:

  • Better project structure e.g. internal, not everything as pkg

About

Tailscale Domain Management Gateway: allow Tailscale nodes to retrieve public (Let's Encrypt) TLS certificates for custom domains.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors