[pending-judge] feat(DC-164 rev4): operator env overrides — fd-pinned contract, fail-loud wiring, untracked license material
Feature: /etc/dashcaddy/operator.env (root-owned KEY=VALUE file) is forwarded into the DashCaddy container as docker -e flags by start.sh. Canonical flag: ALLOW_LIFETIME_LICENSE=true on the CREATOR host only. This is a client-side rail, NOT the security boundary: lifetime codes must HMAC-verify against the creator-only signing secret, which is never shipped to customers (installs carry only .license-secret-hash; the license server never issues lifetime codes). Flipping the flag on a customer host grants nothing. Missing file = normal case (zero flags). Design (per r3 judge blockers): 1. scripts/operator-flags.js — Node, ONE file descriptor: opened with O_RDONLY|O_NOFOLLOW, fstat'ed, and read from that SAME fd. Contract: regular file; no group/other access bits (mode & 077 == 0); no setuid/setgid/sticky; owner uid 0 under root runtime (euid => owner equality otherwise). Symlink refused at open even when the target would pass all checks. No stat-path/reopen-path TOCTOU window. Violation => stderr FATAL + exit 3. Content is pure data: never sourced/evaled/executed; `$(...)` values stay inert text. 2. start.sh: rc 3 => warn + forward NOTHING (fail closed); ANY other non-zero rc => ABORT startup (fail loud, tested policy); missing file => silent zero overrides. Single helper invocation; keys-only audit line at boot (never values). 3. Artifact hygiene: verifier asserts zero full-format license codes in any submission file. (An earlier round leaked one; that code is dead — signing secret rotated, keygen verify reports INVALID.) Root-cause hardening (2026-10-08 incident: deploy checkouts overwriting git-TRACKED runtime state revoked the owner's premium and hid service cards): - untrack dashcaddy-api/data/services.json (stale committed copy had clobbered the live 16-service state incl SamiMail/SamiPanel/Revenue) - untrack dashcaddy-api/.license-secret + .license-counter, extend .gitignore. The secret value in prior git history is already rotated, hence cryptographically dead. Tests: 16 executed operator-flags tests (O_NOFOLLOW symlink-to-valid- target, 0644/0755/setuid/directory/foreign-owner rejections, missing-file-silent, usage error, injection-inert, translation suite); license-manager 76 + tier-enforcement 21 untouched => 113 targeted green.
This commit is contained in:
7 files changed
+340
-144
No files matched your search
@@ -33,6 +33,47 @@ 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 rev4: operator env overrides --------------------------------------
|
||||
# /etc/dashcaddy/operator.env is forwarded into the container as -e flags by
|
||||
# dashcaddy-api/scripts/operator-flags.js (Node; single O_NOFOLLOW fd pinned
|
||||
# for BOTH the contract checks and the read — no TOCTOU window). Contract
|
||||
# violation (rc 3) = fail closed: warn and forward NOTHING. Unexpected error
|
||||
# (any other rc != 0) = fail loud: ABORT startup, do not silently run with
|
||||
# an unverified operator file state. The canonical flag:
|
||||
# ALLOW_LIFETIME_LICENSE=true on the CREATOR's host only. This is a
|
||||
# client-side rail, not the security boundary — lifetime codes still must
|
||||
# HMAC-verify against the creator-only signing secret (never shipped to
|
||||
# customers), so flipping the flag on a customer host grants nothing.
|
||||
# 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.js"
|
||||
if [ -e "$OPERATOR_ENV_FILE" ]; then
|
||||
if [ ! -r "$OP_HELPER" ]; then
|
||||
echo "[start.sh] FATAL: operator.env present but contract checker missing: $OP_HELPER" >&2
|
||||
exit 1
|
||||
fi
|
||||
OP_NODE="${OPERATOR_NODE:-node}"
|
||||
if ! command -v "$OP_NODE" >/dev/null 2>&1; then
|
||||
echo "[start.sh] FATAL: operator.env present but '$OP_NODE' not found (set OPERATOR_NODE)" >&2
|
||||
exit 1
|
||||
fi
|
||||
OP_RAW="$( "$OP_NODE" "$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 [ "$OP_RC" -ne 0 ]; then
|
||||
echo "[start.sh] FATAL: operator contract checker failed (exit $OP_RC) — aborting startup" >&2
|
||||
exit "$OP_RC"
|
||||
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 +261,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}
|
||||
Reference in new issue
Block a user