From 8790e88b305c085b1fdf60647acb630019f64fd3 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sun, 24 Aug 2025 17:21:56 +0100 Subject: [PATCH 01/12] Move default public listen port up --- scripts/container-entrypoint.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 99248c0..68b0346 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -13,6 +13,9 @@ caddyfile_cleartext=/etc/caddy/Caddyfile-http caddyfile_https=/etc/caddy/Caddyfile-https headscale_config="/etc/headscale/config.yaml" +# Defaults used throughout the script +public_listen_port_default=443 + # Caddyfile block placeholders ACME_EAB_BLOCK="" CLOUDFLARE_ACME_BLOCK="" @@ -232,7 +235,7 @@ create_config_from_template() { # Set default or validate PUBLIC_LISTEN_PORT ####################################### check_public_listen_port() { - check_env_var_or_set_default "PUBLIC_LISTEN_PORT" "443" + check_env_var_or_set_default "PUBLIC_LISTEN_PORT" "${public_listen_port_default}" validate_port "PUBLIC_LISTEN_PORT" } From ee95178e99ee2ef8224cec258411fbf605e289e8 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sun, 24 Aug 2025 17:32:40 +0100 Subject: [PATCH 02/12] Move the DNS records var up --- scripts/container-entrypoint.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 68b0346..42f4e82 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -15,6 +15,7 @@ headscale_config="/etc/headscale/config.yaml" # Defaults used throughout the script public_listen_port_default=443 +headscale_extra_records_path_default="/data/headscale/extra-records.json" # Caddyfile block placeholders ACME_EAB_BLOCK="" @@ -346,7 +347,7 @@ validate_oidc_settings() { # Validate extra DNS records settings ####################################### validate_extra_records() { - check_env_var_or_set_default "HEADSCALE_EXTRA_RECORDS_PATH" "/data/headscale/extra-records.json" + check_env_var_or_set_default "HEADSCALE_EXTRA_RECORDS_PATH" "${headscale_extra_records_path_default}" # Ensure the directory exists local records_dir From 2240c0ab12a96672cb6b9bc43a9c8587561c8861 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sun, 24 Aug 2025 17:39:58 +0100 Subject: [PATCH 03/12] Move magic dns up --- scripts/container-entrypoint.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 42f4e82..693eced 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -16,6 +16,7 @@ headscale_config="/etc/headscale/config.yaml" # Defaults used throughout the script public_listen_port_default=443 headscale_extra_records_path_default="/data/headscale/extra-records.json" +headscale_magic_dns_default="true" # Caddyfile block placeholders ACME_EAB_BLOCK="" @@ -379,7 +380,7 @@ check_headscale_environment_vars() { check_litestream_replica_url validate_oidc_settings validate_extra_records - check_env_var_or_set_default "MAGIC_DNS" "true" "^(true|false)$" "Invalid 'MAGIC_DNS'. Must be 'true' or 'false'." + check_env_var_or_set_default "MAGIC_DNS" "${headscale_magic_dns_default}" "^(true|false)$" "Invalid 'MAGIC_DNS'. Must be 'true' or 'false'." check_env_var_or_set_default "IPV6_PREFIX" "fd7a:115c:a1e0::/48" check_env_var_or_set_default "IPV4_PREFIX" "100.64.0.0/10" check_env_var_or_set_default "IP_ALLOCATION" "sequential" "^(sequential|random)$" "Invalid 'IP_ALLOCATION'. Must be either 'sequential' (default) or 'random'." From 1ce9ef2b50ae7083acf72d0e38a134661e606f1d Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sun, 24 Aug 2025 17:40:16 +0100 Subject: [PATCH 04/12] Move default IPv6 prefix up --- scripts/container-entrypoint.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 693eced..6d53179 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -17,6 +17,7 @@ headscale_config="/etc/headscale/config.yaml" public_listen_port_default=443 headscale_extra_records_path_default="/data/headscale/extra-records.json" headscale_magic_dns_default="true" +headscale_ipv6_prefix_default="fd7a:115c:a1e0::/48" # Caddyfile block placeholders ACME_EAB_BLOCK="" @@ -381,7 +382,7 @@ check_headscale_environment_vars() { validate_oidc_settings validate_extra_records check_env_var_or_set_default "MAGIC_DNS" "${headscale_magic_dns_default}" "^(true|false)$" "Invalid 'MAGIC_DNS'. Must be 'true' or 'false'." - check_env_var_or_set_default "IPV6_PREFIX" "fd7a:115c:a1e0::/48" + check_env_var_or_set_default "IPV6_PREFIX" "${headscale_ipv6_prefix_default}" check_env_var_or_set_default "IPV4_PREFIX" "100.64.0.0/10" check_env_var_or_set_default "IP_ALLOCATION" "sequential" "^(sequential|random)$" "Invalid 'IP_ALLOCATION'. Must be either 'sequential' (default) or 'random'." require_env_var "PUBLIC_SERVER_URL" From 61ee559e472bbba741d501c61434fea571451fbd Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sun, 24 Aug 2025 17:40:41 +0100 Subject: [PATCH 05/12] Move IPv4 allocation up --- scripts/container-entrypoint.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 6d53179..cc23112 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -18,6 +18,7 @@ public_listen_port_default=443 headscale_extra_records_path_default="/data/headscale/extra-records.json" headscale_magic_dns_default="true" headscale_ipv6_prefix_default="fd7a:115c:a1e0::/48" +headscale_ipv4_prefix_default="100.64.0.0/10" # Caddyfile block placeholders ACME_EAB_BLOCK="" @@ -383,7 +384,7 @@ check_headscale_environment_vars() { validate_extra_records check_env_var_or_set_default "MAGIC_DNS" "${headscale_magic_dns_default}" "^(true|false)$" "Invalid 'MAGIC_DNS'. Must be 'true' or 'false'." check_env_var_or_set_default "IPV6_PREFIX" "${headscale_ipv6_prefix_default}" - check_env_var_or_set_default "IPV4_PREFIX" "100.64.0.0/10" + check_env_var_or_set_default "IPV4_PREFIX" "${headscale_ipv4_prefix_default}" check_env_var_or_set_default "IP_ALLOCATION" "sequential" "^(sequential|random)$" "Invalid 'IP_ALLOCATION'. Must be either 'sequential' (default) or 'random'." require_env_var "PUBLIC_SERVER_URL" require_env_var "HEADSCALE_DNS_BASE_DOMAIN" From 3fd4bcabdfe343fa24807611c0d823de561afe5b Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sun, 24 Aug 2025 17:41:32 +0100 Subject: [PATCH 06/12] Move default `gomaxprocs` value up This will need some work with the bump, anyway --- scripts/container-entrypoint.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index cc23112..bd30e6d 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -19,6 +19,7 @@ headscale_extra_records_path_default="/data/headscale/extra-records.json" headscale_magic_dns_default="true" headscale_ipv6_prefix_default="fd7a:115c:a1e0::/48" headscale_ipv4_prefix_default="100.64.0.0/10" +headscale_gomaxprocs_default=1 # Caddyfile block placeholders ACME_EAB_BLOCK="" @@ -302,7 +303,7 @@ autodetect_gomaxprocs() { ####################################### configure_gomaxprocs() { if env_var_is_populated "GOMAXPROCS"; then - check_env_var_or_set_default "GOMAXPROCS" "1" "^[1-9][0-9]*$" "Invalid 'GOMAXPROCS'. Must be a positive integer." + check_env_var_or_set_default "GOMAXPROCS" "${headscale_gomaxprocs_default}" "^[1-9][0-9]*$" "Invalid 'GOMAXPROCS'. Must be a positive integer." else autodetect_gomaxprocs fi From d1a1252a89764393d813aec9e3ef72878ad0c107 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sun, 24 Aug 2025 17:41:43 +0100 Subject: [PATCH 07/12] Move default IP allocation up --- scripts/container-entrypoint.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index bd30e6d..9d9507d 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -19,6 +19,7 @@ headscale_extra_records_path_default="/data/headscale/extra-records.json" headscale_magic_dns_default="true" headscale_ipv6_prefix_default="fd7a:115c:a1e0::/48" headscale_ipv4_prefix_default="100.64.0.0/10" +headscale_ip_allocation_default="sequential" headscale_gomaxprocs_default=1 # Caddyfile block placeholders @@ -386,7 +387,7 @@ check_headscale_environment_vars() { check_env_var_or_set_default "MAGIC_DNS" "${headscale_magic_dns_default}" "^(true|false)$" "Invalid 'MAGIC_DNS'. Must be 'true' or 'false'." check_env_var_or_set_default "IPV6_PREFIX" "${headscale_ipv6_prefix_default}" check_env_var_or_set_default "IPV4_PREFIX" "${headscale_ipv4_prefix_default}" - check_env_var_or_set_default "IP_ALLOCATION" "sequential" "^(sequential|random)$" "Invalid 'IP_ALLOCATION'. Must be either 'sequential' (default) or 'random'." + check_env_var_or_set_default "IP_ALLOCATION" "${headscale_ip_allocation_default}" "^(sequential|random)$" "Invalid 'IP_ALLOCATION'. Must be either 'sequential' (default) or 'random'." require_env_var "PUBLIC_SERVER_URL" require_env_var "HEADSCALE_DNS_BASE_DOMAIN" #This is for the v0.26.0 bump. From 2be81cefe95895fc0900f7634875fe3b5c5aa529 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sun, 24 Aug 2025 17:51:04 +0100 Subject: [PATCH 08/12] Extract headscale configury to its own function --- scripts/container-entrypoint.sh | 41 +++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 9d9507d..726e012 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -397,6 +397,29 @@ check_headscale_environment_vars() { fi } +####################################### +# Create our Headscale configuration file +####################################### +create_headscale_config() { + # Ensure all template variables are exported for envsubst + local template_vars=( + "ACME_EAB_BLOCK" + "CLOUDFLARE_ACME_BLOCK" + "SECURITY_HEADERS_BLOCK" + "PUBLIC_LISTEN_PORT" + "MAGIC_DNS" + "IPV6_PREFIX" + "IPV4_PREFIX" + "IP_ALLOCATION" + "HEADSCALE_EXTRA_RECORDS_PATH" + ) + for var in "${template_vars[@]}"; do + export "${var}=${!var}" + done + + create_config_from_template "${headscale_config}" "Headscale configuration file" +} + ####################################### # Validate ZeroSSL EAB credentials if provided and modify Caddyfile as needed ####################################### @@ -568,23 +591,7 @@ check_config_files() { check_caddy_environment_variables - # Ensure all template variables are exported for envsubst - local template_vars=( - "ACME_EAB_BLOCK" - "CLOUDFLARE_ACME_BLOCK" - "SECURITY_HEADERS_BLOCK" - "PUBLIC_LISTEN_PORT" - "MAGIC_DNS" - "IPV6_PREFIX" - "IPV4_PREFIX" - "IP_ALLOCATION" - "HEADSCALE_EXTRA_RECORDS_PATH" - ) - for var in "${template_vars[@]}"; do - export "${var}=${!var}" - done - - create_config_from_template "${headscale_config}" "Headscale configuration file" + create_headscale_config if ${https_enabled}; then create_config_from_template "${caddyfile_https}" "Caddy HTTPS configuration file" From 3cb905fba885d59f1b7bc64f4396888c255778b0 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sun, 24 Aug 2025 18:00:00 +0100 Subject: [PATCH 09/12] Use a heredoc to avoid quoting/escaping issues First step to the remote files looking good lol --- scripts/container-entrypoint.sh | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 726e012..f0c7668 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -428,13 +428,18 @@ check_zerossl_eab() { require_env_var "ACME_EAB_KEY_ID" require_env_var "ACME_EAB_MAC_KEY" - export ACME_EAB_BLOCK="acme_ca https://acme.zerossl.com/v2/DV90 - acme_eab { - key_id ${ACME_EAB_KEY_ID} - mac_key ${ACME_EAB_MAC_KEY} - }" + # Use a heredoc to avoid accidental quoting/escaping issues and preserve formatting + ACME_EAB_BLOCK=$(cat < Date: Sun, 24 Aug 2025 18:14:53 +0100 Subject: [PATCH 10/12] Tighten up `env_var_is_populated` to explicitly allow null/empty string --- scripts/container-entrypoint.sh | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index f0c7668..5ef22c7 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -87,19 +87,22 @@ log_error() { } ####################################### -# Check if an environment variable is populated +# Check if an environment variable is defined. This explicitly includes `null` and `empty string`. # Arguments: # $1 - Variable name # Returns: # `true` if populated, otherwise `false` ####################################### env_var_is_populated() { - # Only allow variable names with letters, numbers, and underscores, not starting with a number - if [[ "${1}" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then - [[ -n "${!1-}" ]] - else - log_error "Invalid environment variable name: '${1}'" - fi + # Only allow variable names with letters, numbers, and underscores, not starting with a number + if ! [[ "${1}" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then + log_error "Invalid environment variable name: '${1}'" + return + fi + + # Consider a variable defined if it is set in the environment, even if the value is an empty string. + # ${param+word} expands to 'word' when the parameter is set (even if null), otherwise empty. + [[ "${!1+set}" == "set" ]] } ####################################### From cd59bb9ed7321f85eb1760a272796b6f155c5f91 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sun, 24 Aug 2025 18:15:32 +0100 Subject: [PATCH 11/12] `env_var_is_populated` => `env_var_is_defined` Better describes its actual function --- scripts/container-entrypoint.sh | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 5ef22c7..d08ef99 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -91,9 +91,9 @@ log_error() { # Arguments: # $1 - Variable name # Returns: -# `true` if populated, otherwise `false` +# `true` if defined, otherwise `false` ####################################### -env_var_is_populated() { +env_var_is_defined() { # Only allow variable names with letters, numbers, and underscores, not starting with a number if ! [[ "${1}" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then log_error "Invalid environment variable name: '${1}'" @@ -113,7 +113,7 @@ env_var_is_populated() { # `true` if populated, otherwise `false` ####################################### require_env_var() { - env_var_is_populated "${1}" || log_error "Environment variable '${1}' is required" + env_var_is_defined "${1}" || log_error "Environment variable '${1}' is required" } ######################################## @@ -145,7 +145,7 @@ check_env_var_or_set_default() { local error_msg="${4:-}" # Set default value if variable is not populated - if ! env_var_is_populated "${var_name}"; then + if ! env_var_is_defined "${var_name}"; then export "${var_name}"="${default_value}" fi @@ -306,7 +306,7 @@ autodetect_gomaxprocs() { # `true` on success, `false` on error ####################################### configure_gomaxprocs() { - if env_var_is_populated "GOMAXPROCS"; then + if env_var_is_defined "GOMAXPROCS"; then check_env_var_or_set_default "GOMAXPROCS" "${headscale_gomaxprocs_default}" "^[1-9][0-9]*$" "Invalid 'GOMAXPROCS'. Must be a positive integer." else autodetect_gomaxprocs @@ -342,7 +342,7 @@ check_litestream_replica_url() { # Validate OIDC settings ####################################### validate_oidc_settings() { - if ! env_var_is_populated "HEADSCALE_OIDC_ISSUER"; then + if ! env_var_is_defined "HEADSCALE_OIDC_ISSUER"; then log_info "OIDC is not enabled, skipping OIDC validation." return fi @@ -394,7 +394,7 @@ check_headscale_environment_vars() { require_env_var "PUBLIC_SERVER_URL" require_env_var "HEADSCALE_DNS_BASE_DOMAIN" #This is for the v0.26.0 bump. - if env_var_is_populated "HEADSCALE_POLICY_V1" ; then + if env_var_is_defined "HEADSCALE_POLICY_V1" ; then export HEADSCALE_POLICY_V1=1 log_warn "Using Headscale policy version 1. Please migrate and remove this variable." fi @@ -427,7 +427,7 @@ create_headscale_config() { # Validate ZeroSSL EAB credentials if provided and modify Caddyfile as needed ####################################### check_zerossl_eab() { - if env_var_is_populated "ACME_EAB_KEY_ID" || env_var_is_populated "ACME_EAB_MAC_KEY"; then + if env_var_is_defined "ACME_EAB_KEY_ID" || env_var_is_defined "ACME_EAB_MAC_KEY"; then require_env_var "ACME_EAB_KEY_ID" require_env_var "ACME_EAB_MAC_KEY" @@ -450,7 +450,7 @@ EOF # Validate the Cloudflare API Key if provided and modify Caddyfile as needed ####################################### check_cloudflare_dns_api_key() { - if env_var_is_populated "CF_API_TOKEN" ; then + if env_var_is_defined "CF_API_TOKEN" ; then export CLOUDFLARE_ACME_BLOCK="tls { dns cloudflare ${CF_API_TOKEN} }" @@ -543,7 +543,7 @@ configure_security_headers() { check_caddy_environment_variables() { configure_security_headers - if env_var_is_populated "CADDY_FRONTEND" && [[ "${CADDY_FRONTEND}" = "DISABLE_HTTPS" ]]; then + if env_var_is_defined "CADDY_FRONTEND" && [[ "${CADDY_FRONTEND}" = "DISABLE_HTTPS" ]]; then https_enabled=false return fi @@ -583,7 +583,7 @@ reuse_or_create_noise_private_key() { return fi - if env_var_is_populated "HEADSCALE_NOISE_PRIVATE_KEY"; then + if env_var_is_defined "HEADSCALE_NOISE_PRIVATE_KEY"; then printf '%s' "${HEADSCALE_NOISE_PRIVATE_KEY}" > "${key_path}" chmod 600 "${key_path}" else @@ -632,9 +632,9 @@ display_configuration_summary() { log_info "IPv4 Prefix: ${IPV4_PREFIX}" log_info "IPv6 Prefix: ${IPV6_PREFIX}" - if env_var_is_populated "HEADSCALE_OIDC_ISSUER"; then + if env_var_is_defined "HEADSCALE_OIDC_ISSUER"; then log_feature_status "OIDC" true "${HEADSCALE_OIDC_ISSUER}" - if env_var_is_populated "HEADSCALE_OIDC_EXTRA_PARAMS_DOMAIN_HINT"; then + if env_var_is_defined "HEADSCALE_OIDC_EXTRA_PARAMS_DOMAIN_HINT"; then log_feature_status "OIDC Domain Hint" true "${HEADSCALE_OIDC_EXTRA_PARAMS_DOMAIN_HINT}" else log_feature_status "OIDC Domain Hint" false "" @@ -642,12 +642,12 @@ display_configuration_summary() { fi if ${https_enabled}; then - if env_var_is_populated "CF_API_TOKEN"; then + if env_var_is_defined "CF_API_TOKEN"; then log_info "DNS Challenge: Cloudflare" else log_info "DNS Challenge: HTTP-01" fi - if env_var_is_populated "ACME_EAB_KEY_ID"; then + if env_var_is_defined "ACME_EAB_KEY_ID"; then log_feature_status "ACME EAB" true "ZeroSSL" else log_feature_status "ACME EAB" false "Let's Encrypt" From aca8fffc146291154bb059e308cb0970d22a45e4 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sun, 24 Aug 2025 18:20:55 +0100 Subject: [PATCH 12/12] Move Caddyfile creation to its own function --- scripts/container-entrypoint.sh | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index d08ef99..6bec04e 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -423,6 +423,17 @@ create_headscale_config() { create_config_from_template "${headscale_config}" "Headscale configuration file" } +####################################### +# Create our Caddyfile +####################################### +create_caddyfile() { + if ${https_enabled}; then + create_config_from_template "${caddyfile_https}" "Caddy HTTPS configuration file" + else + create_config_from_template "${caddyfile_cleartext}" "Caddy HTTP configuration file" + fi +} + ####################################### # Validate ZeroSSL EAB credentials if provided and modify Caddyfile as needed ####################################### @@ -601,11 +612,7 @@ check_config_files() { create_headscale_config - if ${https_enabled}; then - create_config_from_template "${caddyfile_https}" "Caddy HTTPS configuration file" - else - create_config_from_template "${caddyfile_cleartext}" "Caddy HTTP configuration file" - fi + create_caddyfile reuse_or_create_noise_private_key }