From 6ec2516aca2b637cdcc01c640b421f0410d68cf1 Mon Sep 17 00:00:00 2001 From: Ryan Mulligan Date: Tue, 10 Feb 2026 17:53:52 -0800 Subject: [PATCH 1/2] auth: support tagged nodes in tailscale_auth Upstream tailscale_auth rejects all tagged nodes, returning an error. This change instead accepts tagged nodes and exposes their tags as metadata so Caddy expressions can match on them. For tagged nodes: - user.ID is set to the first tag - tailscale_tags metadata contains all tags (comma-separated) - tailscale_tailnet metadata contains the tailnet name This enables use cases like granting CI bots or other tagged infrastructure nodes access to Caddy-protected services via {http.auth.user.tailscale_tags} expressions. Signed-off-by: Ryan Mulligan --- auth.go | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/auth.go b/auth.go index c84ac09..a7281ac 100644 --- a/auth.go +++ b/auth.go @@ -1,12 +1,15 @@ // Copyright (c) Tailscale Inc & contributors // SPDX-License-Identifier: Apache-2.0 +// Patched by Replit to support tagged nodes in tailscale_auth. +// Upstream rejects all tagged nodes; this version exposes tags as metadata +// so Caddy expressions can match on {http.auth.user.tailscale_tags}. + package tscaddy // auth.go contains the TailscaleAuth module and supporting logic. import ( - "fmt" "net" "net/http" "reflect" @@ -126,12 +129,18 @@ type tsnetListener interface { // Authenticate authenticates the request and sets Tailscale user data on the caddy User object. // -// This method will set the following user metadata: +// For regular (non-tagged) users, the following metadata is set: // - tailscale_login: the user's login name without the domain // - tailscale_user: the user's full login name // - tailscale_name: the user's display name // - tailscale_profile_picture: the user's profile picture URL -// - tailscale_tailnet: the user's tailnet name (if the user is not connecting to a shared node) +// - tailscale_tailnet: the user's tailnet name +// +// For tagged nodes, the following metadata is set: +// - tailscale_tags: comma-separated list of node tags (e.g. "tag:zergbot,tag:ci") +// - tailscale_tailnet: the tailnet name +// +// The user ID is set to the login name for regular users, or the first tag for tagged nodes. func (ta Auth) Authenticate(w http.ResponseWriter, r *http.Request) (caddyauth.User, bool, error) { user := caddyauth.User{} @@ -145,10 +154,6 @@ func (ta Auth) Authenticate(w http.ResponseWriter, r *http.Request) (caddyauth.U return user, false, err } - if len(info.Node.Tags) != 0 { - return user, false, fmt.Errorf("node %s has tags", info.Node.Hostinfo.Hostname()) - } - var tailnet string if !info.Node.Hostinfo.ShareeNode() { if s, found := strings.CutPrefix(info.Node.Name, info.Node.ComputedName+"."); found { @@ -156,14 +161,25 @@ func (ta Auth) Authenticate(w http.ResponseWriter, r *http.Request) (caddyauth.U } } - user.ID = info.UserProfile.LoginName - user.Metadata = map[string]string{ - "tailscale_login": strings.Split(info.UserProfile.LoginName, "@")[0], - "tailscale_user": info.UserProfile.LoginName, - "tailscale_name": info.UserProfile.DisplayName, - "tailscale_profile_picture": info.UserProfile.ProfilePicURL, - "tailscale_tailnet": tailnet, + if len(info.Node.Tags) != 0 { + // Tagged node: expose tags as metadata for Caddy expression matching. + // user.ID is set to the first tag for convenient @matcher expressions. + user.ID = info.Node.Tags[0] + user.Metadata = map[string]string{ + "tailscale_tags": strings.Join(info.Node.Tags, ","), + "tailscale_tailnet": tailnet, + } + } else { + user.ID = info.UserProfile.LoginName + user.Metadata = map[string]string{ + "tailscale_login": strings.Split(info.UserProfile.LoginName, "@")[0], + "tailscale_user": info.UserProfile.LoginName, + "tailscale_name": info.UserProfile.DisplayName, + "tailscale_profile_picture": info.UserProfile.ProfilePicURL, + "tailscale_tailnet": tailnet, + } } + return user, true, nil } From 09fbd648f2b50cb09e26724ae367e5418dcc8c7a Mon Sep 17 00:00:00 2001 From: Ryan Mulligan Date: Tue, 10 Feb 2026 17:56:42 -0800 Subject: [PATCH 2/2] auth: remove replit comment, always include tailscale_tags in metadata Include an empty tailscale_tags entry for non-tagged users so that Caddy expressions can compare against the field unconditionally without needing to handle the missing-key case. Signed-off-by: Ryan Mulligan --- auth.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/auth.go b/auth.go index a7281ac..f3a855b 100644 --- a/auth.go +++ b/auth.go @@ -1,10 +1,6 @@ // Copyright (c) Tailscale Inc & contributors // SPDX-License-Identifier: Apache-2.0 -// Patched by Replit to support tagged nodes in tailscale_auth. -// Upstream rejects all tagged nodes; this version exposes tags as metadata -// so Caddy expressions can match on {http.auth.user.tailscale_tags}. - package tscaddy // auth.go contains the TailscaleAuth module and supporting logic. @@ -177,6 +173,7 @@ func (ta Auth) Authenticate(w http.ResponseWriter, r *http.Request) (caddyauth.U "tailscale_name": info.UserProfile.DisplayName, "tailscale_profile_picture": info.UserProfile.ProfilePicURL, "tailscale_tailnet": tailnet, + "tailscale_tags": "", } }