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