diff --git a/Dockerfile b/Dockerfile index bb2c911..ad56bf9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,131 +1,131 @@ -################### -# 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.27.1" -ARG HEADSCALE_SHA256="af2a232ff407c100f05980b4d8fceaafc7fdb2e8de5eba8e184a8bb029cb6c00" - -ARG LITESTREAM_VERSION="0.5.3" -ARG LITESTREAM_SHA256="524406ccc40dcff22048df9b398eb9519dd24f5aa186d4a26edd12ce3510b6a4" - -# No checksum needed for these tools, we pull from official images -ARG CADDY_VERSION="2.10.2" -ARG MAIN_IMAGE_ALPINE_VERSION="3.22.1" -ARG HEADSCALE_ADMIN_VERSION="0.26.0" - -# 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-${LITESTREAM_VERSION}-linux-x86_64.tar.gz" - -################### -# 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 - # Rationale: https://github.com/hadolint/hadolint/wiki/DL4006 - SHELL ["/bin/ash", "-eo", "pipefail", "-c"] - - 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 - 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 - # - gettext provides `envsubst` for templating - # hadolint ignore=DL3018,SC2086 - RUN BUILD_DEPS="wget"; \ - RUNTIME_DEPS="bash gettext"; \ - apk --no-cache upgrade; \ - 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 \ - --waitretry=1 \ - --read-timeout=20 \ - --timeout=15 \ - -t 0 \ - -q \ - -O headscale \ - ${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/; \ - }; \ - # Headscale smoke test - [ "$(command -v headscale)" = '/usr/local/bin/headscale' ]; \ - headscale version; - - # Litestream - RUN set -ex; { \ - wget --retry-connrefused \ - --waitretry=1 \ - --read-timeout=20 \ - --timeout=15 \ - -t 0 \ - -q \ - -O litestream.tar.gz \ - ${LITESTREAM_DOWNLOAD_URL} \ - ; \ - echo "${LITESTREAM_SHA256} *litestream.tar.gz" | sha256sum -c - >/dev/null 2>&1; \ - tar -xf litestream.tar.gz; \ - mv litestream /usr/local/bin/; \ - rm -f litestream.tar.gz; \ - }; \ - # Litestream smoke test - [ "$(command -v litestream)" = '/usr/local/bin/litestream' ]; \ - litestream version; - - # Headscale web GUI - COPY --from=admin-gui /app/admin/ /admin-gui/admin/ - - # Remove build-time dependencies - RUN apk del BuildTimeDeps - - # Copy configuration templates - COPY ./templates/headscale.template.yaml /etc/headscale/config.yaml - COPY ./templates/litestream.template.yml /etc/litestream.yml - COPY ./templates/Caddyfile-http.template /etc/caddy/Caddyfile-http - COPY ./templates/Caddyfile-https.template /etc/caddy/Caddyfile-https - - # Copy and setup scripts into a safe bin directory - COPY --chmod=755 ./scripts/ /usr/local/bin/ - - # Default HTTPS port - override with $PUBLIC_LISTEN_PORT environment variable - EXPOSE 443 - - # Health check to ensure services are running - HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ - CMD headscale version && caddy version || exit 1 - - ENTRYPOINT ["/usr/local/bin/container-entrypoint.sh"] +################### +# 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.27.1" +ARG HEADSCALE_SHA256="af2a232ff407c100f05980b4d8fceaafc7fdb2e8de5eba8e184a8bb029cb6c00" + +ARG LITESTREAM_VERSION="0.5.6" +ARG LITESTREAM_SHA256="959f72ef0b28acf2d7785c622df4936f7813a381931366ef732157048a5a45e5" + +# No checksum needed for these tools, we pull from official images +ARG CADDY_VERSION="2.10.2" +ARG MAIN_IMAGE_ALPINE_VERSION="3.23.2" +ARG HEADSCALE_ADMIN_VERSION="0.26.0" + +# 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-${LITESTREAM_VERSION}-linux-x86_64.tar.gz" + +################### +# 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 + # Rationale: https://github.com/hadolint/hadolint/wiki/DL4006 + SHELL ["/bin/ash", "-eo", "pipefail", "-c"] + + 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 + 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 + # - gettext provides `envsubst` for templating + # hadolint ignore=DL3018,SC2086 + RUN BUILD_DEPS="wget"; \ + RUNTIME_DEPS="bash gettext"; \ + apk --no-cache upgrade; \ + 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 \ + --waitretry=1 \ + --read-timeout=20 \ + --timeout=15 \ + -t 0 \ + -q \ + -O headscale \ + ${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/; \ + }; \ + # Headscale smoke test + [ "$(command -v headscale)" = '/usr/local/bin/headscale' ]; \ + headscale version; + + # Litestream + RUN set -ex; { \ + wget --retry-connrefused \ + --waitretry=1 \ + --read-timeout=20 \ + --timeout=15 \ + -t 0 \ + -q \ + -O litestream.tar.gz \ + ${LITESTREAM_DOWNLOAD_URL} \ + ; \ + echo "${LITESTREAM_SHA256} *litestream.tar.gz" | sha256sum -c - >/dev/null 2>&1; \ + tar -xf litestream.tar.gz; \ + mv litestream /usr/local/bin/; \ + rm -f litestream.tar.gz; \ + }; \ + # Litestream smoke test + [ "$(command -v litestream)" = '/usr/local/bin/litestream' ]; \ + litestream version; + + # Headscale web GUI + COPY --from=admin-gui /app/admin/ /admin-gui/admin/ + + # Remove build-time dependencies + RUN apk del BuildTimeDeps + + # Copy configuration templates + COPY ./templates/headscale.template.yaml /etc/headscale/config.yaml + COPY ./templates/litestream.template.yml /etc/litestream.yml + COPY ./templates/Caddyfile-http.template /etc/caddy/Caddyfile-http + COPY ./templates/Caddyfile-https.template /etc/caddy/Caddyfile-https + + # Copy and setup scripts into a safe bin directory + COPY --chmod=755 ./scripts/ /usr/local/bin/ + + # Default HTTPS port - override with $PUBLIC_LISTEN_PORT environment variable + EXPOSE 443 + + # Health check to ensure services are running + HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD headscale version && caddy version || exit 1 + + ENTRYPOINT ["/usr/local/bin/container-entrypoint.sh"] diff --git a/README.md b/README.md index dcd274c..2b6d44e 100644 --- a/README.md +++ b/README.md @@ -1,74 +1,74 @@ -# 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.22.1`](https://git.alpinelinux.org/aports/log/?h=v3.22.1) | -| [`Headscale`][headscale-wob] | [Headscale Repo][headscale-repo] | [`v0.27.1`](https://github.com/juanfont/headscale/releases/tag/v0.27.1) | -| [`Headscale-Admin`][headscale-admin-wob] | [Headscale-Admin Repo][headscale-admin-repo] | [`0.26.0`](https://github.com/GoodiesHQ/headscale-admin/commit/6cf2bc7d59165757a70f4c918a032225eb5e6e7d) | -| [`Litestream`][litestream-wob] | [Litestream Repo][litestream-repo] | [`v0.5.3`](https://github.com/benbjohnson/litestream/releases/tag/v0.5.3) | -| [`Caddy`][caddy-wob] | [Caddy Repo][caddy-repo] | [`v2.10.2`](https://github.com/caddyserver/caddy/releases/tag/v2.10.2) | - -## Versioning - -Because of the mix of upstream tools included, this project will be tagged using semantic versioning - `YYYY.MM.REVISION`. - -All development should be done against the `develop` branch, `main` is deemed "stable". - -## Requirements - -* Cloudflare DNS for [ACME `DNS-01` authentication][dns-01-challenge] (Can be deliberately disabled to use [`HTTP-01` authentication][http-01-challenge] instead, or HTTPS can be disabled entirely if you plan to use an external termination point.) -* S3(Alike)/Azure for [Litestream][litestream-wob] (Can be deliberately disabled for full ephemerality, or if you plan to use persistent storage) - -## Installation - -Populate your environment variables according to `templates/secrets.template.env` - -The container entrypoint script will guide you on any errors. - -## Deployment and user creation - -Once app is deployed and green, [generate an API Key][headscale-usage] in order to use the admin interface. - -```console -headscale apikeys create -``` - -Navigate to the admin gui on `/admin/` and set up your groups, ACLs, tags etc. - -## Final configuration - -Now that Headscale is running, to have a 100% reproducible setup we need to ensure that private noise key generated during installation is persisted. Within the same console from previous step, print out the server's key: - -```console -cat /data/noise_private.key -``` - -Then set `HEADSCALE_NOISE_PRIVATE_KEY` to the value obtained above. - -Note that applying this will cause your application to restart, but afterwards no other change will be necessary. - -## Known to run on - -* Azure Container Apps -* [Fly.io][fly-io-instructions] -* ??? Let us know! - -[alpine-linux-wob]: https://www.alpinelinux.org/ -[alpine-linux-repo]: https://gitlab.alpinelinux.org/alpine -[caddy-wob]: https://caddyserver.com/ -[caddy-repo]: https://github.com/caddyserver/caddy -[headscale-admin-wob]: https://github.com/GoodiesHQ/headscale-admin -[headscale-admin-repo]: https://github.com/GoodiesHQ/headscale-admin -[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.2`](https://git.alpinelinux.org/aports/log/?h=v3.23.2) | +| [`Headscale`][headscale-wob] | [Headscale Repo][headscale-repo] | [`v0.27.1`](https://github.com/juanfont/headscale/releases/tag/v0.27.1) | +| [`Headscale-Admin`][headscale-admin-wob] | [Headscale-Admin Repo][headscale-admin-repo] | [`0.26.0`](https://github.com/GoodiesHQ/headscale-admin/commit/6cf2bc7d59165757a70f4c918a032225eb5e6e7d) | +| [`Litestream`][litestream-wob] | [Litestream Repo][litestream-repo] | [`0.5.6`](https://github.com/benbjohnson/litestream/releases/tag/v0.5.6) | +| [`Caddy`][caddy-wob] | [Caddy Repo][caddy-repo] | [`v2.10.2`](https://github.com/caddyserver/caddy/releases/tag/v2.10.2) | + +## Versioning + +Because of the mix of upstream tools included, this project will be tagged using semantic versioning - `YYYY.MM.REVISION`. + +All development should be done against the `develop` branch, `main` is deemed "stable". + +## Requirements + +* Cloudflare DNS for [ACME `DNS-01` authentication][dns-01-challenge] (Can be deliberately disabled to use [`HTTP-01` authentication][http-01-challenge] instead, or HTTPS can be disabled entirely if you plan to use an external termination point.) +* S3(Alike)/Azure for [Litestream][litestream-wob] (Can be deliberately disabled for full ephemerality, or if you plan to use persistent storage) + +## Installation + +Populate your environment variables according to `templates/secrets.template.env` + +The container entrypoint script will guide you on any errors. + +## Deployment and user creation + +Once app is deployed and green, [generate an API Key][headscale-usage] in order to use the admin interface. + +```console +headscale apikeys create +``` + +Navigate to the admin gui on `/admin/` and set up your groups, ACLs, tags etc. + +## Final configuration + +Now that Headscale is running, to have a 100% reproducible setup we need to ensure that private noise key generated during installation is persisted. Within the same console from previous step, print out the server's key: + +```console +cat /data/noise_private.key +``` + +Then set `HEADSCALE_NOISE_PRIVATE_KEY` to the value obtained above. + +Note that applying this will cause your application to restart, but afterwards no other change will be necessary. + +## Known to run on + +* Azure Container Apps +* [Fly.io][fly-io-instructions] +* ??? Let us know! + +[alpine-linux-wob]: https://www.alpinelinux.org/ +[alpine-linux-repo]: https://gitlab.alpinelinux.org/alpine +[caddy-wob]: https://caddyserver.com/ +[caddy-repo]: https://github.com/caddyserver/caddy +[headscale-admin-wob]: https://github.com/GoodiesHQ/headscale-admin +[headscale-admin-repo]: https://github.com/GoodiesHQ/headscale-admin +[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/scripts/container-entrypoint.sh b/scripts/container-entrypoint.sh index 336248d..d95f64f 100755 --- a/scripts/container-entrypoint.sh +++ b/scripts/container-entrypoint.sh @@ -279,6 +279,10 @@ check_cloudflare_dns_api_key() { # `true` on success, `false` on error ####################################### configure_security_headers() { + # Note: For documentation on security headers, see: + # - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers + # - https://owasp.org/www-project-secure-headers/ + # Modern security headers with sensible defaults # shellcheck disable=SC2034 # Used via nameref in array_to_caddy_block local default_headers=( @@ -290,18 +294,14 @@ configure_security_headers() { "Cross-Origin-Embedder-Policy \"require-corp\"" "Cross-Origin-Opener-Policy \"same-origin\"" ) - + # Minimal security headers for compatibility # shellcheck disable=SC2034 # Used via nameref in array_to_caddy_block local minimal_headers=( "X-Frame-Options \"DENY\"" "X-Content-Type-Options \"nosniff\"" ) - - # Note: For documentation on security headers, see: - # - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers - # - https://owasp.org/www-project-secure-headers/ - + # Helper function to convert array to multi-line string for Caddy config array_to_caddy_block() { local -n headers_array=${1} diff --git a/scripts/defaults.sh b/scripts/defaults.sh index 8dd9fdc..667b1d2 100644 --- a/scripts/defaults.sh +++ b/scripts/defaults.sh @@ -1,5 +1,6 @@ #!/bin/bash -# shellcheck disable=SC2034 # This is a defaults file +# This is a defaults file +# shellcheck disable=SC2034 public_listen_port_default=443 headscale_ephemeral_node_inactivity_timeout_default="30m"