Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/headscale-config-checker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
167 changes: 91 additions & 76 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions scripts/ci-defaults.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
46 changes: 46 additions & 0 deletions scripts/container-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
EdGeraghty marked this conversation as resolved.

# join items with ', '
local joined
printf -v joined '%s, ' "${items[@]}"
joined=${joined%, }
export GLOBAL_NAMESERVERS_YAML="[ ${joined} ]"
}

#######################################
# Perform all Headscale environment variable checks
#######################################
Expand Down Expand Up @@ -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"
Expand All @@ -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}"
Expand Down
4 changes: 4 additions & 0 deletions scripts/defaults.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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")
6 changes: 1 addition & 5 deletions templates/headscale.template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading