From 7581c2745d2ff39724fb8702a73f99ccd98429c1 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Fri, 10 Apr 2026 17:51:24 +0100 Subject: [PATCH 01/11] Make custom upstream nameservers configurable --- README.md | 15 + scripts/container-entrypoint.sh | 46 ++ scripts/defaults.sh | 4 + templates/headscale.template.yaml | 832 +++++++++++++++--------------- 4 files changed, 479 insertions(+), 418 deletions(-) diff --git a/README.md b/README.md index 407a03a..59540ee 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,21 @@ 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. diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 98dbaef..308583d 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 + 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 + 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..9b62456 100644 --- a/templates/headscale.template.yaml +++ b/templates/headscale.template.yaml @@ -1,418 +1,414 @@ ---- -# headscale will look for a configuration file named `config.yaml` (or `config.json`) in the following order: -# -# - `/etc/headscale` -# - `~/.headscale` -# - current working directory - -# The url clients will connect to. -# Typically this will be a domain like: -# -# https://myheadscale.example.com:443 -# -server_url: https://$PUBLIC_SERVER_URL:$PUBLIC_LISTEN_PORT - -# Address to listen to / bind to on the server -# -# For production: -# listen_addr: 0.0.0.0:8080 -listen_addr: 127.0.0.1:8080 - -# Address to listen to /metrics and /debug, you may want -# to keep this endpoint private to your internal network -metrics_listen_addr: 127.0.0.1:9090 - -# Address to listen for gRPC. -# gRPC is used for controlling a headscale server -# remotely with the CLI -# Note: Remote access _only_ works if you have -# valid certificates. -# -# For production: -# grpc_listen_addr: 0.0.0.0:50443 -grpc_listen_addr: 127.0.0.1:50443 - -# Allow the gRPC admin interface to run in INSECURE -# mode. This is not recommended as the traffic will -# be unencrypted. Only enable if you know what you -# are doing. -grpc_allow_insecure: false - -# The Noise section includes specific configuration for the -# TS2021 Noise protocol -noise: - # The Noise private key is used to encrypt the traffic between headscale and - # Tailscale clients when using the new Noise-based protocol. A missing key - # will be automatically generated. - private_key_path: /data/noise_private.key - -# List of IP prefixes to allocate tailaddresses from. -# Each prefix consists of either an IPv4 or IPv6 address, -# and the associated prefix length, delimited by a slash. -# It must be within IP ranges supported by the Tailscale -# client - i.e., subnets of 100.64.0.0/10 and fd7a:115c:a1e0::/48. -# See below: -# IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71 -# IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33 -# Any other range is NOT supported, and it will cause unexpected issues. -prefixes: - $IP_PREFIXES - - # Strategy used for allocation of IPs to nodes, available options: - # - sequential (default): assigns the next free IP from the previous given IP. - # - random: assigns the next free IP from a pseudo-random IP generator (crypto/rand). - allocation: $IP_ALLOCATION - -# DERP is a relay system that Tailscale uses when a direct -# connection cannot be established. -# https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp -# -# headscale needs a list of DERP servers that can be presented -# to the clients. -derp: - server: - # If enabled, runs the embedded DERP server and merges it into the rest of the DERP config - # The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place - enabled: false - - # Region ID to use for the embedded DERP server. - # The local DERP prevails if the region ID collides with other region ID coming from - # the regular DERP config. - region_id: 999 - - # Region code and name are displayed in the Tailscale UI to identify a DERP region - region_code: "headscale" - region_name: "Headscale Embedded DERP" - - # Only allow clients associated with this server access - verify_clients: true - - # Listens over UDP at the configured address for STUN connections - to help with NAT traversal. - # When the embedded DERP server is enabled stun_listen_addr MUST be defined. - # - # For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/ - stun_listen_addr: "0.0.0.0:3478" - - # Private key used to encrypt the traffic between headscale DERP - # and Tailscale clients. - # The private key file will be autogenerated if it's missing. - # - private_key_path: /var/lib/headscale/derp_server_private.key - - # This flag can be used, so the DERP map entry for the embedded DERP server is not written automatically, - # it enables the creation of your very own DERP map entry using a locally available file with the parameter DERP.paths - # If you enable the DERP server and set this to false, it is required to add the DERP server to the DERP map using DERP.paths - automatically_add_embedded_derp_region: true - - # For better connection stability (especially when using an Exit-Node and DNS is not working), - # it is possible to optionally add the public IPv4 and IPv6 address to the Derp-Map using: - ipv4: 1.2.3.4 - ipv6: 2001:db8::1 - - # List of externally available DERP maps encoded in JSON - urls: - - https://controlplane.tailscale.com/derpmap/default - - # Locally available DERP map files encoded in YAML - # - # This option is mostly interesting for people hosting - # their own DERP servers: - # https://tailscale.com/kb/1118/custom-derp-servers/ - # - # paths: - # - /etc/headscale/derp-example.yaml - paths: [] - - # If enabled, a worker will be set up to periodically - # refresh the given sources and update the derpmap - # will be set up. - auto_update_enabled: true - - # How often should we check for DERP updates? - update_frequency: 3h - -# Disables the automatic check for headscale updates on startup -disable_check_updates: true - -# Time before an inactive ephemeral node is deleted? -ephemeral_node_inactivity_timeout: $EPHEMERAL_NODE_INACTIVITY_TIMEOUT - -database: - # Database type. Available options: sqlite, postgres - # Please note that using Postgres is highly discouraged as it is only supported for legacy reasons. - # All new development, testing and optimisations are done with SQLite in mind. - type: sqlite - - # Enable debug mode. This setting requires the log.level to be set to "debug" or "trace". - debug: false - - # GORM configuration settings. - gorm: - # Enable prepared statements. - prepare_stmt: true - - # Enable parameterized queries. - parameterized_queries: true - - # Skip logging "record not found" errors. - skip_err_record_not_found: true - - # Threshold for slow queries in milliseconds. - slow_threshold: 1000 - - # SQLite config - sqlite: - path: /data/headscale.sqlite3 - - # Enable WAL mode for SQLite. This is recommended for production environments. - # https://www.sqlite.org/wal.html - write_ahead_log: true - - # Maximum number of WAL file frames before the WAL file is automatically checkpointed. - # https://www.sqlite.org/c3ref/wal_autocheckpoint.html - # Set to 0 to disable automatic checkpointing. - wal_autocheckpoint: 1000 - - # # Postgres config - # Please note that using Postgres is highly discouraged as it is only supported for legacy reasons. - # See database.type for more information. - # postgres: - # # If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank. - # host: localhost - # port: 5432 - # name: headscale - # user: foo - # pass: bar - # max_open_conns: 10 - # max_idle_conns: 10 - # conn_max_idle_time_secs: 3600 - - # # If other 'sslmode' is required instead of 'require(true)' and 'disabled(false)', set the 'sslmode' you need - # # in the 'ssl' field. Refers to https://www.postgresql.org/docs/current/libpq-ssl.html Table 34.1. - # ssl: false - -### TLS configuration -# -## Let's encrypt / ACME -# -# headscale supports automatically requesting and setting up -# TLS for a domain with Let's Encrypt. -# -# URL to ACME directory -#acme_url: https://acme-v02.api.letsencrypt.org/directory # DIFF_IGNORE - -# Email to register with ACME provider -#acme_email: "" # DIFF_IGNORE - -# Domain name to request a TLS certificate for: -#tls_letsencrypt_hostname: "" # DIFF_IGNORE - -# Path to store certificates and metadata needed by -# letsencrypt -# For production: -#tls_letsencrypt_cache_dir: /var/lib/headscale/cache # DIFF_IGNORE - -# Type of ACME challenge to use, currently supported types: -# HTTP-01 or TLS-ALPN-01 -# See: docs/ref/tls.md for more information -#tls_letsencrypt_challenge_type: HTTP-01 # DIFF_IGNORE -# When HTTP-01 challenge is chosen, letsencrypt must set up a -# verification endpoint, and it will be listening on: -# :http = port 80 -#tls_letsencrypt_listen: ":http" # DIFF_IGNORE - -## Use already defined certificates: -tls_cert_path: "" -tls_key_path: "" - -log: - # Valid log levels: panic, fatal, error, warn, info, debug, trace - level: info - - # Output formatting for logs: text or json - format: text - -## Policy -# headscale supports Tailscale's ACL policies. -# Please have a look to their KB to better -# understand the concepts: https://tailscale.com/kb/1018/acls/ -policy: - # The mode can be "file" or "database" that defines - # where the ACL policies are stored and read from. - mode: database - # If the mode is set to "file", the path to a - # HuJSON file containing ACL policies. -# path: "" # DIFF_IGNORE - -## DNS -# -# headscale supports Tailscale's DNS configuration and MagicDNS. -# Please have a look to their KB to better understand the concepts: -# -# - https://tailscale.com/kb/1054/dns/ -# - https://tailscale.com/kb/1081/magicdns/ -# - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/ -# -# Please note that for the DNS configuration to have any effect, -# clients must have the `--accept-dns=true` option enabled. This is the -# default for the Tailscale client. This option is enabled by default -# in the Tailscale client. -# -# Setting _any_ of the configuration and `--accept-dns=true` on the -# clients will integrate with the DNS manager on the client or -# overwrite /etc/resolv.conf. -# https://tailscale.com/kb/1235/resolv-conf -# -# If you want stop Headscale from managing the DNS configuration -# all the fields under `dns` should be set to empty values. -dns: - # Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). - magic_dns: $MAGIC_DNS - - # Defines the base domain to create the hostnames for MagicDNS. - # This domain _must_ be different from the server_url domain. - # `base_domain` must be a FQDN, without the trailing dot. - # The FQDN of the hosts will be - # `hostname.base_domain` (e.g., _myhost.example.com_). - base_domain: $HEADSCALE_DNS_BASE_DOMAIN - - # Whether to use the local DNS settings of a node or override the local DNS - # settings (default) and force the use of Headscale's DNS configuration. - override_local_dns: $HEADSCALE_OVERRIDE_LOCAL_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 - - # NextDNS (see https://tailscale.com/kb/1218/nextdns/). - # "abc123" is example NextDNS ID, replace with yours. - # - https://dns.nextdns.io/abc123 - - # Split DNS (see https://tailscale.com/kb/1054/dns/), - # a map of domains and which DNS server to use for each. - split: - {} - # foo.bar.com: - # - 1.1.1.1 - # darp.headscale.net: - # - 1.1.1.1 - # - 8.8.8.8 - - # Set custom DNS search domains. With MagicDNS enabled, - # your tailnet base_domain is always the first search domain. - search_domains: [] - - # Extra DNS records - # so far only A and AAAA records are supported (on the tailscale side) - # See: docs/ref/dns.md - #extra_records: [] # DIFF_IGNORE - # - name: "grafana.myvpn.example.com" - # type: "A" - # value: "100.64.0.3" - # - # # you can also put it in one line - # - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" } - # - # Alternatively, extra DNS records can be loaded from a JSON file. - # Headscale processes this file on each change. - extra_records_path: $HEADSCALE_EXTRA_RECORDS_PATH # DIFF_IGNORE - -# Unix socket used for the CLI to connect without authentication -# Note: for production you will want to set this to something like: -unix_socket: /var/run/headscale/headscale.sock -unix_socket_permission: "0770" -# -# headscale supports experimental OpenID connect support, -# it is still being tested and might have some bugs, please -# help us test it. -# OpenID Connect -oidc: -# only_start_if_oidc_is_available: true -# issuer: "https://your-oidc.issuer.com/path" -# client_id: "your-oidc-client-id" -# client_secret: "your-oidc-client-secret" -# # Alternatively, set `client_secret_path` to read the secret from the file. -# # It resolves environment variables, making integration to systemd's -# # `LoadCredential` straightforward: -# client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret" -# # client_secret and client_secret_path are mutually exclusive. -# -# # The amount of time from a node is authenticated with OpenID until it -# # expires and needs to reauthenticate. -# # Setting the value to "0" will mean no expiry. -# expiry: 180d -# -# # Use the expiry from the token received from OpenID when the user logged -# # in, this will typically lead to frequent need to reauthenticate and should -# # only been enabled if you know what you are doing. -# # Note: enabling this will cause `oidc.expiry` to be ignored. -# use_expiry_from_token: false -# -# # Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query -# # parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email". -# -# scope: ["openid", "profile", "email", "custom"] - extra_params: - prompt: select_account -# domain_hint: example.com -# -# # List allowed principal domains and/or users. If an authenticated user's domain is not in this list, the -# # authentication request will be rejected. -# -# allowed_domains: -# - example.com -# # Note: Groups from keycloak have a leading '/' -# allowed_groups: -# - /headscale -# allowed_users: -# - alice@example.com -# -# # Optional: PKCE (Proof Key for Code Exchange) configuration -# # PKCE adds an additional layer of security to the OAuth 2.0 authorization code flow -# # by preventing authorization code interception attacks -# # See https://datatracker.ietf.org/doc/html/rfc7636 -# pkce: -# # Enable or disable PKCE support (default: false) -# enabled: false -# # PKCE method to use: -# # - plain: Use plain code verifier -# # - S256: Use SHA256 hashed code verifier (default, recommended) -# method: S256 - -# Logtail configuration -# Logtail is Tailscales logging and auditing infrastructure, it allows the control panel -# to instruct tailscale nodes to log their activity to a remote server. -logtail: - # Enable logtail for this headscales clients. - # As there is currently no support for overriding the log server in headscale, this is - # disabled by default. Enabling this will make your clients send logs to Tailscale Inc. - enabled: false - -# Enabling this option makes devices prefer a random port for WireGuard traffic over the -# default static port 41641. This option is intended as a workaround for some buggy -# firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information. -randomize_client_port: false - -# Taildrop configuration -# Taildrop is the file sharing feature of Tailscale, allowing nodes to send files to each other. -# https://tailscale.com/kb/1106/taildrop/ -taildrop: - # Enable or disable Taildrop for all nodes. - # When enabled, nodes can send files to other nodes owned by the same user. - # Tagged devices and cross-user transfers are not permitted by Tailscale clients. - enabled: true -# Advanced performance tuning parameters. -# The defaults are carefully chosen and should rarely need adjustment. -# Only modify these if you have identified a specific performance issue. -# -# tuning: -# # NodeStore write batching configuration. -# # The NodeStore batches write operations before rebuilding peer relationships, -# # which is computationally expensive. Batching reduces rebuild frequency. -# # -# # node_store_batch_size: 100 -# # node_store_batch_timeout: 500ms +--- +# headscale will look for a configuration file named `config.yaml` (or `config.json`) in the following order: +# +# - `/etc/headscale` +# - `~/.headscale` +# - current working directory + +# The url clients will connect to. +# Typically this will be a domain like: +# +# https://myheadscale.example.com:443 +# +server_url: https://$PUBLIC_SERVER_URL:$PUBLIC_LISTEN_PORT + +# Address to listen to / bind to on the server +# +# For production: +# listen_addr: 0.0.0.0:8080 +listen_addr: 127.0.0.1:8080 + +# Address to listen to /metrics and /debug, you may want +# to keep this endpoint private to your internal network +metrics_listen_addr: 127.0.0.1:9090 + +# Address to listen for gRPC. +# gRPC is used for controlling a headscale server +# remotely with the CLI +# Note: Remote access _only_ works if you have +# valid certificates. +# +# For production: +# grpc_listen_addr: 0.0.0.0:50443 +grpc_listen_addr: 127.0.0.1:50443 + +# Allow the gRPC admin interface to run in INSECURE +# mode. This is not recommended as the traffic will +# be unencrypted. Only enable if you know what you +# are doing. +grpc_allow_insecure: false + +# The Noise section includes specific configuration for the +# TS2021 Noise protocol +noise: + # The Noise private key is used to encrypt the traffic between headscale and + # Tailscale clients when using the new Noise-based protocol. A missing key + # will be automatically generated. + private_key_path: /data/noise_private.key + +# List of IP prefixes to allocate tailaddresses from. +# Each prefix consists of either an IPv4 or IPv6 address, +# and the associated prefix length, delimited by a slash. +# It must be within IP ranges supported by the Tailscale +# client - i.e., subnets of 100.64.0.0/10 and fd7a:115c:a1e0::/48. +# See below: +# IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71 +# IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33 +# Any other range is NOT supported, and it will cause unexpected issues. +prefixes: + $IP_PREFIXES + + # Strategy used for allocation of IPs to nodes, available options: + # - sequential (default): assigns the next free IP from the previous given IP. + # - random: assigns the next free IP from a pseudo-random IP generator (crypto/rand). + allocation: $IP_ALLOCATION + +# DERP is a relay system that Tailscale uses when a direct +# connection cannot be established. +# https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp +# +# headscale needs a list of DERP servers that can be presented +# to the clients. +derp: + server: + # If enabled, runs the embedded DERP server and merges it into the rest of the DERP config + # The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place + enabled: false + + # Region ID to use for the embedded DERP server. + # The local DERP prevails if the region ID collides with other region ID coming from + # the regular DERP config. + region_id: 999 + + # Region code and name are displayed in the Tailscale UI to identify a DERP region + region_code: "headscale" + region_name: "Headscale Embedded DERP" + + # Only allow clients associated with this server access + verify_clients: true + + # Listens over UDP at the configured address for STUN connections - to help with NAT traversal. + # When the embedded DERP server is enabled stun_listen_addr MUST be defined. + # + # For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/ + stun_listen_addr: "0.0.0.0:3478" + + # Private key used to encrypt the traffic between headscale DERP + # and Tailscale clients. + # The private key file will be autogenerated if it's missing. + # + private_key_path: /var/lib/headscale/derp_server_private.key + + # This flag can be used, so the DERP map entry for the embedded DERP server is not written automatically, + # it enables the creation of your very own DERP map entry using a locally available file with the parameter DERP.paths + # If you enable the DERP server and set this to false, it is required to add the DERP server to the DERP map using DERP.paths + automatically_add_embedded_derp_region: true + + # For better connection stability (especially when using an Exit-Node and DNS is not working), + # it is possible to optionally add the public IPv4 and IPv6 address to the Derp-Map using: + ipv4: 1.2.3.4 + ipv6: 2001:db8::1 + + # List of externally available DERP maps encoded in JSON + urls: + - https://controlplane.tailscale.com/derpmap/default + + # Locally available DERP map files encoded in YAML + # + # This option is mostly interesting for people hosting + # their own DERP servers: + # https://tailscale.com/kb/1118/custom-derp-servers/ + # + # paths: + # - /etc/headscale/derp-example.yaml + paths: [] + + # If enabled, a worker will be set up to periodically + # refresh the given sources and update the derpmap + # will be set up. + auto_update_enabled: true + + # How often should we check for DERP updates? + update_frequency: 3h + +# Disables the automatic check for headscale updates on startup +disable_check_updates: true + +# Time before an inactive ephemeral node is deleted? +ephemeral_node_inactivity_timeout: $EPHEMERAL_NODE_INACTIVITY_TIMEOUT + +database: + # Database type. Available options: sqlite, postgres + # Please note that using Postgres is highly discouraged as it is only supported for legacy reasons. + # All new development, testing and optimisations are done with SQLite in mind. + type: sqlite + + # Enable debug mode. This setting requires the log.level to be set to "debug" or "trace". + debug: false + + # GORM configuration settings. + gorm: + # Enable prepared statements. + prepare_stmt: true + + # Enable parameterized queries. + parameterized_queries: true + + # Skip logging "record not found" errors. + skip_err_record_not_found: true + + # Threshold for slow queries in milliseconds. + slow_threshold: 1000 + + # SQLite config + sqlite: + path: /data/headscale.sqlite3 + + # Enable WAL mode for SQLite. This is recommended for production environments. + # https://www.sqlite.org/wal.html + write_ahead_log: true + + # Maximum number of WAL file frames before the WAL file is automatically checkpointed. + # https://www.sqlite.org/c3ref/wal_autocheckpoint.html + # Set to 0 to disable automatic checkpointing. + wal_autocheckpoint: 1000 + + # # Postgres config + # Please note that using Postgres is highly discouraged as it is only supported for legacy reasons. + # See database.type for more information. + # postgres: + # # If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank. + # host: localhost + # port: 5432 + # name: headscale + # user: foo + # pass: bar + # max_open_conns: 10 + # max_idle_conns: 10 + # conn_max_idle_time_secs: 3600 + + # # If other 'sslmode' is required instead of 'require(true)' and 'disabled(false)', set the 'sslmode' you need + # # in the 'ssl' field. Refers to https://www.postgresql.org/docs/current/libpq-ssl.html Table 34.1. + # ssl: false + +### TLS configuration +# +## Let's encrypt / ACME +# +# headscale supports automatically requesting and setting up +# TLS for a domain with Let's Encrypt. +# +# URL to ACME directory +#acme_url: https://acme-v02.api.letsencrypt.org/directory # DIFF_IGNORE + +# Email to register with ACME provider +#acme_email: "" # DIFF_IGNORE + +# Domain name to request a TLS certificate for: +#tls_letsencrypt_hostname: "" # DIFF_IGNORE + +# Path to store certificates and metadata needed by +# letsencrypt +# For production: +#tls_letsencrypt_cache_dir: /var/lib/headscale/cache # DIFF_IGNORE + +# Type of ACME challenge to use, currently supported types: +# HTTP-01 or TLS-ALPN-01 +# See: docs/ref/tls.md for more information +#tls_letsencrypt_challenge_type: HTTP-01 # DIFF_IGNORE +# When HTTP-01 challenge is chosen, letsencrypt must set up a +# verification endpoint, and it will be listening on: +# :http = port 80 +#tls_letsencrypt_listen: ":http" # DIFF_IGNORE + +## Use already defined certificates: +tls_cert_path: "" +tls_key_path: "" + +log: + # Valid log levels: panic, fatal, error, warn, info, debug, trace + level: info + + # Output formatting for logs: text or json + format: text + +## Policy +# headscale supports Tailscale's ACL policies. +# Please have a look to their KB to better +# understand the concepts: https://tailscale.com/kb/1018/acls/ +policy: + # The mode can be "file" or "database" that defines + # where the ACL policies are stored and read from. + mode: database + # If the mode is set to "file", the path to a + # HuJSON file containing ACL policies. +# path: "" # DIFF_IGNORE + +## DNS +# +# headscale supports Tailscale's DNS configuration and MagicDNS. +# Please have a look to their KB to better understand the concepts: +# +# - https://tailscale.com/kb/1054/dns/ +# - https://tailscale.com/kb/1081/magicdns/ +# - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/ +# +# Please note that for the DNS configuration to have any effect, +# clients must have the `--accept-dns=true` option enabled. This is the +# default for the Tailscale client. This option is enabled by default +# in the Tailscale client. +# +# Setting _any_ of the configuration and `--accept-dns=true` on the +# clients will integrate with the DNS manager on the client or +# overwrite /etc/resolv.conf. +# https://tailscale.com/kb/1235/resolv-conf +# +# If you want stop Headscale from managing the DNS configuration +# all the fields under `dns` should be set to empty values. +dns: + # Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). + magic_dns: $MAGIC_DNS + + # Defines the base domain to create the hostnames for MagicDNS. + # This domain _must_ be different from the server_url domain. + # `base_domain` must be a FQDN, without the trailing dot. + # The FQDN of the hosts will be + # `hostname.base_domain` (e.g., _myhost.example.com_). + base_domain: $HEADSCALE_DNS_BASE_DOMAIN + + # Whether to use the local DNS settings of a node or override the local DNS + # settings (default) and force the use of Headscale's DNS configuration. + override_local_dns: $HEADSCALE_OVERRIDE_LOCAL_DNS + + # List of DNS servers to expose to clients. + nameservers: + global: $GLOBAL_NAMESERVERS_YAML + + # NextDNS (see https://tailscale.com/kb/1218/nextdns/). + # "abc123" is example NextDNS ID, replace with yours. + # - https://dns.nextdns.io/abc123 + + # Split DNS (see https://tailscale.com/kb/1054/dns/), + # a map of domains and which DNS server to use for each. + split: + {} + # foo.bar.com: + # - 1.1.1.1 + # darp.headscale.net: + # - 1.1.1.1 + # - 8.8.8.8 + + # Set custom DNS search domains. With MagicDNS enabled, + # your tailnet base_domain is always the first search domain. + search_domains: [] + + # Extra DNS records + # so far only A and AAAA records are supported (on the tailscale side) + # See: docs/ref/dns.md + #extra_records: [] # DIFF_IGNORE + # - name: "grafana.myvpn.example.com" + # type: "A" + # value: "100.64.0.3" + # + # # you can also put it in one line + # - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" } + # + # Alternatively, extra DNS records can be loaded from a JSON file. + # Headscale processes this file on each change. + extra_records_path: $HEADSCALE_EXTRA_RECORDS_PATH # DIFF_IGNORE + +# Unix socket used for the CLI to connect without authentication +# Note: for production you will want to set this to something like: +unix_socket: /var/run/headscale/headscale.sock +unix_socket_permission: "0770" +# +# headscale supports experimental OpenID connect support, +# it is still being tested and might have some bugs, please +# help us test it. +# OpenID Connect +oidc: +# only_start_if_oidc_is_available: true +# issuer: "https://your-oidc.issuer.com/path" +# client_id: "your-oidc-client-id" +# client_secret: "your-oidc-client-secret" +# # Alternatively, set `client_secret_path` to read the secret from the file. +# # It resolves environment variables, making integration to systemd's +# # `LoadCredential` straightforward: +# client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret" +# # client_secret and client_secret_path are mutually exclusive. +# +# # The amount of time from a node is authenticated with OpenID until it +# # expires and needs to reauthenticate. +# # Setting the value to "0" will mean no expiry. +# expiry: 180d +# +# # Use the expiry from the token received from OpenID when the user logged +# # in, this will typically lead to frequent need to reauthenticate and should +# # only been enabled if you know what you are doing. +# # Note: enabling this will cause `oidc.expiry` to be ignored. +# use_expiry_from_token: false +# +# # Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query +# # parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email". +# +# scope: ["openid", "profile", "email", "custom"] + extra_params: + prompt: select_account +# domain_hint: example.com +# +# # List allowed principal domains and/or users. If an authenticated user's domain is not in this list, the +# # authentication request will be rejected. +# +# allowed_domains: +# - example.com +# # Note: Groups from keycloak have a leading '/' +# allowed_groups: +# - /headscale +# allowed_users: +# - alice@example.com +# +# # Optional: PKCE (Proof Key for Code Exchange) configuration +# # PKCE adds an additional layer of security to the OAuth 2.0 authorization code flow +# # by preventing authorization code interception attacks +# # See https://datatracker.ietf.org/doc/html/rfc7636 +# pkce: +# # Enable or disable PKCE support (default: false) +# enabled: false +# # PKCE method to use: +# # - plain: Use plain code verifier +# # - S256: Use SHA256 hashed code verifier (default, recommended) +# method: S256 + +# Logtail configuration +# Logtail is Tailscales logging and auditing infrastructure, it allows the control panel +# to instruct tailscale nodes to log their activity to a remote server. +logtail: + # Enable logtail for this headscales clients. + # As there is currently no support for overriding the log server in headscale, this is + # disabled by default. Enabling this will make your clients send logs to Tailscale Inc. + enabled: false + +# Enabling this option makes devices prefer a random port for WireGuard traffic over the +# default static port 41641. This option is intended as a workaround for some buggy +# firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information. +randomize_client_port: false + +# Taildrop configuration +# Taildrop is the file sharing feature of Tailscale, allowing nodes to send files to each other. +# https://tailscale.com/kb/1106/taildrop/ +taildrop: + # Enable or disable Taildrop for all nodes. + # When enabled, nodes can send files to other nodes owned by the same user. + # Tagged devices and cross-user transfers are not permitted by Tailscale clients. + enabled: true +# Advanced performance tuning parameters. +# The defaults are carefully chosen and should rarely need adjustment. +# Only modify these if you have identified a specific performance issue. +# +# tuning: +# # NodeStore write batching configuration. +# # The NodeStore batches write operations before rebuilding peer relationships, +# # which is computationally expensive. Batching reduces rebuild frequency. +# # +# # node_store_batch_size: 100 +# # node_store_batch_timeout: 500ms From 8176ab71e50754ecc6dc209669120311048a552b Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 13 Apr 2026 16:04:23 +0100 Subject: [PATCH 02/11] Fix false positives, check the generated config --- .../workflows/headscale-config-checker.yml | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/.github/workflows/headscale-config-checker.yml b/.github/workflows/headscale-config-checker.yml index 887853a..9d1a6fd 100644 --- a/.github/workflows/headscale-config-checker.yml +++ b/.github/workflows/headscale-config-checker.yml @@ -27,6 +27,27 @@ jobs: echo "Successfully downloaded upstream config" + - name: Generate config from template with defaults + run: | + # Source the defaults + source scripts/defaults.sh + + # Export default values for envsubst + export IP_PREFIXES="${IP_PREFIXES:-"v4: 100.64.0.0/10 + v6: fd7a:115c:a1e0::/48"}" + + export PUBLIC_SERVER_URL="https://example.com" + export PUBLIC_LISTEN_PORT="443" + export HEADSCALE_DNS_BASE_DOMAIN="example.com" + export HEADSCALE_OVERRIDE_LOCAL_DNS="true" + export MAGIC_DNS="true" + export IP_ALLOCATION="sequential" + export HEADSCALE_EXTRA_RECORDS_PATH="/data/headscale/extra-records.json" + export EPHEMERAL_NODE_INACTIVITY_TIMEOUT="30m" + + # Generate config + envsubst < templates/headscale.template.yaml > generated-config.yaml + - name: Check for new options id: check run: | @@ -40,20 +61,20 @@ jobs: # Get list of keys to ignore from DIFF_IGNORE comments (including commented lines) get_ignored_keys() { - grep "# DIFF_IGNORE" "$1" | \ + grep "# DIFF_IGNORE" "templates/headscale.template.yaml" | \ sed -E 's/^[[:space:]]*#?[[:space:]]*//' | \ sed -E 's/:.*# DIFF_IGNORE.*$//' | \ sort -u } echo "=== Getting ignored keys ===" - get_ignored_keys "templates/headscale.template.yaml" > ignored_keys.txt + get_ignored_keys > ignored_keys.txt echo "Keys to ignore:" cat ignored_keys.txt echo "=== End ignored keys ===" # Extract all keys - extract_keys "templates/headscale.template.yaml" > local_all_keys.txt + extract_keys "generated-config.yaml" > local_all_keys.txt extract_keys "upstream-config.yaml" > upstream_all_keys.txt # Normalize keys (strip optional leading '#' and surrounding spaces) and sort-unique From 0b46f75ecf46dfa1a4b47b99a498e1929b60dec3 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 13 Apr 2026 16:24:40 +0100 Subject: [PATCH 03/11] Separate the CI/smoke test defaults from prod --- .github/workflows/headscale-config-checker.yml | 14 +------------- scripts/ci-defaults.sh | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 13 deletions(-) create mode 100644 scripts/ci-defaults.sh diff --git a/.github/workflows/headscale-config-checker.yml b/.github/workflows/headscale-config-checker.yml index 9d1a6fd..ce93101 100644 --- a/.github/workflows/headscale-config-checker.yml +++ b/.github/workflows/headscale-config-checker.yml @@ -31,19 +31,7 @@ jobs: run: | # Source the defaults source scripts/defaults.sh - - # Export default values for envsubst - export IP_PREFIXES="${IP_PREFIXES:-"v4: 100.64.0.0/10 - v6: fd7a:115c:a1e0::/48"}" - - export PUBLIC_SERVER_URL="https://example.com" - export PUBLIC_LISTEN_PORT="443" - export HEADSCALE_DNS_BASE_DOMAIN="example.com" - export HEADSCALE_OVERRIDE_LOCAL_DNS="true" - export MAGIC_DNS="true" - export IP_ALLOCATION="sequential" - export HEADSCALE_EXTRA_RECORDS_PATH="/data/headscale/extra-records.json" - export EPHEMERAL_NODE_INACTIVITY_TIMEOUT="30m" + source scripts/ci-defaults.sh # Generate config envsubst < templates/headscale.template.yaml > generated-config.yaml diff --git a/scripts/ci-defaults.sh b/scripts/ci-defaults.sh new file mode 100644 index 0000000..2a97fe1 --- /dev/null +++ b/scripts/ci-defaults.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# CI-specific defaults for config generation +# These are used in the GitHub Actions workflow to generate a baseline config + +# Export default values for envsubst in templates +# shellcheck disable=SC2034 +export IP_PREFIXES="v4: $headscale_ipv4_prefix_default +v6: $headscale_ipv6_prefix_default" + +export PUBLIC_SERVER_URL="https://example.com" +export PUBLIC_LISTEN_PORT="443" +export HEADSCALE_DNS_BASE_DOMAIN="example.com" +export HEADSCALE_OVERRIDE_LOCAL_DNS="true" +export MAGIC_DNS="true" +export IP_ALLOCATION="sequential" +export HEADSCALE_EXTRA_RECORDS_PATH="/data/headscale/extra-records.json" +export EPHEMERAL_NODE_INACTIVITY_TIMEOUT="30m" From 48d2eaeddd436dd4dbf77f33d811f1eb014f0a29 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 13 Apr 2026 16:26:08 +0100 Subject: [PATCH 04/11] The template already prepends the schema --- scripts/ci-defaults.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci-defaults.sh b/scripts/ci-defaults.sh index 2a97fe1..15604e2 100644 --- a/scripts/ci-defaults.sh +++ b/scripts/ci-defaults.sh @@ -7,7 +7,7 @@ export IP_PREFIXES="v4: $headscale_ipv4_prefix_default v6: $headscale_ipv6_prefix_default" -export PUBLIC_SERVER_URL="https://example.com" +export PUBLIC_SERVER_URL="example.com" export PUBLIC_LISTEN_PORT="443" export HEADSCALE_DNS_BASE_DOMAIN="example.com" export HEADSCALE_OVERRIDE_LOCAL_DNS="true" From 765aeafcbf0ff867b5f42e66ce2586e2bdedbfca Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 13 Apr 2026 16:34:45 +0100 Subject: [PATCH 05/11] Fix indentation --- scripts/ci-defaults.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci-defaults.sh b/scripts/ci-defaults.sh index 15604e2..4265e69 100644 --- a/scripts/ci-defaults.sh +++ b/scripts/ci-defaults.sh @@ -5,7 +5,7 @@ # Export default values for envsubst in templates # shellcheck disable=SC2034 export IP_PREFIXES="v4: $headscale_ipv4_prefix_default -v6: $headscale_ipv6_prefix_default" + v6: $headscale_ipv6_prefix_default" export PUBLIC_SERVER_URL="example.com" export PUBLIC_LISTEN_PORT="443" From 49cdd05f42eacde979edde1596705a4fa12bdde1 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 13 Apr 2026 16:39:27 +0100 Subject: [PATCH 06/11] Use upstream defaults for CI to stop drift --- scripts/ci-defaults.sh | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/scripts/ci-defaults.sh b/scripts/ci-defaults.sh index 4265e69..30c226a 100644 --- a/scripts/ci-defaults.sh +++ b/scripts/ci-defaults.sh @@ -6,12 +6,11 @@ # shellcheck disable=SC2034 export IP_PREFIXES="v4: $headscale_ipv4_prefix_default v6: $headscale_ipv6_prefix_default" - export PUBLIC_SERVER_URL="example.com" -export PUBLIC_LISTEN_PORT="443" +export PUBLIC_LISTEN_PORT="$public_listen_port_default" export HEADSCALE_DNS_BASE_DOMAIN="example.com" -export HEADSCALE_OVERRIDE_LOCAL_DNS="true" -export MAGIC_DNS="true" -export IP_ALLOCATION="sequential" -export HEADSCALE_EXTRA_RECORDS_PATH="/data/headscale/extra-records.json" -export EPHEMERAL_NODE_INACTIVITY_TIMEOUT="30m" +export HEADSCALE_OVERRIDE_LOCAL_DNS="$headscale_override_local_dns_default" +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" From 5180432948bac3b3ec4b74c33b7381c9e5aa7fee Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 13 Apr 2026 16:39:35 +0100 Subject: [PATCH 07/11] Appease the linter --- scripts/ci-defaults.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci-defaults.sh b/scripts/ci-defaults.sh index 30c226a..42a4ef8 100644 --- a/scripts/ci-defaults.sh +++ b/scripts/ci-defaults.sh @@ -3,7 +3,7 @@ # These are used in the GitHub Actions workflow to generate a baseline config # Export default values for envsubst in templates -# shellcheck disable=SC2034 +# shellcheck disable=SC2034,SC2154 export IP_PREFIXES="v4: $headscale_ipv4_prefix_default v6: $headscale_ipv6_prefix_default" export PUBLIC_SERVER_URL="example.com" From e506cb0dc09d4e229d8cb229336a797f20d14bec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:56:20 +0000 Subject: [PATCH 08/11] Initial plan From 78dfc4977f85832578f49b6add3f6b5dbfb843a8 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Wed, 15 Apr 2026 18:57:54 +0100 Subject: [PATCH 09/11] Unfux linebreaks --- README.md | 182 +++---- templates/headscale.template.yaml | 828 +++++++++++++++--------------- 2 files changed, 505 insertions(+), 505 deletions(-) diff --git a/README.md b/README.md index 59540ee..f96f9d5 100644 --- a/README.md +++ b/README.md @@ -1,91 +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. - -## 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 +# 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/templates/headscale.template.yaml b/templates/headscale.template.yaml index 9b62456..cab2b3b 100644 --- a/templates/headscale.template.yaml +++ b/templates/headscale.template.yaml @@ -1,414 +1,414 @@ ---- -# headscale will look for a configuration file named `config.yaml` (or `config.json`) in the following order: -# -# - `/etc/headscale` -# - `~/.headscale` -# - current working directory - -# The url clients will connect to. -# Typically this will be a domain like: -# -# https://myheadscale.example.com:443 -# -server_url: https://$PUBLIC_SERVER_URL:$PUBLIC_LISTEN_PORT - -# Address to listen to / bind to on the server -# -# For production: -# listen_addr: 0.0.0.0:8080 -listen_addr: 127.0.0.1:8080 - -# Address to listen to /metrics and /debug, you may want -# to keep this endpoint private to your internal network -metrics_listen_addr: 127.0.0.1:9090 - -# Address to listen for gRPC. -# gRPC is used for controlling a headscale server -# remotely with the CLI -# Note: Remote access _only_ works if you have -# valid certificates. -# -# For production: -# grpc_listen_addr: 0.0.0.0:50443 -grpc_listen_addr: 127.0.0.1:50443 - -# Allow the gRPC admin interface to run in INSECURE -# mode. This is not recommended as the traffic will -# be unencrypted. Only enable if you know what you -# are doing. -grpc_allow_insecure: false - -# The Noise section includes specific configuration for the -# TS2021 Noise protocol -noise: - # The Noise private key is used to encrypt the traffic between headscale and - # Tailscale clients when using the new Noise-based protocol. A missing key - # will be automatically generated. - private_key_path: /data/noise_private.key - -# List of IP prefixes to allocate tailaddresses from. -# Each prefix consists of either an IPv4 or IPv6 address, -# and the associated prefix length, delimited by a slash. -# It must be within IP ranges supported by the Tailscale -# client - i.e., subnets of 100.64.0.0/10 and fd7a:115c:a1e0::/48. -# See below: -# IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71 -# IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33 -# Any other range is NOT supported, and it will cause unexpected issues. -prefixes: - $IP_PREFIXES - - # Strategy used for allocation of IPs to nodes, available options: - # - sequential (default): assigns the next free IP from the previous given IP. - # - random: assigns the next free IP from a pseudo-random IP generator (crypto/rand). - allocation: $IP_ALLOCATION - -# DERP is a relay system that Tailscale uses when a direct -# connection cannot be established. -# https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp -# -# headscale needs a list of DERP servers that can be presented -# to the clients. -derp: - server: - # If enabled, runs the embedded DERP server and merges it into the rest of the DERP config - # The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place - enabled: false - - # Region ID to use for the embedded DERP server. - # The local DERP prevails if the region ID collides with other region ID coming from - # the regular DERP config. - region_id: 999 - - # Region code and name are displayed in the Tailscale UI to identify a DERP region - region_code: "headscale" - region_name: "Headscale Embedded DERP" - - # Only allow clients associated with this server access - verify_clients: true - - # Listens over UDP at the configured address for STUN connections - to help with NAT traversal. - # When the embedded DERP server is enabled stun_listen_addr MUST be defined. - # - # For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/ - stun_listen_addr: "0.0.0.0:3478" - - # Private key used to encrypt the traffic between headscale DERP - # and Tailscale clients. - # The private key file will be autogenerated if it's missing. - # - private_key_path: /var/lib/headscale/derp_server_private.key - - # This flag can be used, so the DERP map entry for the embedded DERP server is not written automatically, - # it enables the creation of your very own DERP map entry using a locally available file with the parameter DERP.paths - # If you enable the DERP server and set this to false, it is required to add the DERP server to the DERP map using DERP.paths - automatically_add_embedded_derp_region: true - - # For better connection stability (especially when using an Exit-Node and DNS is not working), - # it is possible to optionally add the public IPv4 and IPv6 address to the Derp-Map using: - ipv4: 1.2.3.4 - ipv6: 2001:db8::1 - - # List of externally available DERP maps encoded in JSON - urls: - - https://controlplane.tailscale.com/derpmap/default - - # Locally available DERP map files encoded in YAML - # - # This option is mostly interesting for people hosting - # their own DERP servers: - # https://tailscale.com/kb/1118/custom-derp-servers/ - # - # paths: - # - /etc/headscale/derp-example.yaml - paths: [] - - # If enabled, a worker will be set up to periodically - # refresh the given sources and update the derpmap - # will be set up. - auto_update_enabled: true - - # How often should we check for DERP updates? - update_frequency: 3h - -# Disables the automatic check for headscale updates on startup -disable_check_updates: true - -# Time before an inactive ephemeral node is deleted? -ephemeral_node_inactivity_timeout: $EPHEMERAL_NODE_INACTIVITY_TIMEOUT - -database: - # Database type. Available options: sqlite, postgres - # Please note that using Postgres is highly discouraged as it is only supported for legacy reasons. - # All new development, testing and optimisations are done with SQLite in mind. - type: sqlite - - # Enable debug mode. This setting requires the log.level to be set to "debug" or "trace". - debug: false - - # GORM configuration settings. - gorm: - # Enable prepared statements. - prepare_stmt: true - - # Enable parameterized queries. - parameterized_queries: true - - # Skip logging "record not found" errors. - skip_err_record_not_found: true - - # Threshold for slow queries in milliseconds. - slow_threshold: 1000 - - # SQLite config - sqlite: - path: /data/headscale.sqlite3 - - # Enable WAL mode for SQLite. This is recommended for production environments. - # https://www.sqlite.org/wal.html - write_ahead_log: true - - # Maximum number of WAL file frames before the WAL file is automatically checkpointed. - # https://www.sqlite.org/c3ref/wal_autocheckpoint.html - # Set to 0 to disable automatic checkpointing. - wal_autocheckpoint: 1000 - - # # Postgres config - # Please note that using Postgres is highly discouraged as it is only supported for legacy reasons. - # See database.type for more information. - # postgres: - # # If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank. - # host: localhost - # port: 5432 - # name: headscale - # user: foo - # pass: bar - # max_open_conns: 10 - # max_idle_conns: 10 - # conn_max_idle_time_secs: 3600 - - # # If other 'sslmode' is required instead of 'require(true)' and 'disabled(false)', set the 'sslmode' you need - # # in the 'ssl' field. Refers to https://www.postgresql.org/docs/current/libpq-ssl.html Table 34.1. - # ssl: false - -### TLS configuration -# -## Let's encrypt / ACME -# -# headscale supports automatically requesting and setting up -# TLS for a domain with Let's Encrypt. -# -# URL to ACME directory -#acme_url: https://acme-v02.api.letsencrypt.org/directory # DIFF_IGNORE - -# Email to register with ACME provider -#acme_email: "" # DIFF_IGNORE - -# Domain name to request a TLS certificate for: -#tls_letsencrypt_hostname: "" # DIFF_IGNORE - -# Path to store certificates and metadata needed by -# letsencrypt -# For production: -#tls_letsencrypt_cache_dir: /var/lib/headscale/cache # DIFF_IGNORE - -# Type of ACME challenge to use, currently supported types: -# HTTP-01 or TLS-ALPN-01 -# See: docs/ref/tls.md for more information -#tls_letsencrypt_challenge_type: HTTP-01 # DIFF_IGNORE -# When HTTP-01 challenge is chosen, letsencrypt must set up a -# verification endpoint, and it will be listening on: -# :http = port 80 -#tls_letsencrypt_listen: ":http" # DIFF_IGNORE - -## Use already defined certificates: -tls_cert_path: "" -tls_key_path: "" - -log: - # Valid log levels: panic, fatal, error, warn, info, debug, trace - level: info - - # Output formatting for logs: text or json - format: text - -## Policy -# headscale supports Tailscale's ACL policies. -# Please have a look to their KB to better -# understand the concepts: https://tailscale.com/kb/1018/acls/ -policy: - # The mode can be "file" or "database" that defines - # where the ACL policies are stored and read from. - mode: database - # If the mode is set to "file", the path to a - # HuJSON file containing ACL policies. -# path: "" # DIFF_IGNORE - -## DNS -# -# headscale supports Tailscale's DNS configuration and MagicDNS. -# Please have a look to their KB to better understand the concepts: -# -# - https://tailscale.com/kb/1054/dns/ -# - https://tailscale.com/kb/1081/magicdns/ -# - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/ -# -# Please note that for the DNS configuration to have any effect, -# clients must have the `--accept-dns=true` option enabled. This is the -# default for the Tailscale client. This option is enabled by default -# in the Tailscale client. -# -# Setting _any_ of the configuration and `--accept-dns=true` on the -# clients will integrate with the DNS manager on the client or -# overwrite /etc/resolv.conf. -# https://tailscale.com/kb/1235/resolv-conf -# -# If you want stop Headscale from managing the DNS configuration -# all the fields under `dns` should be set to empty values. -dns: - # Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). - magic_dns: $MAGIC_DNS - - # Defines the base domain to create the hostnames for MagicDNS. - # This domain _must_ be different from the server_url domain. - # `base_domain` must be a FQDN, without the trailing dot. - # The FQDN of the hosts will be - # `hostname.base_domain` (e.g., _myhost.example.com_). - base_domain: $HEADSCALE_DNS_BASE_DOMAIN - - # Whether to use the local DNS settings of a node or override the local DNS - # settings (default) and force the use of Headscale's DNS configuration. - override_local_dns: $HEADSCALE_OVERRIDE_LOCAL_DNS - - # List of DNS servers to expose to clients. - nameservers: - global: $GLOBAL_NAMESERVERS_YAML - - # NextDNS (see https://tailscale.com/kb/1218/nextdns/). - # "abc123" is example NextDNS ID, replace with yours. - # - https://dns.nextdns.io/abc123 - - # Split DNS (see https://tailscale.com/kb/1054/dns/), - # a map of domains and which DNS server to use for each. - split: - {} - # foo.bar.com: - # - 1.1.1.1 - # darp.headscale.net: - # - 1.1.1.1 - # - 8.8.8.8 - - # Set custom DNS search domains. With MagicDNS enabled, - # your tailnet base_domain is always the first search domain. - search_domains: [] - - # Extra DNS records - # so far only A and AAAA records are supported (on the tailscale side) - # See: docs/ref/dns.md - #extra_records: [] # DIFF_IGNORE - # - name: "grafana.myvpn.example.com" - # type: "A" - # value: "100.64.0.3" - # - # # you can also put it in one line - # - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" } - # - # Alternatively, extra DNS records can be loaded from a JSON file. - # Headscale processes this file on each change. - extra_records_path: $HEADSCALE_EXTRA_RECORDS_PATH # DIFF_IGNORE - -# Unix socket used for the CLI to connect without authentication -# Note: for production you will want to set this to something like: -unix_socket: /var/run/headscale/headscale.sock -unix_socket_permission: "0770" -# -# headscale supports experimental OpenID connect support, -# it is still being tested and might have some bugs, please -# help us test it. -# OpenID Connect -oidc: -# only_start_if_oidc_is_available: true -# issuer: "https://your-oidc.issuer.com/path" -# client_id: "your-oidc-client-id" -# client_secret: "your-oidc-client-secret" -# # Alternatively, set `client_secret_path` to read the secret from the file. -# # It resolves environment variables, making integration to systemd's -# # `LoadCredential` straightforward: -# client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret" -# # client_secret and client_secret_path are mutually exclusive. -# -# # The amount of time from a node is authenticated with OpenID until it -# # expires and needs to reauthenticate. -# # Setting the value to "0" will mean no expiry. -# expiry: 180d -# -# # Use the expiry from the token received from OpenID when the user logged -# # in, this will typically lead to frequent need to reauthenticate and should -# # only been enabled if you know what you are doing. -# # Note: enabling this will cause `oidc.expiry` to be ignored. -# use_expiry_from_token: false -# -# # Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query -# # parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email". -# -# scope: ["openid", "profile", "email", "custom"] - extra_params: - prompt: select_account -# domain_hint: example.com -# -# # List allowed principal domains and/or users. If an authenticated user's domain is not in this list, the -# # authentication request will be rejected. -# -# allowed_domains: -# - example.com -# # Note: Groups from keycloak have a leading '/' -# allowed_groups: -# - /headscale -# allowed_users: -# - alice@example.com -# -# # Optional: PKCE (Proof Key for Code Exchange) configuration -# # PKCE adds an additional layer of security to the OAuth 2.0 authorization code flow -# # by preventing authorization code interception attacks -# # See https://datatracker.ietf.org/doc/html/rfc7636 -# pkce: -# # Enable or disable PKCE support (default: false) -# enabled: false -# # PKCE method to use: -# # - plain: Use plain code verifier -# # - S256: Use SHA256 hashed code verifier (default, recommended) -# method: S256 - -# Logtail configuration -# Logtail is Tailscales logging and auditing infrastructure, it allows the control panel -# to instruct tailscale nodes to log their activity to a remote server. -logtail: - # Enable logtail for this headscales clients. - # As there is currently no support for overriding the log server in headscale, this is - # disabled by default. Enabling this will make your clients send logs to Tailscale Inc. - enabled: false - -# Enabling this option makes devices prefer a random port for WireGuard traffic over the -# default static port 41641. This option is intended as a workaround for some buggy -# firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information. -randomize_client_port: false - -# Taildrop configuration -# Taildrop is the file sharing feature of Tailscale, allowing nodes to send files to each other. -# https://tailscale.com/kb/1106/taildrop/ -taildrop: - # Enable or disable Taildrop for all nodes. - # When enabled, nodes can send files to other nodes owned by the same user. - # Tagged devices and cross-user transfers are not permitted by Tailscale clients. - enabled: true -# Advanced performance tuning parameters. -# The defaults are carefully chosen and should rarely need adjustment. -# Only modify these if you have identified a specific performance issue. -# -# tuning: -# # NodeStore write batching configuration. -# # The NodeStore batches write operations before rebuilding peer relationships, -# # which is computationally expensive. Batching reduces rebuild frequency. -# # -# # node_store_batch_size: 100 -# # node_store_batch_timeout: 500ms +--- +# headscale will look for a configuration file named `config.yaml` (or `config.json`) in the following order: +# +# - `/etc/headscale` +# - `~/.headscale` +# - current working directory + +# The url clients will connect to. +# Typically this will be a domain like: +# +# https://myheadscale.example.com:443 +# +server_url: https://$PUBLIC_SERVER_URL:$PUBLIC_LISTEN_PORT + +# Address to listen to / bind to on the server +# +# For production: +# listen_addr: 0.0.0.0:8080 +listen_addr: 127.0.0.1:8080 + +# Address to listen to /metrics and /debug, you may want +# to keep this endpoint private to your internal network +metrics_listen_addr: 127.0.0.1:9090 + +# Address to listen for gRPC. +# gRPC is used for controlling a headscale server +# remotely with the CLI +# Note: Remote access _only_ works if you have +# valid certificates. +# +# For production: +# grpc_listen_addr: 0.0.0.0:50443 +grpc_listen_addr: 127.0.0.1:50443 + +# Allow the gRPC admin interface to run in INSECURE +# mode. This is not recommended as the traffic will +# be unencrypted. Only enable if you know what you +# are doing. +grpc_allow_insecure: false + +# The Noise section includes specific configuration for the +# TS2021 Noise protocol +noise: + # The Noise private key is used to encrypt the traffic between headscale and + # Tailscale clients when using the new Noise-based protocol. A missing key + # will be automatically generated. + private_key_path: /data/noise_private.key + +# List of IP prefixes to allocate tailaddresses from. +# Each prefix consists of either an IPv4 or IPv6 address, +# and the associated prefix length, delimited by a slash. +# It must be within IP ranges supported by the Tailscale +# client - i.e., subnets of 100.64.0.0/10 and fd7a:115c:a1e0::/48. +# See below: +# IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71 +# IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33 +# Any other range is NOT supported, and it will cause unexpected issues. +prefixes: + $IP_PREFIXES + + # Strategy used for allocation of IPs to nodes, available options: + # - sequential (default): assigns the next free IP from the previous given IP. + # - random: assigns the next free IP from a pseudo-random IP generator (crypto/rand). + allocation: $IP_ALLOCATION + +# DERP is a relay system that Tailscale uses when a direct +# connection cannot be established. +# https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp +# +# headscale needs a list of DERP servers that can be presented +# to the clients. +derp: + server: + # If enabled, runs the embedded DERP server and merges it into the rest of the DERP config + # The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place + enabled: false + + # Region ID to use for the embedded DERP server. + # The local DERP prevails if the region ID collides with other region ID coming from + # the regular DERP config. + region_id: 999 + + # Region code and name are displayed in the Tailscale UI to identify a DERP region + region_code: "headscale" + region_name: "Headscale Embedded DERP" + + # Only allow clients associated with this server access + verify_clients: true + + # Listens over UDP at the configured address for STUN connections - to help with NAT traversal. + # When the embedded DERP server is enabled stun_listen_addr MUST be defined. + # + # For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/ + stun_listen_addr: "0.0.0.0:3478" + + # Private key used to encrypt the traffic between headscale DERP + # and Tailscale clients. + # The private key file will be autogenerated if it's missing. + # + private_key_path: /var/lib/headscale/derp_server_private.key + + # This flag can be used, so the DERP map entry for the embedded DERP server is not written automatically, + # it enables the creation of your very own DERP map entry using a locally available file with the parameter DERP.paths + # If you enable the DERP server and set this to false, it is required to add the DERP server to the DERP map using DERP.paths + automatically_add_embedded_derp_region: true + + # For better connection stability (especially when using an Exit-Node and DNS is not working), + # it is possible to optionally add the public IPv4 and IPv6 address to the Derp-Map using: + ipv4: 1.2.3.4 + ipv6: 2001:db8::1 + + # List of externally available DERP maps encoded in JSON + urls: + - https://controlplane.tailscale.com/derpmap/default + + # Locally available DERP map files encoded in YAML + # + # This option is mostly interesting for people hosting + # their own DERP servers: + # https://tailscale.com/kb/1118/custom-derp-servers/ + # + # paths: + # - /etc/headscale/derp-example.yaml + paths: [] + + # If enabled, a worker will be set up to periodically + # refresh the given sources and update the derpmap + # will be set up. + auto_update_enabled: true + + # How often should we check for DERP updates? + update_frequency: 3h + +# Disables the automatic check for headscale updates on startup +disable_check_updates: true + +# Time before an inactive ephemeral node is deleted? +ephemeral_node_inactivity_timeout: $EPHEMERAL_NODE_INACTIVITY_TIMEOUT + +database: + # Database type. Available options: sqlite, postgres + # Please note that using Postgres is highly discouraged as it is only supported for legacy reasons. + # All new development, testing and optimisations are done with SQLite in mind. + type: sqlite + + # Enable debug mode. This setting requires the log.level to be set to "debug" or "trace". + debug: false + + # GORM configuration settings. + gorm: + # Enable prepared statements. + prepare_stmt: true + + # Enable parameterized queries. + parameterized_queries: true + + # Skip logging "record not found" errors. + skip_err_record_not_found: true + + # Threshold for slow queries in milliseconds. + slow_threshold: 1000 + + # SQLite config + sqlite: + path: /data/headscale.sqlite3 + + # Enable WAL mode for SQLite. This is recommended for production environments. + # https://www.sqlite.org/wal.html + write_ahead_log: true + + # Maximum number of WAL file frames before the WAL file is automatically checkpointed. + # https://www.sqlite.org/c3ref/wal_autocheckpoint.html + # Set to 0 to disable automatic checkpointing. + wal_autocheckpoint: 1000 + + # # Postgres config + # Please note that using Postgres is highly discouraged as it is only supported for legacy reasons. + # See database.type for more information. + # postgres: + # # If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank. + # host: localhost + # port: 5432 + # name: headscale + # user: foo + # pass: bar + # max_open_conns: 10 + # max_idle_conns: 10 + # conn_max_idle_time_secs: 3600 + + # # If other 'sslmode' is required instead of 'require(true)' and 'disabled(false)', set the 'sslmode' you need + # # in the 'ssl' field. Refers to https://www.postgresql.org/docs/current/libpq-ssl.html Table 34.1. + # ssl: false + +### TLS configuration +# +## Let's encrypt / ACME +# +# headscale supports automatically requesting and setting up +# TLS for a domain with Let's Encrypt. +# +# URL to ACME directory +#acme_url: https://acme-v02.api.letsencrypt.org/directory # DIFF_IGNORE + +# Email to register with ACME provider +#acme_email: "" # DIFF_IGNORE + +# Domain name to request a TLS certificate for: +#tls_letsencrypt_hostname: "" # DIFF_IGNORE + +# Path to store certificates and metadata needed by +# letsencrypt +# For production: +#tls_letsencrypt_cache_dir: /var/lib/headscale/cache # DIFF_IGNORE + +# Type of ACME challenge to use, currently supported types: +# HTTP-01 or TLS-ALPN-01 +# See: docs/ref/tls.md for more information +#tls_letsencrypt_challenge_type: HTTP-01 # DIFF_IGNORE +# When HTTP-01 challenge is chosen, letsencrypt must set up a +# verification endpoint, and it will be listening on: +# :http = port 80 +#tls_letsencrypt_listen: ":http" # DIFF_IGNORE + +## Use already defined certificates: +tls_cert_path: "" +tls_key_path: "" + +log: + # Valid log levels: panic, fatal, error, warn, info, debug, trace + level: info + + # Output formatting for logs: text or json + format: text + +## Policy +# headscale supports Tailscale's ACL policies. +# Please have a look to their KB to better +# understand the concepts: https://tailscale.com/kb/1018/acls/ +policy: + # The mode can be "file" or "database" that defines + # where the ACL policies are stored and read from. + mode: database + # If the mode is set to "file", the path to a + # HuJSON file containing ACL policies. +# path: "" # DIFF_IGNORE + +## DNS +# +# headscale supports Tailscale's DNS configuration and MagicDNS. +# Please have a look to their KB to better understand the concepts: +# +# - https://tailscale.com/kb/1054/dns/ +# - https://tailscale.com/kb/1081/magicdns/ +# - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/ +# +# Please note that for the DNS configuration to have any effect, +# clients must have the `--accept-dns=true` option enabled. This is the +# default for the Tailscale client. This option is enabled by default +# in the Tailscale client. +# +# Setting _any_ of the configuration and `--accept-dns=true` on the +# clients will integrate with the DNS manager on the client or +# overwrite /etc/resolv.conf. +# https://tailscale.com/kb/1235/resolv-conf +# +# If you want stop Headscale from managing the DNS configuration +# all the fields under `dns` should be set to empty values. +dns: + # Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). + magic_dns: $MAGIC_DNS + + # Defines the base domain to create the hostnames for MagicDNS. + # This domain _must_ be different from the server_url domain. + # `base_domain` must be a FQDN, without the trailing dot. + # The FQDN of the hosts will be + # `hostname.base_domain` (e.g., _myhost.example.com_). + base_domain: $HEADSCALE_DNS_BASE_DOMAIN + + # Whether to use the local DNS settings of a node or override the local DNS + # settings (default) and force the use of Headscale's DNS configuration. + override_local_dns: $HEADSCALE_OVERRIDE_LOCAL_DNS + + # List of DNS servers to expose to clients. + nameservers: + global: $GLOBAL_NAMESERVERS_YAML + + # NextDNS (see https://tailscale.com/kb/1218/nextdns/). + # "abc123" is example NextDNS ID, replace with yours. + # - https://dns.nextdns.io/abc123 + + # Split DNS (see https://tailscale.com/kb/1054/dns/), + # a map of domains and which DNS server to use for each. + split: + {} + # foo.bar.com: + # - 1.1.1.1 + # darp.headscale.net: + # - 1.1.1.1 + # - 8.8.8.8 + + # Set custom DNS search domains. With MagicDNS enabled, + # your tailnet base_domain is always the first search domain. + search_domains: [] + + # Extra DNS records + # so far only A and AAAA records are supported (on the tailscale side) + # See: docs/ref/dns.md + #extra_records: [] # DIFF_IGNORE + # - name: "grafana.myvpn.example.com" + # type: "A" + # value: "100.64.0.3" + # + # # you can also put it in one line + # - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" } + # + # Alternatively, extra DNS records can be loaded from a JSON file. + # Headscale processes this file on each change. + extra_records_path: $HEADSCALE_EXTRA_RECORDS_PATH # DIFF_IGNORE + +# Unix socket used for the CLI to connect without authentication +# Note: for production you will want to set this to something like: +unix_socket: /var/run/headscale/headscale.sock +unix_socket_permission: "0770" +# +# headscale supports experimental OpenID connect support, +# it is still being tested and might have some bugs, please +# help us test it. +# OpenID Connect +oidc: +# only_start_if_oidc_is_available: true +# issuer: "https://your-oidc.issuer.com/path" +# client_id: "your-oidc-client-id" +# client_secret: "your-oidc-client-secret" +# # Alternatively, set `client_secret_path` to read the secret from the file. +# # It resolves environment variables, making integration to systemd's +# # `LoadCredential` straightforward: +# client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret" +# # client_secret and client_secret_path are mutually exclusive. +# +# # The amount of time from a node is authenticated with OpenID until it +# # expires and needs to reauthenticate. +# # Setting the value to "0" will mean no expiry. +# expiry: 180d +# +# # Use the expiry from the token received from OpenID when the user logged +# # in, this will typically lead to frequent need to reauthenticate and should +# # only been enabled if you know what you are doing. +# # Note: enabling this will cause `oidc.expiry` to be ignored. +# use_expiry_from_token: false +# +# # Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query +# # parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email". +# +# scope: ["openid", "profile", "email", "custom"] + extra_params: + prompt: select_account +# domain_hint: example.com +# +# # List allowed principal domains and/or users. If an authenticated user's domain is not in this list, the +# # authentication request will be rejected. +# +# allowed_domains: +# - example.com +# # Note: Groups from keycloak have a leading '/' +# allowed_groups: +# - /headscale +# allowed_users: +# - alice@example.com +# +# # Optional: PKCE (Proof Key for Code Exchange) configuration +# # PKCE adds an additional layer of security to the OAuth 2.0 authorization code flow +# # by preventing authorization code interception attacks +# # See https://datatracker.ietf.org/doc/html/rfc7636 +# pkce: +# # Enable or disable PKCE support (default: false) +# enabled: false +# # PKCE method to use: +# # - plain: Use plain code verifier +# # - S256: Use SHA256 hashed code verifier (default, recommended) +# method: S256 + +# Logtail configuration +# Logtail is Tailscales logging and auditing infrastructure, it allows the control panel +# to instruct tailscale nodes to log their activity to a remote server. +logtail: + # Enable logtail for this headscales clients. + # As there is currently no support for overriding the log server in headscale, this is + # disabled by default. Enabling this will make your clients send logs to Tailscale Inc. + enabled: false + +# Enabling this option makes devices prefer a random port for WireGuard traffic over the +# default static port 41641. This option is intended as a workaround for some buggy +# firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information. +randomize_client_port: false + +# Taildrop configuration +# Taildrop is the file sharing feature of Tailscale, allowing nodes to send files to each other. +# https://tailscale.com/kb/1106/taildrop/ +taildrop: + # Enable or disable Taildrop for all nodes. + # When enabled, nodes can send files to other nodes owned by the same user. + # Tagged devices and cross-user transfers are not permitted by Tailscale clients. + enabled: true +# Advanced performance tuning parameters. +# The defaults are carefully chosen and should rarely need adjustment. +# Only modify these if you have identified a specific performance issue. +# +# tuning: +# # NodeStore write batching configuration. +# # The NodeStore batches write operations before rebuilding peer relationships, +# # which is computationally expensive. Batching reduces rebuild frequency. +# # +# # node_store_batch_size: 100 +# # node_store_batch_timeout: 500ms From 195522f2da22c841f2435dcaca0ce24a44f89384 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 18:00:26 +0000 Subject: [PATCH 10/11] Export GLOBAL_NAMESERVERS_YAML in all function paths Agent-Logs-Url: https://github.com/privacyint/docker-headscale/sessions/a1109ee5-2afa-49fa-ad6b-4d3d5da3ba71 Co-authored-by: EdGeraghty <20861699+EdGeraghty@users.noreply.github.com> --- scripts/container-entrypoint.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 308583d..5bdcba0 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -195,7 +195,7 @@ build_global_nameservers_yaml() { fi if [[ ${#ns_array[@]} -eq 0 ]]; then - GLOBAL_NAMESERVERS_YAML='[]' + export GLOBAL_NAMESERVERS_YAML='[]' return fi @@ -209,7 +209,7 @@ build_global_nameservers_yaml() { done if [[ ${#items[@]} -eq 0 ]]; then - GLOBAL_NAMESERVERS_YAML='[]' + export GLOBAL_NAMESERVERS_YAML='[]' return fi From d8b2d7df7bca2573f324e696fe7b5a123986a1cd Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Wed, 15 Apr 2026 19:07:14 +0100 Subject: [PATCH 11/11] Use upstream defaults for DNS on config tests Also ignore line items in config drift testing --- .github/workflows/headscale-config-checker.yml | 2 +- scripts/ci-defaults.sh | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) 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/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"