cmd/tailscale/cli: allow fetching keys from AWS Parameter Store

This allows fetching auth keys, OAuth client secrets, and ID tokens (for
workload identity federation) from AWS Parameter Store by passing an ARN
as the value. This is a relatively low-overhead mechanism for fetching
these values from an external secret store without needing to run a
secret service.

Usage examples:

    # Auth key
    tailscale up \
      --auth-key=arn:aws:ssm:us-east-1:123456789012:parameter/tailscale/auth-key

    # OAuth client secret
    tailscale up \
      --client-secret=arn:aws:ssm:us-east-1:123456789012:parameter/tailscale/oauth-secret \
      --advertise-tags=tag:server

    # ID token (for workload identity federation)
    tailscale up \
      --client-id=my-client \
      --id-token=arn:aws:ssm:us-east-1:123456789012:parameter/tailscale/id-token \
      --advertise-tags=tag:server

Updates tailscale/corp#28792

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
This commit is contained in:
Andrew Dunham
2026-01-14 02:29:06 -05:00
committed by Andrew Dunham
parent 65d6793204
commit bcceef3682
9 changed files with 327 additions and 12 deletions
+76
View File
@@ -6,11 +6,14 @@ package cli
import (
"bytes"
stdcmp "cmp"
"context"
"encoding/json"
"flag"
"fmt"
"io"
"net/netip"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
@@ -20,6 +23,7 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/envknob"
"tailscale.com/health/healthmsg"
"tailscale.com/internal/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
@@ -1696,6 +1700,78 @@ func TestDocs(t *testing.T) {
walk(t, root)
}
func TestUpResolves(t *testing.T) {
const testARN = "arn:aws:ssm:us-east-1:123456789012:parameter/my-parameter"
undo := tailscale.HookResolveValueFromParameterStore.SetForTest(func(_ context.Context, valueOrARN string) (string, error) {
if valueOrARN == testARN {
return "resolved-value", nil
}
return valueOrARN, nil
})
defer undo()
const content = "file-content"
fpath := filepath.Join(t.TempDir(), "testfile")
if err := os.WriteFile(fpath, []byte(content), 0600); err != nil {
t.Fatal(err)
}
testCases := []struct {
name string
arg string
want string
}{
{"parameter_store", testARN, "resolved-value"},
{"file", "file:" + fpath, "file-content"},
}
for _, tt := range testCases {
t.Run(tt.name+"_auth_key", func(t *testing.T) {
args := upArgsT{authKeyOrFile: tt.arg}
got, err := args.getAuthKey(t.Context())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
t.Run(tt.name+"_client_secret", func(t *testing.T) {
args := upArgsT{clientSecretOrFile: tt.arg}
got, err := args.getClientSecret(t.Context())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
t.Run(tt.name+"_id_token", func(t *testing.T) {
args := upArgsT{idTokenOrFile: tt.arg}
got, err := args.getIDToken(t.Context())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
t.Run("passthrough", func(t *testing.T) {
args := upArgsT{authKeyOrFile: "tskey-abcd1234"}
got, err := args.getAuthKey(t.Context())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != "tskey-abcd1234" {
t.Errorf("got %q, want %q", got, "tskey-abcd1234")
}
})
}
func TestDeps(t *testing.T) {
deptest.DepChecker{
GOOS: "linux",