From 4e0c2b855616e08f3745d4db7f0264b402cee19f Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 28 Apr 2026 16:09:51 +0000 Subject: [PATCH] cmd/headscale/cli: validate users in policy check Add --bypass-grpc-and-access-database-directly to policy check so the new ambiguous-user validator runs against the live user list. Without the flag, policy check stays a syntax-only check and the success message says so. Updates #3160 --- CHANGELOG.md | 2 ++ cmd/headscale/cli/policy.go | 28 ++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88c50559..f765f981 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -133,6 +133,7 @@ connected" routers that maintain their control session but cannot route packets. - Fix non-wildcard source IPs being dropped when combined with wildcard `*` in the same ACL rule [#2180](https://github.com/juanfont/headscale/pull/2180) - Fix exit node approval not triggering filter rule recalculation for peers [#2180](https://github.com/juanfont/headscale/pull/2180) - Policy validation error messages now include field context (e.g., `src=`, `dst=`) and are more descriptive [#2180](https://github.com/juanfont/headscale/pull/2180) +- Reject policies whose `user@` tokens match multiple DB users; rename the duplicate via `headscale users rename` to load [#3160](https://github.com/juanfont/headscale/issues/3160) #### Grants @@ -151,6 +152,7 @@ connected" routers that maintain their control session but cannot route packets. - Add `headscale auth register`, `headscale auth approve`, and `headscale auth reject` CLI commands [#1850](https://github.com/juanfont/headscale/pull/1850) - Deprecate `headscale nodes register --key` in favour of `headscale auth register --auth-id` [#1850](https://github.com/juanfont/headscale/pull/1850) +- `headscale policy check --bypass-grpc-and-access-database-directly` validates `user@` tokens against the live user database [#3160](https://github.com/juanfont/headscale/issues/3160) - Remove deprecated `--namespace` flag from `nodes list`, `nodes register`, and `debug create-node` commands (use `--user` instead) [#3093](https://github.com/juanfont/headscale/pull/3093) - Remove deprecated `namespace`/`ns` command aliases for `users` and `machine`/`machines` aliases for `nodes` [#3093](https://github.com/juanfont/headscale/pull/3093) - **User deletion**: Fix `DestroyUser` deleting all pre-auth keys in the database instead of only the target user's keys [#3155](https://github.com/juanfont/headscale/pull/3155) diff --git a/cmd/headscale/cli/policy.go b/cmd/headscale/cli/policy.go index 9fda35b2..2c3365ed 100644 --- a/cmd/headscale/cli/policy.go +++ b/cmd/headscale/cli/policy.go @@ -48,6 +48,7 @@ func init() { policyCmd.AddCommand(setPolicy) checkPolicy.Flags().StringP("file", "f", "", "Path to a policy file in HuJSON format") + checkPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing gRPC and does not require the server to be running. Required to validate that user@ tokens resolve against the user database; without it, the check is syntax-only.") mustMarkRequired(checkPolicy, "file") policyCmd.AddCommand(checkPolicy) } @@ -178,12 +179,35 @@ var checkPolicy = &cobra.Command{ return fmt.Errorf("reading policy file: %w", err) } - _, err = policy.NewPolicyManager(policyBytes, nil, views.Slice[types.NodeView]{}) + var users []types.User + + if bypass, _ := cmd.Flags().GetBool(bypassFlag); bypass { + if !confirmAction(cmd, "DO NOT run this command if an instance of headscale is running, are you sure headscale is not running?") { + return errAborted + } + + d, err := bypassDatabase() + if err != nil { + return err + } + defer d.Close() + + users, err = d.ListUsers() + if err != nil { + return fmt.Errorf("loading users for policy validation: %w", err) + } + } + + _, err = policy.NewPolicyManager(policyBytes, users, views.Slice[types.NodeView]{}) if err != nil { return fmt.Errorf("parsing policy file: %w", err) } - fmt.Println("Policy is valid") + if users == nil { + fmt.Println("Policy syntax is valid (run with --" + bypassFlag + " to also validate user references against the database)") + } else { + fmt.Println("Policy is valid") + } return nil },