How to Publish Tailcat Tokens Using DNS TXT Records: A Complete Guide
Tailcat allows you to publish connection tokens via DNS TXT records by creating a record containing tailcat=<token>, enabling users to connect using a hostname instead of a raw token string.
Tailcat is a secure tunneling tool that identifies servers using connection tokens (the "tc-…" strings). Rather than distributing long base64-encoded tokens manually, you can publish these tokens using DNS TXT records, making it easier to share and rotate credentials across distributed teams.
How DNS Resolution Works in Tailcat
When you provide an argument containing a dot (.) to any Tailcat subcommand, the CLI treats it as a DNS name rather than a raw token. In cmd/tailcat/tailcat.go, the addrBlobArg function (lines 50-71) performs a TXT lookup against the hostname and extracts the value prefixed with tailcat=. If no such record exists, the CLI aborts with an error.
The resolution happens before the client constructor runs, meaning any subcommand that accepts a token—including ssh, ping, and direct port forwarding—supports DNS-based token resolution automatically.
Step-by-Step Guide to Publishing Tokens via DNS
Generate a Stable Server Token
First, create a persistent token that survives server restarts. Without this step, the token embeds a random DERP region that changes on every restart, invalidating your DNS record.
$ tailcat genkey --fixed-region
tcVx2D4Yg9Y8cZKQ0pVYz8rUeGz3Y6k8...
This command stores the private key in ~/.config/tailcat/keys/default.private.json and outputs the public token starting with tc. The --fixed-region flag pins the server to a specific DERP region, embedding this information directly into the token string.
Configure the DNS TXT Record
Create a TXT record for your chosen hostname with the exact value tailcat=<token>. The prefix is mandatory; Tailcat strips it during resolution to extract the actual connection token.
For BIND-style zone files:
my-server.example.com. 300 IN TXT "tailcat=tcVx2D4Yg9Y8cZKQ0pVYz8rUeGz3Y6k8..."
For Cloudflare or other managed DNS providers:
- Name:
my-server.example.com - Content:
tailcat=tcVx2D4Yg9Y8cZKQ0pVYz8rUeGz3Y6k8... - TTL: 300 seconds (5 minutes)
Set a modest TTL (300 seconds recommended) to enable rapid key rotation without waiting for long cache expirations.
Connect Using the Hostname
Once propagated, use the hostname in place of the token for any Tailcat operation:
# Forward local traffic to server port 8080
$ tailcat my-server.example.com 8080
# SSH through the tunnel
$ tailcat ssh my-server.example.com
# Test connectivity
$ tailcat ping my-server.example.com
The CLI automatically resolves the TXT record, validates the tailcat= prefix, and establishes the connection using the extracted token.
Technical Implementation Details
According to the Tailcat source code in cmd/tailcat/tailcat.go, the addrBlobArg function handles the resolution logic. When the input argument contains a dot, the function queries DNS for TXT records, searches for the entry beginning with tailcat=, and returns the substring following the equals sign.
Tokens are case-sensitive, but DNS TXT values preserve the exact string content even when the hostname is lowercased (as browsers and DNS systems typically do). This ensures the client receives the correct token regardless of how the hostname is capitalized during lookup.
Best Practices for DNS Token Management
- Always pin the region: Use
--fixed-regionor explicitly set--region=<name>when generating keys. This prevents token invalidation when the server restarts and selects a new random DERP region. - Keep TTL short: A 300-second TTL balances performance with the ability to rotate compromised keys quickly.
- Secure your DNS: Since anyone with DNS visibility can read the TXT record, treat published tokens as public credentials and rely on Tailcat's underlying encryption for security, or restrict DNS access using split-horizon DNS for internal zones.
Programmatic Usage in Go
The Tailcat Go library supports the same DNS resolution for programmatic connections. Use tailcat.ConnBlob() with a hostname to resolve the token via TXT lookup:
package main
import (
"context"
"log"
"github.com/tailscale/tailcat"
)
func main() {
// Resolve hostname – internally performs DNS TXT lookup
blob := tailcat.ConnBlob("my-server.example.com")
// Load or generate client-side key
clientKey := key.NewNode()
cl := tailcat.NewClient(blob, clientKey)
defer cl.Close()
// Dial TCP port through the tunnel
c, err := cl.DialTCPPort(context.Background(), 8080)
if err != nil {
log.Fatal(err)
}
defer c.Close()
// Use connection for read/write operations...
}
The tailcat.ConnBlob function implements the same logic as the CLI's addrBlobArg, performing the TXT record lookup and prefix stripping automatically.
Summary
- Publish Tailcat tokens by creating DNS TXT records containing
tailcat=<token>for your chosen hostname. - Generate stable tokens using
tailcat genkey --fixed-regionto prevent invalidation on server restarts. - The
addrBlobArgfunction incmd/tailcat/tailcat.go(lines 50-71) handles automatic DNS resolution when hostnames contain dots. - Set DNS TTL to approximately 300 seconds to facilitate rapid key rotation.
- All Tailcat subcommands support DNS-based tokens because resolution occurs before client construction.
Frequently Asked Questions
What is the exact format required for the DNS TXT record?
The TXT record must contain the literal string tailcat= followed immediately by the token, such as tailcat=tcVx2D4Yg9Y8cZKQ0pVYz8rUeGz3Y6k8.... Tailcat searches for this prefix specifically and extracts everything after the equals sign as the connection token. Records without this prefix are ignored.
Why does my token become invalid after restarting the Tailcat server?
By default, Tailcat generates tokens that embed the currently selected DERP region. When the server restarts, it may select a different region, changing the token string. Use the --fixed-region flag when running tailcat genkey to embed a specific region in the token, ensuring it remains valid across restarts.
Can I use DNS TXT records with any Tailcat subcommand?
Yes. Because the DNS lookup occurs in the shared addrBlobArg function before the client is constructed, any subcommand that accepts a token argument—including ssh, ping, and port forwarding—automatically supports hostname resolution via DNS TXT records.
How does Tailcat handle case sensitivity in DNS lookups?
Connection tokens are case-sensitive, but DNS systems typically lowercase hostnames while preserving the case of TXT record values. Tailcat relies on this behavior: it queries the lowercased hostname but receives the exact token string from the TXT record, preserving uppercase and lowercase characters correctly.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →