Skip to content

Commit 5934a4e

Browse files
committed
docs: add HeadscaleAutoApprover
Signed-off-by: Seena Fallah <seenafallah@gmail.com>
1 parent a065c21 commit 5934a4e

1 file changed

Lines changed: 172 additions & 0 deletions

File tree

README.md

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The Headscale Operator simplifies the deployment and management of Headscale ins
1111
- **Declarative Configuration**: Define your entire Headscale setup as a Kubernetes Custom Resource
1212
- **Automatic Deployment**: Manages StatefulSets, Services, ConfigMaps, and PersistentVolumes
1313
- **API Key Management**: Automatic API key creation and rotation with configurable expiration
14+
- **ACL Policy Management**: Declarative auto-approval of subnet routes and exit nodes via the `HeadscaleAutoApprover` CRD
1415
- **Full Config Support**: Supports all Headscale configuration options including:
1516
- Database configuration (SQLite/PostgreSQL)
1617
- DERP server configuration
@@ -43,6 +44,13 @@ The Headscale Operator simplifies the deployment and management of Headscale ins
4344
- [Retrieving PreAuth Keys](#retrieving-preauth-keys)
4445
- [Using PreAuth Keys](#using-preauth-keys)
4546
- [PreAuth Key Examples](#preauth-key-examples)
47+
- [Managing Auto-Approve Routes](#managing-auto-approve-routes)
48+
- [Prerequisites](#prerequisites-1)
49+
- [Creating an Auto-Approver](#creating-an-auto-approver)
50+
- [Auto-Approver Properties](#auto-approver-properties)
51+
- [Viewing Auto-Approvers](#viewing-auto-approvers)
52+
- [Combining With PreAuth Keys](#combining-with-preauth-keys)
53+
- [Auto-Approver Examples](#auto-approver-examples)
4654
- [API Key Management](#api-key-management)
4755
- [Uninstallation](#uninstallation)
4856
- [Development](#development)
@@ -320,6 +328,170 @@ spec:
320328
- "tag:test"
321329
```
322330
331+
### Managing Auto-Approve Routes
332+
333+
The operator provides the `HeadscaleAutoApprover` custom resource to declaratively manage [auto-approved subnet routes and exit nodes](https://headscale.net/stable/ref/routes/#automatically-approve-routes-of-a-subnet-router) in your Headscale instance. Each `HeadscaleAutoApprover` contributes entries to the parent Headscale's policy document; the operator merges all auto-approvers targeting a Headscale and pushes the result via the gRPC `SetPolicy` API.
334+
335+
#### Prerequisites
336+
337+
Auto-approval lives in the Headscale policy document, which the operator can only write when Headscale is configured with `policy.mode: database`. The parent `Headscale` resource must also declare the tag owners that the auto-approver references. Both fields are set on the `Headscale` spec:
338+
339+
```yaml
340+
apiVersion: headscale.infrado.cloud/v1beta1
341+
kind: Headscale
342+
metadata:
343+
name: headscale-sample
344+
namespace: headscale
345+
spec:
346+
# ...
347+
config:
348+
policy:
349+
mode: database # required for SetPolicy
350+
acl_policy:
351+
tag_owners:
352+
"tag:router": ["admin@example.com"]
353+
"tag:exit": ["admin@example.com"]
354+
inline: | # optional base policy for acls/groups/hosts/ssh
355+
{
356+
"acls": [{"action": "accept", "src": ["*"], "dst": ["*:*"]}]
357+
}
358+
```
359+
360+
`acl_policy.inline` accepts JSON or HuJSON (comments and trailing commas are fine). The operator parses it, merges in `tag_owners` and any `HeadscaleAutoApprover` entries, then pushes the result via `SetPolicy`. The CR is the source of truth — `headscale policy get` shows the rendered output as strict JSON.
361+
362+
#### Creating an Auto-Approver
363+
364+
```yaml
365+
apiVersion: headscale.infrado.cloud/v1beta1
366+
kind: HeadscaleAutoApprover
367+
metadata:
368+
name: k8s-network
369+
namespace: headscale
370+
spec:
371+
# Reference to the Headscale instance (same namespace)
372+
headscaleRef: headscale-sample
373+
374+
# Routes a node carrying any of the listed tags will have
375+
# auto-approved when announced via --advertise-routes.
376+
routes:
377+
- cidr: 10.10.0.0/16
378+
tags: ["tag:router"]
379+
380+
# Tags whose nodes will be auto-approved as exit nodes.
381+
exitNodeTags: ["tag:exit"]
382+
```
383+
384+
Apply:
385+
386+
```sh
387+
kubectl apply -f headscaleautoapprover.yaml
388+
```
389+
390+
#### Auto-Approver Properties
391+
392+
- **headscaleRef**: Name of the Headscale instance in the same namespace. Required.
393+
- **routes**: List of `{cidr, tags}` pairs. A node registered with one of the listed tags has the matching CIDR auto-approved when it advertises that route. Each tag must be declared in the parent's `acl_policy.tag_owners`.
394+
- **exitNodeTags**: List of tags whose nodes are auto-approved as exit nodes when they advertise themselves as such.
395+
396+
At least one of `routes` or `exitNodeTags` must be specified. Multiple `HeadscaleAutoApprover` resources may target the same Headscale; the operator merges them deterministically into a single `autoApprovers` block.
397+
398+
#### Viewing Auto-Approvers
399+
400+
```sh
401+
# List all auto-approvers in a namespace
402+
kubectl get headscaleautoapprover -n headscale
403+
404+
# Inspect the Ready condition (status=True / reason=PolicyApplied means push succeeded)
405+
kubectl get headscaleautoapprover k8s-network -n headscale \
406+
-o jsonpath='{.status.conditions[?(@.type=="Ready")]}'
407+
408+
# Inspect the live merged policy stored in Headscale
409+
kubectl exec -n headscale -c headscale <headscale-pod> -- headscale policy get
410+
```
411+
412+
The `Ready` condition reasons are:
413+
414+
- `PolicyApplied` — the merged policy was successfully pushed via gRPC.
415+
- `HeadscaleNotFound` — the referenced Headscale doesn't exist in the same namespace.
416+
- `PolicyModeUnsupported` — the parent Headscale isn't configured with `policy.mode: database`.
417+
- `PolicyPushFailed` — the gRPC `SetPolicy` call returned an error (see `.status.conditions[].message` for details).
418+
419+
#### Combining With PreAuth Keys
420+
421+
An auto-approver only takes effect when a node is actually carrying the matching tag. The most common pattern is to issue a tagged preauth key for the subnet router:
422+
423+
```yaml
424+
apiVersion: headscale.infrado.cloud/v1beta1
425+
kind: HeadscalePreAuthKey
426+
metadata:
427+
name: subnet-router-key
428+
namespace: headscale
429+
spec:
430+
headscaleRef: headscale-sample
431+
headscaleUserRef: alice
432+
reusable: true
433+
tags: ["tag:router"]
434+
```
435+
436+
A node registered with this key, advertising e.g. `--advertise-routes=10.10.0.0/16`, will have its routes auto-approved.
437+
438+
#### Auto-Approver Examples
439+
440+
**Auto-approve a Kubernetes pod CIDR via a subnet router:**
441+
442+
```yaml
443+
apiVersion: headscale.infrado.cloud/v1beta1
444+
kind: HeadscaleAutoApprover
445+
metadata:
446+
name: pod-network
447+
spec:
448+
headscaleRef: headscale-sample
449+
routes:
450+
- cidr: 10.244.0.0/16
451+
tags: ["tag:router"]
452+
```
453+
454+
**Auto-approve a tagged exit node:**
455+
456+
```yaml
457+
apiVersion: headscale.infrado.cloud/v1beta1
458+
kind: HeadscaleAutoApprover
459+
metadata:
460+
name: home-exit-node
461+
spec:
462+
headscaleRef: headscale-sample
463+
exitNodeTags: ["tag:exit"]
464+
```
465+
466+
**Per-team segmentation — each team owns its own CIDR:**
467+
468+
```yaml
469+
---
470+
apiVersion: headscale.infrado.cloud/v1beta1
471+
kind: HeadscaleAutoApprover
472+
metadata:
473+
name: team-prod-routes
474+
namespace: headscale
475+
spec:
476+
headscaleRef: headscale-sample
477+
routes:
478+
- cidr: 10.10.0.0/16
479+
tags: ["tag:router"]
480+
---
481+
apiVersion: headscale.infrado.cloud/v1beta1
482+
kind: HeadscaleAutoApprover
483+
metadata:
484+
name: team-staging-routes
485+
namespace: headscale
486+
spec:
487+
headscaleRef: headscale-sample
488+
routes:
489+
- cidr: 10.20.0.0/16
490+
tags: ["tag:router"]
491+
```
492+
493+
The operator merges both into a single `autoApprovers.routes` map and pushes one consolidated policy.
494+
323495
### API Key Management
324496

325497
When API key auto-management is enabled, the sidecar creates a Kubernetes Secret containing the API key:

0 commit comments

Comments
 (0)