Files
57_Wolve e23557b4fb feat(firewall): add deny-by-default host firewall (harden-firewall.sh)
Add a reusable iptables baseline that hardens hosts with ICMP + SSH
defaults and lets deployments register the ports they need. INPUT is
deny-by-default (loopback, established, ICMP, SSH on the configured port,
plus registered ports); OUTPUT stays open and FORWARD is left untouched so
Docker container networking is unaffected.
Persistence is native -- no boot hook. Rules are saved and restored by the
distro's own package (iptables/ip6tables on Alpine, iptables-persistent on
Debian, iptables-services on Alma) via the new oslib helpers
install_iptables / fw_save_cmd / fw_enable_restore. The saved ruleset
carries the INPUT->sshguard jump, so brute-force protection survives reboot
without the old sshguard-iptables hook.
A self-contained /usr/local/sbin/firewall-apply rebuilds INPUT from
declarative drop-ins under /etc/firewall/ports.d and runs the native save,
so deployments add a port without needing the repo present:
  printf '80/tcp\n443/tcp\n' > /etc/firewall/ports.d/mystack.rule
  /usr/local/sbin/firewall-apply
- SSH port read live from sshd_config (custom bastion ports just work);
  FW_SSH_SOURCE restricts the source CIDR; FW_ALLOW_PING gates echo
- harden-ssh.sh / harden-jumphost.sh install it when ENABLE_FIREWALL=1
  (default) and skip the sshguard-only hook; ENABLE_FIREWALL=0 keeps it
- cloud-init base.yml / jumphost.yml forward the toggle
- the four stack deploy.sh open_web_ports() register 80/443 via the
  firewall (ufw/firewalld kept as fallback); Docker-published ports bypass
  INPUT, so this is belt-and-braces and self-documenting
- README + cloud-init/README document the mechanism, Docker caveat, and the
  `disable` recovery path
2026-06-12 17:06:25 -05:00
..

headscale

Headscale — a self-hosted Tailscale control server — behind Caddy, with OIDC login delegated to pocket-id.

Prerequisite

Register an OIDC client in pocket-id's admin UI with redirect URI:

https://${HEADSCALE_DOMAIN}/oidc/callback

Then paste its OIDC_CLIENT_ID / OIDC_CLIENT_SECRET below.

Required .env values

Variable Notes
HEADSCALE_DOMAIN Public hostname (e.g. hs.example.com). Clients connect here over HTTPS.
ACME_EMAIL Let's Encrypt registration email.
TAILNET_DOMAIN MagicDNS suffix (e.g. tail.example.com). Must differ from HEADSCALE_DOMAIN.
POCKETID_DOMAIN OIDC issuer hostname (your pocket-id).
OIDC_CLIENT_ID / OIDC_CLIENT_SECRET From the pocket-id client above.

The deploy substitutes these into config.yaml from the embedded template. See .env.example.

Deploy

./automations.sh        # Deploy on this host → deploy: headscale

Or build + run the self-contained artifact:

./build.sh
scp deploy.sh root@host:
ssh root@host 'bash deploy.sh'
# non-interactive: pass HEADSCALE_DOMAIN, ACME_EMAIL, TAILNET_DOMAIN,
#   POCKETID_DOMAIN, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, SKIP_PROMPTS=1

Unattended provisioning: cloud-init.yml.

DNS for HEADSCALE_DOMAIN must resolve to the host and 80/443 be reachable before deploy.

Headplane (web UI)

Headplane ships in the stack, served by Caddy at https://HEADSCALE_DOMAIN/admin. It runs API-only — it talks to the headscale API with a key and does not get the Docker socket, so it can't control the host. deploy.sh mints the headscale API key automatically on first deploy (stored in .env) and generates headplane.yaml.

Login: to use OIDC via pocket-id, register a second OIDC client in pocket-id with redirect URI:

https://HEADSCALE_DOMAIN/admin/oidc/callback

and pass HEADPLANE_OIDC_CLIENT_ID / HEADPLANE_OIDC_CLIENT_SECRET (prompted by the launcher, or set in .env). Leave them blank to use headplane's API-key login instead. Pin the image with HEADPLANE_TAG (defaults to latest).

Gate /admin to a superuser group: restrict it at pocket-id — edit the headplane OIDC client and set its Allowed User Groups to your superuser group. Only members of that group can complete the OIDC login, so only they can reach /admin. (Enforced at the IdP, so it covers the UI regardless of headplane's own checks.)

headplane integration is version-sensitive; if the UI doesn't come up, check docker compose logs headplane and verify headplane.yaml / the minted HEADPLANE_HS_API_KEY in .env.

ACL policy

A starter ACL ships in policy.hujson (installed to $STACK_DIR/policy.hujson on first deploy; your edits survive re-deploys). headscale uses Tailscale's legacy acls format, not the newer grants syntax from the Tailscale admin console — the bundled policy is that default translated over (per-user self-access, a tag:shared everyone can reach, and SSH to your own devices). Some pieces (autogroup:self, the ssh block) are newer/experimental in headscale; a permissive fallback is included in the file.

$EDITOR /srv/headscale/policy.hujson
headscale policy check                          # validate (host CLI wrapper)
cd /srv/headscale && docker compose restart headscale   # apply