How Tailcat’s `--allow` Flag Controls Server Access Control
The --allow flag restricts which client identity keys may complete the WireGuard handshake with a Tailcat server, supporting an empty string (allow all), none (deny all), or a comma-separated list of specific public keys.
Tailcat operates in two modes: as a server listening for incoming connections or as a client dialing outbound. When running as a server, the --allow flag provides a lightweight, public-key-based access control list (ACL) that filters incoming WireGuard handshakes before a tunnel is established. This mechanism is implemented directly in the low-level networking backend to reject unauthorized clients at the earliest possible stage.
Understanding the --allow Flag Syntax
The flag accepts three distinct value types that determine server accessibility:
- Empty string (
""): The default behavior permits all clients to connect. none: Blocks every client; the server ignores all incoming handshakes.- Comma-separated public keys: Only clients possessing the listed
key.NodePublicvalues may establish a connection.
Parsing and Storing Allowed Client Keys
In main/cmd/tailcat/tailcat.go, the flag is defined with its default and description:
flagAllow = flag.String("allow", "", "comma-separated list of public keys to allow access to the server, or 'none' to allow no clients. If empty, all clients are allowed.")
When the server initializes via the server() function, the flag value is split on commas and processed sequentially:
if *flagAllow != "" {
for _, ks := range strings.Split(*flagAllow, ",") {
if ks == "none" {
s.AddAllowedClient(key.NodePublic{}) // empty key → deny everybody
continue
}
var k key.NodePublic
if err := k.UnmarshalText([]byte(ks)); err != nil {
log.Fatalf("invalid key %q in --allow: %v", ks, err)
}
s.AddAllowedClient(k)
}
}
Each valid public key string is unmarshaled into a tailscale.com/types/key.NodePublic struct. The Server.AddAllowedClient method (located in main/tailcat.go, lines 78-91) records these keys in an internal allow-list stored within the server's locoBackend instance. Invalid key formats trigger an immediate fatal error during startup, preventing the server from launching with malformed ACLs.
Runtime Enforcement During the WireGuard Handshake
The actual access control enforcement occurs in the low-level backend when processing the initial "Meow" handshake from a client. Before adding the client to the WireGuard peer set, the backend checks the allowedClients map:
// in tailcat.go, around line 1355
if b.allowedClients != nil && !b.allowedClients[src] {
b.logf("ignoring meow from %v: not in allowedClients", src.String())
// no peer is added → the client never gets a response
return nil
}
The b.allowedClients field is a map[key.NodePublic]bool. If the map is nil (indicating no --allow flag was provided), the check is skipped and all clients are accepted. If the map is populated but the incoming client's public key (src) is absent, the handshake is silently ignored. This prevents the unauthorized client from receiving any response, effectively blocking tunnel establishment without revealing server configuration details.
Practical Usage Examples
Generate a client identity key to use with the --allow flag:
$ tailcat genkey --client
# wrote file to ~/.config/tailcat/keys/client-default.private.json
nodekey:cfb6bf...ddfd16 # <-- public key to use with --allow
Start a server restricted to a specific client:
$ tailcat --serve=22 --allow=nodekey:cfb6bf...ddfd16
# 🐈 Server listening with saved key "default": tcXYZ...
Attempts from non-whitelisted clients will fail silently; the server logs the rejection while the client times out waiting for a handshake response.
Block all client connections for maintenance or debugging:
$ tailcat --serve=all --allow=none
# → only the server itself can initiate a handshake; no client will ever connect.
Summary
- The
--allowflag implements a public-key whitelist for Tailcat servers, parsed at startup inmain/cmd/tailcat/tailcat.go. - Three modes are supported: allow-all (default), deny-all (
none), or specific key enumeration. - Storage occurs via
Server.AddAllowedClient, which populateslocoBackend.allowedClientswithkey.NodePublicvalues. - Enforcement happens during the WireGuard "Meow" handshake in
main/tailcat.go(line ~1355), where unauthorized keys are silently dropped before peer registration.
Frequently Asked Questions
What happens if I provide an invalid public key to --allow?
The server will immediately exit with a fatal error during startup. The parsing logic in main/cmd/tailcat/tailcat.go calls key.NodePublic.UnmarshalText() on each key, and any failure prints invalid key %q in --allow before terminating the process.
Can I update the allowed client list without restarting the server?
No. The --allow flag is parsed only once during server initialization in the server() function. Changes to the ACL require a server restart to re-parse the flag values and rebuild the allowedClients map.
Why does the server silently ignore unauthorized clients instead of returning an error?
Silently dropping the handshake in main/tailcat.go (line ~1355) is a security design choice. By returning nil without adding the peer, the server avoids revealing its existence or configuration to potential attackers scanning for Tailcat endpoints. The client simply sees a timeout while the server logs ignoring meow internally.
What is the difference between --allow="" and --allow=none?
An empty string leaves the allowedClients map as nil, causing the enforcement check to be skipped entirely so all clients connect. The none value explicitly inserts an empty key.NodePublic into the map, creating a non-nil map that causes the enforcement logic to reject every incoming public key, including valid ones.
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 →