Skip to content

YouSysAdmin/headscale-pf

Repository files navigation

headscale-pf

Stand with Ukraine

headscale-pf is a command-line tool and Docker container that automates the generation and management of Headscale Access Control List (ACL) policy files. It synchronizes user group memberships from external identity providers into Headscale policy definitions, eliminating the need to manually maintain user lists in policy files.

The tool reads a policy template file (in HJSON format), queries one or more identity providers for group membership information, merges that data into the template, and outputs a complete policy file (in JSON format) that can be applied to a Headscale server.


Compatibility

  • For Headscale < 0.26, use headscale-pf version 0.0.3.
  • From 1.0.0 onward, headscale-pf uses a new user definition format compatible with Headscale ≥ 0.26.

Supported Sources

IMPORTANT: This utility was developed for our own use and exclusively for use with Jumpcloud. Other sources were added at the request of the community.

If you have a problem, please create an issue.

  • Jumpcloud
  • Authentik
  • LDAP / OpenLDAP / Active Directory** (tested with Jumpcloud LDAP)
  • Keycloak

Planned:

  • Auth0
  • CSV
  • JSON
  • REMOTE JSON
  • ...

Installation

Using Go

go install github.com/YouSysAdmin/headscale-pf/cmd/headscale-pf@latest

Using install script

curl -L https://raw.githubusercontent.com/YouSysAdmin/headscale-pf/master/scripts/install.sh | bash

Options:

  • -b → install directory (default: ~/.bin)
  • -d → enable debug logging
  • [tag] → optional release tag (latest if omitted)

Brew

brew install yousysadmin/apps/headscale-pf

Development

Install the git hooks after cloning (lefthook required):

make hooks

On every commit this runs gofmt, go vet, go build, and go test (see lefthook.yml). Bypass in a pinch with git commit --no-verify. Other useful targets: make build, make test, make lint.


Usage

headscale-pf [command] [flags]

Commands

  • prepare – fetch group membership and generate a Headscale policy
  • completion – generate autocomplete script for your shell
  • help – show help for any command

Sources

  • jc, jumpcloud - Jumpcloud
  • ak, authentik - Authentik
  • ldap, ldaps - LDAP
  • kk, keycloak - Keycloak

Global Flags

Flag / Option Description Env var Default
--source string Source type (jc, ak, ldap, kk) PF_SOURCE
--endpoint string Source endpoint PF_ENDPOINT
--token string API token PF_TOKEN
--input-policy string Input policy template ./policy.hjson
--output-policy string Output policy file ./current.hjson
--output-format string Output format: auto, hjson, or json auto
--ldap-base-dn string LDAP base DN PF_LDAP_BASE_DN
--ldap-bind-dn string LDAP bind DN PF_LDAP_BIND_DN
--ldap-bind-password string LDAP password PF_LDAP_BIND_PASSWORD
--ldap-default-email-domain LDAP default email domain PF_LDAP_DEFAULT_USER_EMAIL_DOMAIN
--keycloak-realm string Keycloak Realm PF_KEYCLOAK_REALM
--no-color Disable colored output
-v, --version Show version

Output format

--output-format controls how the prepared policy is written:

  • auto (default) — detect from the input template: if the template is strict JSON (no comments or trailing commas) the output is json; otherwise it is hjson.
  • hjson — the output mirrors the HuJSON template one-to-one: comments, key order, and formatting are preserved byte-for-byte; only group members are filled in.
  • json — plain RFC-8259 JSON, pretty-printed with a 2-space indent (comments and trailing commas removed, key order preserved).

Headscale reads either format. The flag only changes serialization — it does not rename the output file, so pair an explicit format with a matching --output-policy (e.g. --output-format json --output-policy ./current.json).


Examples

Jumpcloud

headscale-pf prepare \
            --source=jc \
            --token=$JC_TOKEN \
            --input-policy=policy.hjson \
            --output-policy=out.json

headscale policy set -f out.json

Authentik

headscale-pf prepare \
            --source=ak \
            --endpoint="https://auth.example.com" \
            --token=$AK_TOKEN \
            --input-policy=policy.hjson \
            --output-policy=out.json

headscale policy set -f out.json

LDAP

headscale-pf prepare \
            --source=ldap \
            --endpoint=ldap.example.com:636 \
            --ldap-base-dn="ou=Users,dc=example,dc=com" \
            --ldap-bind-dn="cn=service,ou=Users,dc=example,dc=com" \
            --ldap-bind-password=$LDAP_PASS \
            --ldap-default-email-domain="example.com" \
            --input-policy=policy.hjson \
            --output-policy=out.json

headscale policy set -f out.json

Keycloak

# Get API Token
# Replace the url/username/password with your own.
KK_TOKEN=$(curl -X POST \
  --url http://auth.example.com/realms/master/protocol/openid-connect/token \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data client_id=admin-cli \
  --data grant_type=password \
  --data username=admin \
  --data password=admin | jq -r '.access_token')

# Prepare policy
headscale-pf prepare \
            --source=kk \
            --endpoint="https://auth.example.com" \
            --token=$KK_TOKEN \
            --keycloak-realm="master" \
            --input-policy=policy.hjson \
            --output-policy=out.json

# Apply policy
headscale policy set -f out.json

Adding a New Source

  1. Create a new file under internal/sources/.
  2. Implement the interface:
    • GetGroupByName(groupName string) (*models.Group, error)
    • GetGroupMembers(groupID string) ([]models.User, error)
    • GetUserInfo(userID string) (models.User, error)
  3. Register it in internal/sources/sources.go.

Use CI

Use the Headscale-PF Docker image inside your CI (the Docker image contains the Headscale CLI)

PF_TOKEN - Jumpcloud/etc. API token
HEADSCALE_CLI_ADDRESS - Headscale GRPC Endpoint
HEADSCALE_CLI_API_KEY - Headscale GRPC Token

# .github/workflows/policy.yaml
name: Headscale Policy

on:
  workflow_dispatch:
    inputs:
      apply_policy:
        description: "Apply policy after generation?"
        type: boolean
        required: true
        default: true
      save_artifact:
        description: "Save generated policy.json as artifact?"
        type: boolean
        required: true
        default: true

permissions:
  contents: read

jobs:
  generate-policy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Run policy prepare
        env:
          IMAGE: ghcr.io/yousysadmin/headscale-pf:latest
          PF_TOKEN: ${{ secrets.PF_TOKEN }}
          HEADSCALE_CLI_ADDRESS: ${{ secrets.HEADSCALE_CLI_ADDRESS }}
          HEADSCALE_CLI_API_KEY: ${{ secrets.HEADSCALE_CLI_API_KEY }}
          APPLY_INPUT: ${{ github.event.inputs.apply_policy }}
        run: |
          set -euxo pipefail

          docker pull "$IMAGE"

          APPLY_FLAG=""
          if [ "${APPLY_INPUT}" = "true" ]; then
            APPLY_FLAG="-e APPLY_POLICY=1"
          fi

          # fix: headscale config file should be present anyway
          touch config.yaml

          docker run --rm \
            --user "$(id -u)":"$(id -g)" \
            -e PF_TOKEN="${PF_TOKEN}" \
            ${APPLY_FLAG} \
            -e HEADSCALE_CLI_ADDRESS="${HEADSCALE_CLI_ADDRESS}" \
            -e HEADSCALE_CLI_API_KEY="${HEADSCALE_CLI_API_KEY}" \
            -e INPUT_POLICY="/work/policy.hjson" \
            -e OUTPUT_POLICY="/work/current.hjson" \
            -e SOURCE="jc" \
            -e RETRIES="5" \
            -e RETRY_DELAY_SEC="6" \
            -v "$GITHUB_WORKSPACE:/work" \
            "$IMAGE"

      - name: Upload current.json artifact
        if: ${{ github.event.inputs.save_artifact == 'true' }}
        uses: actions/upload-artifact@v4
        with:
          name: current.hjson
          path: ${{ github.workspace }}/current.hjson

About

Obtaining information about groups and group members from external sources and populating groups for Headscale policy.

Resources

License

Stars

17 stars

Watchers

1 watching

Forks

Sponsor this project

Packages

 
 
 

Contributors

Languages