tsnet: enable node registration via federated identity

Updates: tailscale.com/corp#34148

Signed-off-by: Gesa Stupperich <gesa@tailscale.com>
This commit is contained in:
Gesa Stupperich
2025-11-25 08:45:11 +00:00
committed by Gesa Stupperich
parent 957a443b23
commit 536188c1b5
7 changed files with 522 additions and 47 deletions
+44 -37
View File
@@ -33,54 +33,22 @@ func init() {
// false. The "baseURL" defaults to https://api.tailscale.com.
// The passed in tags are required, and must be non-empty. These will be
// set on the authkey generated by the OAuth2 dance.
func resolveAuthKey(ctx context.Context, v string, tags []string) (string, error) {
if !strings.HasPrefix(v, "tskey-client-") {
return v, nil
func resolveAuthKey(ctx context.Context, clientSecret string, tags []string) (string, error) {
if !strings.HasPrefix(clientSecret, "tskey-client-") {
return clientSecret, nil
}
if len(tags) == 0 {
return "", errors.New("oauth authkeys require --advertise-tags")
}
clientSecret, named, _ := strings.Cut(v, "?")
attrs, err := url.ParseQuery(named)
strippedSecret, ephemeral, preauth, baseURL, err := parseOptionalAttributes(clientSecret)
if err != nil {
return "", err
}
for k := range attrs {
switch k {
case "ephemeral", "preauthorized", "baseURL":
default:
return "", fmt.Errorf("unknown attribute %q", k)
}
}
getBool := func(name string, def bool) (bool, error) {
v := attrs.Get(name)
if v == "" {
return def, nil
}
ret, err := strconv.ParseBool(v)
if err != nil {
return false, fmt.Errorf("invalid attribute boolean attribute %s value %q", name, v)
}
return ret, nil
}
ephemeral, err := getBool("ephemeral", true)
if err != nil {
return "", err
}
preauth, err := getBool("preauthorized", false)
if err != nil {
return "", err
}
baseURL := "https://api.tailscale.com"
if v := attrs.Get("baseURL"); v != "" {
baseURL = v
}
credentials := clientcredentials.Config{
ClientID: "some-client-id", // ignored
ClientSecret: clientSecret,
ClientSecret: strippedSecret,
TokenURL: baseURL + "/api/v2/oauth/token",
}
@@ -106,3 +74,42 @@ func resolveAuthKey(ctx context.Context, v string, tags []string) (string, error
}
return authkey, nil
}
func parseOptionalAttributes(clientSecret string) (strippedSecret string, ephemeral bool, preauth bool, baseURL string, err error) {
strippedSecret, named, _ := strings.Cut(clientSecret, "?")
attrs, err := url.ParseQuery(named)
if err != nil {
return "", false, false, "", err
}
for k := range attrs {
switch k {
case "ephemeral", "preauthorized", "baseURL":
default:
return "", false, false, "", fmt.Errorf("unknown attribute %q", k)
}
}
getBool := func(name string, def bool) (bool, error) {
v := attrs.Get(name)
if v == "" {
return def, nil
}
ret, err := strconv.ParseBool(v)
if err != nil {
return false, fmt.Errorf("invalid attribute boolean attribute %s value %q", name, v)
}
return ret, nil
}
ephemeral, err = getBool("ephemeral", true)
if err != nil {
return "", false, false, "", err
}
preauth, err = getBool("preauthorized", false)
if err != nil {
return "", false, false, "", err
}
baseURL = "https://api.tailscale.com"
if v := attrs.Get("baseURL"); v != "" {
baseURL = v
}
return strippedSecret, ephemeral, preauth, baseURL, nil
}