From b6ed87e65f43ee2e958d2ba38912be6bb5de6fd2 Mon Sep 17 00:00:00 2001 From: wave2a-trial Date: Sun, 11 Oct 2026 18:58:09 +0200 Subject: [PATCH 1/3] =?UTF-8?q?fix(setup):=20root-only=20staged=20secret?= =?UTF-8?q?=20files=20carry=20the=20install-time=20credentials=20through?= =?UTF-8?q?=20apk=20=E2=80=94=20the=20environment=20channel=20does=20not?= =?UTF-8?q?=20survive=20apk-tools=20(#888)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit operator_chosen_admin_password and the private-WiFi key now fall back to consumed-on-read, mode-600-enforced secret files (/tmp/tollgate-admin-password, /tmp/tollgate-wifi-key) when the environment is absent: apk-tools on 25.12 does not deliver the installer's environment to package scripts (verified end-to-end on the vlab lane — hash-mismatch proof plus a manual sh -x run that honors the env), so the documented env-only UX silently degraded to the provisional path. Files are unlinked on first read (a stale file can never re-apply an old password on a later boot) and a world-readable staging is refused and removed. The env channel stays for manual re-runs. --- .../files/etc/uci-defaults/99-tollgate-setup | 66 +++++++++++++++---- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/packaging/files/etc/uci-defaults/99-tollgate-setup b/packaging/files/etc/uci-defaults/99-tollgate-setup index bf945a215..342e59ab3 100755 --- a/packaging/files/etc/uci-defaults/99-tollgate-setup +++ b/packaging/files/etc/uci-defaults/99-tollgate-setup @@ -1257,17 +1257,52 @@ 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 + if [ "$(stat -c '%a' "$1" 2>/dev/null)" != "600" ]; 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 +2358,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 From 1b58ec2e46f75b2b691dd587786a01e5099bf2e3 Mon Sep 17 00:00:00 2001 From: wave2a-trial Date: Sun, 11 Oct 2026 19:19:25 +0200 Subject: [PATCH 2/3] =?UTF-8?q?fix(setup):=20mode-gate=20the=20staged=20se?= =?UTF-8?q?crets=20with=20find=20-perm=20=E2=80=94=20the=20stat=20applet?= =?UTF-8?q?=20is=20absent=20on=20real=20router=20images=20(25.12.5=20x86?= =?UTF-8?q?=20verified),=20which=20refused=20every=20staging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packaging/files/etc/uci-defaults/99-tollgate-setup | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packaging/files/etc/uci-defaults/99-tollgate-setup b/packaging/files/etc/uci-defaults/99-tollgate-setup index 342e59ab3..e0d2f2cf5 100755 --- a/packaging/files/etc/uci-defaults/99-tollgate-setup +++ b/packaging/files/etc/uci-defaults/99-tollgate-setup @@ -1286,7 +1286,10 @@ generate_admin_password() { # install log. consume_install_secret() { # — print a root-staged secret once, then unlink it [ -r "$1" ] || return 0 - if [ "$(stat -c '%a' "$1" 2>/dev/null)" != "600" ]; then + # 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 From 68d297bd6463cf987a2a4c7722ef802e54092faa Mon Sep 17 00:00:00 2001 From: wave2a-trial Date: Sun, 11 Oct 2026 22:35:29 +0200 Subject: [PATCH 3/3] docs(changelog): fragment for the staged-secret install channel (#894) --- .../894-install-credential-file-channel.fixed.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 changelog.d/894-install-credential-file-channel.fixed.md 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)).