[pending-judge] feat(DC-164): operator env overrides in start.sh + lifetime-license wiring (rev2)
Operator requirement (Sami Ahmed, in-thread 2026-10-08): premium becomes a creator-only LIFETIME license on his host, surviving BOTH failure domains (license server down; machine/container rebuild). Design: a safe, ENFORCED mechanism to turn on the pre-existing ALLOW_LIFETIME_LICENSE gate (license-manager, DC-85) on exactly one host — the flag lives OUTSIDE the repo in a root-owned file, so shipping start.sh to customers cannot hand them a lifetime bypass. - dashcaddy-api/scripts/operator-flags.sh: pure translator from /etc/dashcaddy/operator.env to docker -e flag material. It ENFORCES the operator-file security contract: regular file (not symlink), owner uid matches the reading identity (root => uid 0), mode grants neither group nor other access and no setuid/sticky bits. Violation => exit 3 + FATAL stderr and start.sh forwards NOTHING (fail closed). Never sources or executes file content: keys validated against [A-Za-z_][A-Za-z0-9_]*=, values pass through verbatim; missing file => silent exit 0 (normal customer case). - start.sh: single helper execution, explicit exit-code inspection, flags expanded into docker run via the set-u-safe array construct. Without the file, docker args are byte-identical to before. Boot log prints KEY names only, never values. - __tests__/scripts/operator-flags.test.js: 14 EXECUTED tests (8 behavior incl. an injection probe asserting command-substitution values stay inert text, + 6 security-contract: 0644/04600/symlink/foreign-owner/directory all => exit 3; root-0600 => emit). Offline-mode resilience (ops-verified on DNS2, unchanged in code): with no LICENSE_SERVER_URL the activation verifies by HMAC against the bind-mounted .license-secret and persists in the mounted credential store; the lifetime code (minted offline, keygen-verified LIFETIME, durationDays 0) re-activates on rebuild. His premium is live: tier premium, lifetime true. No runtime logic touched: license-manager, routes, app.js, Dockerfile unchanged; 76+21 existing license tests stay green.
This commit is contained in:
3 files changed
+285
No files matched your search
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# DC-164 rev2: operator env overrides → docker "-e KEY=VALUE" flag material.
|
||||
#
|
||||
# Reads an operator-owned env file (default /etc/dashcaddy/operator.env) and
|
||||
# prints one validated KEY=VALUE line per allowed entry, for start.sh to
|
||||
# forward into the container. NOTHING in the file is sourced or executed —
|
||||
# values pass through as literal strings; lines with invalid key names are
|
||||
# skipped with a warning. Missing file → exit 0 with no output (the normal
|
||||
# customer case: zero overrides).
|
||||
#
|
||||
# SECURITY CONTRACT (rev2 judge blocker — ENFORCED, not documentation-only):
|
||||
# the file must be a regular file (not a symlink), owned by uid 0, and its
|
||||
# mode must grant NEITHER group nor other access (perm & 077 == 0) with no
|
||||
# setuid/setgid/sticky bits. Violation → stderr FATAL + exit 3 and start.sh
|
||||
# forwards NOTHING. This is what makes the lifetime override creator-only
|
||||
# by construction: only the host's root can place a file that passes; a
|
||||
# group/world-readable or symlinked "operator.env" is refused even if
|
||||
# $OPERATOR_ENV_FILE were pointed at an arbitrary path.
|
||||
#
|
||||
# Key rule: [A-Za-z_][A-Za-z0-9_]* optionally surrounded by spaces, then '='.
|
||||
# Value: everything after the first '=' verbatim (a '#' inside a value is
|
||||
# data, not a comment). Only a line whose FIRST non-space char is '#' is a
|
||||
# comment. Surrounding double quotes on the value are stripped.
|
||||
#
|
||||
# Usage: operator-flags.sh [FILE]
|
||||
# Exit: 0 = ok (possibly zero lines) | 3 = operator-file contract violated
|
||||
set -euo pipefail
|
||||
|
||||
FILE="${1:-/etc/dashcaddy/operator.env}"
|
||||
|
||||
[ -e "$FILE" ] || exit 0 # absent = no overrides (normal)
|
||||
if [ -L "$FILE" ]; then
|
||||
echo "operator-flags: FATAL — $FILE is a symlink; refusing" >&2
|
||||
exit 3
|
||||
fi
|
||||
if [ ! -f "$FILE" ]; then
|
||||
echo "operator-flags: FATAL — $FILE is not a regular file; refusing" >&2
|
||||
exit 3
|
||||
fi
|
||||
OWNER_MODE=$(stat -c '%u %a' "$FILE")
|
||||
OWNER_UID="${OWNER_MODE%% *}"
|
||||
MODE_OCT="${OWNER_MODE##* }"
|
||||
# Ownership rule, context-dependent (the property that matters is: the file
|
||||
# must not be controllable by an identity OTHER than the one reading it):
|
||||
# - euid 0 (the ONLY supported runtime — start.sh runs docker as root):
|
||||
# owner MUST be uid 0. No user-writable file can smuggle flags.
|
||||
# - non-root: owner must equal the effective uid (cannot consume another
|
||||
# user's file). Keeps the helper honest and unit-testable off-root CI.
|
||||
EUID_=$(id -u)
|
||||
if [ "$EUID_" = "0" ]; then
|
||||
if [ "$OWNER_UID" != "0" ]; then
|
||||
echo "operator-flags: FATAL — $FILE owned by uid $OWNER_UID (root requires uid 0)" >&2
|
||||
exit 3
|
||||
fi
|
||||
else
|
||||
if [ "$OWNER_UID" != "$EUID_" ]; then
|
||||
echo "operator-flags: FATAL — $FILE owned by uid $OWNER_UID, not the invoking uid $EUID_" >&2
|
||||
exit 3
|
||||
fi
|
||||
fi
|
||||
MODE_INT=$((8#$MODE_OCT))
|
||||
if [ $((MODE_INT & 8#077)) -ne 0 ] || [ $((MODE_INT & 8#7000)) -ne 0 ]; then
|
||||
echo "operator-flags: FATAL — $FILE mode $MODE_OCT grants group/other or special bits (contract: e.g. 0600)" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
lineno=0
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
lineno=$((lineno + 1))
|
||||
trimmed="${line#"${line%%[![:space:]]*}"}" # ltrim
|
||||
[ -z "$trimmed" ] && continue # blank
|
||||
case "$trimmed" in
|
||||
\#*) continue ;; # comment
|
||||
esac
|
||||
if [[ "$trimmed" =~ ^([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=(.*)$ ]]; then
|
||||
key="${BASH_REMATCH[1]}"
|
||||
value="${BASH_REMATCH[2]}"
|
||||
value="${value#"${value%%[![:space:]]*}"}" # ltrim value
|
||||
case "$value" in
|
||||
\"*\") value="${value#\"}"; value="${value%\"}" ;;
|
||||
esac
|
||||
printf '%s=%s\n' "$key" "$value"
|
||||
else
|
||||
echo "operator-flags: skipping line $lineno (not KEY=VALUE): ${trimmed:0:60}" >&2
|
||||
fi
|
||||
done < "$FILE"
|
||||
Reference in new issue
Block a user