Integrations

Headscale keeps its DNS settings and OIDC sign-in restrictions in its own config file, not behind its API. For Northplane to show or change them, it needs access to that file. To apply a change, Headscale has to restart, and an integration is what lets Northplane do that itself.

What each setup can do

Machines, users, auth keys and ACLs go through Headscale's API and work in every setup. The rest depends on how much access Northplane has:

SetupDNS settings and sign-in restrictions
No access to Headscale's configNot shown
Config file mounted read-onlyDNS settings shown read-only, sign-in restrictions hidden
Writable config file, no integrationSaved to the file, applied the next time Headscale restarts
Writable config file and integrationApplied right away, Northplane restarts Headscale

Sign-in restrictions edit the allowed_domains, allowed_groups and allowed_users options in Headscale's oidc section, so they only appear when Headscale signs people in through OIDC.

Sign-in restrictions in Northplane
Limiting Headscale sign-ins to an email domain

Running without access to the config was previously called Limited Mode. It's a fine setup when Headscale runs on another machine. The agent, Browser SSH and SSO all work without it.

Sharing Headscale's config file

Set headscale.config_path to where Northplane can find Headscale's config.yaml. In a container, that's the path inside Northplane's container. Changes need write access to the file, and saving keeps its comments.

headscale:
  config_path: "/etc/headscale/config.yaml"

Pick one integration below to apply changes. Enabling more than one turns them all off, with an error in the log.

Why a restart?

Headscale reloads only its ACL policy on SIGHUP. Every other setting in config.yaml is read once at startup, so changes need a full restart.

DNS records

DNS records are the exception: Northplane only edits them through the JSON file Headscale reads from dns.extra_records_path. Headscale watches that file, so record changes apply right away, without an integration or a restart.

# Headscale's config.yaml
dns:
  extra_records_path: /var/lib/headscale/extra-records.json

The file holds a JSON array of records:

[{ "name": "grafana.example.com", "type": "A", "value": "100.64.0.3" }]

The path comes from Headscale's config, and the file has to be writable for Northplane. When it sits at a different path in Northplane's container, set headscale.dns_records_path to that path. Setting dns_records_path without an extra_records_path in Headscale's config stops Northplane from starting.

Records listed inline under Headscale's dns.extra_records are shown read-only. Move them into the JSON file to manage them from Northplane.

Docker

Use this when Headscale runs in a Docker container. The integration restarts the container through the Docker API, then waits for Headscale to come back. It needs:

  • Headscale's config file, mounted into both containers
  • The Docker socket, or a socket proxy such as docker-socket-proxy that allows listing containers and POST requests (CONTAINERS=1 and POST=1)
  • A way to find the Headscale container: the me.tale.headplane.target=headscale label by default
services:  headplane:    image: ghcr.io/tale/headplane:latest    container_name: headplane    restart: unless-stopped    ports:      - "3000:3000"    volumes:      - "/path/to/headplane/config.yaml:/etc/headplane/config.yaml"      - "/path/to/data/storage:/var/lib/headplane"      # Headscale's config file. The path inside this container must match      # `headscale.config_path` in Northplane's config.      - "/path/to/headscale/config.yaml:/etc/headscale/config.yaml"      # To edit DNS records, mount the file from Headscale's      # dns.extra_records_path too. If the path in this container differs,      # set `headscale.dns_records_path` in Northplane's config.      - "/path/to/headscale/dns_records.json:/etc/headscale/dns_records.json"      # The Docker socket (or a socket proxy), used to restart Headscale      - "/var/run/docker.sock:/var/run/docker.sock"  headscale:    image: headscale/headscale:0.29.5    container_name: headscale    restart: unless-stopped    command: serve    labels:      # How Northplane finds the Headscale container      me.tale.headplane.target: headscale    ports:      - "8080:8080"    volumes:      # The same files, at the same paths, as in Northplane's container      - "/path/to/headscale/config.yaml:/etc/headscale/config.yaml"      - "/path/to/headscale/dns_records.json:/etc/headscale/dns_records.json"      - "/path/to/headscale/data/storage:/var/lib/headscale"

Then enable the integration in Northplane's config:

integration:
  docker:
    enabled: true
    # The defaults, shown for reference
    container_label: "me.tale.headplane.target=headscale"
    socket: "unix:///var/run/docker.sock"

To select the container by name instead, set container_name, which takes precedence over the label. For a remote Docker daemon, set socket to a tcp:// address; HTTPS isn't supported.

Docker API 1.24 (Docker Engine 1.12) and newer are supported, including Podman's Docker-compatible socket.

Kubernetes

Use this when Northplane runs as a sidecar in the same pod as Headscale. The two containers share a process namespace, so Northplane can stop Headscale with SIGTERM. The kubelet then restarts the container, and Northplane waits for Headscale to come back.

The pod needs:

  • shareProcessNamespace: true
  • Headscale's config file on a writable volume shared by both containers. ConfigMap mounts are read-only, so copy the file onto a writable volume first, for example with an init container
  • Northplane running as the same user as Headscale, or with the KILL capability, so it can signal Headscale
  • The pod's name, passed to Northplane through the Downward API
spec:
  shareProcessNamespace: true
  containers:
    - name: headplane
      image: ghcr.io/tale/headplane:latest
      env:
        - name: HEADPLANE_INTEGRATION__KUBERNETES__POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
integration:
  kubernetes:
    enabled: true

At startup, Northplane checks through the Kubernetes API that the pod shares its process namespace. Its service account needs permission to get pods in its namespace. To skip the check and the permission, set integration.kubernetes.validate_manifest: false.

proc

Use this when Northplane and Headscale run directly on the same Linux host. For each change, Northplane finds the headscale serve process in /proc, stops it with SIGTERM and waits for a new one to come up healthy.

That only works if something restarts Headscale. Headscale's own systemd unit sets Restart=always, and so does the NixOS module. Without a supervisor, Headscale stays down after the first change, and Northplane reports that it didn't come back.

Signaling Headscale also takes permission: run both as the same user, or Northplane as root.

integration:
  proc:
    enabled: true

On this page