[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:
Hermes committed 2026-10-08 22:29:49 -07:00
1 parent b0fcfddc2c
commit c721956a32
3 files changed
+285

No files matched your search

+26
View File
@@ -33,6 +33,31 @@ DNS_PRIMARY="100.121.150.22" # Technitium (Tailscale IP) — resolves *.sami
# is correct.
DNS_FALLBACK="100.71.97.12" # DNS1 Technitium secondary — serves *.sami + recurses
# --- DC-164 rev2: operator env overrides --------------------------------------
# /etc/dashcaddy/operator.env is forwarded into the container as -e flags via
# scripts/operator-flags.sh. The helper ENFORCES a security contract (regular
# file, uid 0 owner, mode 0600-style, no symlink): violation => exit 3, NO
# flags forwarded (fail closed; stderr explains). The canonical use:
# ALLOW_LIFETIME_LICENSE=true on the CREATOR's host only — the lifetime
# code tier is rejected everywhere else (license-manager keeps that gate).
# Missing file / no overrides = normal customer case (zero flags added).
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OPERATOR_ENV_FILE="${OPERATOR_ENV_FILE:-/etc/dashcaddy/operator.env}"
OPERATOR_FLAGS=()
OP_HELPER="$SCRIPT_DIR/dashcaddy-api/scripts/operator-flags.sh"
if [ -f "$OP_HELPER" ] && [ -e "$OPERATOR_ENV_FILE" ]; then
# single execution; stdout captured, rc inspected explicitly
OP_RAW="$(bash "$OP_HELPER" "$OPERATOR_ENV_FILE")" || OP_RC=$?; OP_RC=${OP_RC:-0}
if [ "$OP_RC" -eq 3 ]; then
echo "[start.sh] WARNING: operator env file rejected by contract check — forwarding NO overrides" >&2
elif [ -n "$OP_RAW" ]; then
while IFS= read -r kv; do
[ -n "$kv" ] && OPERATOR_FLAGS+=(-e "$kv")
done <<< "$OP_RAW"
echo "[start.sh] operator.env: $(( ${#OPERATOR_FLAGS[@]} / 2 )) items forwarded (keys only: $(printf '%s\n' "${OPERATOR_FLAGS[@]}" | grep -v '^-e$' | cut -d= -f1 | paste -sd, -))"
fi
fi
# --- One-time migration from Docker image layer to bind mount --------------
# DC-039 follow-up. Before v1.14.10, certain modules (audit-logger, license-
# keygen, credential-manager) defaulted their files to /app/src/* via
@@ -220,4 +245,5 @@ docker run -d --restart unless-stopped --name ${CONTAINER_NAME} \
-e SHIPDECK_BRIDGE_URL=http://172.17.0.1:8977 \
-e SHIPDECK_BRIDGE_TOKEN_FILE=/app/data/shipdeck-bridge-token \
-e CA_CERT_PATH=/etc/ssl/sami-ca/root.crt \
${OPERATOR_FLAGS[@]+"${OPERATOR_FLAGS[@]}"} \
${IMAGE}