Merge branch 'main' into sanitise-machine-key-url

This commit is contained in:
Kristoffer Dalby 2022-09-23 11:09:23 +02:00 committed by GitHub
commit d06ba7b522
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 741 additions and 309 deletions

View file

@ -16,6 +16,8 @@
- 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)
- Sanitise the node key passed to registration url [#823](https://github.com/juanfont/headscale/pull/823) - Sanitise the node key passed to registration url [#823](https://github.com/juanfont/headscale/pull/823)
- 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)

View file

@ -114,7 +114,7 @@ func (s *Suite) TestValidExpandTagOwnersInSources(c *check.C) {
namespace, err := app.CreateNamespace("user1") namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("user1", "testmachine") _, err = app.GetMachine("user1", "testmachine")
@ -164,7 +164,7 @@ func (s *Suite) TestValidExpandTagOwnersInDestinations(c *check.C) {
namespace, err := app.CreateNamespace("user1") namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("user1", "testmachine") _, err = app.GetMachine("user1", "testmachine")
@ -214,7 +214,7 @@ func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
namespace, err := app.CreateNamespace("user1") namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("user1", "testmachine") _, err = app.GetMachine("user1", "testmachine")
@ -263,7 +263,7 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
namespace, err := app.CreateNamespace("user1") namespace, err := app.CreateNamespace("user1")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("user1", "webserver") _, err = app.GetMachine("user1", "webserver")
@ -395,7 +395,7 @@ func (s *Suite) TestPortNamespace(c *check.C) {
namespace, err := app.CreateNamespace("testnamespace") namespace, err := app.CreateNamespace("testnamespace")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("testnamespace", "testmachine") _, err = app.GetMachine("testnamespace", "testmachine")
@ -437,7 +437,7 @@ func (s *Suite) TestPortGroup(c *check.C) {
namespace, err := app.CreateNamespace("testnamespace") namespace, err := app.CreateNamespace("testnamespace")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("testnamespace", "testmachine") _, err = app.GetMachine("testnamespace", "testmachine")

View file

@ -11,11 +11,12 @@ import (
// ACLPolicy represents a Tailscale ACL Policy. // ACLPolicy represents a Tailscale ACL Policy.
type ACLPolicy struct { type ACLPolicy struct {
Groups Groups `json:"groups" yaml:"groups"` Groups Groups `json:"groups" yaml:"groups"`
Hosts Hosts `json:"hosts" yaml:"hosts"` Hosts Hosts `json:"hosts" yaml:"hosts"`
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
}

View file

@ -3,6 +3,7 @@ package cli
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
"time" "time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
@ -33,6 +34,8 @@ func init() {
Bool("ephemeral", false, "Preauthkey for ephemeral nodes") Bool("ephemeral", false, "Preauthkey for ephemeral nodes")
createPreAuthKeyCmd.Flags(). createPreAuthKeyCmd.Flags().
StringP("expiration", "e", DefaultPreAuthKeyExpiry, "Human-readable expiration of the key (e.g. 30m, 24h)") StringP("expiration", "e", DefaultPreAuthKeyExpiry, "Human-readable expiration of the key (e.g. 30m, 24h)")
createPreAuthKeyCmd.Flags().
StringSlice("tags", []string{}, "Tags to automatically assign to node")
} }
var preauthkeysCmd = &cobra.Command{ var preauthkeysCmd = &cobra.Command{
@ -81,7 +84,16 @@ var listPreAuthKeys = &cobra.Command{
} }
tableData := pterm.TableData{ tableData := pterm.TableData{
{"ID", "Key", "Reusable", "Ephemeral", "Used", "Expiration", "Created"}, {
"ID",
"Key",
"Reusable",
"Ephemeral",
"Used",
"Expiration",
"Created",
"Tags",
},
} }
for _, key := range response.PreAuthKeys { for _, key := range response.PreAuthKeys {
expiration := "-" expiration := "-"
@ -96,6 +108,14 @@ var listPreAuthKeys = &cobra.Command{
reusable = fmt.Sprintf("%v", key.GetReusable()) reusable = fmt.Sprintf("%v", key.GetReusable())
} }
aclTags := ""
for _, tag := range key.AclTags {
aclTags += "," + tag
}
aclTags = strings.TrimLeft(aclTags, ",")
tableData = append(tableData, []string{ tableData = append(tableData, []string{
key.GetId(), key.GetId(),
key.GetKey(), key.GetKey(),
@ -104,6 +124,7 @@ var listPreAuthKeys = &cobra.Command{
strconv.FormatBool(key.GetUsed()), strconv.FormatBool(key.GetUsed()),
expiration, expiration,
key.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"), key.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"),
aclTags,
}) })
} }
@ -136,6 +157,7 @@ var createPreAuthKeyCmd = &cobra.Command{
reusable, _ := cmd.Flags().GetBool("reusable") reusable, _ := cmd.Flags().GetBool("reusable")
ephemeral, _ := cmd.Flags().GetBool("ephemeral") ephemeral, _ := cmd.Flags().GetBool("ephemeral")
tags, _ := cmd.Flags().GetStringSlice("tags")
log.Trace(). log.Trace().
Bool("reusable", reusable). Bool("reusable", reusable).
@ -147,6 +169,7 @@ var createPreAuthKeyCmd = &cobra.Command{
Namespace: namespace, Namespace: namespace,
Reusable: reusable, Reusable: reusable,
Ephemeral: ephemeral, Ephemeral: ephemeral,
AclTags: tags,
} }
durationStr, _ := cmd.Flags().GetString("expiration") durationStr, _ := cmd.Flags().GetString("expiration")

5
db.go
View file

@ -131,6 +131,11 @@ func (h *Headscale) initDB() error {
return err return err
} }
err = db.AutoMigrate(&PreAuthKeyACLTag{})
if err != nil {
return err
}
_ = db.Migrator().DropTable("shared_machines") _ = db.Migrator().DropTable("shared_machines")
err = db.AutoMigrate(&APIKey{}) err = db.AutoMigrate(&APIKey{})

View file

@ -126,6 +126,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -134,6 +135,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -142,6 +144,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -150,6 +153,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -269,6 +273,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -277,6 +282,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -285,6 +291,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -293,6 +300,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.27.1 // protoc-gen-go v1.28.1
// protoc (unknown) // protoc (unknown)
// source: headscale/v1/apikey.proto // source: headscale/v1/apikey.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.27.1 // protoc-gen-go v1.28.1
// protoc (unknown) // protoc (unknown)
// source: headscale/v1/device.proto // source: headscale/v1/device.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.27.1 // protoc-gen-go v1.28.1
// protoc (unknown) // protoc (unknown)
// source: headscale/v1/headscale.proto // source: headscale/v1/headscale.proto

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,8 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc (unknown)
// source: headscale/v1/headscale.proto
package v1 package v1

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.27.1 // protoc-gen-go v1.28.1
// protoc (unknown) // protoc (unknown)
// source: headscale/v1/machine.proto // source: headscale/v1/machine.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.27.1 // protoc-gen-go v1.28.1
// protoc (unknown) // protoc (unknown)
// source: headscale/v1/namespace.proto // source: headscale/v1/namespace.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.27.1 // protoc-gen-go v1.28.1
// protoc (unknown) // protoc (unknown)
// source: headscale/v1/preauthkey.proto // source: headscale/v1/preauthkey.proto
@ -34,6 +34,7 @@ type PreAuthKey struct {
Used bool `protobuf:"varint,6,opt,name=used,proto3" json:"used,omitempty"` Used bool `protobuf:"varint,6,opt,name=used,proto3" json:"used,omitempty"`
Expiration *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=expiration,proto3" json:"expiration,omitempty"` Expiration *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=expiration,proto3" json:"expiration,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` CreatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
AclTags []string `protobuf:"bytes,9,rep,name=acl_tags,json=aclTags,proto3" json:"acl_tags,omitempty"`
} }
func (x *PreAuthKey) Reset() { func (x *PreAuthKey) Reset() {
@ -124,6 +125,13 @@ func (x *PreAuthKey) GetCreatedAt() *timestamppb.Timestamp {
return nil return nil
} }
func (x *PreAuthKey) GetAclTags() []string {
if x != nil {
return x.AclTags
}
return nil
}
type CreatePreAuthKeyRequest struct { type CreatePreAuthKeyRequest struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@ -133,6 +141,7 @@ type CreatePreAuthKeyRequest struct {
Reusable bool `protobuf:"varint,2,opt,name=reusable,proto3" json:"reusable,omitempty"` Reusable bool `protobuf:"varint,2,opt,name=reusable,proto3" json:"reusable,omitempty"`
Ephemeral bool `protobuf:"varint,3,opt,name=ephemeral,proto3" json:"ephemeral,omitempty"` Ephemeral bool `protobuf:"varint,3,opt,name=ephemeral,proto3" json:"ephemeral,omitempty"`
Expiration *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expiration,proto3" json:"expiration,omitempty"` Expiration *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expiration,proto3" json:"expiration,omitempty"`
AclTags []string `protobuf:"bytes,5,rep,name=acl_tags,json=aclTags,proto3" json:"acl_tags,omitempty"`
} }
func (x *CreatePreAuthKeyRequest) Reset() { func (x *CreatePreAuthKeyRequest) Reset() {
@ -195,6 +204,13 @@ func (x *CreatePreAuthKeyRequest) GetExpiration() *timestamppb.Timestamp {
return nil return nil
} }
func (x *CreatePreAuthKeyRequest) GetAclTags() []string {
if x != nil {
return x.AclTags
}
return nil
}
type CreatePreAuthKeyResponse struct { type CreatePreAuthKeyResponse struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@ -436,7 +452,7 @@ var file_headscale_v1_preauthkey_proto_rawDesc = []byte{
0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
0x0c, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x0c, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74,
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x91, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xac,
0x02, 0x0a, 0x0a, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x02, 0x0a, 0x0a, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a,
0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69,
@ -454,42 +470,45 @@ var file_headscale_v1_preauthkey_proto_rawDesc = []byte{
0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69,
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,
0x41, 0x74, 0x22, 0xad, 0x01, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65, 0x41, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x63, 0x6c, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x09,
0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x6c, 0x54, 0x61, 0x67, 0x73, 0x22, 0xc8, 0x01,
0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b,
0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d,
0x72, 0x65, 0x75, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61,
0x72, 0x65, 0x75, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x75, 0x73, 0x61,
0x6d, 0x65, 0x72, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x65, 0x70, 0x68, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x75, 0x73, 0x61,
0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x12, 0x3a, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c,
0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x6c, 0x12, 0x3a, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18,
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x6f, 0x6e, 0x22, 0x56, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65, 0x41, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x70, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a,
0x0a, 0x0c, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x08, 0x61, 0x63, 0x6c, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x07, 0x61, 0x63, 0x6c, 0x54, 0x61, 0x67, 0x73, 0x22, 0x56, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61,
0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x0a, 0x74, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70,
0x70, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x22, 0x49, 0x0a, 0x17, 0x45, 0x78, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68,
0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61,
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74,
0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79,
0x61, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x22, 0x49, 0x0a, 0x17, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74,
0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x1a, 0x0a, 0x18, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e,
0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x22, 0x36, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68,
0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e,
0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x57, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79,
0x74, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x1a, 0x0a, 0x18, 0x45,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52,
0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x36, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x50,
0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x0b, 0x70, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01,
0x79, 0x73, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22,
0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x57, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65,
0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72,
0x72, 0x6f, 0x74, 0x6f, 0x33, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31,
0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x0b, 0x70, 0x72, 0x65,
0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f,
0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f,
0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.27.1 // protoc-gen-go v1.28.1
// protoc (unknown) // protoc (unknown)
// source: headscale/v1/routes.proto // source: headscale/v1/routes.proto

View file

@ -824,6 +824,12 @@
"expiration": { "expiration": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
},
"aclTags": {
"type": "array",
"items": {
"type": "string"
}
} }
} }
}, },
@ -1102,6 +1108,12 @@
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
},
"aclTags": {
"type": "array",
"items": {
"type": "string"
}
} }
} }
}, },

View file

@ -106,11 +106,21 @@ func (api headscaleV1APIServer) CreatePreAuthKey(
expiration = request.GetExpiration().AsTime() expiration = request.GetExpiration().AsTime()
} }
for _, tag := range request.AclTags {
err := validateTag(tag)
if err != nil {
return &v1.CreatePreAuthKeyResponse{
PreAuthKey: nil,
}, status.Error(codes.InvalidArgument, err.Error())
}
}
preAuthKey, err := api.h.CreatePreAuthKey( preAuthKey, err := api.h.CreatePreAuthKey(
request.GetNamespace(), request.GetNamespace(),
request.GetReusable(), request.GetReusable(),
request.GetEphemeral(), request.GetEphemeral(),
&expiration, &expiration,
request.AclTags,
) )
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -260,6 +260,8 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() {
"24h", "24h",
"--output", "--output",
"json", "json",
"--tags",
"tag:test1,tag:test2",
}, },
[]string{}, []string{},
) )
@ -333,6 +335,11 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() {
listedPreAuthKeys[4].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), listedPreAuthKeys[4].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
) )
// Test that tags are present
for i := 0; i < count; i++ {
assert.Equal(s.T(), listedPreAuthKeys[i].AclTags, []string{"tag:test1", "tag:test2"})
}
// Expire three keys // Expire three keys
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
_, _, err := ExecuteCommand( _, _, err := ExecuteCommand(

View file

@ -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()

View file

@ -18,7 +18,7 @@ func (s *Suite) TestGetMachine(c *check.C) {
namespace, err := app.CreateNamespace("test") namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("test", "testmachine") _, err = app.GetMachine("test", "testmachine")
@ -44,7 +44,7 @@ func (s *Suite) TestGetMachineByID(c *check.C) {
namespace, err := app.CreateNamespace("test") namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachineByID(0) _, err = app.GetMachineByID(0)
@ -70,7 +70,7 @@ func (s *Suite) TestGetMachineByNodeKey(c *check.C) {
namespace, err := app.CreateNamespace("test") namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachineByID(0) _, err = app.GetMachineByID(0)
@ -98,7 +98,7 @@ func (s *Suite) TestGetMachineByAnyNodeKey(c *check.C) {
namespace, err := app.CreateNamespace("test") namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachineByID(0) _, err = app.GetMachineByID(0)
@ -171,7 +171,7 @@ func (s *Suite) TestListPeers(c *check.C) {
namespace, err := app.CreateNamespace("test") namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachineByID(0) _, err = app.GetMachineByID(0)
@ -214,7 +214,7 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
for _, name := range []string{"test", "admin"} { for _, name := range []string{"test", "admin"} {
namespace, err := app.CreateNamespace(name) namespace, err := app.CreateNamespace(name)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
stor = append(stor, base{namespace, pak}) stor = append(stor, base{namespace, pak})
} }
@ -294,7 +294,7 @@ func (s *Suite) TestExpireMachine(c *check.C) {
namespace, err := app.CreateNamespace("test") namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("test", "testmachine") _, err = app.GetMachine("test", "testmachine")
@ -350,7 +350,7 @@ func (s *Suite) TestSetTags(c *check.C) {
namespace, err := app.CreateNamespace("test") namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("test", "testmachine") _, err = app.GetMachine("test", "testmachine")
@ -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)
}

View file

@ -31,7 +31,7 @@ func (s *Suite) TestDestroyNamespaceErrors(c *check.C) {
namespace, err := app.CreateNamespace("test") namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
err = app.DestroyNamespace("test") err = app.DestroyNamespace("test")
@ -44,7 +44,7 @@ func (s *Suite) TestDestroyNamespaceErrors(c *check.C) {
namespace, err = app.CreateNamespace("test") namespace, err = app.CreateNamespace("test")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err = app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err = app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machine := Machine{ machine := Machine{
@ -107,6 +107,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -115,6 +116,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -123,6 +125,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -131,6 +134,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
false, false,
false, false,
nil, nil,
nil,
) )
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -380,7 +384,7 @@ func (s *Suite) TestSetMachineNamespace(c *check.C) {
newNamespace, err := app.CreateNamespace("new") newNamespace, err := app.CreateNamespace("new")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(oldNamespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(oldNamespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machine := Machine{ machine := Machine{

View file

@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"strconv" "strconv"
"strings"
"time" "time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
@ -18,6 +19,7 @@ const (
ErrPreAuthKeyExpired = Error("AuthKey expired") ErrPreAuthKeyExpired = Error("AuthKey expired")
ErrSingleUseAuthKeyHasBeenUsed = Error("AuthKey has already been used") ErrSingleUseAuthKeyHasBeenUsed = Error("AuthKey has already been used")
ErrNamespaceMismatch = Error("namespace mismatch") ErrNamespaceMismatch = Error("namespace mismatch")
ErrPreAuthKeyACLTagInvalid = Error("AuthKey tag is invalid")
) )
// PreAuthKey describes a pre-authorization key usable in a particular namespace. // PreAuthKey describes a pre-authorization key usable in a particular namespace.
@ -29,23 +31,38 @@ type PreAuthKey struct {
Reusable bool Reusable bool
Ephemeral bool `gorm:"default:false"` Ephemeral bool `gorm:"default:false"`
Used bool `gorm:"default:false"` Used bool `gorm:"default:false"`
ACLTags []PreAuthKeyACLTag
CreatedAt *time.Time CreatedAt *time.Time
Expiration *time.Time Expiration *time.Time
} }
// PreAuthKeyACLTag describes an autmatic tag applied to a node when registered with the associated PreAuthKey.
type PreAuthKeyACLTag struct {
ID uint64 `gorm:"primary_key"`
PreAuthKeyID uint64
Tag string
}
// CreatePreAuthKey creates a new PreAuthKey in a namespace, and returns it. // CreatePreAuthKey creates a new PreAuthKey in a namespace, and returns it.
func (h *Headscale) CreatePreAuthKey( func (h *Headscale) CreatePreAuthKey(
namespaceName string, namespaceName string,
reusable bool, reusable bool,
ephemeral bool, ephemeral bool,
expiration *time.Time, expiration *time.Time,
aclTags []string,
) (*PreAuthKey, error) { ) (*PreAuthKey, error) {
namespace, err := h.GetNamespace(namespaceName) namespace, err := h.GetNamespace(namespaceName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, tag := range aclTags {
if !strings.HasPrefix(tag, "tag:") {
return nil, fmt.Errorf("%w: '%s' did not begin with 'tag:'", ErrPreAuthKeyACLTagInvalid, tag)
}
}
now := time.Now().UTC() now := time.Now().UTC()
kstr, err := h.generateKey() kstr, err := h.generateKey()
if err != nil { if err != nil {
@ -62,8 +79,32 @@ func (h *Headscale) CreatePreAuthKey(
Expiration: expiration, Expiration: expiration,
} }
if err := h.db.Save(&key).Error; err != nil { err = h.db.Transaction(func(db *gorm.DB) error {
return nil, fmt.Errorf("failed to create key in the database: %w", err) if err := db.Save(&key).Error; err != nil {
return fmt.Errorf("failed to create key in the database: %w", err)
}
if len(aclTags) > 0 {
seenTags := map[string]bool{}
for _, tag := range aclTags {
if !seenTags[tag] {
if err := db.Save(&PreAuthKeyACLTag{PreAuthKeyID: key.ID, Tag: tag}).Error; err != nil {
return fmt.Errorf(
"failed to ceate key tag in the database: %w",
err,
)
}
seenTags[tag] = true
}
}
}
return nil
})
if err != nil {
return nil, err
} }
return &key, nil return &key, nil
@ -77,7 +118,7 @@ func (h *Headscale) ListPreAuthKeys(namespaceName string) ([]PreAuthKey, error)
} }
keys := []PreAuthKey{} keys := []PreAuthKey{}
if err := h.db.Preload("Namespace").Where(&PreAuthKey{NamespaceID: namespace.ID}).Find(&keys).Error; err != nil { if err := h.db.Preload("Namespace").Preload("ACLTags").Where(&PreAuthKey{NamespaceID: namespace.ID}).Find(&keys).Error; err != nil {
return nil, err return nil, err
} }
@ -101,11 +142,17 @@ func (h *Headscale) GetPreAuthKey(namespace string, key string) (*PreAuthKey, er
// DestroyPreAuthKey destroys a preauthkey. Returns error if the PreAuthKey // DestroyPreAuthKey destroys a preauthkey. Returns error if the PreAuthKey
// does not exist. // does not exist.
func (h *Headscale) DestroyPreAuthKey(pak PreAuthKey) error { func (h *Headscale) DestroyPreAuthKey(pak PreAuthKey) error {
if result := h.db.Unscoped().Delete(pak); result.Error != nil { return h.db.Transaction(func(db *gorm.DB) error {
return result.Error if result := db.Unscoped().Where(PreAuthKeyACLTag{PreAuthKeyID: pak.ID}).Delete(&PreAuthKeyACLTag{}); result.Error != nil {
} return result.Error
}
return nil if result := db.Unscoped().Delete(pak); result.Error != nil {
return result.Error
}
return nil
})
} }
// MarkExpirePreAuthKey marks a PreAuthKey as expired. // MarkExpirePreAuthKey marks a PreAuthKey as expired.
@ -131,7 +178,7 @@ func (h *Headscale) UsePreAuthKey(k *PreAuthKey) error {
// If returns no error and a PreAuthKey, it can be used. // If returns no error and a PreAuthKey, it can be used.
func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) { func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) {
pak := PreAuthKey{} pak := PreAuthKey{}
if result := h.db.Preload("Namespace").First(&pak, "key = ?", k); errors.Is( if result := h.db.Preload("Namespace").Preload("ACLTags").First(&pak, "key = ?", k); errors.Is(
result.Error, result.Error,
gorm.ErrRecordNotFound, gorm.ErrRecordNotFound,
) { ) {
@ -176,6 +223,7 @@ func (key *PreAuthKey) toProto() *v1.PreAuthKey {
Ephemeral: key.Ephemeral, Ephemeral: key.Ephemeral,
Reusable: key.Reusable, Reusable: key.Reusable,
Used: key.Used, Used: key.Used,
AclTags: make([]string, len(key.ACLTags)),
} }
if key.Expiration != nil { if key.Expiration != nil {
@ -186,5 +234,9 @@ func (key *PreAuthKey) toProto() *v1.PreAuthKey {
protoKey.CreatedAt = timestamppb.New(*key.CreatedAt) protoKey.CreatedAt = timestamppb.New(*key.CreatedAt)
} }
for idx := range key.ACLTags {
protoKey.AclTags[idx] = key.ACLTags[idx].Tag
}
return &protoKey return &protoKey
} }

View file

@ -7,14 +7,14 @@ import (
) )
func (*Suite) TestCreatePreAuthKey(c *check.C) { func (*Suite) TestCreatePreAuthKey(c *check.C) {
_, err := app.CreatePreAuthKey("bogus", true, false, nil) _, err := app.CreatePreAuthKey("bogus", true, false, nil, nil)
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
namespace, err := app.CreateNamespace("test") namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
key, err := app.CreatePreAuthKey(namespace.Name, true, false, nil) key, err := app.CreatePreAuthKey(namespace.Name, true, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
// Did we get a valid key? // Did we get a valid key?
@ -40,7 +40,7 @@ func (*Suite) TestExpiredPreAuthKey(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
now := time.Now() now := time.Now()
pak, err := app.CreatePreAuthKey(namespace.Name, true, false, &now) pak, err := app.CreatePreAuthKey(namespace.Name, true, false, &now, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
key, err := app.checkKeyValidity(pak.Key) key, err := app.checkKeyValidity(pak.Key)
@ -58,7 +58,7 @@ func (*Suite) TestValidateKeyOk(c *check.C) {
namespace, err := app.CreateNamespace("test3") namespace, err := app.CreateNamespace("test3")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, true, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, true, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
key, err := app.checkKeyValidity(pak.Key) key, err := app.checkKeyValidity(pak.Key)
@ -70,7 +70,7 @@ func (*Suite) TestAlreadyUsedKey(c *check.C) {
namespace, err := app.CreateNamespace("test4") namespace, err := app.CreateNamespace("test4")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machine := Machine{ machine := Machine{
@ -94,7 +94,7 @@ func (*Suite) TestReusableBeingUsedKey(c *check.C) {
namespace, err := app.CreateNamespace("test5") namespace, err := app.CreateNamespace("test5")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, true, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, true, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machine := Machine{ machine := Machine{
@ -118,7 +118,7 @@ func (*Suite) TestNotReusableNotBeingUsedKey(c *check.C) {
namespace, err := app.CreateNamespace("test6") namespace, err := app.CreateNamespace("test6")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
key, err := app.checkKeyValidity(pak.Key) key, err := app.checkKeyValidity(pak.Key)
@ -130,7 +130,7 @@ func (*Suite) TestEphemeralKey(c *check.C) {
namespace, err := app.CreateNamespace("test7") namespace, err := app.CreateNamespace("test7")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, true, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, true, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
now := time.Now() now := time.Now()
@ -165,7 +165,7 @@ func (*Suite) TestExpirePreauthKey(c *check.C) {
namespace, err := app.CreateNamespace("test3") namespace, err := app.CreateNamespace("test3")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, true, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, true, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(pak.Expiration, check.IsNil) c.Assert(pak.Expiration, check.IsNil)
@ -182,7 +182,7 @@ func (*Suite) TestNotReusableMarkedAsUsed(c *check.C) {
namespace, err := app.CreateNamespace("test6") namespace, err := app.CreateNamespace("test6")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak.Used = true pak.Used = true
app.db.Save(&pak) app.db.Save(&pak)
@ -190,3 +190,20 @@ func (*Suite) TestNotReusableMarkedAsUsed(c *check.C) {
_, err = app.checkKeyValidity(pak.Key) _, err = app.checkKeyValidity(pak.Key)
c.Assert(err, check.Equals, ErrSingleUseAuthKeyHasBeenUsed) c.Assert(err, check.Equals, ErrSingleUseAuthKeyHasBeenUsed)
} }
func (*Suite) TestPreAuthKeyACLTags(c *check.C) {
namespace, err := app.CreateNamespace("test8")
c.Assert(err, check.IsNil)
_, err = app.CreatePreAuthKey(namespace.Name, false, false, nil, []string{"badtag"})
c.Assert(err, check.NotNil) // Confirm that malformed tags are rejected
tags := []string{"tag:test1", "tag:test2"}
tagsWithDuplicate := []string{"tag:test1", "tag:test2", "tag:test2"}
_, err = app.CreatePreAuthKey(namespace.Name, false, false, nil, tagsWithDuplicate)
c.Assert(err, check.IsNil)
listedPaks, err := app.ListPreAuthKeys("test8")
c.Assert(err, check.IsNil)
c.Assert(listedPaks[0].toProto().AclTags, check.DeepEquals, tags)
}

View file

@ -13,6 +13,7 @@ message PreAuthKey {
bool used = 6; bool used = 6;
google.protobuf.Timestamp expiration = 7; google.protobuf.Timestamp expiration = 7;
google.protobuf.Timestamp created_at = 8; google.protobuf.Timestamp created_at = 8;
repeated string acl_tags = 9;
} }
message CreatePreAuthKeyRequest { message CreatePreAuthKeyRequest {
@ -20,6 +21,7 @@ message CreatePreAuthKeyRequest {
bool reusable = 2; bool reusable = 2;
bool ephemeral = 3; bool ephemeral = 3;
google.protobuf.Timestamp expiration = 4; google.protobuf.Timestamp expiration = 4;
repeated string acl_tags = 5;
} }
message CreatePreAuthKeyResponse { message CreatePreAuthKeyResponse {

View file

@ -353,6 +353,24 @@ func (h *Headscale) handleAuthKeyCommon(
return return
} }
aclTags := pak.toProto().AclTags
if len(aclTags) > 0 {
// This conditional preserves the existing behaviour, although SaaS would reset the tags on auth-key login
err = h.SetTags(machine, aclTags)
if err != nil {
log.Error().
Caller().
Bool("noise", machineKey.IsZero()).
Str("machine", machine.Hostname).
Strs("aclTags", aclTags).
Err(err).
Msg("Failed to set tags after refreshing machine")
return
}
}
} else { } else {
now := time.Now().UTC() now := time.Now().UTC()
@ -378,6 +396,7 @@ func (h *Headscale) handleAuthKeyCommon(
NodeKey: nodeKey, NodeKey: nodeKey,
LastSeen: &now, LastSeen: &now,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
ForcedTags: pak.toProto().AclTags,
} }
machine, err = h.RegisterMachine( machine, err = h.RegisterMachine(

View file

@ -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,

View file

@ -11,7 +11,7 @@ func (s *Suite) TestGetRoutes(c *check.C) {
namespace, err := app.CreateNamespace("test") namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("test", "test_get_route_machine") _, err = app.GetMachine("test", "test_get_route_machine")
@ -55,7 +55,7 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
namespace, err := app.CreateNamespace("test") namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("test", "test_enable_route_machine") _, err = app.GetMachine("test", "test_enable_route_machine")

View 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"],
}
}
}

View file

@ -25,7 +25,7 @@ func (s *Suite) TestGetUsedIps(c *check.C) {
namespace, err := app.CreateNamespace("test-ip") namespace, err := app.CreateNamespace("test-ip")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("test", "testmachine") _, err = app.GetMachine("test", "testmachine")
@ -73,7 +73,7 @@ func (s *Suite) TestGetMultiIp(c *check.C) {
ips, err := app.getAvailableIPs() ips, err := app.getAvailableIPs()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("test", "testmachine") _, err = app.GetMachine("test", "testmachine")
@ -163,7 +163,7 @@ func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) {
namespace, err := app.CreateNamespace("test-ip") namespace, err := app.CreateNamespace("test-ip")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = app.GetMachine("test", "testmachine") _, err = app.GetMachine("test", "testmachine")