From 96775e76c7d96384d51772f2e67bbafb8d2399c5 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 9 Jun 2025 10:48:08 +0100 Subject: [PATCH 01/42] add missing trailing newline --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index b95b99f..ceeb9b9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,4 +28,4 @@ Details about configuration options. ## Contributing -Guidelines for contributing to the project. \ No newline at end of file +Guidelines for contributing to the project. From b95a82699fb7750c0221f41a412f558c42c20f89 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 9 Jun 2025 10:50:19 +0100 Subject: [PATCH 02/42] Replace our `sed` with `envsubst` It was just getting a little silly --- scripts/container-entrypoint.sh | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 036d6a1..e2e886a 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -198,19 +198,14 @@ check_required_environment_vars() { ####################################### create_headscale_config() { local config_path="/etc/headscale/config.yaml" + local temp_config_path="/tmp/config.yaml" log_info "Generating Headscale configuration file..." - sed -i \ - -e "s@\$PUBLIC_SERVER_URL@$PUBLIC_SERVER_URL@" \ - -e "s@\$HEADSCALE_LISTEN_ADDRESS@$HEADSCALE_LISTEN_ADDRESS@" \ - -e "s@\$PUBLIC_LISTEN_PORT@$PUBLIC_LISTEN_PORT@" \ - -e "s@\$IPV6_PREFIX@$IPV6_PREFIX@" \ - -e "s@\$IPV4_PREFIX@$IPV4_PREFIX@" \ - -e "s@\$IP_ALLOCATION@$IP_ALLOCATION@" \ - -e "s@\$HEADSCALE_DNS_CONFIG_BASE_DOMAIN@$HEADSCALE_DNS_CONFIG_BASE_DOMAIN@" \ - -e "s@\$MAGIC_DNS@$MAGIC_DNS@" \ - "$config_path" || log_error "Unable to generate Headscale configuration file" + # shellcheck disable=SC2015 # We're not using this as in if condition, both could error out + envsubst < "$config_path" > "$temp_config_path" \ + && mv "$temp_config_path" "$config_path" \ + || log_error "Unable to generate Headscale configuration file" } ####################################### From 59aa8e872cddd80cd931a95c8a61dfb61961fef6 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 9 Jun 2025 10:59:32 +0100 Subject: [PATCH 03/42] Fix variables to work with `envsubst` --- templates/caddy.https.template.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/caddy.https.template.yaml b/templates/caddy.https.template.yaml index 5cbbc51..46445b8 100644 --- a/templates/caddy.https.template.yaml +++ b/templates/caddy.https.template.yaml @@ -3,12 +3,12 @@ root /data/caddy } - email {$ACME_ISSUANCE_EMAIL} + email ${ACME_ISSUANCE_EMAIL} <> } -{$PUBLIC_SERVER_URL}:{$PUBLIC_LISTEN_PORT} { +${PUBLIC_SERVER_URL}:${PUBLIC_LISTEN_PORT} { handle_path /admin* { root * /admin-gui/admin encode gzip zstd From 01a191e0d40280fde35d5a334d9a500336420dc0 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 9 Jun 2025 11:08:55 +0100 Subject: [PATCH 04/42] Handle `/admin` endpoint properly --- templates/caddy.https.template.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/templates/caddy.https.template.yaml b/templates/caddy.https.template.yaml index 46445b8..0660278 100644 --- a/templates/caddy.https.template.yaml +++ b/templates/caddy.https.template.yaml @@ -16,6 +16,10 @@ ${PUBLIC_SERVER_URL}:${PUBLIC_LISTEN_PORT} { file_server } + handle /admin { + redirect /admin/ + } + handle { reverse_proxy 127.0.0.1:8080 } From dc20cc46a3a63ea6a5cfe7a037e30614dbf65167 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 9 Jun 2025 14:12:39 +0100 Subject: [PATCH 05/42] =?UTF-8?q?`-z`=20flag=20is=20GNU-specific=20?= =?UTF-8?q?=F0=9F=A4=A6=E2=80=8D=E2=99=82=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 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 e2e886a..6f41029 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -236,7 +236,7 @@ check_zerossl_eab() { require_env_var "ACME_EAB_KEY_ID" require_env_var "ACME_EAB_MAC_KEY" - sed -iz \ + sed -i \ "s@<>@acme_ca https://acme.zerossl.com/v2/DV90\nacme_eab {\n key_id ${ACME_EAB_KEY_ID}\n mac_key ${ACME_EAB_MAC_KEY}\n }@" \ $caddyfile_https || abort_config=1 else @@ -252,7 +252,7 @@ check_cloudflare_dns_api_key() { if env_var_is_populated "CF_API_TOKEN" ; then log_info "Using Cloudflare for ACME DNS Challenge." - sed -iz \ + sed -i \ "s@<>@tls {\n dns cloudflare $CF_API_TOKEN\n }@" \ $caddyfile_https || abort_config=1 else From eb57c92fcf305abb588f394867bd70706da2b639 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 9 Jun 2025 14:16:25 +0100 Subject: [PATCH 06/42] Add timestamps to logging --- 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 6f41029..f28f860 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -17,7 +17,7 @@ caddyfile_https=/etc/caddy/Caddyfile-https # Message to `STDOUT` ####################################### log_info() { - echo "INFO: $1" + echo "$(date +"%Y-%m-%d %H:%M:%S") INFO: $1" } ####################################### @@ -32,7 +32,7 @@ log_info() { # Message to `STDERR` ####################################### log_error() { - echo >&2 "ERROR: $1" + echo >&2 "$(date +"%Y-%m-%d %H:%M:%S") ERROR: $1" abort_config=true false } From 5b2dbada7cd3a34c6fe0dcfe31da790cab952566 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 9 Jun 2025 15:28:45 +0100 Subject: [PATCH 07/42] use `log_error` instead of returning failure --- scripts/container-entrypoint.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index f28f860..a4e4cca 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -292,9 +292,9 @@ check_config_files() { # Create required directories ####################################### check_needed_directories() { - mkdir -p /var/run/headscale || return - mkdir -p /data/headscale || return - mkdir -p /data/caddy || return + mkdir -p /var/run/headscale || log_error "Unable to create /var/run/headscale directory." + mkdir -p /data/headscale || log_error "Unable to create /data/headscale directory." + mkdir -p /data/caddy || log_error "Unable to create /data/caddy directory." } ####################################### From 8e1992264f1d25b4c55363c288e847b9d8ccabba Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 9 Jun 2025 15:35:12 +0100 Subject: [PATCH 08/42] Typo --- scripts/container-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index a4e4cca..f0cd074 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -13,7 +13,7 @@ caddyfile_https=/etc/caddy/Caddyfile-https # Log an informational message # Arguments: # `$1` - Message to log -# Ouputs: +# Outputs: # Message to `STDOUT` ####################################### log_info() { From f8fc3bf7ab73fc2ba225459c01c5bf6505bae6aa Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 9 Jun 2025 15:42:13 +0100 Subject: [PATCH 09/42] Make port checking more robust --- scripts/container-entrypoint.sh | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index f0cd074..607e92c 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -69,15 +69,23 @@ require_env_var() { # `true` if deemed valid, otherwise `false` ####################################### validate_port() { - port="$1" - case "${!port}" in - '' | *[!0123456789]*) log_error "'$port' is not numeric." && return ;; - 0*[!0]*) log_error "'$port' has a leading zero." && return ;; - esac - - if [ "${!port}" -lt 1 ] || [ "${!port}" -gt 65535 ] ; then - log_error "'$port' must be a valid port within the range of 1-65535." && return - fi + port="$1" + value="${!port}" + + # Make sure our port is numeric + if ! [[ "$value" =~ ^[0-9]+$ ]]; then + log_error "Port '$port' is not numeric." && return + fi + + # Check no leading zeros (except for port '0') + if [[ "$value" =~ ^0[0-9]+$ ]]; then + log_error "Port '$port' has a leading zero." && return + fi + + # Check port is within valid range + if [ "$value" -lt 1 ] || [ "$value" -gt 65535 ]; then + log_error "Port '$port' must be a valid port within the range of 1-65535." && return + fi } ####################################### From d27eb9e1c69ea693ad81e2d54c6a81d5044330e8 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 9 Jun 2025 15:44:06 +0100 Subject: [PATCH 10/42] Lol, another typo copypasta FTW --- scripts/container-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 607e92c..6ed6553 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -28,7 +28,7 @@ log_info() { # `abort_config` # Returns: # `false` -# Ouputs: +# Outputs: # Message to `STDERR` ####################################### log_error() { From 438b1ab7386e3e062fc369de7cc9b44c47149243 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 9 Jun 2025 16:07:29 +0100 Subject: [PATCH 11/42] Add `-u` to exit when we try and use an unset var set -u: exit on unset variable --- scripts/container-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 6ed6553..f1ee9fe 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -e +set -eu # Global flags abort_config=false From 79d2468d6e0820020d511c717d63cf6eae317455 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 9 Jun 2025 16:07:54 +0100 Subject: [PATCH 12/42] Add `-o pipefail` Purely futureproofing. Not needed right now --- scripts/container-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index f1ee9fe..95e9868 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -eu +set -euo pipefail # Global flags abort_config=false From de8b38fb77c5c5e73b4f52328a81ffccca72b62d Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 9 Jun 2025 16:11:06 +0100 Subject: [PATCH 13/42] Harden variable name checking --- scripts/container-entrypoint.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 95e9868..96bcd3d 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -45,7 +45,12 @@ log_error() { # `true` if populated, otherwise `false` ####################################### env_var_is_populated() { - [ -n "${!1}" ] + # 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 } ####################################### From e4e96fb745062ca0d83d3f6ff18888485ba342dc Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 9 Jun 2025 17:10:21 +0100 Subject: [PATCH 14/42] Make it more DRY --- 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 96bcd3d..7b41d78 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -10,14 +10,23 @@ caddyfile_cleartext=/etc/caddy/Caddyfile-http caddyfile_https=/etc/caddy/Caddyfile-https ####################################### -# Log an informational message +# Log a message # Arguments: # `$1` - Message to log # Outputs: # Message to `STDOUT` ####################################### +log() { + echo "$(date +"%Y-%m-%d %H:%M:%S") $1" +} + +####################################### +# Log an informational message +# Arguments: +# `$1` - Message to log +####################################### log_info() { - echo "$(date +"%Y-%m-%d %H:%M:%S") INFO: $1" + log "INFO: $1" } ####################################### @@ -28,11 +37,9 @@ log_info() { # `abort_config` # Returns: # `false` -# Outputs: -# Message to `STDERR` ####################################### log_error() { - echo >&2 "$(date +"%Y-%m-%d %H:%M:%S") ERROR: $1" + log >&2 "ERROR: $1" abort_config=true false } From 155d2119f031f6f09045092a2c481b3b8fcdf43d Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 9 Jun 2025 17:20:08 +0100 Subject: [PATCH 15/42] Swap `headscale-admin` to the `0.26` branch --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a30d2f6..42bfab9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ ARG LITESTREAM_SHA256="eb75a3de5cab03875cdae9f5f539e6aedadd66607003d9b1e7a907794 # Bump these every time there is a new release. No checksum needed. ARG CADDY_VERSION="2.10.0" ARG MAIN_IMAGE_ALPINE_VERSION="3.22.0" -ARG HEADSCALE_ADMIN_VERSION="dev" +ARG HEADSCALE_ADMIN_VERSION="0.26" # Currently a beta branch - switch to a stable version ASAP # --- # Tool download links From 161185df99c253cd321f581dee6355da2232bef2 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 9 Jun 2025 17:54:11 +0100 Subject: [PATCH 16/42] Revert "Swap `headscale-admin` to the `0.26` branch" This reverts commit 155d2119f031f6f09045092a2c481b3b8fcdf43d. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 42bfab9..a30d2f6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ ARG LITESTREAM_SHA256="eb75a3de5cab03875cdae9f5f539e6aedadd66607003d9b1e7a907794 # Bump these every time there is a new release. No checksum needed. ARG CADDY_VERSION="2.10.0" ARG MAIN_IMAGE_ALPINE_VERSION="3.22.0" -ARG HEADSCALE_ADMIN_VERSION="0.26" # Currently a beta branch - switch to a stable version ASAP +ARG HEADSCALE_ADMIN_VERSION="dev" # --- # Tool download links From 8d720656416241a914be9074c558919b96332c80 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Mon, 9 Jun 2025 19:24:58 +0100 Subject: [PATCH 17/42] Add missing `gettext` dependency for `envsubst` --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a30d2f6..809d72b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -59,7 +59,7 @@ FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} # - We need GNU sed # hadolint ignore=DL3018,SC2086 RUN BUILD_DEPS="wget"; \ - RUNTIME_DEPS="bash sed"; \ + RUNTIME_DEPS="bash sed gettext"; \ apk --no-cache upgrade; \ apk add --no-cache --virtual BuildTimeDeps ${BUILD_DEPS}; \ apk add --no-cache ${RUNTIME_DEPS} From ae221798e68eb04266b5147f049d186ee1a0b3e2 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Tue, 10 Jun 2025 20:17:31 +0100 Subject: [PATCH 18/42] Futureproof logging --- scripts/container-entrypoint.sh | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 7b41d78..b261ebb 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -10,14 +10,27 @@ caddyfile_cleartext=/etc/caddy/Caddyfile-http caddyfile_https=/etc/caddy/Caddyfile-https ####################################### -# Log a message +# Log with different levels # Arguments: -# `$1` - Message to log -# Outputs: -# Message to `STDOUT` -####################################### -log() { - echo "$(date +"%Y-%m-%d %H:%M:%S") $1" +# $1 - Log level (INFO, WARN, ERROR) +# $2 - Message to log +####################################### +log_with_level() { + local level="$1" + local message="$2" + local timestamp=$(date +"%Y-%m-%d %H:%M:%S") + + case "$level" in + ERROR) + echo "[$timestamp] ERROR: $message" >&2 + ;; + WARN) + echo "[$timestamp] WARN: $message" >&2 + ;; + *) + echo "[$timestamp] INFO: $message" + ;; + esac } ####################################### @@ -26,7 +39,7 @@ log() { # `$1` - Message to log ####################################### log_info() { - log "INFO: $1" + log_with_level "INFO" "$1" } ####################################### @@ -39,7 +52,7 @@ log_info() { # `false` ####################################### log_error() { - log >&2 "ERROR: $1" + log_with_level "ERROR" "$1" abort_config=true false } From 5dd1a658211ac3244c54b143d2c4a662f7a36575 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Tue, 10 Jun 2025 20:18:03 +0100 Subject: [PATCH 19/42] Add a `WARN` logging level it's not yet used, but may be useful in future --- scripts/container-entrypoint.sh | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index b261ebb..b846dbf 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -42,6 +42,15 @@ log_info() { log_with_level "INFO" "$1" } +####################################### +# Log a warning message +# Arguments: +# `$1` - Message to log +####################################### +log_warn() { + log_with_level "WARN" "$1" +} + ####################################### # Log an error message and set abort flag # Arguments: @@ -53,8 +62,8 @@ log_info() { ####################################### log_error() { log_with_level "ERROR" "$1" - abort_config=true - false + abort_config=true + false } ####################################### From 4f73c7feb59164de9ebe84637a03ab1140613aad Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Tue, 10 Jun 2025 21:42:59 +0100 Subject: [PATCH 20/42] There are no returns to catch --- 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 b846dbf..0a52a91 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -343,9 +343,9 @@ check_needed_directories() { # Main logic ####################################### run() { - check_needed_directories || log_error "Unable to create required configuration directories." + check_needed_directories - check_config_files || log_error "We don't have enough information to run our services." + check_config_files if ! $abort_config ; then log_info "Starting Caddy using our environment variables. HTTPS is $([ "$cleartext_only" ] && echo "disabled" || echo "enabled")." From 0c5b1a42b0abc3c25466698ebb865f2bc79e7c61 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Tue, 10 Jun 2025 21:46:18 +0100 Subject: [PATCH 21/42] Catch Caddy failing to start before continuing --- scripts/container-entrypoint.sh | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 0a52a91..907db5f 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -351,19 +351,22 @@ run() { log_info "Starting Caddy using our environment variables. HTTPS is $([ "$cleartext_only" ] && echo "disabled" || echo "enabled")." if $cleartext_only ; then - caddy start --config "$caddyfile_cleartext" + caddy start --config "$caddyfile_cleartext" || log_error "Failed to start Caddy with cleartext config" else - caddy start --config "$caddyfile_https" + caddy start --config "$caddyfile_https" || log_error "Failed to start Caddy with HTTPS config" fi - if ! $litestream_disabled ; then - log_info "Attempt to restore previous Headscale database if there's a replica" && \ - litestream restore -if-db-not-exists -if-replica-exists /data/headscale.sqlite3 && \ - \ - log_info "Starting Headscale using Litestream and our Environment Variables..." && \ - litestream replicate -exec 'headscale serve' - else - headscale serve + # Make sure Caddy started successfully before starting headscale + if ! $abort_config ; then + if ! $litestream_disabled ; then + log_info "Attempt to restore previous Headscale database if there's a replica" && \ + litestream restore -if-db-not-exists -if-replica-exists /data/headscale.sqlite3 && \ + \ + log_info "Starting Headscale using Litestream and our Environment Variables..." && \ + litestream replicate -exec 'headscale serve' + else + headscale serve + fi fi fi From c5f09838812f257e3e000e1f466a226ccbf6d7ef Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Tue, 10 Jun 2025 21:51:00 +0100 Subject: [PATCH 22/42] Fix up `litestream` logic --- scripts/container-entrypoint.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 907db5f..72a579d 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -359,10 +359,11 @@ run() { # Make sure Caddy started successfully before starting headscale if ! $abort_config ; then if ! $litestream_disabled ; then - log_info "Attempt to restore previous Headscale database if there's a replica" && \ - litestream restore -if-db-not-exists -if-replica-exists /data/headscale.sqlite3 && \ - \ - log_info "Starting Headscale using Litestream and our Environment Variables..." && \ + log_info "Attempt to restore previous Headscale database if there's a replica" + litestream restore -if-db-not-exists -if-replica-exists /data/headscale.sqlite3 || + log_warn "No replica found, or unable to restore database." + + log_info "Starting Headscale using Litestream and our Environment Variables..." litestream replicate -exec 'headscale serve' else headscale serve From d07d01b13518041dcd7d566febf457e4080b6a83 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Tue, 10 Jun 2025 21:53:45 +0100 Subject: [PATCH 23/42] Fix the `DEBUG` check so it doesn't fail out --- scripts/container-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 72a579d..6a78149 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -372,7 +372,7 @@ run() { fi log_error "Something went wrong." - if [ -n "$DEBUG" ] ; then + if [ -n "${DEBUG:-}" ] ; then log_info "Sleeping so you can connect and debug" # Allow us to start a terminal in the container for debugging sleep infinity From b383478a020ecf6624f75e47090bcaa60bb00d30 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Tue, 10 Jun 2025 21:59:26 +0100 Subject: [PATCH 24/42] Improve error handling with `sed` --- scripts/container-entrypoint.sh | 36 +++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 6a78149..cccbe01 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -278,12 +278,18 @@ check_zerossl_eab() { require_env_var "ACME_EAB_KEY_ID" require_env_var "ACME_EAB_MAC_KEY" - sed -i \ + if ! sed -i \ "s@<>@acme_ca https://acme.zerossl.com/v2/DV90\nacme_eab {\n key_id ${ACME_EAB_KEY_ID}\n mac_key ${ACME_EAB_MAC_KEY}\n }@" \ - $caddyfile_https || abort_config=1 + "$caddyfile_https"; then + log_error "Failed to modify Caddyfile with ACME EAB credentials" + fi else log_info "No ACME EAB credentials provided" - sed -i "s@<>@@" $caddyfile_https || abort_config=1 + if ! sed -i \ + "s@<>@@" \ + "$caddyfile_https" ; then + log_error "Failed to modify Caddyfile to remove ACME EAB placeholder" + fi fi } @@ -291,16 +297,20 @@ check_zerossl_eab() { # 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 - log_info "Using Cloudflare for ACME DNS Challenge." - - sed -i \ - "s@<>@tls {\n dns cloudflare $CF_API_TOKEN\n }@" \ - $caddyfile_https || abort_config=1 - else - log_info "Using HTTP authentication for ACME DNS Challenge" - sed -i "s@<>@@" $caddyfile_https || abort_config=1 - fi + if env_var_is_populated "CF_API_TOKEN" ; then + log_info "Using Cloudflare for ACME DNS Challenge." + + if ! sed -i \ + "s@<>@tls {\n dns cloudflare $CF_API_TOKEN\n }@" \ + "$caddyfile_https"; then + log_error "Failed to configure Cloudflare DNS in Caddyfile" + fi + else + log_info "Using HTTP authentication for ACME DNS Challenge" + if ! sed -i "s@<>@@" "$caddyfile_https"; then + log_error "Failed to remove Cloudflare placeholder from Caddyfile" + fi + fi } ####################################### From 692ac0cc45103f21cd9e3cc33f9bc0925e5e0f11 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Tue, 10 Jun 2025 22:06:18 +0100 Subject: [PATCH 25/42] Make sure our noise key has appropriate perms --- scripts/container-entrypoint.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index cccbe01..4b81ec3 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -258,12 +258,14 @@ reuse_or_create_noise_private_key() { if [ -f "$key_path" ]; then log_info "Using existing private Noise key on disk." + chmod 600 "$key_path" return fi if env_var_is_populated "HEADSCALE_NOISE_PRIVATE_KEY"; then log_info "Using provided private Noise key from environment variable." echo -n "$HEADSCALE_NOISE_PRIVATE_KEY" > "$key_path" + chmod 600 "$key_path" else log_info "Generating a new private Noise key." fi From 2f236a76bee014ec7a0192c0af62876f60dc84e1 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Tue, 10 Jun 2025 22:16:00 +0100 Subject: [PATCH 26/42] Fix up `create_headscale_config()` Deal with our temp file and fix permissions --- scripts/container-entrypoint.sh | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 4b81ec3..84fefdc 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -239,15 +239,28 @@ check_required_environment_vars() { # Create Headscale configuration file ####################################### create_headscale_config() { - local config_path="/etc/headscale/config.yaml" - local temp_config_path="/tmp/config.yaml" - - log_info "Generating Headscale configuration file..." - - # shellcheck disable=SC2015 # We're not using this as in if condition, both could error out - envsubst < "$config_path" > "$temp_config_path" \ - && mv "$temp_config_path" "$config_path" \ - || log_error "Unable to generate Headscale configuration file" + local config_path="/etc/headscale/config.yaml" + local temp_config_path + + temp_config_path=$(mktemp) || { + log_error "Unable to create temporary file" + return + } + + log_info "Generating Headscale configuration file..." + + if envsubst < "$config_path" > "$temp_config_path"; then + chmod 600 "$temp_config_path" + if mv "$temp_config_path" "$config_path"; then + log_info "Headscale configuration file created successfully" + else + log_error "Unable to move Headscale configuration file" + rm -f "$temp_config_path" + fi + else + log_error "Unable to generate Headscale configuration file" + rm -f "$temp_config_path" + fi } ####################################### From 9bcc15145e9debe55d0f5d19998024981c568674 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Tue, 10 Jun 2025 22:18:47 +0100 Subject: [PATCH 27/42] Swap `echo -n` to `printf` --- scripts/container-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 84fefdc..82af569 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -277,7 +277,7 @@ reuse_or_create_noise_private_key() { if env_var_is_populated "HEADSCALE_NOISE_PRIVATE_KEY"; then log_info "Using provided private Noise key from environment variable." - echo -n "$HEADSCALE_NOISE_PRIVATE_KEY" > "$key_path" + printf '%s' "$HEADSCALE_NOISE_PRIVATE_KEY" > "$key_path" chmod 600 "$key_path" else log_info "Generating a new private Noise key." From 98d991befd5d97df5952fe222de861724e62c535 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Tue, 10 Jun 2025 22:20:33 +0100 Subject: [PATCH 28/42] `exec` our final command so it properly passes through signals --- 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 82af569..a2a0398 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -389,9 +389,9 @@ run() { log_warn "No replica found, or unable to restore database." log_info "Starting Headscale using Litestream and our Environment Variables..." - litestream replicate -exec 'headscale serve' + exec litestream replicate -exec 'headscale serve' else - headscale serve + exec headscale serve fi fi fi From 65c6820df1593d47dd8d23b5d8cf1870a61cbeb6 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Tue, 10 Jun 2025 22:25:44 +0100 Subject: [PATCH 29/42] Make boolean logic explicit --- scripts/container-entrypoint.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index a2a0398..34f0b9d 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -373,9 +373,9 @@ run() { check_config_files if ! $abort_config ; then - log_info "Starting Caddy using our environment variables. HTTPS is $([ "$cleartext_only" ] && echo "disabled" || echo "enabled")." + log_info "Starting Caddy using our environment variables. HTTPS is $([ "$cleartext_only" = true ] && echo "disabled" || echo "enabled")." - if $cleartext_only ; then + if [ "$cleartext_only" = true ] ; then caddy start --config "$caddyfile_cleartext" || log_error "Failed to start Caddy with cleartext config" else caddy start --config "$caddyfile_https" || log_error "Failed to start Caddy with HTTPS config" @@ -383,7 +383,7 @@ run() { # Make sure Caddy started successfully before starting headscale if ! $abort_config ; then - if ! $litestream_disabled ; then + if [ "$litestream_disabled" = false ] ; then log_info "Attempt to restore previous Headscale database if there's a replica" litestream restore -if-db-not-exists -if-replica-exists /data/headscale.sqlite3 || log_warn "No replica found, or unable to restore database." From b8a8432470ccb1ccb4a5fab4f99edeee1eb620df Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Tue, 10 Jun 2025 22:26:39 +0100 Subject: [PATCH 30/42] Exit properly when it errors out --- scripts/container-entrypoint.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 34f0b9d..0a398d4 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -403,8 +403,7 @@ run() { sleep infinity fi - log_error "Exiting with code ${abort_config}" - exit "$abort_config" + exit 1 } run From 76db274ff323282534fd6822c05f397c15b6fce6 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Tue, 10 Jun 2025 22:28:13 +0100 Subject: [PATCH 31/42] Add a log message if we're not using litestream --- scripts/container-entrypoint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 0a398d4..018d58d 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -391,6 +391,7 @@ run() { log_info "Starting Headscale using Litestream and our Environment Variables..." exec litestream replicate -exec 'headscale serve' else + log_info "Starting Headscale without Litestream" exec headscale serve fi fi From 4b68b6fc484fff54b2c226c64c94ce95d9886d81 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Tue, 10 Jun 2025 22:29:46 +0100 Subject: [PATCH 32/42] Remove redundant log message --- scripts/container-entrypoint.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 018d58d..d208331 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -397,7 +397,6 @@ run() { fi fi - log_error "Something went wrong." if [ -n "${DEBUG:-}" ] ; then log_info "Sleeping so you can connect and debug" # Allow us to start a terminal in the container for debugging From f48a757cff5e82b8138d7c194ba8350d06d65e2c Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Tue, 10 Jun 2025 22:38:36 +0100 Subject: [PATCH 33/42] Stop running as `root` inside the container --- Dockerfile | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 809d72b..1a82fb9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -117,6 +117,12 @@ FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} # Remove build-time dependencies RUN apk del BuildTimeDeps + # Create non-root user for security + RUN addgroup -g 1001 -S headscale && \ + adduser -u 1001 -S headscale -G headscale && \ + mkdir -p /data /var/lib/headscale && \ + chown -R headscale:headscale /data /var/lib/headscale /admin-gui + # --- # copy configuration and templates COPY ./templates/headscale.template.yaml /etc/headscale/config.yaml @@ -124,6 +130,8 @@ FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} COPY ./templates/caddy.http.template.yaml /etc/caddy/Caddyfile-http COPY ./templates/caddy.https.template.yaml /etc/caddy/Caddyfile-https COPY ./scripts/container-entrypoint.sh /container-entrypoint.sh - RUN chmod +x /container-entrypoint.sh + RUN chmod +x /container-entrypoint.sh && \ + chown headscale:headscale /container-entrypoint.sh + USER headscale ENTRYPOINT ["/container-entrypoint.sh"] From 4009671fe6e1d6e65233d2a72e89fb574730bdff Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Tue, 10 Jun 2025 22:41:13 +0100 Subject: [PATCH 34/42] Revert "Stop running as `root` inside the container" This reverts commit f48a757cff5e82b8138d7c194ba8350d06d65e2c. --- Dockerfile | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1a82fb9..809d72b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -117,12 +117,6 @@ FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} # Remove build-time dependencies RUN apk del BuildTimeDeps - # Create non-root user for security - RUN addgroup -g 1001 -S headscale && \ - adduser -u 1001 -S headscale -G headscale && \ - mkdir -p /data /var/lib/headscale && \ - chown -R headscale:headscale /data /var/lib/headscale /admin-gui - # --- # copy configuration and templates COPY ./templates/headscale.template.yaml /etc/headscale/config.yaml @@ -130,8 +124,6 @@ FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} COPY ./templates/caddy.http.template.yaml /etc/caddy/Caddyfile-http COPY ./templates/caddy.https.template.yaml /etc/caddy/Caddyfile-https COPY ./scripts/container-entrypoint.sh /container-entrypoint.sh - RUN chmod +x /container-entrypoint.sh && \ - chown headscale:headscale /container-entrypoint.sh + RUN chmod +x /container-entrypoint.sh - USER headscale ENTRYPOINT ["/container-entrypoint.sh"] From dd470aca018260250b798ba3b765e761193a130d Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Wed, 11 Jun 2025 15:38:05 +0100 Subject: [PATCH 35/42] Separate the variable and its value to not mask --- scripts/container-entrypoint.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index d208331..2dc0e24 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -18,8 +18,10 @@ caddyfile_https=/etc/caddy/Caddyfile-https log_with_level() { local level="$1" local message="$2" - local timestamp=$(date +"%Y-%m-%d %H:%M:%S") - + local timestamp; + + timestamp=$(date +"%Y-%m-%d %H:%M:%S") + case "$level" in ERROR) echo "[$timestamp] ERROR: $message" >&2 From 1debb6a8b292ca47fca669a22deaf35778427781 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Wed, 11 Jun 2025 15:39:38 +0100 Subject: [PATCH 36/42] Case insensitivity --- scripts/container-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 2dc0e24..6a90b26 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -22,7 +22,7 @@ log_with_level() { timestamp=$(date +"%Y-%m-%d %H:%M:%S") - case "$level" in + case "${level^^}" in ERROR) echo "[$timestamp] ERROR: $message" >&2 ;; From bac098b017bb9d0a7823d697f82701116ec69d9a Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sat, 14 Jun 2025 21:14:44 +0100 Subject: [PATCH 37/42] Handle errors better --- Dockerfile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 809d72b..736100e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,7 +73,7 @@ FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} # --- # Headscale - RUN { \ + RUN set -ex; { \ wget --retry-connrefused \ --waitretry=1 \ --read-timeout=20 \ @@ -81,8 +81,10 @@ FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} -t 0 \ -q \ -O headscale \ - ${HEADSCALE_DOWNLOAD_URL} \ - ; \ + ${HEADSCALE_DOWNLOAD_URL} || { \ + echo "Failed to download Headscale from ${HEADSCALE_DOWNLOAD_URL}"; \ + exit 1; \ + }; \ echo "${HEADSCALE_SHA256} *headscale" | sha256sum -c - >/dev/null 2>&1; \ chmod +x headscale; \ mv headscale /usr/local/bin/; \ @@ -92,7 +94,7 @@ FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} headscale version; # Litestream - RUN { \ + RUN set -ex; { \ wget --retry-connrefused \ --waitretry=1 \ --read-timeout=20 \ From 4a5339808d7852e6176a9455cc8f00ade695ee3a Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sat, 14 Jun 2025 21:51:31 +0100 Subject: [PATCH 38/42] Add a default `EXPOSE` to our Dockerfile --- Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index 736100e..6a56d51 100644 --- a/Dockerfile +++ b/Dockerfile @@ -128,4 +128,8 @@ FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} COPY ./scripts/container-entrypoint.sh /container-entrypoint.sh RUN chmod +x /container-entrypoint.sh + # --- + # Note this is the default listening port and may be overridden at runtime by `$PUBLIC_LISTEN_PORT` + EXPOSE 443 + ENTRYPOINT ["/container-entrypoint.sh"] From 7b493d0ca7ba452c331ca422d218310514175921 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sat, 14 Jun 2025 22:06:02 +0100 Subject: [PATCH 39/42] Normalise comments --- Dockerfile | 43 ++++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6a56d51..324a121 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,30 +1,29 @@ -# --- -# Tool version args -# Bump these every time there is a new release. Don't forget the checksum! +################### +# BUILD PREP +################### +# Tool version arguments +# Bump these every time there is a new release. +# We're pulling these from github source, don't forget to bump the checksum! ARG HEADSCALE_VERSION="0.26.1" ARG HEADSCALE_SHA256="5012577e6fc5d4234aab7b4be0d6e271ea1a4ec38521a8aa472f80ea1fe81cba" ARG LITESTREAM_VERSION="0.3.13" ARG LITESTREAM_SHA256="eb75a3de5cab03875cdae9f5f539e6aedadd66607003d9b1e7a9077948818ba0" -# --- -# Container version args -# Bump these every time there is a new release. No checksum needed. +# No checksum needed for these tools, we pull from official images ARG CADDY_VERSION="2.10.0" ARG MAIN_IMAGE_ALPINE_VERSION="3.22.0" ARG HEADSCALE_ADMIN_VERSION="dev" -# --- -# Tool download links +# github download links # These should never need adjusting unless the URIs change ARG HEADSCALE_DOWNLOAD_URL="https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_amd64" ARG LITESTREAM_DOWNLOAD_URL="https://github.com/benbjohnson/litestream/releases/download/v${LITESTREAM_VERSION}/litestream-v${LITESTREAM_VERSION}-linux-amd64.tar.gz" -########### -# LOGIC STARTS HERE -########### +################### +# BUILD PROCESS +################### -# --- # Build caddy with Cloudflare DNS support FROM caddy:${CADDY_VERSION}-builder AS caddy-builder # Set SHELL flags for RUN commands to allow -e and pipefail @@ -34,25 +33,21 @@ FROM caddy:${CADDY_VERSION}-builder AS caddy-builder RUN xcaddy build \ --with github.com/caddy-dns/cloudflare -# --- # Docker hates variables in COPY, apparently. Hello, workaround. FROM goodieshq/headscale-admin:${HEADSCALE_ADMIN_VERSION} AS admin-gui -# --- # Build our main image FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} # Set SHELL flags for RUN commands to allow -e and pipefail # Rationale: https://github.com/hadolint/hadolint/wiki/DL4006 SHELL ["/bin/ash", "-eo", "pipefail", "-c"] - # --- - # import our "global" `ARG` values into this stage + # Import our "global" `ARG` values into this stage ARG HEADSCALE_DOWNLOAD_URL ARG HEADSCALE_SHA256 ARG LITESTREAM_DOWNLOAD_URL ARG LITESTREAM_SHA256 - # --- # Upgrade system and install various dependencies # - BusyBox's wget isn't reliable enough # - I'm gonna need a better shell @@ -64,14 +59,12 @@ FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} apk add --no-cache --virtual BuildTimeDeps ${BUILD_DEPS}; \ apk add --no-cache ${RUNTIME_DEPS} - # --- # Copy caddy from the first stage COPY --from=caddy-builder /usr/bin/caddy /usr/local/bin/caddy # Caddy smoke test RUN [ "$(command -v caddy)" = '/usr/local/bin/caddy' ]; \ caddy version - # --- # Headscale RUN set -ex; { \ wget --retry-connrefused \ @@ -89,7 +82,7 @@ FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} chmod +x headscale; \ mv headscale /usr/local/bin/; \ }; \ - # smoke test + # Headscale smoke test [ "$(command -v headscale)" = '/usr/local/bin/headscale' ]; \ headscale version; @@ -109,7 +102,7 @@ FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} mv litestream /usr/local/bin/; \ rm -f litestream.tar.gz; \ }; \ - # smoke test + # Litestream smoke test [ "$(command -v litestream)" = '/usr/local/bin/litestream' ]; \ litestream version; @@ -119,17 +112,17 @@ FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} # Remove build-time dependencies RUN apk del BuildTimeDeps - # --- - # copy configuration and templates + # Copy configuration templates COPY ./templates/headscale.template.yaml /etc/headscale/config.yaml COPY ./templates/litestream.template.yml /etc/litestream.yml COPY ./templates/caddy.http.template.yaml /etc/caddy/Caddyfile-http COPY ./templates/caddy.https.template.yaml /etc/caddy/Caddyfile-https + + # Copy and setup entrypoint script COPY ./scripts/container-entrypoint.sh /container-entrypoint.sh RUN chmod +x /container-entrypoint.sh - # --- - # Note this is the default listening port and may be overridden at runtime by `$PUBLIC_LISTEN_PORT` + # Default HTTPS port - override with $PUBLIC_LISTEN_PORT environment variable EXPOSE 443 ENTRYPOINT ["/container-entrypoint.sh"] From e78786a19ec927b11c0e12369a0f7b11f9e6fad0 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sat, 14 Jun 2025 22:06:58 +0100 Subject: [PATCH 40/42] Combine the final COPY and chmod to reduce layers --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 324a121..718fafc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -119,8 +119,7 @@ FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} COPY ./templates/caddy.https.template.yaml /etc/caddy/Caddyfile-https # Copy and setup entrypoint script - COPY ./scripts/container-entrypoint.sh /container-entrypoint.sh - RUN chmod +x /container-entrypoint.sh + COPY --chmod=755 ./scripts/container-entrypoint.sh /container-entrypoint.sh # Default HTTPS port - override with $PUBLIC_LISTEN_PORT environment variable EXPOSE 443 From feba07e26648432728ade1677436aa33af3c428c Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sun, 15 Jun 2025 21:32:35 +0100 Subject: [PATCH 41/42] Use `envsubst` for Caddyfile too --- scripts/container-entrypoint.sh | 61 ++++++++++++++++++++--------- templates/caddy.https.template.yaml | 4 +- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 6a90b26..2561bf8 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -8,6 +8,8 @@ litestream_disabled=false cleartext_only=false caddyfile_cleartext=/etc/caddy/Caddyfile-http caddyfile_https=/etc/caddy/Caddyfile-https +ACME_EAB_BLOCK="" # Placeholder for ACME EAB block in Caddyfile +CLOUDFLARE_ACME_BLOCK="" # Placeholder for Cloudflare ACME block in Caddyfile ####################################### # Log with different levels @@ -295,18 +297,14 @@ check_zerossl_eab() { require_env_var "ACME_EAB_KEY_ID" require_env_var "ACME_EAB_MAC_KEY" - if ! sed -i \ - "s@<>@acme_ca https://acme.zerossl.com/v2/DV90\nacme_eab {\n key_id ${ACME_EAB_KEY_ID}\n mac_key ${ACME_EAB_MAC_KEY}\n }@" \ - "$caddyfile_https"; then - log_error "Failed to modify Caddyfile with ACME EAB credentials" - fi + 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} + }" else log_info "No ACME EAB credentials provided" - if ! sed -i \ - "s@<>@@" \ - "$caddyfile_https" ; then - log_error "Failed to modify Caddyfile to remove ACME EAB placeholder" - fi + export ACME_EAB_BLOCK="" fi } @@ -316,17 +314,12 @@ check_zerossl_eab() { check_cloudflare_dns_api_key() { if env_var_is_populated "CF_API_TOKEN" ; then log_info "Using Cloudflare for ACME DNS Challenge." - - if ! sed -i \ - "s@<>@tls {\n dns cloudflare $CF_API_TOKEN\n }@" \ - "$caddyfile_https"; then - log_error "Failed to configure Cloudflare DNS in Caddyfile" - fi + export CLOUDFLARE_ACME_BLOCK="tls { + dns cloudflare ${CF_API_TOKEN} + }" else log_info "Using HTTP authentication for ACME DNS Challenge" - if ! sed -i "s@<>@@" "$caddyfile_https"; then - log_error "Failed to remove Cloudflare placeholder from Caddyfile" - fi + export CLOUDFLARE_ACME_BLOCK="" fi } @@ -344,6 +337,34 @@ check_caddy_specific_environment_variables() { check_zerossl_eab } +####################################### +# Create Caddy HTTPS configuration file +####################################### +create_caddy_https_config() { + local config_path=${caddyfile_https} + local temp_config_path + + temp_config_path=$(mktemp) || { + log_error "Unable to create temporary file" + return + } + + log_info "Adding ACME info to our Caddy config..." + + if envsubst < "$config_path" > "$temp_config_path"; then + chmod 600 "$temp_config_path" + if mv "$temp_config_path" "$config_path"; then + log_info "Caddyfile created successfully" + else + log_error "Unable to move Caddyfile" + rm -f "$temp_config_path" + fi + else + log_error "Unable to generate Caddyfile" + rm -f "$temp_config_path" + fi +} + ####################################### # Create our configuration files ####################################### @@ -352,6 +373,8 @@ check_config_files() { check_caddy_specific_environment_variables + create_caddy_https_config + create_headscale_config reuse_or_create_noise_private_key diff --git a/templates/caddy.https.template.yaml b/templates/caddy.https.template.yaml index 0660278..e50666e 100644 --- a/templates/caddy.https.template.yaml +++ b/templates/caddy.https.template.yaml @@ -5,7 +5,7 @@ email ${ACME_ISSUANCE_EMAIL} - <> + ${ACME_EAB_BLOCK} } ${PUBLIC_SERVER_URL}:${PUBLIC_LISTEN_PORT} { @@ -24,5 +24,5 @@ ${PUBLIC_SERVER_URL}:${PUBLIC_LISTEN_PORT} { reverse_proxy 127.0.0.1:8080 } - <> + ${CLOUDFLARE_ACME_BLOCK} } From e094f14b2a38510ec769586f58d566e5d4b6a59d Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sun, 15 Jun 2025 21:46:36 +0100 Subject: [PATCH 42/42] DRY config file creation --- scripts/container-entrypoint.sh | 83 ++++++++++++++++----------------- 1 file changed, 39 insertions(+), 44 deletions(-) diff --git a/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 2561bf8..b99ea9d 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -8,6 +8,7 @@ litestream_disabled=false cleartext_only=false caddyfile_cleartext=/etc/caddy/Caddyfile-http caddyfile_https=/etc/caddy/Caddyfile-https +headscale_config="/etc/headscale/config.yaml" ACME_EAB_BLOCK="" # Placeholder for ACME EAB block in Caddyfile CLOUDFLARE_ACME_BLOCK="" # Placeholder for Cloudflare ACME block in Caddyfile @@ -126,6 +127,42 @@ validate_port() { fi } +####################################### +# Generic configuration file creator with template substitution +# Arguments: +# $1 - Target config file path +# $2 - Description for logging +# $3 - File permissions (optional, defaults to 600) +####################################### +create_config_from_template() { + local config_path="$1" + local description="$2" + local permissions="${3:-600}" + local temp_config_path + + temp_config_path=$(mktemp) || { + log_error "Unable to create temporary file for $description" + return + } + + log_info "Generating $description..." + + if envsubst < "$config_path" > "$temp_config_path"; then + chmod "$permissions" "$temp_config_path" + if mv "$temp_config_path" "$config_path"; then + log_info "$description created successfully" + else + log_error "Unable to move $description to final location" + rm -f "$temp_config_path" + fi + else + log_error "Unable to generate $description" + rm -f "$temp_config_path" + fi + + return +} + ####################################### # Set default or validate PUBLIC_LISTEN_PORT ####################################### @@ -243,28 +280,7 @@ check_required_environment_vars() { # Create Headscale configuration file ####################################### create_headscale_config() { - local config_path="/etc/headscale/config.yaml" - local temp_config_path - - temp_config_path=$(mktemp) || { - log_error "Unable to create temporary file" - return - } - - log_info "Generating Headscale configuration file..." - - if envsubst < "$config_path" > "$temp_config_path"; then - chmod 600 "$temp_config_path" - if mv "$temp_config_path" "$config_path"; then - log_info "Headscale configuration file created successfully" - else - log_error "Unable to move Headscale configuration file" - rm -f "$temp_config_path" - fi - else - log_error "Unable to generate Headscale configuration file" - rm -f "$temp_config_path" - fi + create_config_from_template "$headscale_config" "Headscale configuration file" } ####################################### @@ -341,28 +357,7 @@ check_caddy_specific_environment_variables() { # Create Caddy HTTPS configuration file ####################################### create_caddy_https_config() { - local config_path=${caddyfile_https} - local temp_config_path - - temp_config_path=$(mktemp) || { - log_error "Unable to create temporary file" - return - } - - log_info "Adding ACME info to our Caddy config..." - - if envsubst < "$config_path" > "$temp_config_path"; then - chmod 600 "$temp_config_path" - if mv "$temp_config_path" "$config_path"; then - log_info "Caddyfile created successfully" - else - log_error "Unable to move Caddyfile" - rm -f "$temp_config_path" - fi - else - log_error "Unable to generate Caddyfile" - rm -f "$temp_config_path" - fi + create_config_from_template "$caddyfile_https" "Caddy HTTPS configuration file" } #######################################