Skip to content

Commit

Permalink
Added manual approval of nodes in the network
Browse files Browse the repository at this point in the history
  • Loading branch information
hopleus committed Oct 16, 2024
1 parent 3073435 commit 5c274f2
Show file tree
Hide file tree
Showing 14 changed files with 254 additions and 37 deletions.
44 changes: 44 additions & 0 deletions cmd/headscale/cli/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ func init() {
}
nodeCmd.AddCommand(registerNodeCmd)

approveNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = approveNodeCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
nodeCmd.AddCommand(approveNodeCmd)

expireNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = expireNodeCmd.MarkFlagRequired("identifier")
if err != nil {
Expand Down Expand Up @@ -206,6 +213,43 @@ var listNodesCmd = &cobra.Command{
},
}

var approveNodeCmd = &cobra.Command{
Use: "approve",
Short: "Approve a node in your network",
Aliases: []string{"a"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
identifier, err := cmd.Flags().GetUint64("identifier")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error converting ID to integer: %s", err),
output,
)
return
}
ctx, client, conn, cancel := newHeadscaleCLIWithConfig()
defer cancel()
defer conn.Close()
request := &v1.ApproveNodeRequest{
NodeId: identifier,
}
response, err := client.ApproveNode(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Cannot expire node: %s\n",
status.Convert(err).Message(),
),
output,
)
return
}
SuccessOutput(response.GetNode(), "Node approved", output)
},
}

var expireNodeCmd = &cobra.Command{
Use: "expire",
Short: "Expire (log out) a node in your network",
Expand Down
14 changes: 10 additions & 4 deletions cmd/headscale/cli/preauthkeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ func init() {
preauthkeysCmd.AddCommand(expirePreAuthKeyCmd)
createPreAuthKeyCmd.PersistentFlags().
Bool("reusable", false, "Make the preauthkey reusable")
createPreAuthKeyCmd.PersistentFlags().
Bool("pre-approved", false, "Make the preauthkey with node pre-approval")
createPreAuthKeyCmd.PersistentFlags().
Bool("ephemeral", false, "Preauthkey for ephemeral nodes")
createPreAuthKeyCmd.Flags().
Expand Down Expand Up @@ -90,6 +92,7 @@ var listPreAuthKeys = &cobra.Command{
"ID",
"Key",
"Reusable",
"Pre-Approved",
"Ephemeral",
"Used",
"Expiration",
Expand All @@ -115,6 +118,7 @@ var listPreAuthKeys = &cobra.Command{
key.GetId(),
key.GetKey(),
strconv.FormatBool(key.GetReusable()),
strconv.FormatBool(key.GetPreApproved()),
strconv.FormatBool(key.GetEphemeral()),
strconv.FormatBool(key.GetUsed()),
expiration,
Expand Down Expand Up @@ -147,14 +151,16 @@ var createPreAuthKeyCmd = &cobra.Command{
}

reusable, _ := cmd.Flags().GetBool("reusable")
preApproved, _ := cmd.Flags().GetBool("pre-approved")
ephemeral, _ := cmd.Flags().GetBool("ephemeral")
tags, _ := cmd.Flags().GetStringSlice("tags")

request := &v1.CreatePreAuthKeyRequest{
User: user,
Reusable: reusable,
Ephemeral: ephemeral,
AclTags: tags,
User: user,
Reusable: reusable,
PreApproved: preApproved,
Ephemeral: ephemeral,
AclTags: tags,
}

durationStr, _ := cmd.Flags().GetString("expiration")
Expand Down
6 changes: 6 additions & 0 deletions config-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -384,3 +384,9 @@ logtail:
# default static port 41641. This option is intended as a workaround for some buggy
# firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information.
randomize_client_port: false

# Node management
# See https://tailscale.com/kb/1099/device-approval for more information.
node_management:
# Require new nodes to be approved by admins before they can access the network.
manual_approve_new_node: false
1 change: 1 addition & 0 deletions hscontrol/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {
app.db, err = db.NewHeadscaleDatabase(
cfg.Database,
cfg.BaseDomain,
cfg.NodeManagement,
registrationCache,
)
if err != nil {
Expand Down
14 changes: 12 additions & 2 deletions hscontrol/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,11 @@ func (h *Headscale) handleAuthKey(

nodeKey := registerRequest.NodeKey

nodeApproved := true
if h.cfg.NodeManagement.ManualApproveNewNode {
nodeApproved = pak.PreApproved
}

// retrieve node information if it exist
// The error is not important, because if it does not
// exist, then this is a new node and we will move
Expand All @@ -308,6 +313,10 @@ func (h *Headscale) handleAuthKey(
node.AuthKeyID = ptr.To(pak.ID)
}

if node.Approved == false {
node.Approved = nodeApproved
}

node.Expiry = &registerRequest.Expiry
node.User = pak.User
node.UserID = pak.UserID
Expand Down Expand Up @@ -349,6 +358,7 @@ func (h *Headscale) handleAuthKey(
User: pak.User,
MachineKey: machineKey,
RegisterMethod: util.RegisterMethodAuthKey,
Approved: nodeApproved,
Expiry: &registerRequest.Expiry,
NodeKey: nodeKey,
LastSeen: &now,
Expand Down Expand Up @@ -399,7 +409,7 @@ func (h *Headscale) handleAuthKey(
return
}

resp.MachineAuthorized = true
resp.MachineAuthorized = node.IsApproved()
resp.User = *pak.User.TailscaleUser()
// Provide LoginName when registering with pre-auth key
// Otherwise it will need to exec `tailscale up` twice to fetch the *LoginName*
Expand Down Expand Up @@ -562,7 +572,7 @@ func (h *Headscale) handleNodeWithValidRegistration(
Msg("Client is registered and we have the current NodeKey. All clear to /map")

resp.AuthURL = ""
resp.MachineAuthorized = true
resp.MachineAuthorized = node.IsApproved()
resp.User = *node.User.TailscaleUser()
resp.Login = *node.User.TailscaleLogin()

Expand Down
49 changes: 47 additions & 2 deletions hscontrol/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,16 @@ type HSDatabase struct {
cfg *types.DatabaseConfig
regCache *zcache.Cache[string, types.Node]

baseDomain string
baseDomain string
nodeManagement *types.NodeManagement
}

// TODO(kradalby): assemble this struct from toptions or something typed
// rather than arguments.
func NewHeadscaleDatabase(
cfg types.DatabaseConfig,
baseDomain string,
nodeManagement types.NodeManagement,
regCache *zcache.Cache[string, types.Node],
) (*HSDatabase, error) {
dbConn, err := openDB(cfg)
Expand Down Expand Up @@ -485,6 +487,48 @@ func NewHeadscaleDatabase(
},
Rollback: func(db *gorm.DB) error { return nil },
},
{
ID: "202410071005",
Migrate: func(tx *gorm.DB) error {
err = tx.AutoMigrate(&types.PreAuthKey{})
if err != nil {
return err
}

err = tx.AutoMigrate(&types.Node{})
if err != nil {
return err
}

if tx.Migrator().HasColumn(&types.Node{}, "approved") {
nodes := types.Nodes{}
if err := tx.Find(&nodes).Error; err != nil {
log.Error().Err(err).Msg("Error accessing db")
}

for item, node := range nodes {
if node.IsApproved() == false {
err = tx.Model(nodes[item]).Updates(types.Node{
Approved: true,
}).Error
if err != nil {
log.Error().
Caller().
Str("hostname", node.Hostname).
Bool("approved", node.IsApproved()).
Err(err).
Msg("Failed to add approval option to existing nodes during database migration")
}
}
}

return nil
}

return fmt.Errorf("no node approved column in DB")
},
Rollback: func(db *gorm.DB) error { return nil },
},
},
)

Expand All @@ -497,7 +541,8 @@ func NewHeadscaleDatabase(
cfg: &cfg,
regCache: regCache,

baseDomain: baseDomain,
baseDomain: baseDomain,
nodeManagement: &nodeManagement,
}

return &db, err
Expand Down
4 changes: 2 additions & 2 deletions hscontrol/db/ip.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,10 @@ func isTailscaleReservedIP(ip netip.Addr) bool {
// it will be added.
// If a prefix type has been removed (IPv4 or IPv6), it
// will remove the IPs in that family from the node.
func (db *HSDatabase) BackfillNodeIPs(i *IPAllocator) ([]string, error) {
func (hsdb *HSDatabase) BackfillNodeIPs(i *IPAllocator) ([]string, error) {
var err error
var ret []string
err = db.Write(func(tx *gorm.DB) error {
err = hsdb.Write(func(tx *gorm.DB) error {
if i == nil {
return errors.New("backfilling IPs: ip allocator was nil")
}
Expand Down
28 changes: 26 additions & 2 deletions hscontrol/db/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ func ListPeers(tx *gorm.DB, nodeID types.NodeID) (types.Nodes, error) {
Preload("AuthKey.User").
Preload("User").
Preload("Routes").
Where("id <> ?",
nodeID).Find(&nodes).Error; err != nil {
Where("id <> ?", nodeID).
Where("approved = ?", true).
Find(&nodes).Error; err != nil {
return types.Nodes{}, err
}

Expand Down Expand Up @@ -261,6 +262,19 @@ func RenameNode(tx *gorm.DB,
return nil
}

func (hsdb *HSDatabase) NodeSetApprove(nodeID types.NodeID, approved bool) error {
return hsdb.Write(func(tx *gorm.DB) error {
return NodeSetApprove(tx, nodeID, approved)
})
}

// NodeSetApprove takes a Node struct and a set approval option
func NodeSetApprove(tx *gorm.DB,
nodeID types.NodeID, approved bool,
) error {
return tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("approved", approved).Error
}

func (hsdb *HSDatabase) NodeSetExpiry(nodeID types.NodeID, expiry time.Time) error {
return hsdb.Write(func(tx *gorm.DB) error {
return NodeSetExpiry(tx, nodeID, expiry)
Expand Down Expand Up @@ -328,6 +342,8 @@ func (hsdb *HSDatabase) RegisterNodeFromAuthCallback(
ipv6 *netip.Addr,
) (*types.Node, error) {
return Write(hsdb.DB, func(tx *gorm.DB) (*types.Node, error) {
manualApprovedNode := hsdb.nodeManagement.ManualApproveNewNode

if node, ok := hsdb.regCache.Get(mkey.String()); ok {
user, err := GetUserByID(tx, userID)
if err != nil {
Expand All @@ -341,6 +357,7 @@ func (hsdb *HSDatabase) RegisterNodeFromAuthCallback(
Str("machine_key", mkey.ShortString()).
Str("username", user.Username()).
Str("registrationMethod", registrationMethod).
Bool("manualApprovedNode", manualApprovedNode).
Str("expiresAt", fmt.Sprintf("%v", nodeExpiry)).
Msg("Registering node from API/CLI or auth callback")

Expand All @@ -354,6 +371,10 @@ func (hsdb *HSDatabase) RegisterNodeFromAuthCallback(
node.User = *user
node.RegisterMethod = registrationMethod

if node.IsApproved() == false && manualApprovedNode == false {
node.Approved = true
}

if nodeExpiry != nil {
node.Expiry = nodeExpiry
}
Expand Down Expand Up @@ -388,6 +409,7 @@ func RegisterNode(tx *gorm.DB, node types.Node, ipv4 *netip.Addr, ipv6 *netip.Ad
Str("machine_key", node.MachineKey.ShortString()).
Str("node_key", node.NodeKey.ShortString()).
Str("user", node.User.Username()).
Bool("approved", node.IsApproved()).
Msg("Registering node")

// If the node exists and it already has IP(s), we just save it
Expand All @@ -404,6 +426,7 @@ func RegisterNode(tx *gorm.DB, node types.Node, ipv4 *netip.Addr, ipv6 *netip.Ad
Str("machine_key", node.MachineKey.ShortString()).
Str("node_key", node.NodeKey.ShortString()).
Str("user", node.User.Username()).
Bool("approved", node.IsApproved()).
Msg("Node authorized again")

return &node, nil
Expand All @@ -428,6 +451,7 @@ func RegisterNode(tx *gorm.DB, node types.Node, ipv4 *netip.Addr, ipv6 *netip.Ad
log.Trace().
Caller().
Str("node", node.Hostname).
Bool("approved", node.IsApproved()).
Msg("Node registered with the database")

return &node, nil
Expand Down
21 changes: 12 additions & 9 deletions hscontrol/db/preauth_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ func (hsdb *HSDatabase) CreatePreAuthKey(
// TODO(kradalby): Should be ID, not name
userName string,
reusable bool,
preApproved bool,
ephemeral bool,
expiration *time.Time,
aclTags []string,
) (*types.PreAuthKey, error) {
return Write(hsdb.DB, func(tx *gorm.DB) (*types.PreAuthKey, error) {
return CreatePreAuthKey(tx, userName, reusable, ephemeral, expiration, aclTags)
return CreatePreAuthKey(tx, userName, reusable, preApproved, ephemeral, expiration, aclTags)
})
}

Expand All @@ -41,6 +42,7 @@ func CreatePreAuthKey(
// TODO(kradalby): Should be ID, not name
userName string,
reusable bool,
preApproved bool,
ephemeral bool,
expiration *time.Time,
aclTags []string,
Expand Down Expand Up @@ -72,14 +74,15 @@ func CreatePreAuthKey(
}

key := types.PreAuthKey{
Key: kstr,
UserID: user.ID,
User: *user,
Reusable: reusable,
Ephemeral: ephemeral,
CreatedAt: &now,
Expiration: expiration,
Tags: aclTags,
Key: kstr,
UserID: user.ID,
User: *user,
Reusable: reusable,
PreApproved: preApproved,
Ephemeral: ephemeral,
CreatedAt: &now,
Expiration: expiration,
Tags: aclTags,
}

if err := tx.Save(&key).Error; err != nil {
Expand Down
Loading

0 comments on commit 5c274f2

Please sign in to comment.