Skip to content

Commit

Permalink
Merge pull request #763 from tsujamin/autoapprovers
Browse files Browse the repository at this point in the history
  • Loading branch information
kradalby committed Sep 23, 2022
2 parents 8fa05c1 + 7761a7b commit a507a04
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -16,6 +16,7 @@
- Fix subnet routers with Primary Routes [#811](https://github.com/juanfont/headscale/pull/811)
- Added support for JSON logs [#653](https://github.com/juanfont/headscale/issues/653)
- Add support for generating pre-auth keys with tags [#767](https://github.com/juanfont/headscale/pull/767)
- Add support for evaluating `autoApprovers` ACL entries when a machine is registered [#763](https://github.com/juanfont/headscale/pull/763)

## 0.16.4 (2022-08-21)

Expand Down
43 changes: 38 additions & 5 deletions acls_types.go
Expand Up @@ -11,11 +11,12 @@ import (

// ACLPolicy represents a Tailscale ACL Policy.
type ACLPolicy struct {
Groups Groups `json:"groups" yaml:"groups"`
Hosts Hosts `json:"hosts" yaml:"hosts"`
TagOwners TagOwners `json:"tagOwners" yaml:"tagOwners"`
ACLs []ACL `json:"acls" yaml:"acls"`
Tests []ACLTest `json:"tests" yaml:"tests"`
Groups Groups `json:"groups" yaml:"groups"`
Hosts Hosts `json:"hosts" yaml:"hosts"`
TagOwners TagOwners `json:"tagOwners" yaml:"tagOwners"`
ACLs []ACL `json:"acls" yaml:"acls"`
Tests []ACLTest `json:"tests" yaml:"tests"`
AutoApprovers AutoApprovers `json:"autoApprovers" yaml:"autoApprovers"`
}

// ACL is a basic rule for the ACL Policy.
Expand All @@ -42,6 +43,13 @@ type ACLTest struct {
Deny []string `json:"deny,omitempty" yaml:"deny,omitempty"`
}

// AutoApprovers specify which users (namespaces?), groups or tags have their advertised routes
// or exit node status automatically enabled.
type AutoApprovers struct {
Routes map[string][]string `json:"routes" yaml:"routes"`
ExitNode []string `json:"exitNode" yaml:"exitNode"`
}

// UnmarshalJSON allows to parse the Hosts directly into netip objects.
func (hosts *Hosts) UnmarshalJSON(data []byte) error {
newHosts := Hosts{}
Expand Down Expand Up @@ -100,3 +108,28 @@ func (policy ACLPolicy) IsZero() bool {

return false
}

// Returns the list of autoApproving namespaces, groups or tags for a given IPPrefix.
func (autoApprovers *AutoApprovers) GetRouteApprovers(
prefix netip.Prefix,
) ([]string, error) {
if prefix.Bits() == 0 {
return autoApprovers.ExitNode, nil // 0.0.0.0/0, ::/0 or equivalent
}

approverAliases := []string{}

for autoApprovedPrefix, autoApproverAliases := range autoApprovers.Routes {
autoApprovedPrefix, err := netip.ParsePrefix(autoApprovedPrefix)
if err != nil {
return nil, err
}

if autoApprovedPrefix.Bits() >= prefix.Bits() &&
autoApprovedPrefix.Contains(prefix.Masked().Addr()) {
approverAliases = append(approverAliases, autoApproverAliases...)
}
}

return approverAliases, nil
}
58 changes: 58 additions & 0 deletions machine.go
Expand Up @@ -949,6 +949,64 @@ func (h *Headscale) EnableRoutes(machine *Machine, routeStrs ...string) error {
return nil
}

// Enabled any routes advertised by a machine that match the ACL autoApprovers policy.
func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) {
if len(machine.IPAddresses) == 0 {
return // This machine has no IPAddresses, so can't possibly match any autoApprovers ACLs
}

approvedRoutes := make([]netip.Prefix, 0, len(machine.HostInfo.RoutableIPs))
thisMachine := []Machine{*machine}

for _, advertisedRoute := range machine.HostInfo.RoutableIPs {
if contains(machine.EnabledRoutes, advertisedRoute) {
continue // Skip routes that are already enabled for the node
}

routeApprovers, err := h.aclPolicy.AutoApprovers.GetRouteApprovers(
advertisedRoute,
)
if err != nil {
log.Err(err).
Str("advertisedRoute", advertisedRoute.String()).
Uint64("machineId", machine.ID).
Msg("Failed to resolve autoApprovers for advertised route")

return
}

for _, approvedAlias := range routeApprovers {
if approvedAlias == machine.Namespace.Name {
approvedRoutes = append(approvedRoutes, advertisedRoute)
} else {
approvedIps, err := expandAlias(thisMachine, *h.aclPolicy, approvedAlias, h.cfg.OIDC.StripEmaildomain)
if err != nil {
log.Err(err).
Str("alias", approvedAlias).
Msg("Failed to expand alias when processing autoApprovers policy")

return
}

// approvedIPs should contain all of machine's IPs if it matches the rule, so check for first
if contains(approvedIps, machine.IPAddresses[0].String()) {
approvedRoutes = append(approvedRoutes, advertisedRoute)
}
}
}
}

for _, approvedRoute := range approvedRoutes {
if !contains(machine.EnabledRoutes, approvedRoute) {
log.Info().
Str("route", approvedRoute.String()).
Uint64("client", machine.ID).
Msg("Enabling autoApproved route for client")
machine.EnabledRoutes = append(machine.EnabledRoutes, approvedRoute)
}
}
}

func (machine *Machine) RoutesToProto() *v1.Routes {
availableRoutes := machine.GetAdvertisedRoutes()

Expand Down
41 changes: 41 additions & 0 deletions machine_test.go
Expand Up @@ -1050,3 +1050,44 @@ func TestHeadscale_GenerateGivenName(t *testing.T) {
})
}
}

func (s *Suite) TestAutoApproveRoutes(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_autoapprovers.hujson")
c.Assert(err, check.IsNil)

namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)

pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil)

nodeKey := key.NewNode()

defaultRoute := netip.MustParsePrefix("0.0.0.0/0")
route1 := netip.MustParsePrefix("10.10.0.0/16")
route2 := netip.MustParsePrefix("10.11.0.0/16")

machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: NodePublicKeyStripPrefix(nodeKey.Public()),
DiscoKey: "faa",
Hostname: "test",
NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
HostInfo: HostInfo{
RequestTags: []string{"tag:exit"},
RoutableIPs: []netip.Prefix{defaultRoute, route1, route2},
},
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")},
}

app.db.Save(&machine)

machine0ByID, err := app.GetMachineByID(0)
c.Assert(err, check.IsNil)

app.EnableAutoApprovedRoutes(machine0ByID)
c.Assert(machine0ByID.GetEnabledRoutes(), check.HasLen, 3)
}
4 changes: 4 additions & 0 deletions protocol_common_poll.go
Expand Up @@ -42,7 +42,11 @@ func (h *Headscale) handlePollCommon(
Str("machine", machine.Hostname).
Err(err)
}

// update routes with peer information
h.EnableAutoApprovedRoutes(machine)
}

// From Tailscale client:
//
// ReadOnly is whether the client just wants to fetch the MapResponse,
Expand Down
24 changes: 24 additions & 0 deletions tests/acls/acl_policy_autoapprovers.hujson
@@ -0,0 +1,24 @@
// This ACL validates autoApprovers support for
// exit nodes and advertised routes

{
"tagOwners": {
"tag:exit": ["test"],
},

"groups": {
"group:test": ["test"]
},

"acls": [
{"action": "accept", "users": ["*"], "ports": ["*:*"]},
],

"autoApprovers": {
"exitNode": ["tag:exit"],
"routes": {
"10.10.0.0/16": ["group:test"],
"10.11.0.0/16": ["test"],
}
}
}

0 comments on commit a507a04

Please sign in to comment.