Skip to content

Commit

Permalink
validate policy against nodes, error if not valid
Browse files Browse the repository at this point in the history
this commit aims to improve the feedback of "runtime" policy
errors which would only manifest when the rules are compiled to
filter rules with nodes.

this change will in;

file-based mode load the nodes from the db and try to compile the rules on
start up and return an error if they would not work as intended.

database-based mode prevent a new ACL being written to the database if
it does not compile with the current set of node.

Fixes juanfont#2073
Fixes juanfont#2044

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
  • Loading branch information
kradalby committed Aug 30, 2024
1 parent fffd9d7 commit 4bd6936
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 3 deletions.
26 changes: 26 additions & 0 deletions hscontrol/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,32 @@ func (h *Headscale) loadACLPolicy() error {
if err != nil {
return fmt.Errorf("failed to load ACL policy from file: %w", err)
}

// Validate and reject configuration that would error when applied
// when creating a map response. This requires nodes, so there is still
// a scenario where they might be allowed if the server has no nodes
// yet, but it should help for the general case and for hot reloading
// configurations.
// Note that this check is only done for file-based policies in this function
// as the database-based policies are checked in the gRPC API where it is not
// allowed to be written to the database.
nodes, err := h.db.ListNodes()
if err != nil {
return fmt.Errorf("loading nodes from database to validate policy: %w", err)
}

_, err = pol.CompileFilterRules(nodes)
if err != nil {
return fmt.Errorf("verifying policy rules: %w", err)
}

if len(nodes) > 0 {
_, err = pol.CompileSSHPolicy(nodes[0], nodes)
if err != nil {
return fmt.Errorf("verifying SSH rules: %w", err)
}
}

case types.PolicyModeDB:
p, err := h.db.GetPolicy()
if err != nil {
Expand Down
29 changes: 26 additions & 3 deletions hscontrol/grpcv1.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package hscontrol
import (
"context"
"errors"
"fmt"
"io"
"os"
"sort"
Expand Down Expand Up @@ -721,17 +722,39 @@ func (api headscaleV1APIServer) SetPolicy(

p := request.GetPolicy()

valid, err := policy.LoadACLPolicyFromBytes([]byte(p))
pol, err := policy.LoadACLPolicyFromBytes([]byte(p))
if err != nil {
return nil, err
return nil, fmt.Errorf("loading ACL policy file: %w", err)
}

// Validate and reject configuration that would error when applied
// when creating a map response. This requires nodes, so there is still
// a scenario where they might be allowed if the server has no nodes
// yet, but it should help for the general case and for hot reloading
// configurations.
nodes, err := api.h.db.ListNodes()
if err != nil {
return nil, fmt.Errorf("loading nodes from database to validate policy: %w", err)
}

_, err = pol.CompileFilterRules(nodes)
if err != nil {
return nil, fmt.Errorf("verifying policy rules: %w", err)
}

if len(nodes) > 0 {
_, err = pol.CompileSSHPolicy(nodes[0], nodes)
if err != nil {
return nil, fmt.Errorf("verifying SSH rules: %w", err)
}
}

updated, err := api.h.db.SetPolicy(p)
if err != nil {
return nil, err
}

api.h.ACLPolicy = valid
api.h.ACLPolicy = pol

ctx := types.NotifyCtx(context.Background(), "acl-update", "na")
api.h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{
Expand Down

0 comments on commit 4bd6936

Please sign in to comment.