mirror of
https://github.com/juanfont/headscale.git
synced 2026-04-21 08:11:43 +02:00
policy/v2: implement autogroup:danger-all support
Add autogroup:danger-all as a valid source alias that matches ALL IP addresses including non-Tailscale addresses. When used as a source, it resolves to 0.0.0.0/0 + ::/0 internally but produces SrcIPs: ["*"] in filter rules. When used as a destination, it is rejected with an error matching Tailscale SaaS behavior. Key changes: - Add AutoGroupDangerAll constant and validation - Add sourcesHaveDangerAll() helper and hasDangerAll parameter to srcIPsWithRoutes() across all compilation paths - Add ErrAutogroupDangerAllDst for destination rejection - Remove 3 AUTOGROUP_DANGER_ALL skip entries (K6, K7, K8) Updates #2180
This commit is contained in:
@@ -97,13 +97,32 @@ func sourcesHaveWildcard(srcs Aliases) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sourcesHaveDangerAll returns true if any of the source aliases is
|
||||||
|
// autogroup:danger-all. When present, SrcIPs should be ["*"] to
|
||||||
|
// represent all IP addresses including non-Tailscale addresses.
|
||||||
|
func sourcesHaveDangerAll(srcs Aliases) bool {
|
||||||
|
for _, src := range srcs {
|
||||||
|
if ag, ok := src.(*AutoGroup); ok && ag.Is(AutoGroupDangerAll) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// srcIPsWithRoutes returns the SrcIPs string slice, appending
|
// srcIPsWithRoutes returns the SrcIPs string slice, appending
|
||||||
// approved subnet routes when the sources include a wildcard.
|
// approved subnet routes when the sources include a wildcard.
|
||||||
|
// When hasDangerAll is true, returns ["*"] to represent all IPs.
|
||||||
func srcIPsWithRoutes(
|
func srcIPsWithRoutes(
|
||||||
resolved ResolvedAddresses,
|
resolved ResolvedAddresses,
|
||||||
hasWildcard bool,
|
hasWildcard bool,
|
||||||
|
hasDangerAll bool,
|
||||||
nodes views.Slice[types.NodeView],
|
nodes views.Slice[types.NodeView],
|
||||||
) []string {
|
) []string {
|
||||||
|
if hasDangerAll {
|
||||||
|
return []string{"*"}
|
||||||
|
}
|
||||||
|
|
||||||
ips := resolved.Strings()
|
ips := resolved.Strings()
|
||||||
if hasWildcard {
|
if hasWildcard {
|
||||||
ips = append(ips, approvedSubnetRoutes(nodes)...)
|
ips = append(ips, approvedSubnetRoutes(nodes)...)
|
||||||
@@ -146,13 +165,14 @@ func (pol *Policy) compileFilterRules(
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasWildcard := sourcesHaveWildcard(grant.Sources)
|
hasWildcard := sourcesHaveWildcard(grant.Sources)
|
||||||
|
hasDangerAll := sourcesHaveDangerAll(grant.Sources)
|
||||||
|
|
||||||
for _, ipp := range grant.InternetProtocols {
|
for _, ipp := range grant.InternetProtocols {
|
||||||
destPorts := pol.destinationsToNetPortRange(users, nodes, grant.Destinations, ipp.Ports)
|
destPorts := pol.destinationsToNetPortRange(users, nodes, grant.Destinations, ipp.Ports)
|
||||||
|
|
||||||
if len(destPorts) > 0 {
|
if len(destPorts) > 0 {
|
||||||
rules = append(rules, tailcfg.FilterRule{
|
rules = append(rules, tailcfg.FilterRule{
|
||||||
SrcIPs: srcIPsWithRoutes(srcIPs, hasWildcard, nodes),
|
SrcIPs: srcIPsWithRoutes(srcIPs, hasWildcard, hasDangerAll, nodes),
|
||||||
DstPorts: destPorts,
|
DstPorts: destPorts,
|
||||||
IPProto: ipp.Protocol.toIANAProtocolNumbers(),
|
IPProto: ipp.Protocol.toIANAProtocolNumbers(),
|
||||||
})
|
})
|
||||||
@@ -180,7 +200,7 @@ func (pol *Policy) compileFilterRules(
|
|||||||
dstIPStrings = append(dstIPStrings, ips.Strings()...)
|
dstIPStrings = append(dstIPStrings, ips.Strings()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
srcIPStrs := srcIPsWithRoutes(srcIPs, hasWildcard, nodes)
|
srcIPStrs := srcIPsWithRoutes(srcIPs, hasWildcard, hasDangerAll, nodes)
|
||||||
rules = append(rules, tailcfg.FilterRule{
|
rules = append(rules, tailcfg.FilterRule{
|
||||||
SrcIPs: srcIPStrs,
|
SrcIPs: srcIPStrs,
|
||||||
CapGrant: capGrants,
|
CapGrant: capGrants,
|
||||||
@@ -390,7 +410,8 @@ func (pol *Policy) compileViaGrant(
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasWildcard := sourcesHaveWildcard(grant.Sources)
|
hasWildcard := sourcesHaveWildcard(grant.Sources)
|
||||||
srcIPStrs := srcIPsWithRoutes(srcResolved, hasWildcard, nodes)
|
hasDangerAll := sourcesHaveDangerAll(grant.Sources)
|
||||||
|
srcIPStrs := srcIPsWithRoutes(srcResolved, hasWildcard, hasDangerAll, nodes)
|
||||||
|
|
||||||
// Build DstPorts from the matching via prefixes.
|
// Build DstPorts from the matching via prefixes.
|
||||||
var rules []tailcfg.FilterRule
|
var rules []tailcfg.FilterRule
|
||||||
@@ -491,6 +512,7 @@ func (pol *Policy) compileGrantWithAutogroupSelf(
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasWildcard := sourcesHaveWildcard(grant.Sources)
|
hasWildcard := sourcesHaveWildcard(grant.Sources)
|
||||||
|
hasDangerAll := sourcesHaveDangerAll(grant.Sources)
|
||||||
|
|
||||||
for _, ipp := range grant.InternetProtocols {
|
for _, ipp := range grant.InternetProtocols {
|
||||||
// Handle non-self destinations first to match Tailscale's
|
// Handle non-self destinations first to match Tailscale's
|
||||||
@@ -513,7 +535,7 @@ func (pol *Policy) compileGrantWithAutogroupSelf(
|
|||||||
destPorts := pol.destinationsToNetPortRange(users, nodes, otherDests, ipp.Ports)
|
destPorts := pol.destinationsToNetPortRange(users, nodes, otherDests, ipp.Ports)
|
||||||
|
|
||||||
if len(destPorts) > 0 {
|
if len(destPorts) > 0 {
|
||||||
srcIPStrs := srcIPsWithRoutes(srcResolved, hasWildcard, nodes)
|
srcIPStrs := srcIPsWithRoutes(srcResolved, hasWildcard, hasDangerAll, nodes)
|
||||||
|
|
||||||
// When sources include a wildcard (*) alongside
|
// When sources include a wildcard (*) alongside
|
||||||
// explicit sources (tags, groups, etc.), Tailscale
|
// explicit sources (tags, groups, etc.), Tailscale
|
||||||
@@ -625,7 +647,7 @@ func (pol *Policy) compileGrantWithAutogroupSelf(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !srcResolved.Empty() {
|
if !srcResolved.Empty() {
|
||||||
srcIPStrs = srcIPsWithRoutes(srcResolved, hasWildcard, nodes)
|
srcIPStrs = srcIPsWithRoutes(srcResolved, hasWildcard, hasDangerAll, nodes)
|
||||||
|
|
||||||
if hasWildcard && len(nonWildcardSrcs) > 0 {
|
if hasWildcard && len(nonWildcardSrcs) > 0 {
|
||||||
seen := make(map[string]bool, len(srcIPStrs))
|
seen := make(map[string]bool, len(srcIPStrs))
|
||||||
|
|||||||
@@ -214,10 +214,9 @@ func loadGrantTestFile(t *testing.T, path string) grantTestFile {
|
|||||||
//
|
//
|
||||||
// Impact summary (highest first):
|
// Impact summary (highest first):
|
||||||
//
|
//
|
||||||
// AUTOGROUP_DANGER_ALL - 3 tests: Implement autogroup:danger-all support
|
|
||||||
// USER_PASSKEY_WILDCARD - 2 tests: user:*@passkey wildcard pattern unresolvable
|
// USER_PASSKEY_WILDCARD - 2 tests: user:*@passkey wildcard pattern unresolvable
|
||||||
//
|
//
|
||||||
// Total: 5 tests skipped, ~232 tests expected to pass.
|
// Total: 2 tests skipped, ~235 tests expected to pass.
|
||||||
var grantSkipReasons = map[string]string{
|
var grantSkipReasons = map[string]string{
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// USER_PASSKEY_WILDCARD (2 tests)
|
// USER_PASSKEY_WILDCARD (2 tests)
|
||||||
@@ -235,23 +234,6 @@ var grantSkipReasons = map[string]string{
|
|||||||
// ========================================================================
|
// ========================================================================
|
||||||
"GRANT-K20": "USER_PASSKEY_WILDCARD: src=user:*@passkey, dst=tag:server — source can't be resolved, no rules produced",
|
"GRANT-K20": "USER_PASSKEY_WILDCARD: src=user:*@passkey, dst=tag:server — source can't be resolved, no rules produced",
|
||||||
"GRANT-K21": "USER_PASSKEY_WILDCARD: src=*, dst=user:*@passkey — destination can't be resolved, no rules produced",
|
"GRANT-K21": "USER_PASSKEY_WILDCARD: src=*, dst=user:*@passkey — destination can't be resolved, no rules produced",
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// AUTOGROUP_DANGER_ALL (3 tests)
|
|
||||||
//
|
|
||||||
// TODO: Implement autogroup:danger-all support.
|
|
||||||
//
|
|
||||||
// autogroup:danger-all matches ALL IPs including non-Tailscale addresses.
|
|
||||||
// When used as a source, it should expand to 0.0.0.0/0 and ::/0.
|
|
||||||
// When used as a destination, Tailscale rejects it with an error.
|
|
||||||
//
|
|
||||||
// GRANT-K6: autogroup:danger-all as src (success test, produces rules)
|
|
||||||
// GRANT-K7: autogroup:danger-all as dst (error: "cannot use autogroup:danger-all as a dst")
|
|
||||||
// GRANT-K8: autogroup:danger-all as both src and dst (error: same message)
|
|
||||||
// ========================================================================
|
|
||||||
"GRANT-K6": "AUTOGROUP_DANGER_ALL",
|
|
||||||
"GRANT-K7": "AUTOGROUP_DANGER_ALL",
|
|
||||||
"GRANT-K8": "AUTOGROUP_DANGER_ALL",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestGrantsCompat is a data-driven test that loads all 237 GRANT-*.json
|
// TestGrantsCompat is a data-driven test that loads all 237 GRANT-*.json
|
||||||
@@ -269,10 +251,9 @@ var grantSkipReasons = map[string]string{
|
|||||||
//
|
//
|
||||||
// Skip category impact summary (highest first):
|
// Skip category impact summary (highest first):
|
||||||
//
|
//
|
||||||
// AUTOGROUP_DANGER_ALL - 3 tests: Implement autogroup:danger-all support
|
|
||||||
// USER_PASSKEY_WILDCARD - 2 tests: user:*@passkey wildcard pattern unresolvable
|
// USER_PASSKEY_WILDCARD - 2 tests: user:*@passkey wildcard pattern unresolvable
|
||||||
//
|
//
|
||||||
// Total: 5 tests skipped, ~232 tests expected to pass.
|
// Total: 2 tests skipped, ~235 tests expected to pass.
|
||||||
func TestGrantsCompat(t *testing.T) {
|
func TestGrantsCompat(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ var (
|
|||||||
ErrAutogroupSelfSrc = errors.New("\"autogroup:self\" not valid on the src side of a rule")
|
ErrAutogroupSelfSrc = errors.New("\"autogroup:self\" not valid on the src side of a rule")
|
||||||
ErrAutogroupNotSupportedACLSrc = errors.New("autogroup not supported for ACL sources")
|
ErrAutogroupNotSupportedACLSrc = errors.New("autogroup not supported for ACL sources")
|
||||||
ErrAutogroupNotSupportedACLDst = errors.New("autogroup not supported for ACL destinations")
|
ErrAutogroupNotSupportedACLDst = errors.New("autogroup not supported for ACL destinations")
|
||||||
|
ErrAutogroupDangerAllDst = errors.New("cannot use autogroup:danger-all as a dst")
|
||||||
ErrAutogroupNotSupportedSSHSrc = errors.New("autogroup not supported for SSH sources")
|
ErrAutogroupNotSupportedSSHSrc = errors.New("autogroup not supported for SSH sources")
|
||||||
ErrAutogroupNotSupportedSSHDst = errors.New("autogroup not supported for SSH destinations")
|
ErrAutogroupNotSupportedSSHDst = errors.New("autogroup not supported for SSH destinations")
|
||||||
ErrAutogroupNotSupportedSSHUsr = errors.New("autogroup not supported for SSH user")
|
ErrAutogroupNotSupportedSSHUsr = errors.New("autogroup not supported for SSH user")
|
||||||
@@ -701,11 +702,12 @@ func (p *Prefix) resolve(_ *Policy, _ types.Users, _ views.Slice[types.NodeView]
|
|||||||
type AutoGroup string
|
type AutoGroup string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AutoGroupInternet AutoGroup = "autogroup:internet"
|
AutoGroupInternet AutoGroup = "autogroup:internet"
|
||||||
AutoGroupMember AutoGroup = "autogroup:member"
|
AutoGroupMember AutoGroup = "autogroup:member"
|
||||||
AutoGroupNonRoot AutoGroup = "autogroup:nonroot"
|
AutoGroupNonRoot AutoGroup = "autogroup:nonroot"
|
||||||
AutoGroupTagged AutoGroup = "autogroup:tagged"
|
AutoGroupTagged AutoGroup = "autogroup:tagged"
|
||||||
AutoGroupSelf AutoGroup = "autogroup:self"
|
AutoGroupSelf AutoGroup = "autogroup:self"
|
||||||
|
AutoGroupDangerAll AutoGroup = "autogroup:danger-all"
|
||||||
)
|
)
|
||||||
|
|
||||||
var autogroups = []AutoGroup{
|
var autogroups = []AutoGroup{
|
||||||
@@ -714,6 +716,7 @@ var autogroups = []AutoGroup{
|
|||||||
AutoGroupNonRoot,
|
AutoGroupNonRoot,
|
||||||
AutoGroupTagged,
|
AutoGroupTagged,
|
||||||
AutoGroupSelf,
|
AutoGroupSelf,
|
||||||
|
AutoGroupDangerAll,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ag *AutoGroup) Validate() error {
|
func (ag *AutoGroup) Validate() error {
|
||||||
@@ -786,6 +789,15 @@ func (ag *AutoGroup) resolve(p *Policy, users types.Users, nodes views.Slice[typ
|
|||||||
// specially during policy compilation per-node for security.
|
// specially during policy compilation per-node for security.
|
||||||
return nil, ErrAutogroupSelfRequiresPerNodeResolution
|
return nil, ErrAutogroupSelfRequiresPerNodeResolution
|
||||||
|
|
||||||
|
case AutoGroupDangerAll:
|
||||||
|
// autogroup:danger-all matches ALL IP addresses, including
|
||||||
|
// non-Tailscale addresses. Resolves to 0.0.0.0/0 + ::/0.
|
||||||
|
// Filter compilation converts this to SrcIPs: ["*"].
|
||||||
|
build.AddPrefix(netip.MustParsePrefix("0.0.0.0/0"))
|
||||||
|
build.AddPrefix(netip.MustParsePrefix("::/0"))
|
||||||
|
|
||||||
|
return build.IPSet()
|
||||||
|
|
||||||
case AutoGroupNonRoot:
|
case AutoGroupNonRoot:
|
||||||
// autogroup:nonroot represents non-root users on multi-user devices.
|
// autogroup:nonroot represents non-root users on multi-user devices.
|
||||||
// This is not supported in headscale and requires OS-level user detection.
|
// This is not supported in headscale and requires OS-level user detection.
|
||||||
@@ -1986,7 +1998,7 @@ type Policy struct {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// TODO(kradalby): Add these checks for tagOwners and autoApprovers.
|
// TODO(kradalby): Add these checks for tagOwners and autoApprovers.
|
||||||
autogroupForSrc = []AutoGroup{AutoGroupMember, AutoGroupTagged}
|
autogroupForSrc = []AutoGroup{AutoGroupMember, AutoGroupTagged, AutoGroupDangerAll}
|
||||||
autogroupForDst = []AutoGroup{AutoGroupInternet, AutoGroupMember, AutoGroupTagged, AutoGroupSelf}
|
autogroupForDst = []AutoGroup{AutoGroupInternet, AutoGroupMember, AutoGroupTagged, AutoGroupSelf}
|
||||||
autogroupForSSHSrc = []AutoGroup{AutoGroupMember, AutoGroupTagged}
|
autogroupForSSHSrc = []AutoGroup{AutoGroupMember, AutoGroupTagged}
|
||||||
autogroupForSSHDst = []AutoGroup{AutoGroupMember, AutoGroupTagged, AutoGroupSelf}
|
autogroupForSSHDst = []AutoGroup{AutoGroupMember, AutoGroupTagged, AutoGroupSelf}
|
||||||
@@ -2033,6 +2045,10 @@ func validateAutogroupForDst(dst *AutoGroup) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dst.Is(AutoGroupDangerAll) {
|
||||||
|
return ErrAutogroupDangerAllDst
|
||||||
|
}
|
||||||
|
|
||||||
if !slices.Contains(autogroupForDst, *dst) {
|
if !slices.Contains(autogroupForDst, *dst) {
|
||||||
return fmt.Errorf("%w: %q, can be %v", ErrAutogroupNotSupportedACLDst, *dst, autogroupForDst)
|
return fmt.Errorf("%w: %q, can be %v", ErrAutogroupNotSupportedACLDst, *dst, autogroupForDst)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -459,7 +459,7 @@ func TestUnmarshalPolicy(t *testing.T) {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
wantErr: `invalid autogroup: got "autogroup:invalid", must be one of [autogroup:internet autogroup:member autogroup:nonroot autogroup:tagged autogroup:self]`,
|
wantErr: `invalid autogroup: got "autogroup:invalid", must be one of [autogroup:internet autogroup:member autogroup:nonroot autogroup:tagged autogroup:self autogroup:danger-all]`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "undefined-hostname-errors-2490",
|
name: "undefined-hostname-errors-2490",
|
||||||
|
|||||||
Reference in New Issue
Block a user