diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 99248c0..6bec04e 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -13,6 +13,15 @@ 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 +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 ACME_EAB_BLOCK="" CLOUDFLARE_ACME_BLOCK="" @@ -78,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` +# `true` if defined, 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 +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}'" + 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" ]] } ####################################### @@ -101,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" } ######################################## @@ -133,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 @@ -232,7 +244,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" } @@ -294,8 +306,8 @@ autodetect_gomaxprocs() { # `true` on success, `false` on error ####################################### 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." + 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 fi @@ -330,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 @@ -343,7 +355,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 @@ -375,34 +387,73 @@ 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 "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'." + 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" "${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. - 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 } +####################################### +# 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" +} + +####################################### +# 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 ####################################### 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" - 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 < "${key_path}" chmod 600 "${key_path}" else @@ -559,29 +610,9 @@ 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" - else - create_config_from_template "${caddyfile_cleartext}" "Caddy HTTP configuration file" - fi + create_caddyfile reuse_or_create_noise_private_key } @@ -608,9 +639,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 "" @@ -618,12 +649,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"