diff --git a/hscontrol/policy/v2/types.go b/hscontrol/policy/v2/types.go index 4e248323..50a89f8c 100644 --- a/hscontrol/policy/v2/types.go +++ b/hscontrol/policy/v2/types.go @@ -2271,6 +2271,10 @@ var tailscaleCapAllowlist = map[tailcfg.PeerCapability]bool{ tailcfg.PeerCapabilityWebUI: true, // tailscale.com/cap/webui tailcfg.PeerCapabilityKubernetes: true, // tailscale.com/cap/kubernetes tailcfg.PeerCapabilityTsIDP: true, // tailscale.com/cap/tsidp + + // tailscale.com/cap/secrets is the capability used by setec + // (github.com/tailscale/setec); allow it so it can be granted via policy. + tailcfg.PeerCapability("tailscale.com/cap/secrets"): true, } // validateGrantSrcDstCombination validates [Grant]-specific source/destination diff --git a/hscontrol/policy/v2/types_test.go b/hscontrol/policy/v2/types_test.go index 1a046f12..19134e5b 100644 --- a/hscontrol/policy/v2/types_test.go +++ b/hscontrol/policy/v2/types_test.go @@ -6244,3 +6244,27 @@ func TestUnmarshalPolicySSHTests(t *testing.T) { }) } } + +func TestValidateCapabilityName(t *testing.T) { + tests := []struct { + name string + cap string + wantErr error + }{ + {name: "custom domain allowed", cap: "example.com/cap/foo", wantErr: nil}, + {name: "allowlisted tailscale cap", cap: "tailscale.com/cap/drive", wantErr: nil}, + {name: "setec secrets cap allowed", cap: "tailscale.com/cap/secrets", wantErr: nil}, + {name: "non-allowlisted tailscale cap rejected", cap: "tailscale.com/cap/nope", wantErr: ErrCapNameTailscaleDomain}, + {name: "url scheme rejected", cap: "https://tailscale.com/cap/drive", wantErr: ErrCapNameInvalidForm}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateCapabilityName(tt.cap) + if tt.wantErr == nil { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, tt.wantErr) + } + }) + } +}