#!/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"