diff --git a/changelog.d/894-install-credential-file-channel.fixed.md b/changelog.d/894-install-credential-file-channel.fixed.md new file mode 100644 index 000000000..ec1c9fdde --- /dev/null +++ b/changelog.d/894-install-credential-file-channel.fixed.md @@ -0,0 +1,16 @@ +- **Install-time credentials now survive `apk add`.** The operator-chosen + admin password and private-WiFi key are delivered through root-only + staged secret files (`/tmp/tollgate-admin-password`, + `/tmp/tollgate-wifi-key`), consumed — unlinked on first read — by the + setup script: apk-tools on 25.12 does not hand the installer's + environment to package scripts, so the previously documented + `TOLLGATE_ADMIN_PASSWORD=… apk add …` flow silently degraded to a random + provisional credential the operator never chose (found end-to-end on the + vlab installer lane). Mode-600 is enforced with a `find -perm` gate — + the minimal busybox on real router images ships without `stat`, which + the first revision used and which refused every staging — and the + environment channel remains for manual re-runs. The installer-contract + lane (#892) asserts the whole contract: staged secret becomes root's + hash (sha256crypt-verified), nothing plaintext in the install output, + files consumed + ([#894](https://github.com/OpenTollGate/tollgate-module-basic-go/pull/894)). diff --git a/packaging/files/etc/uci-defaults/99-tollgate-setup b/packaging/files/etc/uci-defaults/99-tollgate-setup index bf945a215..e0d2f2cf5 100755 --- a/packaging/files/etc/uci-defaults/99-tollgate-setup +++ b/packaging/files/etc/uci-defaults/99-tollgate-setup @@ -1257,17 +1257,55 @@ generate_admin_password() { # The operator may CHOOSE the admin credential at install time instead of # receiving a generated one they have to write down in the seconds it is on -# screen. This is the supported mechanism because uci-defaults run from the -# package manager with no controlling terminal: there is nowhere to hold a -# prompt, so the choice arrives in the environment. -# -# TOLLGATE_ADMIN_PASSWORD='…' apk add --allow-untrusted /tmp/tollgate-wrt.apk -# -# The value is taken as-is (no trimming — a deliberate leading/trailing space -# is the operator's business) and is NEVER echoed: it is already known to the -# operator, so printing it would only leak it into the install log. +# screen. uci-defaults run from the package manager with no controlling +# terminal, so there is nowhere to hold a prompt: the choice has to arrive +# through a side channel. There are two, because apk-tools on 25.12 does not +# hand the installer's environment to the package scripts (verified +# end-to-end on the vlab lane, #888): +# +# - the ROOT-ONLY STAGED FILE, which works through `apk add`: +# +# printf '%s' 'my-password' > /tmp/tollgate-admin-password +# printf '%s' 'my-wifi-key' > /tmp/tollgate-wifi-key +# chmod 600 /tmp/tollgate-admin-password /tmp/tollgate-wifi-key +# apk add --allow-untrusted /tmp/tollgate-wrt.apk +# +# Each file is CONSUMED — unlinked on first read — so a stale file can +# never re-apply an old password on a later boot, and the value never +# sits on a world-readable path (the operator's 600 is preserved; a file +# staged world-readable is refused). +# +# - the ENVIRONMENT, which works for a manual re-run of this script (and +# for installers that do preserve their environment): +# +# TOLLGATE_ADMIN_PASSWORD='…' /etc/uci-defaults/99-tollgate-setup +# +# The values are taken as-is (no trimming — a deliberate leading/trailing +# space is the operator's business) and are NEVER echoed: they are already +# known to the operator, so printing them would only leak them into the +# install log. +consume_install_secret() { # — print a root-staged secret once, then unlink it + [ -r "$1" ] || return 0 + # Mode gate via find -perm, not stat -c: the minimal busybox on real + # router images ships without the stat applet (verified on 25.12.5 x86), + # and an unreadable mode answer must never fall open. + if [ -z "$(find "$1" -maxdepth 0 -perm 0600 2>/dev/null)" ]; then + log "ERROR: staged install secret $1 must be mode 600 (root-only) — refusing to read it" + rm -f "$1" + return 0 + fi + local value + value="$(head -n 1 "$1" 2>/dev/null)" + rm -f "$1" + [ -n "$value" ] && printf '%s' "$value" +} + operator_chosen_admin_password() { - printf '%s' "${TOLLGATE_ADMIN_PASSWORD:-}" + if [ -n "${TOLLGATE_ADMIN_PASSWORD:-}" ]; then + printf '%s' "$TOLLGATE_ADMIN_PASSWORD" + return 0 + fi + consume_install_secret "${TOLLGATE_ADMIN_PASSWORD_FILE:-/tmp/tollgate-admin-password}" } # Marks a credential this package GENERATED rather than one the operator @@ -2323,7 +2361,16 @@ setup_private_network() { # Operator chose it (install-time prompt, B) — never generated-and-printed. private_key="$TOLLGATE_PRIVATE_WIFI_PASSWORD" log "Private WiFi key set from TOLLGATE_PRIVATE_WIFI_PASSWORD (not printed)" - elif [ -z "$private_key" ]; then + fi + if [ -z "$private_key" ]; then + # The apk-reliable channel, same contract as the admin password's + # (#888): consumed on read, mode-600 enforced, never echoed. + private_key="$(consume_install_secret "${TOLLGATE_WIFI_PASSWORD_FILE:-/tmp/tollgate-wifi-key}")" + if [ -n "$private_key" ]; then + log "Private WiFi key set from the staged install file (not printed)" + fi + fi + if [ -z "$private_key" ]; then # Memorable password: 3 words + 2-digit number. Single # /dev/urandom read, awk picks words — avoids forking per word. # NOT printed: the owner sets their own in the board's first-run page