Publish privacy-safe Headscale deployment examples
This commit is contained in:
commit
08bb76a26b
8 changed files with 392 additions and 0 deletions
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Privacy-first public repository: ignore every file unless explicitly allowed.
|
||||||
|
*
|
||||||
|
|
||||||
|
# Public documentation and Compose template.
|
||||||
|
!.gitignore
|
||||||
|
!README.md
|
||||||
|
!docker-compose.yml.example
|
||||||
|
|
||||||
|
# Headscale templates.
|
||||||
|
!headscale/
|
||||||
|
headscale/*
|
||||||
|
!headscale/config/
|
||||||
|
headscale/config/*
|
||||||
|
!headscale/config/*.example
|
||||||
|
|
||||||
|
# Headplane templates.
|
||||||
|
!headplane/
|
||||||
|
headplane/*
|
||||||
|
!headplane/config/
|
||||||
|
headplane/config/*
|
||||||
|
!headplane/config/*.example
|
||||||
|
|
||||||
|
# Traefik templates.
|
||||||
|
!traefik/
|
||||||
|
traefik/*
|
||||||
|
!traefik/traefik.yml.example
|
||||||
|
!traefik/dynamic/
|
||||||
|
traefik/dynamic/*
|
||||||
|
!traefik/dynamic/*.example
|
||||||
111
README.md
Normal file
111
README.md
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
# Headscale + Headplane + Traefik
|
||||||
|
|
||||||
|
This repository contains privacy-safe example files for running a self-hosted
|
||||||
|
Headscale control plane with the Headplane web UI behind Traefik and automatic
|
||||||
|
Let's Encrypt certificates.
|
||||||
|
|
||||||
|
Only `README.md`, `.gitignore`, and files ending in `.example` are published.
|
||||||
|
Real configuration, credentials, databases, generated keys, certificates,
|
||||||
|
backups, and container images are ignored by design.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- **Headscale** provides the Tailscale-compatible control plane and embedded DERP/STUN.
|
||||||
|
- **Headplane** provides the administration UI at `/admin`.
|
||||||
|
- **Traefik** terminates HTTPS and routes requests to Headscale or Headplane.
|
||||||
|
|
||||||
|
The example pins Headscale `v0.29.2`, Headplane `0.7.0`, and Traefik `v3.7.9`.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- A Linux server with Docker Engine and Docker Compose v2
|
||||||
|
- A public IPv4 address
|
||||||
|
- A domain such as `headscale.example.com` pointing to that address
|
||||||
|
- Inbound `80/tcp`, `443/tcp`, and `3478/udp` allowed by the firewall
|
||||||
|
|
||||||
|
## Prepare configuration
|
||||||
|
|
||||||
|
Clone the repository, then create the ignored runtime files from the examples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp docker-compose.yml.example docker-compose.yml
|
||||||
|
cp headscale/config/config.yaml.example headscale/config/config.yaml
|
||||||
|
cp headscale/config/acl.hujson.example headscale/config/acl.hujson
|
||||||
|
cp headplane/config/config.yaml.example headplane/config/config.yaml
|
||||||
|
cp traefik/traefik.yml.example traefik/traefik.yml
|
||||||
|
cp traefik/dynamic/headscale.yml.example traefik/dynamic/headscale.yml
|
||||||
|
|
||||||
|
mkdir -p headscale/data headscale/run headplane/data traefik/letsencrypt
|
||||||
|
touch traefik/letsencrypt/acme.json
|
||||||
|
chmod 600 traefik/letsencrypt/acme.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the example values in the copied files:
|
||||||
|
|
||||||
|
- `headscale.example.com`: the public control-plane domain
|
||||||
|
- `dns.example.com`: the MagicDNS base domain
|
||||||
|
- `203.0.113.10`: the server's public IPv4 address
|
||||||
|
- `admin@example.com`: the Let's Encrypt account email
|
||||||
|
- `REPLACE_WITH_OPENSSL_RAND_HEX_32`: output of `openssl rand -hex 32`
|
||||||
|
|
||||||
|
Do not put real credentials in a file ending in `.example`.
|
||||||
|
|
||||||
|
## Start and configure
|
||||||
|
|
||||||
|
Validate the Compose model, then start Headscale and Traefik first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose config --quiet
|
||||||
|
docker compose up -d headscale traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
Create an API key for Headplane:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec headscale headscale apikeys create --expiration 365d
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the resulting key as `headscale.api_key` in the ignored
|
||||||
|
`headplane/config/config.yaml`, set a random cookie secret, and start Headplane:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d headplane
|
||||||
|
docker compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
Headscale is served at `https://headscale.example.com`; Headplane is served at
|
||||||
|
`https://headscale.example.com/admin`.
|
||||||
|
|
||||||
|
## Add a user and client
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec headscale headscale users create default
|
||||||
|
docker compose exec headscale headscale preauthkeys create \
|
||||||
|
--user default --reusable --expiration 24h
|
||||||
|
```
|
||||||
|
|
||||||
|
Join a client with the generated pre-auth key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tailscale up \
|
||||||
|
--login-server https://headscale.example.com \
|
||||||
|
--auth-key YOUR_PREAUTH_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optional OIDC
|
||||||
|
|
||||||
|
The Headscale and Headplane examples include commented OIDC sections. Copy and
|
||||||
|
uncomment them only in the ignored runtime configuration files. Keep the OIDC
|
||||||
|
client secret out of Git and use the same public issuer and client details in
|
||||||
|
both services.
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs -f headscale headplane traefik
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Back up `headscale/data`, `headplane/data`, and `traefik/letsencrypt` securely.
|
||||||
|
These directories contain private state and are intentionally never published.
|
||||||
61
docker-compose.yml.example
Normal file
61
docker-compose.yml.example
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
name: headscale
|
||||||
|
|
||||||
|
services:
|
||||||
|
headscale:
|
||||||
|
image: headscale/headscale:v0.29.2
|
||||||
|
container_name: headscale
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["serve", "-c", "/etc/headscale/config.yaml"]
|
||||||
|
volumes:
|
||||||
|
- ./headscale/config:/etc/headscale:ro
|
||||||
|
- ./headscale/data:/var/lib/headscale
|
||||||
|
- ./headscale/run:/var/run/headscale
|
||||||
|
expose:
|
||||||
|
- "8080"
|
||||||
|
ports:
|
||||||
|
- "3478:3478/udp"
|
||||||
|
labels:
|
||||||
|
- me.tale.headplane.target=headscale
|
||||||
|
networks:
|
||||||
|
- headscale-net
|
||||||
|
|
||||||
|
headplane:
|
||||||
|
image: ghcr.io/tale/headplane:0.7.0
|
||||||
|
container_name: headplane
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- headscale
|
||||||
|
volumes:
|
||||||
|
- ./headplane/config/config.yaml:/etc/headplane/config.yaml:ro
|
||||||
|
- ./headplane/data:/var/lib/headplane
|
||||||
|
- ./headscale/config:/etc/headscale:ro
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
expose:
|
||||||
|
- "3000"
|
||||||
|
networks:
|
||||||
|
- headscale-net
|
||||||
|
|
||||||
|
traefik:
|
||||||
|
image: traefik:v3.7.9
|
||||||
|
container_name: traefik
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- --configFile=/etc/traefik/traefik.yml
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "traefik", "healthcheck", "--ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 2s
|
||||||
|
retries: 10
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro
|
||||||
|
- ./traefik/dynamic:/etc/traefik/dynamic:ro
|
||||||
|
- ./traefik/letsencrypt:/letsencrypt
|
||||||
|
networks:
|
||||||
|
- headscale-net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
headscale-net:
|
||||||
|
driver: bridge
|
||||||
37
headplane/config/config.yaml.example
Normal file
37
headplane/config/config.yaml.example
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
server:
|
||||||
|
host: 0.0.0.0
|
||||||
|
port: 3000
|
||||||
|
base_url: https://headscale.example.com
|
||||||
|
cookie_secret: REPLACE_WITH_OPENSSL_RAND_HEX_32
|
||||||
|
cookie_secure: true
|
||||||
|
data_path: /var/lib/headplane
|
||||||
|
|
||||||
|
headscale:
|
||||||
|
url: http://headscale:8080
|
||||||
|
public_url: https://headscale.example.com
|
||||||
|
config_path: /etc/headscale/config.yaml
|
||||||
|
config_strict: true
|
||||||
|
api_key: REPLACE_WITH_HEADSCALE_API_KEY
|
||||||
|
|
||||||
|
# Optional OIDC example. Uncomment only in the ignored config.yaml file.
|
||||||
|
# oidc:
|
||||||
|
# issuer: https://auth.example.com/application/o/headscale/
|
||||||
|
# client_id: REPLACE_WITH_OIDC_CLIENT_ID
|
||||||
|
# client_secret: REPLACE_WITH_OIDC_CLIENT_SECRET
|
||||||
|
# use_pkce: true
|
||||||
|
# scope: openid email profile groups headplane_role
|
||||||
|
# default_role: member
|
||||||
|
# role_claim: headplane_role
|
||||||
|
|
||||||
|
integration:
|
||||||
|
agent:
|
||||||
|
enabled: false
|
||||||
|
docker:
|
||||||
|
enabled: true
|
||||||
|
container_label: me.tale.headplane.target=headscale
|
||||||
|
socket: unix:///var/run/docker.sock
|
||||||
|
kubernetes:
|
||||||
|
enabled: false
|
||||||
|
pod_name: ""
|
||||||
|
proc:
|
||||||
|
enabled: false
|
||||||
7
headscale/config/acl.hujson.example
Normal file
7
headscale/config/acl.hujson.example
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
// Permissive starter policy. Replace it with least-privilege rules.
|
||||||
|
"acls": [
|
||||||
|
{ "action": "accept", "src": ["*"], "dst": ["*:*"] }
|
||||||
|
],
|
||||||
|
"ssh": []
|
||||||
|
}
|
||||||
89
headscale/config/config.yaml.example
Normal file
89
headscale/config/config.yaml.example
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
server_url: https://headscale.example.com
|
||||||
|
listen_addr: 0.0.0.0:8080
|
||||||
|
metrics_listen_addr: 127.0.0.1:9090
|
||||||
|
grpc_listen_addr: 127.0.0.1:50443
|
||||||
|
grpc_allow_insecure: false
|
||||||
|
|
||||||
|
private_key_path: /var/lib/headscale/private.key
|
||||||
|
noise:
|
||||||
|
private_key_path: /var/lib/headscale/noise_private.key
|
||||||
|
|
||||||
|
prefixes:
|
||||||
|
v4: 100.64.0.0/10
|
||||||
|
v6: fd7a:115c:a1e0::/48
|
||||||
|
allocation: sequential
|
||||||
|
|
||||||
|
derp:
|
||||||
|
server:
|
||||||
|
enabled: true
|
||||||
|
region_id: 999
|
||||||
|
region_code: headscale
|
||||||
|
region_name: Headscale Embedded DERP
|
||||||
|
verify_clients: true
|
||||||
|
stun_listen_addr: 0.0.0.0:3478
|
||||||
|
private_key_path: /var/lib/headscale/derp_server_private.key
|
||||||
|
automatically_add_embedded_derp_region: true
|
||||||
|
# Replace this documentation-only address with the server's public IPv4.
|
||||||
|
ipv4: 203.0.113.10
|
||||||
|
urls:
|
||||||
|
- https://controlplane.tailscale.com/derpmap/default
|
||||||
|
paths: []
|
||||||
|
auto_update_enabled: false
|
||||||
|
|
||||||
|
disable_check_updates: false
|
||||||
|
node:
|
||||||
|
expiry: 0
|
||||||
|
ephemeral:
|
||||||
|
inactivity_timeout: 1h
|
||||||
|
|
||||||
|
database:
|
||||||
|
type: sqlite
|
||||||
|
debug: false
|
||||||
|
gorm:
|
||||||
|
prepare_stmt: true
|
||||||
|
parameterized_queries: true
|
||||||
|
skip_err_record_not_found: true
|
||||||
|
slow_threshold: 1000
|
||||||
|
sqlite:
|
||||||
|
path: /var/lib/headscale/db.sqlite
|
||||||
|
write_ahead_log: true
|
||||||
|
wal_autocheckpoint: 1000
|
||||||
|
|
||||||
|
# TLS is terminated by Traefik.
|
||||||
|
acme_url: https://acme-v02.api.letsencrypt.org/directory
|
||||||
|
acme_email: ""
|
||||||
|
tls_letsencrypt_hostname: ""
|
||||||
|
|
||||||
|
dns:
|
||||||
|
magic_dns: true
|
||||||
|
base_domain: dns.example.com
|
||||||
|
nameservers:
|
||||||
|
global:
|
||||||
|
- 1.1.1.1
|
||||||
|
- 8.8.8.8
|
||||||
|
|
||||||
|
policy:
|
||||||
|
mode: file
|
||||||
|
path: /etc/headscale/acl.hujson
|
||||||
|
|
||||||
|
log:
|
||||||
|
level: info
|
||||||
|
format: text
|
||||||
|
unix_socket: /var/run/headscale/headscale.sock
|
||||||
|
unix_socket_permission: "0770"
|
||||||
|
logtail:
|
||||||
|
enabled: false
|
||||||
|
taildrop:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Optional OIDC example. Uncomment only in the ignored config.yaml file.
|
||||||
|
# oidc:
|
||||||
|
# only_start_if_oidc_is_available: false
|
||||||
|
# issuer: https://auth.example.com/application/o/headscale/
|
||||||
|
# client_id: REPLACE_WITH_OIDC_CLIENT_ID
|
||||||
|
# client_secret: REPLACE_WITH_OIDC_CLIENT_SECRET
|
||||||
|
# scope: [openid, profile, email, groups]
|
||||||
|
# allowed_groups: [headscale-users]
|
||||||
|
# pkce:
|
||||||
|
# enabled: true
|
||||||
|
# method: S256
|
||||||
25
traefik/dynamic/headscale.yml.example
Normal file
25
traefik/dynamic/headscale.yml.example
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
http:
|
||||||
|
routers:
|
||||||
|
headplane:
|
||||||
|
rule: "Host(`headscale.example.com`) && PathPrefix(`/admin`)"
|
||||||
|
entryPoints: [websecure]
|
||||||
|
service: headplane
|
||||||
|
priority: 100
|
||||||
|
tls:
|
||||||
|
certResolver: le
|
||||||
|
headscale:
|
||||||
|
rule: "Host(`headscale.example.com`)"
|
||||||
|
entryPoints: [websecure]
|
||||||
|
service: headscale
|
||||||
|
priority: 10
|
||||||
|
tls:
|
||||||
|
certResolver: le
|
||||||
|
services:
|
||||||
|
headscale:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: http://headscale:8080
|
||||||
|
headplane:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: http://headplane:3000
|
||||||
33
traefik/traefik.yml.example
Normal file
33
traefik/traefik.yml.example
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
api:
|
||||||
|
dashboard: false
|
||||||
|
|
||||||
|
ping: {}
|
||||||
|
|
||||||
|
log:
|
||||||
|
level: INFO
|
||||||
|
|
||||||
|
accessLog: {}
|
||||||
|
|
||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: ":80"
|
||||||
|
http:
|
||||||
|
redirections:
|
||||||
|
entryPoint:
|
||||||
|
to: websecure
|
||||||
|
scheme: https
|
||||||
|
websecure:
|
||||||
|
address: ":443"
|
||||||
|
|
||||||
|
providers:
|
||||||
|
file:
|
||||||
|
directory: /etc/traefik/dynamic
|
||||||
|
watch: true
|
||||||
|
|
||||||
|
certificatesResolvers:
|
||||||
|
le:
|
||||||
|
acme:
|
||||||
|
email: admin@example.com
|
||||||
|
storage: /letsencrypt/acme.json
|
||||||
|
httpChallenge:
|
||||||
|
entryPoint: web
|
||||||
Loading…
Add table
Add a link
Reference in a new issue