1
0
mirror of https://github.com/juanfont/headscale.git synced 2025-09-25 17:51:11 +02:00

policy: only rebuild ssh policy when needed

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2025-09-16 16:11:20 +02:00
parent 911f0e926b
commit 11e5cb4f7b
No known key found for this signature in database

View File

@ -3,7 +3,6 @@ package v2
import (
"encoding/json"
"fmt"
"maps"
"net/netip"
"slices"
"strings"
@ -37,7 +36,6 @@ type PolicyManager struct {
autoApproveMapHash deephash.Sum
autoApproveMap map[netip.Prefix]*netipx.IPSet
sshPolicyHash deephash.Sum
// Lazy map of SSH policies
sshPolicyMap map[types.NodeID]*tailcfg.SSHPolicy
}
@ -69,9 +67,11 @@ func NewPolicyManager(b []byte, users []types.User, nodes views.Slice[types.Node
// updateLocked updates the filter rules based on the current policy and nodes.
// It must be called with the lock held.
func (pm *PolicyManager) updateLocked() (bool, error) {
// Save current SSH policy map for comparison
oldSSHPolicyMap := make(map[types.NodeID]*tailcfg.SSHPolicy)
maps.Copy(oldSSHPolicyMap, pm.sshPolicyMap)
// Clear the SSH policy map to ensure it's recalculated with the new policy.
// TODO(kradalby): This could potentially be optimized by only clearing the
// policies for nodes that have changed. Particularly if the only difference is
// that nodes has been added or removed.
clear(pm.sshPolicyMap)
filter, err := pm.pol.compileFilterRules(pm.users, pm.nodes)
if err != nil {
@ -144,45 +144,8 @@ func (pm *PolicyManager) updateLocked() (bool, error) {
pm.exitSet = exitSet
pm.exitSetHash = exitSetHash
// Check if SSH policy compilation results have changed by computing
// SSH policies for all nodes and comparing their hash
var sshPolicyChanged bool
if len(pm.pol.SSHs) > 0 {
newSSHPolicies := make(map[types.NodeID]*tailcfg.SSHPolicy)
for i := 0; i < pm.nodes.Len(); i++ {
node := pm.nodes.At(i)
sshPol, err := pm.pol.compileSSHPolicy(pm.users, node, pm.nodes)
if err != nil {
// If compilation fails, store nil policy
newSSHPolicies[node.ID()] = nil
} else {
newSSHPolicies[node.ID()] = sshPol
}
}
sshPolicyHash := deephash.Hash(&newSSHPolicies)
sshPolicyChanged = sshPolicyHash != pm.sshPolicyHash
if sshPolicyChanged {
log.Debug().
Str("sshPolicy.hash.old", pm.sshPolicyHash.String()[:8]).
Str("sshPolicy.hash.new", sshPolicyHash.String()[:8]).
Msg("SSH policy hash changed")
// Clear and update the SSH policy map with the newly computed policies
clear(pm.sshPolicyMap)
pm.sshPolicyMap = newSSHPolicies
}
pm.sshPolicyHash = sshPolicyHash
} else {
// If no SSH policy is defined, clear the map if it's not already empty
if len(pm.sshPolicyMap) > 0 {
sshPolicyChanged = true
clear(pm.sshPolicyMap)
pm.sshPolicyHash = deephash.Sum{}
}
}
// If none of the calculated values changed, no need to update nodes
if !filterChanged && !tagOwnerChanged && !autoApproveChanged && !exitSetChanged && !sshPolicyChanged {
// If neither of the calculated values changed, no need to update nodes
if !filterChanged && !tagOwnerChanged && !autoApproveChanged && !exitSetChanged {
log.Trace().
Msg("Policy evaluation detected no changes - all hashes match")
return false, nil
@ -193,7 +156,6 @@ func (pm *PolicyManager) updateLocked() (bool, error) {
Bool("tagOwners.changed", tagOwnerChanged).
Bool("autoApprovers.changed", autoApproveChanged).
Bool("exitNodes.changed", exitSetChanged).
Bool("sshPolicy.changed", sshPolicyChanged).
Msg("Policy changes require node updates")
return true, nil
@ -266,7 +228,23 @@ func (pm *PolicyManager) SetUsers(users []types.User) (bool, error) {
defer pm.mu.Unlock()
pm.users = users
return pm.updateLocked()
// Clear SSH policy map when users change to force SSH policy recomputation
// This ensures that if SSH policy compilation previously failed due to missing users,
// it will be retried with the new user list
clear(pm.sshPolicyMap)
changed, err := pm.updateLocked()
if err != nil {
return false, err
}
// If SSH policies exist, force a policy change when users are updated
// This ensures nodes get updated SSH policies even if other policy hashes didn't change
if pm.pol != nil && pm.pol.SSHs != nil && len(pm.pol.SSHs) > 0 {
return true, nil
}
return changed, nil
}
// SetNodes updates the nodes in the policy manager and updates the filter rules.