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 setheadscale_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_versionempty - the managed headscale version is pinned by headscale-admin (gRPC proto coupling); upgrade it from the Settings page.