Installation

Ansible

Use the bundled role to install the binary, render config + secrets, set up systemd, and optionally pre-configure the managed headscale.

The repo ships an Ansible role at examples/headscale-admin-ansible/ that automates the whole bare-metal install: it installs the binary, writes config.yaml plus an env file for HSA_* secrets, drops a hardened systemd unit (with logrotate), and can optionally pre-seed the managed headscale’s headscale.yaml. The role README is the full variable reference; this page is the orientation.

What it creates

Path Purpose
headscale-admin:headscale-admin system user / group
/opt/headscale-admin/config.yaml non-secret config (root:group, 0640)
/opt/headscale-admin/env HSA_* secrets (root:group, 0640)
/opt/headscale-admin/bin/ binaries (headscale.bin_dir)
/opt/headscale-admin/data/ headscale.work_dir - headscale data
/var/log/headscale-admin/ logs (rotated via logrotate)

All paths are overridable. Secrets (session_secret, OIDC client secret, headscale-pf token) are written only to the env file as HSA_* overrides - never into config.yaml. Store them with Ansible Vault.

1. Installing the binary

Pick the source with headscale_admin_install_source:

# GitHub release (default)
headscale_admin_install_source: github
headscale_admin_version: latest          # or a tag like v0.3.0

# …or a direct URL
# headscale_admin_install_source: url
# headscale_admin_download_url: "https://example.com/headscale-admin-{{ headscale_admin_os }}-{{ headscale_admin_arch }}"

# …or S3 / MinIO (needs the amazon.aws collection)
# headscale_admin_install_source: s3
# headscale_admin_s3_bucket: my-bucket
# headscale_admin_s3_object: headscale-admin/headscale-admin-linux-amd64

Integrity: set headscale_admin_sha256 for an explicit checksum, or rely on the release checksums.txt (headscale_admin_checksum_required: true to fail when none can be determined).

2. Configuring headscale-admin

Non-secret settings map to config.yaml; secrets go to the env file automatically.

headscale_admin_server_addr: ":443"
headscale_admin_public_url: "https://hs.example.com"   # must match how clients connect

# Built-in TLS (Let's Encrypt)
headscale_admin_tls_mode: acme
headscale_admin_tls_acme_email: [email protected]
headscale_admin_tls_acme_hosts: ["hs.example.com"]

# Auth: the session-cookie HMAC secret (kept out of config.yaml, >= 32 chars)
headscale_admin_session_secret: "{{ vault_hsa_session_secret }}"

To use OIDC SSO for the console instead of local passwords, set the headscale_admin_oidc_* variables, including the RBAC mapping (admin_emails also pins those users to admin, so they skip the admin create bootstrap):

headscale_admin_oidc_enabled: true
headscale_admin_oidc_issuer: "https://accounts.example.com"
headscale_admin_oidc_client_id: "headscale-admin"
headscale_admin_oidc_client_secret: "{{ vault_hsa_oidc_secret }}"   # -> env file
headscale_admin_oidc_redirect_url: "https://hs.example.com/admin/api/auth/oidc/callback"
headscale_admin_oidc_admin_emails: ["[email protected]"]
headscale_admin_oidc_admin_groups: ["hs-admins"]
headscale_admin_oidc_operator_groups: ["hs-operators"]
headscale_admin_oidc_groups_claim: "groups"

3. Pre-configuring the managed headscale (optional)

By default headscale-admin generates headscale.yaml on first run and owns it (the Settings UI edits it). To pre-seed it from Ansible, set headscale_preconfigure: true and the DNS / DERP / policy knobs you care about - the listeners, socket paths, and trusted proxies are fixed to what headscale-admin expects (and there is intentionally no TLS section, since TLS is terminated at the front):

headscale_preconfigure: true
headscale_preconfigure_force: false      # false: don't clobber UI edits on re-run
headscale_dns_base_domain: "ts.example.com"
headscale_dns_nameservers: ["1.1.1.1", "9.9.9.9"]
headscale_policy_mode: file              # or "database" for the visual editor

headscale’s own OIDC (so Tailscale clients authenticate via your IdP - independent of the console login above) lives under the headscale_oidc_* variables. You can also pin headscale’s noise/DERP keys via Vault. See the role README for the DERP-map, key-pinning, and client-OIDC details.

4. External-user sync (optional)

Enable the User sync page (the bundled headscale-pf) to fill ACL group: members from an identity source. Non-secret settings go to config.yaml; tokens / LDAP bind passwords go to the env file:

headscale_admin_pf_enabled: true
headscale_admin_pf_source: "jc"                  # jc | ak | kk | ldap
headscale_admin_pf_token: "{{ vault_pf_token }}" # -> env file
# Authentik / Keycloak / LDAP also need headscale_admin_pf_endpoint (+ ldap/keycloak settings)

Example play

- hosts: headscale
  become: true
  roles:
    - role: headscale-admin
      vars:
        headscale_admin_public_url: "https://hs.example.com"
        headscale_admin_server_addr: ":443"
        headscale_admin_tls_mode: acme
        headscale_admin_tls_acme_email: [email protected]
        headscale_admin_tls_acme_hosts: ["hs.example.com"]
        headscale_admin_session_secret: "{{ vault_hsa_session_secret }}"
        headscale_preconfigure: true
        headscale_dns_base_domain: "ts.example.com"

Notes

  • First start needs outbound network (it downloads the pinned headscale release into bin/).
  • With auth enabled, headscale-admin refuses to start until an admin exists. After the first install, either bootstrap one on the host - sudo -u headscale-admin headscale-admin admin create --config /opt/headscale-admin/config.yaml - or set headscale_admin_oidc_admin_emails (no password needed under OIDC).
  • The systemd unit uses KillMode=control-group, so the supervised headscale stops together with headscale-admin.
  • Leave headscale_admin_headscale_version empty - the managed headscale version is pinned by headscale-admin (gRPC proto coupling); upgrade it from the Settings page.