Merge pull request #763 from tsujamin/autoapprovers
This commit is contained in:
commit
a507a04650
6 changed files with 166 additions and 5 deletions
|
@ -16,6 +16,7 @@
|
||||||
- Fix subnet routers with Primary Routes [#811](https://github.com/juanfont/headscale/pull/811)
|
- 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)
|
- 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 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)
|
## 0.16.4 (2022-08-21)
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ type ACLPolicy struct {
|
||||||
TagOwners TagOwners `json:"tagOwners" yaml:"tagOwners"`
|
TagOwners TagOwners `json:"tagOwners" yaml:"tagOwners"`
|
||||||
ACLs []ACL `json:"acls" yaml:"acls"`
|
ACLs []ACL `json:"acls" yaml:"acls"`
|
||||||
Tests []ACLTest `json:"tests" yaml:"tests"`
|
Tests []ACLTest `json:"tests" yaml:"tests"`
|
||||||
|
AutoApprovers AutoApprovers `json:"autoApprovers" yaml:"autoApprovers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ACL is a basic rule for the ACL Policy.
|
// ACL is a basic rule for the ACL Policy.
|
||||||
|
@ -42,6 +43,13 @@ type ACLTest struct {
|
||||||
Deny []string `json:"deny,omitempty" yaml:"deny,omitempty"`
|
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.
|
// UnmarshalJSON allows to parse the Hosts directly into netip objects.
|
||||||
func (hosts *Hosts) UnmarshalJSON(data []byte) error {
|
func (hosts *Hosts) UnmarshalJSON(data []byte) error {
|
||||||
newHosts := Hosts{}
|
newHosts := Hosts{}
|
||||||
|
@ -100,3 +108,28 @@ func (policy ACLPolicy) IsZero() bool {
|
||||||
|
|
||||||
return false
|
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
machine.go
58
machine.go
|
@ -949,6 +949,64 @@ func (h *Headscale) EnableRoutes(machine *Machine, routeStrs ...string) error {
|
||||||
return nil
|
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 {
|
func (machine *Machine) RoutesToProto() *v1.Routes {
|
||||||
availableRoutes := machine.GetAdvertisedRoutes()
|
availableRoutes := machine.GetAdvertisedRoutes()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -42,7 +42,11 @@ func (h *Headscale) handlePollCommon(
|
||||||
Str("machine", machine.Hostname).
|
Str("machine", machine.Hostname).
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update routes with peer information
|
||||||
|
h.EnableAutoApprovedRoutes(machine)
|
||||||
}
|
}
|
||||||
|
|
||||||
// From Tailscale client:
|
// From Tailscale client:
|
||||||
//
|
//
|
||||||
// ReadOnly is whether the client just wants to fetch the MapResponse,
|
// ReadOnly is whether the client just wants to fetch the MapResponse,
|
||||||
|
|
24
tests/acls/acl_policy_autoapprovers.hujson
Normal file
24
tests/acls/acl_policy_autoapprovers.hujson
Normal file
|
@ -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"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue