Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

validate policy against nodes, error if not valid #2089

Merged
merged 5 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test-integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
- TestNodeRenameCommand
- TestNodeMoveCommand
- TestPolicyCommand
- TestPolicyBrokenConfigCommand
- TestResolveMagicDNS
- TestValidateResolvConf
- TestDERPServerScenario
Expand Down
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)
}
}
kradalby marked this conversation as resolved.
Show resolved Hide resolved

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)
}
kradalby marked this conversation as resolved.
Show resolved Hide resolved
}

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
74 changes: 74 additions & 0 deletions integration/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1676,3 +1676,77 @@ func TestPolicyCommand(t *testing.T) {
assert.Len(t, output.ACLs, 1)
assert.Equal(t, output.TagOwners["tag:exists"], []string{"policy-user"})
}

func TestPolicyBrokenConfigCommand(t *testing.T) {
IntegrationSkip(t)
t.Parallel()

scenario, err := NewScenario(dockertestMaxWait())
assertNoErr(t, err)
defer scenario.Shutdown()

spec := map[string]int{
"policy-user": 1,
}

err = scenario.CreateHeadscaleEnv(
spec,
[]tsic.Option{},
hsic.WithTestName("clins"),
hsic.WithConfigEnv(map[string]string{
"HEADSCALE_POLICY_MODE": "database",
}),
)
assertNoErr(t, err)

headscale, err := scenario.Headscale()
assertNoErr(t, err)

p := policy.ACLPolicy{
ACLs: []policy.ACL{
{
// This is an unknown action, so it will return an error
// and the config will not be applied.
Action: "acccept",
Sources: []string{"*"},
Destinations: []string{"*:*"},
},
},
TagOwners: map[string][]string{
"tag:exists": {"policy-user"},
},
}

pBytes, _ := json.Marshal(p)

policyFilePath := "/etc/headscale/policy.json"

err = headscale.WriteFile(policyFilePath, pBytes)
assertNoErr(t, err)

// No policy is present at this time.
// Add a new policy from a file.
_, err = headscale.Execute(
[]string{
"headscale",
"policy",
"set",
"-f",
policyFilePath,
},
)
assert.ErrorContains(t, err, "verifying policy rules: invalid action")

// The new policy was invalid, the old one should still be in place, which
// is none.
_, err = headscale.Execute(
[]string{
"headscale",
"policy",
"get",
"--output",
"json",
},
)
assert.ErrorContains(t, err, "acl policy not found")
}
2 changes: 1 addition & 1 deletion integration/dockertestutil/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func ExecuteCommand(
exitCode, err := resource.Exec(
cmd,
dockertest.ExecOptions{
Env: append(env, "HEADSCALE_LOG_LEVEL=disabled"),
Env: append(env, "HEADSCALE_LOG_LEVEL=info"),
StdOut: &stdout,
StdErr: &stderr,
},
Expand Down
2 changes: 1 addition & 1 deletion integration/hsic/hsic.go
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ func (t *HeadscaleInContainer) Execute(
log.Printf("command stdout: %s\n", stdout)
}

return "", err
return stdout, fmt.Errorf("executing command in docker: %w, stderr: %s", err, stderr)
}

return stdout, nil
Expand Down
Loading