#!/bin/sh # The safety rail: refuse to run with an unauthenticated ADMIN app bound to # anything but loopback. # # The editor has no authentication of its own. It shells out to yt-dlp, deletes # media, and streams job logs. umtool is the same shape. Exposing either of them # to a network without an auth layer in front should take deliberate effort, not # a typo in a bind address — so this runs both in the app entrypoint and in the # Caddy container (the one that actually publishes the ports). # # POSIX sh: this has to run under the busybox shell in caddy:alpine as well as # under bash in the app image. No arrays, no [[ ]], no local. # # Exit 0 = safe to start. Exit 1 = refused, with the reason on stderr. set -eu # Loopback, or unset (compose's default is 127.0.0.1 — an unset value here means # the variable was never passed, not that the app is on 0.0.0.0). is_loopback() { case "$1" in "" | localhost | ::1 | "[::1]") return 0 ;; 127.*) return 0 ;; *) return 1 ;; esac } auth_mode="${ARCHILYZER_AUTH_MODE:-basic}" # Is the admin apps' front door accounted for? # # "none" counts. It is not "no protection is needed" — it is the operator saying # in writing that they have their own (a proxy they trust, a tailnet-only # address). That assertion is the entire point of the escape hatch, so honour it # here rather than making it the one setting that cannot be used. auth_accounted_for() { case "${auth_mode}" in none) return 0 ;; forward) [ -n "${ARCHILYZER_FORWARD_AUTH_UPSTREAM:-}" ] && return 0 || return 1 ;; *) [ -n "${ARCHILYZER_AUTH_HASH:-}" ] && return 0 || return 1 ;; esac } refuse() { app="$1" bind="$2" var="$3" cat >&2 < 2. Leave it on loopback and reach it over Tailscale or an SSH tunnel: ${var}=127.0.0.1 ssh -N -L 8081:127.0.0.1:8081 you@this-machine 3. Put a real identity provider in front of it — see the forward-auth overlays in RUNNING_IN_DOCKER.md (ARCHILYZER_AUTH_MODE=forward). 4. You genuinely have your own auth in front (a reverse proxy you trust, a tailnet-only address). Say so explicitly: ARCHILYZER_AUTH_MODE=none └────────────────────────────────────────────────────────────────────────┘ EOF exit 1 } # Only the two PRIVATE apps are guarded. The export site and the homepage are # public archives; serving them to the world is what they are for. check() { is_loopback "$2" && return 0 auth_accounted_for && return 0 refuse "$1" "$2" "$3" } check "editor" "${EDITOR_BIND:-}" "EDITOR_BIND" check "umtool" "${UMTOOL_BIND:-}" "UMTOOL_BIND"