Repository: juanfont/headscale (Go)

Issue:
[Bug] Default ACLs differ from headscale
### Is this a support request?

- [x] This is not a support request

### Is there an existing issue for this?

- [x] I have searched the existing issues

### Current Behavior

Currently, when setting empty ACLs (`{}`), a deny-all default policy is applied. This is the exact opposite of Tailscale (see [documentation](https://tailscale.com/kb/1192/acl-samples#allow-all-default-acl)). Being unable to verify through the docs, issues, or commits whether or not this is intentional, I decided to file an issue in case it is not, and to document it, in case it is.

### Expected Behavior

Headscale mirrors Tailscale's default behaviour.

### Steps To Reproduce

1. clear ACLs
2. ping any host
3. apply allow-all ACL
4. ping any host

### Environment

```markdown
- OS: Linux
- Headscale version: 0.26.1
- Tailscale version: 1.84.0
```

### Runtime environment

- [x] Headscale is behind a (reverse) proxy
- [x] Headscale runs in a container

### Debug information

<img width="768" height="137" alt="Image" src="https://github.com/user-attachments/assets/1568e23a-c20a-48a7-970a-68dec065563b" />


Discussion:
I have this problem as well.
This means that I can't point to any ACL file (or have it in database mode), as it can't be empty (needs to have at least the root object), so it applies a deny all rule.
The only way it works for me is to have it in "file" mode, but with an empty file path.



Patch:
diff --git a/docs/ref/acls.md b/docs/ref/acls.md
index 63f83ae2e6..d74fea6c9a 100644
--- a/docs/ref/acls.md
+++ b/docs/ref/acls.md
@@ -9,9 +9,38 @@ When using ACL's the User borders are no longer applied. All machines
 whichever the User have the ability to communicate with other hosts as
 long as the ACL's permits this exchange.
 
-## ACLs use case example
+## ACL Setup
 
-Let's build an example use case for a small business (It may be the place where
+To enable and configure ACLs in Headscale, you need to specify the path to your ACL policy file in the `policy.path` key in `config.yaml`.
+
+Your ACL policy file must be formatted using [huJSON](https://github.com/tailscale/hujson).
+
+Info on how these policies are written can be found
+[here](https://tailscale.com/kb/1018/acls/).
+
+Please reload or restart Headscale after updating the ACL file. Headscale may be reloaded either via its systemd service
+(`sudo systemctl reload headscale`) or by sending a SIGHUP signal (`sudo kill -HUP $(pidof headscale)`) to the main
+process. Headscale logs the result of ACL policy processing after each reload.
+
+## Simple Examples
+
+- [**Allow All**](https://tailscale.com/kb/1192/acl-samples#allow-all-default-acl): If you define an ACL file but completely omit the `"acls"` field from its content, Headscale will default to an "allow all" policy. This means all devices connected to your tailnet will be able to communicate freely with each other.
+
+    ```json
+    {}
+    ```
+
+- [**Deny All**](https://tailscale.com/kb/1192/acl-samples#deny-all): To prevent all communication within your tailnet, you can include an empty array for the `"acls"` field in your policy file.
+
+    ```json
+    {
+      "acls": []
+    }
+    ```
+
+## Complex Example
+
+Let's build a more complex example use case for a small business (It may be the place where
 ACL's are the most useful).
 
 We have a small company with a boss, an admin, two developers and an intern.
@@ -38,10 +67,6 @@ servers.
 
 ![ACL implementation example](../images/headscale-acl-network.png)
 
-## ACL setup
-
-ACLs have to be written in [huJSON](https://github.com/tailscale/hujson).
-
 When [registering the servers](../usage/getting-started.md#register-a-node) we
 will need to add the flag `--advertise-tags=tag:<tag1>,tag:<tag2>`, and the user
 that is registering the server should be allowed to do it. Since anyone can add
@@ -49,14 +74,6 @@ tags to a server they can register, the check of the tags is done on headscale
 server and only valid tags are applied. A tag is valid if the user that is
 registering it is allowed to do it.
 
-To use ACLs in headscale, you must edit your `config.yaml` file. In there you will find a `policy.path` parameter. This
-will need to point to your ACL file. More info on how these policies are written can be found
-[here](https://tailscale.com/kb/1018/acls/).
-
-Please reload or restart Headscale after updating the ACL file. Headscale may be reloaded either via its systemd service
-(`sudo systemctl reload headscale`) or by sending a SIGHUP signal (`sudo kill -HUP $(pidof headscale)`) to the main
-process. Headscale logs the result of ACL policy processing after each reload.
-
 Here are the ACL's to implement the same permissions as above:
 
 ```json title="acl.json"
diff --git a/hscontrol/policy/v2/filter.go b/hscontrol/policy/v2/filter.go
index c546eb20e7..ecd8f83e4c 100644
--- a/hscontrol/policy/v2/filter.go
+++ b/hscontrol/policy/v2/filter.go
@@ -21,7 +21,7 @@ func (pol *Policy) compileFilterRules(
 	users types.Users,
 	nodes views.Slice[types.NodeView],
 ) ([]tailcfg.FilterRule, error) {
-	if pol == nil {
+	if pol == nil || pol.ACLs == nil {
 		return tailcfg.FilterAllowAll, nil
 	}
 


Test patch:
diff --git a/hscontrol/policy/v2/policy_test.go b/hscontrol/policy/v2/policy_test.go
index a91831ad56..0140653e0f 100644
--- a/hscontrol/policy/v2/policy_test.go
+++ b/hscontrol/policy/v2/policy_test.go
@@ -40,8 +40,8 @@ func TestPolicyManager(t *testing.T) {
 			name:         "empty-policy",
 			pol:          "{}",
 			nodes:        types.Nodes{},
-			wantFilter:   nil,
-			wantMatchers: []matcher.Match{},
+			wantFilter:   tailcfg.FilterAllowAll,
+			wantMatchers: matcher.MatchesFromFilterRules(tailcfg.FilterAllowAll),
 		},
 	}