diff --git a/.github/workflows/headscale-config-checker.yml b/.github/workflows/headscale-config-checker.yml index ce93101..2b26ba3 100644 --- a/.github/workflows/headscale-config-checker.yml +++ b/.github/workflows/headscale-config-checker.yml @@ -41,7 +41,7 @@ jobs: run: | # Simple approach: extract keys and apply ignores extract_keys() { - grep -E "^[[:space:]]*[^#].*:" "$1" | \ + grep -E "^[[:space:]]*[^#\-].*:" "$1" | \ sed 's/:[[:space:]]*.*$//' | \ sed 's/^[[:space:]]*//' | \ sort -u diff --git a/README.md b/README.md index 407a03a..f96f9d5 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,91 @@ -# Headscale on an immutable Docker image - -Deploy [Headscale][headscale-wob] using a "serverless" immutable docker image with real-time [Litestream][litestream-wob] database backup and (by default) inbuilt [Caddy][caddy-wob] SSL termination, using a miniscule [Alpine Linux][alpine-linux-wob] base image. Provides a stateless [headscale-admin][headscale-admin-wob] panel at `/admin/`. - -## Included upstream versions - -| Tool | Upstream Repository | Version | -| --- | --- | --- | -| [`Alpine Linux`][alpine-linux-wob] | [Alpine Linux Repo][alpine-linux-repo] | [`v3.23.3`](https://git.alpinelinux.org/aports/log/?h=v3.23.3) | -| [`Headscale`][headscale-wob] | [Headscale Repo][headscale-repo] | [`v0.28.0`](https://github.com/juanfont/headscale/releases/tag/v0.28.0) | -| [`Headscale-Admin`][headscale-admin-wob] | [Headscale-Admin Repo][headscale-admin-repo] | [`7da5aa3`](https://github.com/serein-213/headscale-admin-il18n/commit/7da5aa3f89cb1027d086256c176cdb2112d6641c) | -| [`Litestream`][litestream-wob] | [Litestream Repo][litestream-repo] | [`0.5.11`](https://github.com/benbjohnson/litestream/releases/tag/v0.5.11) | -| [`Caddy`][caddy-wob] | [Caddy Repo][caddy-repo] | [`v2.11.2`](https://github.com/caddyserver/caddy/releases/tag/v2.11.2) | - -DEPRECATION NOTICE: `Headscale-Admin` is deprecated in this release as it appears to have been abandoned by upstream. We have moved to a fork with patches so we can take advantage of the improvements in Headscale's `0.28.X` release, but are actively testing replacement admin panels before Headscale's `0.29.X` releases. - -## Versioning - -Because of the mix of upstream tools included, this project will be tagged using the versioning style `YYYY.MM.REVISION`. - -All development should be done against the `develop` branch, `main` is deemed "stable". - -## Requirements - -* Cloudflare DNS for [ACME `DNS-01` authentication][dns-01-challenge] (Can be deliberately disabled to use [`HTTP-01` authentication][http-01-challenge] instead, or HTTPS can be disabled entirely if you plan to use an external termination point.) -* S3(Alike)/Azure for [Litestream][litestream-wob] (Can be deliberately disabled for full ephemerality, or if you plan to use persistent storage) - -## Installation - -Populate your environment variables according to `templates/secrets.template.env` - -The container entrypoint script will guide you on any errors. - -## Deployment and user creation - -Once app is deployed and green, [generate an API Key][headscale-usage] in order to use the admin interface. - -```console -headscale apikeys create -``` - -Navigate to the admin gui on `/admin/` and set up your groups, ACLs, tags etc. - -## Final configuration - -Now that Headscale is running, to have a 100% reproducible setup we need to ensure that private noise key generated during installation is persisted. Within the same console from previous step, print out the server's key: - -```console -cat /data/noise_private.key -``` - -Then set `HEADSCALE_NOISE_PRIVATE_KEY` to the value obtained above. - -Note that applying this will cause your application to restart, but afterwards no other change will be necessary. - -## Known to run on - -* Azure Container Apps -* [Fly.io][fly-io-instructions] -* ??? Let us know! - -[alpine-linux-wob]: https://www.alpinelinux.org/ -[alpine-linux-repo]: https://gitlab.alpinelinux.org/alpine -[caddy-wob]: https://caddyserver.com/ -[caddy-repo]: https://github.com/caddyserver/caddy -[headscale-admin-wob]: https://github.com/serein-213/headscale-admin-il18n -[headscale-admin-repo]: https://github.com/serein-213/headscale-admin-il18n -[headscale-wob]: https://headscale.net/ -[headscale-repo]: https://github.com/juanfont/headscale -[litestream-wob]: https://litestream.io/ -[litestream-repo]: https://github.com/benbjohnson/litestream - -[dns-01-challenge]: https://letsencrypt.org/docs/challenge-types/#dns-01-challenge -[http-01-challenge]: https://letsencrypt.org/docs/challenge-types/#http-01-challenge -[headscale-usage]: https://headscale.net/stable/ref/remote-cli/#create-an-api-key -[fly-io-instructions]: docs/backends/fly-io.md +# Headscale on an immutable Docker image + +Deploy [Headscale][headscale-wob] using a "serverless" immutable docker image with real-time [Litestream][litestream-wob] database backup and (by default) inbuilt [Caddy][caddy-wob] SSL termination, using a miniscule [Alpine Linux][alpine-linux-wob] base image. Provides a stateless [headscale-admin][headscale-admin-wob] panel at `/admin/`. + +## Included upstream versions + +| Tool | Upstream Repository | Version | +| --- | --- | --- | +| [`Alpine Linux`][alpine-linux-wob] | [Alpine Linux Repo][alpine-linux-repo] | [`v3.23.3`](https://git.alpinelinux.org/aports/log/?h=v3.23.3) | +| [`Headscale`][headscale-wob] | [Headscale Repo][headscale-repo] | [`v0.28.0`](https://github.com/juanfont/headscale/releases/tag/v0.28.0) | +| [`Headscale-Admin`][headscale-admin-wob] | [Headscale-Admin Repo][headscale-admin-repo] | [`7da5aa3`](https://github.com/serein-213/headscale-admin-il18n/commit/7da5aa3f89cb1027d086256c176cdb2112d6641c) | +| [`Litestream`][litestream-wob] | [Litestream Repo][litestream-repo] | [`0.5.11`](https://github.com/benbjohnson/litestream/releases/tag/v0.5.11) | +| [`Caddy`][caddy-wob] | [Caddy Repo][caddy-repo] | [`v2.11.2`](https://github.com/caddyserver/caddy/releases/tag/v2.11.2) | + +DEPRECATION NOTICE: `Headscale-Admin` is deprecated in this release as it appears to have been abandoned by upstream. We have moved to a fork with patches so we can take advantage of the improvements in Headscale's `0.28.X` release, but are actively testing replacement admin panels before Headscale's `0.29.X` releases. + +## Versioning + +Because of the mix of upstream tools included, this project will be tagged using the versioning style `YYYY.MM.REVISION`. + +All development should be done against the `develop` branch, `main` is deemed "stable". + +## Requirements + +* Cloudflare DNS for [ACME `DNS-01` authentication][dns-01-challenge] (Can be deliberately disabled to use [`HTTP-01` authentication][http-01-challenge] instead, or HTTPS can be disabled entirely if you plan to use an external termination point.) +* S3(Alike)/Azure for [Litestream][litestream-wob] (Can be deliberately disabled for full ephemerality, or if you plan to use persistent storage) + +## Installation + +Populate your environment variables according to `templates/secrets.template.env` + +The container entrypoint script will guide you on any errors. + +## Configuring upstream/global nameservers + +You can now control the nameservers exposed to clients via the `GLOBAL_NAMESERVERS` environment variable. Provide a space-separated list of IP addresses (IPv4 or IPv6). If omitted, the container falls back to the defaults defined in `scripts/defaults.sh`. + +The entrypoint converts the list into a YAML flow-style sequence and injects it into the Headscale config, e.g. `global: [ "1.1.1.1", "8.8.8.8" ]` so there are no YAML indentation issues regardless of the number of entries. + +Example (set in Fly config or your environment): + +```toml +[env] +GLOBAL_NAMESERVERS = "94.140.14.15 94.140.15.16 2a10:50c0::bad1:ff 2a10:50c0::bad2:ff" +``` + +The script performs permissive validation (allows IPv4/IPv6 characters). If you need stricter validation or alternative input formats (commas, JSON), say so and I'll update the parser. + +## Deployment and user creation + +Once app is deployed and green, [generate an API Key][headscale-usage] in order to use the admin interface. + +```console +headscale apikeys create +``` + +Navigate to the admin gui on `/admin/` and set up your groups, ACLs, tags etc. + +## Final configuration + +Now that Headscale is running, to have a 100% reproducible setup we need to ensure that private noise key generated during installation is persisted. Within the same console from previous step, print out the server's key: + +```console +cat /data/noise_private.key +``` + +Then set `HEADSCALE_NOISE_PRIVATE_KEY` to the value obtained above. + +Note that applying this will cause your application to restart, but afterwards no other change will be necessary. + +## Known to run on + +* Azure Container Apps +* [Fly.io][fly-io-instructions] +* ??? Let us know! + +[alpine-linux-wob]: https://www.alpinelinux.org/ +[alpine-linux-repo]: https://gitlab.alpinelinux.org/alpine +[caddy-wob]: https://caddyserver.com/ +[caddy-repo]: https://github.com/caddyserver/caddy +[headscale-admin-wob]: https://github.com/serein-213/headscale-admin-il18n +[headscale-admin-repo]: https://github.com/serein-213/headscale-admin-il18n +[headscale-wob]: https://headscale.net/ +[headscale-repo]: https://github.com/juanfont/headscale +[litestream-wob]: https://litestream.io/ +[litestream-repo]: https://github.com/benbjohnson/litestream + +[dns-01-challenge]: https://letsencrypt.org/docs/challenge-types/#dns-01-challenge +[http-01-challenge]: https://letsencrypt.org/docs/challenge-types/#http-01-challenge +[headscale-usage]: https://headscale.net/stable/ref/remote-cli/#create-an-api-key +[fly-io-instructions]: docs/backends/fly-io.md diff --git a/scripts/ci-defaults.sh b/scripts/ci-defaults.sh index 42a4ef8..7f3de52 100644 --- a/scripts/ci-defaults.sh +++ b/scripts/ci-defaults.sh @@ -14,3 +14,8 @@ export MAGIC_DNS="$headscale_magic_dns_default" export IP_ALLOCATION="$headscale_ip_allocation_default" export HEADSCALE_EXTRA_RECORDS_PATH="$headscale_extra_records_path_default" export EPHEMERAL_NODE_INACTIVITY_TIMEOUT="$headscale_ephemeral_node_inactivity_timeout_default" +export GLOBAL_NAMESERVERS_YAML=" + - 1.1.1.1 + - 1.0.0.1 + - 2606:4700:4700::1111 + - 2606:4700:4700::1001" diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 98dbaef..5bdcba0 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -177,6 +177,49 @@ check_ip_address_settings() { fi } + +####################################### +# Build YAML flow list for GLOBAL_NAMESERVERS +# Produces GLOBAL_NAMESERVERS_YAML like: [ "1.1.1.1", "8.8.8.8" ] +####################################### +build_global_nameservers_yaml() { + local -a ns_array=() + local -a items=() + local ip + + if [[ -n "${GLOBAL_NAMESERVERS:-}" ]]; then + read -r -a ns_array <<< "${GLOBAL_NAMESERVERS}" + else + # Use defaults from defaults.sh if available + ns_array=("${headscale_global_nameservers_default[@]:-}") + fi + + if [[ ${#ns_array[@]} -eq 0 ]]; then + export GLOBAL_NAMESERVERS_YAML='[]' + return + fi + + for ip in "${ns_array[@]}"; do + # permissive validation: allow hex digits, dots and colons (IPv4/IPv6) + if [[ ! ${ip} =~ ^[0-9A-Fa-f:\.]+$ ]]; then + log_warn "Skipping invalid GLOBAL_NAMESERVERS entry: ${ip}" + continue + fi + items+=("\"${ip}\"") + done + + if [[ ${#items[@]} -eq 0 ]]; then + export GLOBAL_NAMESERVERS_YAML='[]' + return + fi + + # join items with ', ' + local joined + printf -v joined '%s, ' "${items[@]}" + joined=${joined%, } + export GLOBAL_NAMESERVERS_YAML="[ ${joined} ]" +} + ####################################### # Perform all Headscale environment variable checks ####################################### @@ -374,6 +417,8 @@ check_config_files() { check_caddy_environment_variables + build_global_nameservers_yaml + # Ensure all template variables are exported for envsubst local template_vars=( "ACME_EAB_BLOCK" @@ -387,6 +432,7 @@ check_config_files() { "IP_ALLOCATION" "HEADSCALE_EXTRA_RECORDS_PATH" "EPHEMERAL_NODE_INACTIVITY_TIMEOUT" + "GLOBAL_NAMESERVERS_YAML" ) for var in "${template_vars[@]}"; do export "${var}=${!var}" diff --git a/scripts/defaults.sh b/scripts/defaults.sh index 667b1d2..d3e09cb 100644 --- a/scripts/defaults.sh +++ b/scripts/defaults.sh @@ -16,3 +16,7 @@ headscale_override_local_dns_default="true" caddyfile_cleartext=/etc/caddy/Caddyfile-http caddyfile_https=/etc/caddy/Caddyfile-https headscale_config="/etc/headscale/config.yaml" + +# Default global nameservers (bash array). Can be overridden by setting GLOBAL_NAMESERVERS +# as a space-separated string in the environment (e.g. "1.1.1.1 8.8.8.8"). +headscale_global_nameservers_default=("1.1.1.1" "1.0.0.1" "2606:4700:4700::1111" "2606:4700:4700::1001") diff --git a/templates/headscale.template.yaml b/templates/headscale.template.yaml index 5e59fa5..cab2b3b 100644 --- a/templates/headscale.template.yaml +++ b/templates/headscale.template.yaml @@ -282,11 +282,7 @@ dns: # List of DNS servers to expose to clients. nameservers: - global: - - 1.1.1.1 - - 1.0.0.1 - - 2606:4700:4700::1111 - - 2606:4700:4700::1001 + global: $GLOBAL_NAMESERVERS_YAML # NextDNS (see https://tailscale.com/kb/1218/nextdns/). # "abc123" is example NextDNS ID, replace with yours.