From e6e73b710d734c939cbb8a15ed9cd347c2b4a107 Mon Sep 17 00:00:00 2001 From: White-Lotus <95460219+mangyan1@users.noreply.github.com> Date: Sat, 3 Oct 2026 12:19:08 -0600 Subject: [PATCH] refuse setup-script downgrades and failed image pulls in safe update The safe-update wrapper deployed whatever candidate script invoked it, so running an older release (installer v1.60 over an installed v1.66) pinned older image channels over a database already migrated by newer code and crash-looped the app at startup until the health check reverted. The wrapper now refuses a candidate whose SCRIPT_VERSION is older than the installed script while the stack is still healthy; override with NPMPLUS_FORCE_DOWNGRADE=true. pin_image ignored docker pull failures and re-pinned the local image's stale RepoDigest, so a transient registry error deployed yesterday's image while the update reported success. A failed pull now fails the pin so the wrapper reverts instead. Safe-update wrapper v7; installer v1.67. --- .github/workflows/smoke-test.yml | 4 ++-- CHANGELOG.md | 2 ++ setup-npmplus.sh | 23 +++++++++++++++++++---- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index 10bd83f09f..59b6c51bb4 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -363,7 +363,7 @@ jobs: # failure: Docker preserved an empty resolver file in the existing # host-networked container. The downloaded current script must repair # all three before entering the transactional update. - sudo sed -i 's/NPMPLUS_SAFE_UPDATE_WRAPPER_VERSION=6/NPMPLUS_SAFE_UPDATE_WRAPPER_VERSION=3/' /usr/local/bin/npmplus-safe-update + sudo sed -i 's/NPMPLUS_SAFE_UPDATE_WRAPPER_VERSION=7/NPMPLUS_SAFE_UPDATE_WRAPPER_VERSION=3/' /usr/local/bin/npmplus-safe-update sudo chmod 0644 /opt/npmplus/setup-npmplus.sh for _ in $(seq 1 30); do health="$(sudo docker inspect --format '{{.State.Health.Status}}' npmplus)" @@ -434,7 +434,7 @@ jobs: fi rm -f "$update_log" sudo test -x /opt/npmplus/setup-npmplus.sh - sudo grep -qx '# NPMPLUS_SAFE_UPDATE_WRAPPER_VERSION=6' /usr/local/bin/npmplus-safe-update + sudo grep -qx '# NPMPLUS_SAFE_UPDATE_WRAPPER_VERSION=7' /usr/local/bin/npmplus-safe-update sudo test -f /var/backups/npmplus-last-good/override.yaml test "$(stat -c '%u:%g' /opt/anubis-data)" = '1000:1000' sudo test -f /opt/anubis-data/anubis/honeypot.addrs diff --git a/CHANGELOG.md b/CHANGELOG.md index 398ef4d7d1..65af4fb595 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to the NPMplus Security Fork are documented here. The fork u ## Unreleased +- Installer v1.67 / safe-update wrapper v7 refuse to deploy an older setup script: a candidate whose `SCRIPT_VERSION` predates the installed script is rejected while the stack is still healthy, since an older script pins older image channels over a database already migrated by newer code and the app crash-loops at startup until the health check reverts. `NPMPLUS_FORCE_DOWNGRADE=true` overrides the refusal. `pin_image` also no longer treats a failed `docker pull` as success — a transient registry error previously re-pinned the local image's stale digest and reported a healthy update on yesterday's image; the pin now fails so the wrapper reverts instead. + - Installer v1.66 adds an admin-only **Enable CRS observation** button under CrowdSec → WAF for managed stacks with AppSec enabled. A private, single-action host helper validates the shared observation policy, snapshots configuration, waits for CrowdSec health, and rolls back failures without exposing Docker or arbitrary host commands to the application. Ordinary installer updates install the helper; existing deployments require that update once. - Added installer v1.65 `--update --enable-crs` for transactional CRS observation beside existing high-confidence AppSec blocking. The WAF dashboard separates installer configuration, observed CRS matches, and in-band warnings; match counts stay distinct from blocked requests. Hub-managed policies and ordinary-update choices remain intact. Safe-update wrapper v6 also snapshots and restores the bouncer configuration. diff --git a/setup-npmplus.sh b/setup-npmplus.sh index 2a3bc29acf..008824c51b 100644 --- a/setup-npmplus.sh +++ b/setup-npmplus.sh @@ -11,7 +11,7 @@ set -euo pipefail # bump this on every meaningful change - the script compares it against the # copy on github at startup and tells the operator when theirs is stale -SCRIPT_VERSION="1.66" +SCRIPT_VERSION="1.67" DATA_DIR="/opt/npmplus" CROWDSEC_DIR="/opt/crowdsec" @@ -2134,7 +2134,10 @@ finalize_admin_bootstrap() { pin_image() { # mutable channel -> immutable local platform repo digest local channel="$1" repository short_repository docker_repository candidate digest="" - docker pull "$channel" >/dev/null + docker pull "$channel" >/dev/null || { + echo "could not pull $channel" >&2 + return 1 + } repository=${channel%:*} short_repository=${repository#docker.io/} docker_repository=${short_repository#library/} @@ -3000,7 +3003,7 @@ if [[ -s "$DATA_DIR/setup-npmplus.sh" ]]; then chmod 700 "$DATA_DIR/setup-npmplus.sh" write_root_file /usr/local/bin/npmplus-safe-update 700 <<'EOF' #!/bin/bash -# NPMPLUS_SAFE_UPDATE_WRAPPER_VERSION=6 +# NPMPLUS_SAFE_UPDATE_WRAPPER_VERSION=7 # monthly npmplus update with a safety net: snapshots the running state, # runs the update, health-checks it, and reverts to the snapshot on failure set -euo pipefail @@ -3240,6 +3243,18 @@ trap - EXIT log "running update" if [[ -n "$CANDIDATE" && "$CANDIDATE" != "$SETUP" ]]; then + # An older candidate pins older image channels over a database already + # migrated by newer code; the app then crash-loops at startup until the + # health check reverts. Refuse it while the stack is still healthy. + candidate_version=$(sed -n 's/^SCRIPT_VERSION="\([^"]*\)".*/\1/p' "$CANDIDATE" 2>/dev/null | head -1) || true + installed_version=$(sed -n 's/^SCRIPT_VERSION="\([^"]*\)".*/\1/p' "$SETUP" 2>/dev/null | head -1) || true + if [[ -n "$candidate_version" && -n "$installed_version" && "$candidate_version" != "$installed_version" ]] && + [[ "$(printf '%s\n%s\n' "$candidate_version" "$installed_version" | sort -V | tail -1)" == "$installed_version" ]] && + [[ "${NPMPLUS_FORCE_DOWNGRADE:-false}" != "true" ]]; then + log "candidate script v$candidate_version is older than the installed v$installed_version - refusing the downgrade" + echo "the running stack is untouched; set NPMPLUS_FORCE_DOWNGRADE=true to override" >&2 + exit 1 + fi cp -a "$CANDIDATE" "$SETUP" || revert chmod 700 "$SETUP" || revert fi @@ -3850,7 +3865,7 @@ if [[ "${1:-}" == "--update" ]]; then # overwrite themselves in place during a nested tooling refresh. Replace # them before delegation; this also repairs a missing execute bit. if [[ ! -x /usr/local/bin/npmplus-safe-update ]] || \ - ! grep -qx '# NPMPLUS_SAFE_UPDATE_WRAPPER_VERSION=6' /usr/local/bin/npmplus-safe-update; then + ! grep -qx '# NPMPLUS_SAFE_UPDATE_WRAPPER_VERSION=7' /usr/local/bin/npmplus-safe-update; then install_host_tooling fi # Repair the v1.6 root-owned Anubis bind mount before the wrapper checks