Files

664 lines
30 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Test Matrix
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
jobs:
# -------------------------------------------------------------------
# 1. POSIX lint with shellcheck — runs once, NOT in matrix.
# -------------------------------------------------------------------
shellcheck:
name: ShellCheck (POSIX sh)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Run shellcheck
run: |
sudo apt-get update -qq
sudo apt-get install -y shellcheck
shellcheck --version
# -s sh : POSIX shell mode (catches bashisms)
# SC1091 : "not following sourced file" — script is self-contained
# SC2317 : "unreachable" — shellcheck doesn't follow trap callbacks
# SC2015 : A && B || C info — used intentionally throughout
# SC2012 : ls for parsing — we only use ls -ln to extract uid (POSIX-safe)
shellcheck -s sh -e SC1091,SC2317,SC2015,SC2012 init.sh
# -------------------------------------------------------------------
# 2. Per-distro install test with --delay-restart (no live sshd churn)
# -------------------------------------------------------------------
test-distros:
name: Install test (delay-restart) — ${{ matrix.container }}
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
container:
- 'debian:11'
- 'debian:12'
- 'ubuntu:22.04'
- 'ubuntu:24.04'
- 'alpine:latest'
- 'almalinux:9'
- 'rockylinux:9'
- 'quay.io/centos/centos:stream9'
include:
# CentOS 7 is EOL (2024-06-30) — kept for legacy users but allowed to fail.
- container: 'centos:7'
allow_failure: true
continue-on-error: ${{ matrix.allow_failure == true }}
steps:
- uses: actions/checkout@v4
- name: Run init.sh in ${{ matrix.container }}
run: |
echo "🚀 Testing on: ${{ matrix.container }}"
chmod +x init.sh
docker run --rm --privileged -v "$(pwd)":/mnt:ro -w /mnt "${{ matrix.container }}" /bin/sh -s <<'CONTAINER'
set -e
# -----------------------------------------------------------------
# CentOS 7 vault patch — only needed because mirror.centos.org is down
# -----------------------------------------------------------------
if [ -f /etc/redhat-release ] && grep -q "release 7" /etc/redhat-release; then
echo "💀 CentOS 7 (EOL) — repointing repos to vault.centos.org"
sed -i "s/^mirrorlist/#mirrorlist/g" /etc/yum.repos.d/*.repo
sed -i "s|^#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g" /etc/yum.repos.d/*.repo
sed -i "s|^baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g" /etc/yum.repos.d/*.repo
yum clean all
rm -rf /var/cache/yum
fi
# -----------------------------------------------------------------
# Dependencies
# -----------------------------------------------------------------
echo "📦 Installing dependencies..."
if [ -f /etc/debian_version ]; then
apt-get update -qq && apt-get install -y -qq curl sudo openssh-server iproute2
ssh-keygen -A
mkdir -p /run/sshd
elif [ -f /etc/alpine-release ]; then
apk add --no-cache curl sudo openssh-server bash
ssh-keygen -A
elif [ -f /etc/redhat-release ]; then
if command -v dnf >/dev/null; then
dnf install -y --allowerasing curl sudo openssh-server iproute
else
yum install -y curl sudo openssh-server iproute
fi
ssh-keygen -A
mkdir -p /run/sshd
fi
# -----------------------------------------------------------------
# Make /bin/sh point to dash where available so dash-isms surface.
# -----------------------------------------------------------------
if [ -x /bin/dash ]; then
ln -sf /bin/dash /bin/sh
echo "ℹ️ /bin/sh now resolves to: $(readlink -f /bin/sh)"
fi
# -----------------------------------------------------------------
# Run init.sh
# -----------------------------------------------------------------
echo "🚀 Running init.sh..."
cp init.sh /tmp/init.sh
chmod +x /tmp/init.sh
/tmp/init.sh \
--user=_247like \
--port=2222 \
--key-raw="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPQQDejyTCPJO3Jyw5UX9jmojb/zjgcqVuRO28fmpWU5 ci@test" \
--no-update \
--bbr \
--delay-restart \
--no-ip-probe \
--yes \
--strict
# -----------------------------------------------------------------
# Assertions
# -----------------------------------------------------------------
echo "🔍 Verifying outcome..."
grep -q "BEGIN SERVER-INIT MANAGED BLOCK" /etc/ssh/sshd_config \
|| { echo "❌ managed block missing"; exit 1; }
first_line=$(sed -n '1p' /etc/ssh/sshd_config)
test "$first_line" = "# BEGIN SERVER-INIT MANAGED BLOCK" \
|| { echo "❌ managed block is not the first line: $first_line"; exit 1; }
grep -Eq "^[[:space:]]*Port[[:space:]]+2222([[:space:]]|$)" /etc/ssh/sshd_config \
|| { echo "❌ Port 2222 not in config"; exit 1; }
id _247like >/dev/null \
|| { echo "❌ user _247like not created"; exit 1; }
test -s /home/_247like/.ssh/authorized_keys \
|| { echo "❌ authorized_keys missing or empty"; exit 1; }
grep -q "ssh-ed25519 AAAA" /home/_247like/.ssh/authorized_keys \
|| { echo "❌ deployed key content missing"; exit 1; }
test -f /etc/sudoers.d/server-init-_247like \
|| { echo "❌ stable sudoers file missing"; exit 1; }
test -f /etc/profile.d/z99-ssh-init-banner.sh \
|| { echo "❌ login banner missing"; exit 1; }
# sshd -t against the rewritten config must pass.
sshd -t -f /etc/ssh/sshd_config \
|| { echo "❌ sshd -t rejected the new config"; exit 1; }
# Audit log must exist and record the DONE phase.
test -f /var/log/server-init-audit.log \
|| { echo "❌ audit log missing"; exit 1; }
grep -q "ACTION: DONE" /var/log/server-init-audit.log \
|| { echo "❌ audit log missing DONE entry"; exit 1; }
# [v4.7.8] Verify the new v4.7.4 audit entries are emitted on the
# happy path so silent regressions are caught.
grep -q "ACTION: USER_CREATED" /var/log/server-init-audit.log \
|| { echo "❌ audit log missing USER_CREATED entry"; exit 1; }
grep -q "ACTION: KEYS_DEPLOYED" /var/log/server-init-audit.log \
|| { echo "❌ audit log missing KEYS_DEPLOYED entry"; exit 1; }
grep -q "ACTION: PASSWORD_CLEARED" /var/log/server-init-audit.log \
|| { echo "❌ audit log missing PASSWORD_CLEARED entry"; exit 1; }
grep -q "ACTION: MANAGED_BLOCK_INSTALLED" /var/log/server-init-audit.log \
|| { echo "❌ audit log missing MANAGED_BLOCK_INSTALLED entry"; exit 1; }
# Health report must exist.
test -f /var/log/server-init-health.log \
|| { echo "❌ health report missing"; exit 1; }
# Backup directory must have a verifiable restore.sh.
backup_dir=$(ls -dt /var/backups/ssh-config/*/ 2>/dev/null | head -n 1)
test -n "$backup_dir" \
|| { echo "❌ backup directory missing"; exit 1; }
test -x "$backup_dir/restore.sh" \
|| { echo "❌ restore.sh missing or not executable"; exit 1; }
test -f "$backup_dir/checksums.sha256" \
|| { echo "❌ checksums.sha256 missing"; exit 1; }
( cd "$backup_dir" && sha256sum -c checksums.sha256 ) \
|| { echo "❌ backup checksums do not verify"; exit 1; }
# Re-run idempotency: second invocation must not duplicate the managed block.
echo "🔁 Re-running to confirm idempotency..."
/tmp/init.sh \
--user=_247like \
--port=2222 \
--key-raw="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPQQDejyTCPJO3Jyw5UX9jmojb/zjgcqVuRO28fmpWU5 ci@test" \
--no-update --no-bbr --delay-restart --no-ip-probe --yes --strict
block_count=$(grep -c "BEGIN SERVER-INIT MANAGED BLOCK" /etc/ssh/sshd_config || true)
test "$block_count" = "1" \
|| { echo "❌ managed block duplicated on re-run (count=$block_count)"; exit 1; }
sudoers_count=$(ls /etc/sudoers.d/server-init-_247like* 2>/dev/null | wc -l)
test "$sudoers_count" = "1" \
|| { echo "❌ sudoers files multiplied on re-run (count=$sudoers_count)"; exit 1; }
for lock in /run/server-init/script.lock /var/lib/server-init/script.lock; do
test ! -d "$lock" \
|| { echo "❌ script lock left behind after successful run: $lock"; exit 1; }
done
echo "✅ ALL ASSERTIONS PASSED"
CONTAINER
# -------------------------------------------------------------------
# 3. Live restart + listen + connection test on one representative distro
# Uses the v4.7.1 direct-spawn fallback in restart_sshd, so this works
# in a plain (non-systemd) container.
# -------------------------------------------------------------------
test-live-restart:
name: Live restart & connection (ubuntu:22.04)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Live test
run: |
chmod +x init.sh
docker run --rm --privileged -v "$(pwd)":/mnt:ro -w /mnt ubuntu:22.04 /bin/sh -s <<'CONTAINER'
set -e
apt-get update -qq
apt-get install -y -qq curl sudo openssh-server iproute2 netcat-openbsd procps
ssh-keygen -A
mkdir -p /run/sshd
# Pre-start sshd manually (no systemd in this container).
/usr/sbin/sshd
# Wait for sshd on 22.
for i in 1 2 3 4 5; do
ss -ltn | grep -q ":22 " && break
sleep 1
done
cp init.sh /tmp/init.sh && chmod +x /tmp/init.sh
# Live invocation — no --delay-restart. The v4.7.1 direct-spawn
# fallback in restart_sshd kicks in because systemd is absent.
/tmp/init.sh \
--user=_247like \
--port=2222 \
--key-raw="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPQQDejyTCPJO3Jyw5UX9jmojb/zjgcqVuRO28fmpWU5 ci@test" \
--no-update --no-bbr --no-ip-probe --yes --strict
# After live restart, sshd must be listening on 2222.
# Give the freshly-spawned daemon a moment to bind.
for i in 1 2 3 4 5; do
ss -ltn | grep -q ":2222 " && break
sleep 1
done
ss -ltn | grep -q ":2222 " || { echo "❌ sshd not listening on 2222"; ss -ltn; exit 1; }
# SSH banner on the new port.
banner=$(printf "SSH-2.0-TEST\r\n" | nc -w 3 127.0.0.1 2222 | head -1 || true)
echo "Banner: $banner"
echo "$banner" | grep -q "SSH-2.0" || { echo "❌ no SSH banner on 2222"; exit 1; }
for lock in /run/server-init/script.lock /var/lib/server-init/script.lock; do
test ! -d "$lock" \
|| { echo "❌ script lock left behind after live restart: $lock"; exit 1; }
done
echo "✅ Live restart + listen + banner OK"
CONTAINER
# -------------------------------------------------------------------
# 4. Pre-existing-user safety test — ensure passwd -d does NOT clobber an
# existing admin password (the v4.7.0 → v4.7.1 R1 regression test).
# -------------------------------------------------------------------
test-existing-user-safe:
name: Pre-existing user password preserved
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Verify pre-existing user is left alone
run: |
chmod +x init.sh
docker run --rm --privileged -v "$(pwd)":/mnt:ro -w /mnt ubuntu:22.04 /bin/sh -s <<'CONTAINER'
set -e
apt-get update -qq
apt-get install -y -qq curl sudo openssh-server iproute2
ssh-keygen -A
mkdir -p /run/sshd
if [ -x /bin/dash ]; then ln -sf /bin/dash /bin/sh; fi
# Create an existing admin with a known password hash.
useradd -m -s /bin/bash existing_admin
echo "existing_admin:hunter2_super_secret" | chpasswd
# Capture the shadow line BEFORE the script runs.
shadow_before=$(getent shadow existing_admin | cut -d: -f2)
echo "shadow_before: $shadow_before"
test -n "$shadow_before" || { echo "no shadow entry"; exit 1; }
test "$shadow_before" != "" || { echo "empty shadow"; exit 1; }
test "$shadow_before" != "!" || { echo "locked unexpectedly"; exit 1; }
cp init.sh /tmp/init.sh && chmod +x /tmp/init.sh
/tmp/init.sh \
--user=existing_admin \
--port=2222 \
--key-raw="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPQQDejyTCPJO3Jyw5UX9jmojb/zjgcqVuRO28fmpWU5 ci@test" \
--no-update --no-bbr --delay-restart --no-ip-probe --yes --strict
shadow_after=$(getent shadow existing_admin | cut -d: -f2)
echo "shadow_after: $shadow_after"
test "$shadow_before" = "$shadow_after" || {
echo "❌ REGRESSION: existing_admin password was changed!"
echo " before: $shadow_before"
echo " after: $shadow_after"
exit 1
}
# [v4.7.3] Positive assertion: passwd -S should report `P` (password
# set) — not `NP` (no password) or `L` (locked).
ps_out=$(passwd -S existing_admin 2>/dev/null || true)
echo "passwd -S: $ps_out"
echo "$ps_out" | awk "{print \$2}" | grep -qE "^P\$" || {
echo "❌ REGRESSION: existing_admin lost its password set state (expected P, got: $ps_out)"
exit 1
}
# [v4.7.2] safe_configure_sudo is only invoked from the
# new-user branch of safe_ensure_user. For PRE-EXISTING accounts,
# safe_ensure_user returns early and never touches sudoers. The
# absence of the sudoers file is the correct, expected behavior.
if [ -f /etc/sudoers.d/server-init-existing_admin ]; then
echo "❌ REGRESSION: script wrote sudoers for a pre-existing account"
exit 1
fi
echo "✅ Pre-existing user password preserved and sudoers untouched"
CONTAINER
# -------------------------------------------------------------------
# 5. Cross-user sudoers cleanup safety — verify the legacy-glob cleanup
# cannot delete another user'\''s stable sudoers file when usernames
# share a hyphen-prefix (e.g. running script for "admin" must not
# remove "/etc/sudoers.d/server-init-admin-bot").
# -------------------------------------------------------------------
test-cross-user-sudoers-safe:
name: Cross-user sudoers cleanup glob safety
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Verify glob does not match sibling usernames
run: |
chmod +x init.sh
docker run --rm --privileged -v "$(pwd)":/mnt:ro -w /mnt ubuntu:22.04 /bin/sh -s <<'CONTAINER'
set -e
apt-get update -qq
apt-get install -y -qq curl sudo openssh-server iproute2
ssh-keygen -A
mkdir -p /run/sshd
# Pre-create a sibling user with a HYPHENATED name that shares a
# prefix with the script target. Also create an operator-managed
# sudoers file with a non-numeric suffix. The seed content uses a
# Defaults-only line (NOT a user/group rule) so it does NOT match
# safe_configure_sudo's principal-prefix check `^[[:space:]]*admin
# [[:space:]]` — otherwise the script would believe `admin` already
# has sudo and would NOT write the new stable file, breaking the
# third assertion below.
useradd -m admin-bot
mkdir -p /etc/sudoers.d
echo "admin-bot ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/server-init-admin-bot
chmod 440 /etc/sudoers.d/server-init-admin-bot
# Operator-managed file: Defaults block, no user/group rule that
# could match the "admin" principal regex in safe_configure_sudo.
# NOTE: cannot use printf with single-quoted format because the
# outer `sh -c '\''...'\''` wrap would terminate at any internal
# single quote — use double quotes with escaped inner quotes.
printf "%s\n" "Defaults env_keep += \"PROXY_URL\"" > /etc/sudoers.d/server-init-admin-special-policy
chmod 440 /etc/sudoers.d/server-init-admin-special-policy
cp init.sh /tmp/init.sh && chmod +x /tmp/init.sh
# Run for user "admin" — the legacy-cleanup glob would naively
# match server-init-admin-* including admin-bot (HIGH bug fixed in
# v4.7.2 by requiring digits-only suffix).
/tmp/init.sh \
--user=admin \
--port=2222 \
--key-raw="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPQQDejyTCPJO3Jyw5UX9jmojb/zjgcqVuRO28fmpWU5 ci@test" \
--no-update --no-bbr --delay-restart --no-ip-probe --yes --strict
# Sibling sudoers MUST survive.
test -f /etc/sudoers.d/server-init-admin-bot \
|| { echo "❌ REGRESSION: sibling stable sudoers file was deleted"; ls /etc/sudoers.d/; exit 1; }
# Operator-managed non-numeric-suffix file MUST survive.
test -f /etc/sudoers.d/server-init-admin-special-policy \
|| { echo "❌ REGRESSION: operator-managed sudoers file was deleted"; ls /etc/sudoers.d/; exit 1; }
# New stable file for admin SHOULD exist.
test -f /etc/sudoers.d/server-init-admin \
|| { echo "❌ admin stable sudoers missing"; ls /etc/sudoers.d/; exit 1; }
echo "✅ Cross-user sudoers glob safety + operator-file safety verified"
CONTAINER
# -------------------------------------------------------------------
# 6. Legacy v4.6.x timestamped sudoers SHOULD be cleaned on re-run.
# -------------------------------------------------------------------
test-legacy-sudoers-cleanup:
name: Legacy timestamped sudoers auto-cleanup
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Verify legacy-pattern file is removed
run: |
chmod +x init.sh
docker run --rm --privileged -v "$(pwd)":/mnt:ro -w /mnt ubuntu:22.04 /bin/sh -s <<'CONTAINER'
set -e
apt-get update -qq
apt-get install -y -qq curl sudo openssh-server iproute2
ssh-keygen -A
mkdir -p /run/sshd
# Simulate a v4.6.x leftover for user "deploy".
mkdir -p /etc/sudoers.d
echo "deploy ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/server-init-deploy-20250520123045
chmod 440 /etc/sudoers.d/server-init-deploy-20250520123045
ls -l /etc/sudoers.d/
cp init.sh /tmp/init.sh && chmod +x /tmp/init.sh
/tmp/init.sh \
--user=deploy \
--port=2222 \
--key-raw="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPQQDejyTCPJO3Jyw5UX9jmojb/zjgcqVuRO28fmpWU5 ci@test" \
--no-update --no-bbr --delay-restart --no-ip-probe --yes --strict
# Legacy file must be gone.
test ! -f /etc/sudoers.d/server-init-deploy-20250520123045 \
|| { echo "❌ legacy timestamped sudoers was NOT cleaned"; ls -l /etc/sudoers.d/; exit 1; }
# Stable file must exist.
test -f /etc/sudoers.d/server-init-deploy \
|| { echo "❌ stable sudoers missing"; ls -l /etc/sudoers.d/; exit 1; }
# Audit log must record the removal.
grep -q "ACTION: LEGACY_SUDOERS_REMOVED" /var/log/server-init-audit.log \
|| { echo "❌ audit log missing LEGACY_SUDOERS_REMOVED entry"; exit 1; }
grep -q "server-init-deploy-20250520123045" /var/log/server-init-audit.log \
|| { echo "❌ audit log missing specific path"; exit 1; }
echo "✅ Legacy sudoers cleanup verified"
CONTAINER
# -------------------------------------------------------------------
# 7. CLI flag smoke — --version / --help / --no-ip-probe
# -------------------------------------------------------------------
test-cli-flags:
name: CLI flag smoke (--version / --help / --no-ip-probe)
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
- name: Smoke-test CLI flags
run: |
chmod +x init.sh
docker run --rm -v "$(pwd)":/mnt:ro -w /mnt ubuntu:22.04 /bin/sh -s <<'CONTAINER'
set -e
# We need root for the script to run past the preflight check.
# --version / --help exit BEFORE the root check, so they work
# for any user.
# --version
ver=$(./init.sh --version)
echo "version: $ver"
echo "$ver" | grep -q "v4.7.8" \
|| { echo "❌ --version did not report v4.7.8: $ver"; exit 1; }
# --help should list all known flags
./init.sh --help | grep -q -- "--no-ip-probe" \
|| { echo "❌ --help missing --no-ip-probe entry"; exit 1; }
./init.sh --help | grep -q -- "--strict" \
|| { echo "❌ --help missing --strict entry"; exit 1; }
# Short forms
./init.sh -V | grep -q "v4.7.8" \
|| { echo "❌ -V failed"; exit 1; }
./init.sh -h | grep -q -- "--help" \
|| { echo "❌ -h failed"; exit 1; }
check_no_lock() {
label="$1"
for lock in /run/server-init/script.lock /var/lib/server-init/script.lock; do
test ! -d "$lock" \
|| { echo "❌ script lock leaked after $label: $lock"; exit 1; }
done
}
check_no_lock "--version/--help"
if ./init.sh --user=a --user=b --yes >/tmp/dup-user.out 2>&1; then
echo "❌ duplicate --user unexpectedly succeeded"
cat /tmp/dup-user.out
exit 1
fi
check_no_lock "duplicate --user"
if ./init.sh --port= --yes >/tmp/empty-port.out 2>&1; then
echo "❌ empty --port unexpectedly succeeded"
cat /tmp/empty-port.out
exit 1
fi
check_no_lock "empty --port"
if ./init.sh \
--user=deploy \
--port=03306 \
--key-raw="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPQQDejyTCPJO3Jyw5UX9jmojb/zjgcqVuRO28fmpWU5 ci@test" \
--no-update --no-bbr --delay-restart --no-ip-probe --yes --strict \
>/tmp/leading-zero-port.out 2>&1; then
echo "❌ leading-zero --port unexpectedly succeeded"
cat /tmp/leading-zero-port.out
exit 1
fi
check_no_lock "leading-zero --port"
echo "✅ CLI flag smoke OK"
CONTAINER
# -------------------------------------------------------------------
# 8. --no-ip-probe actually skips the ipify lookup
# -------------------------------------------------------------------
test-no-ip-probe:
name: --no-ip-probe skips external IP lookup
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Verify --no-ip-probe skips ipify call
run: |
chmod +x init.sh
docker run --rm --privileged -v "$(pwd)":/mnt:ro -w /mnt ubuntu:22.04 /bin/sh -s <<'CONTAINER'
set -e
apt-get update -qq
apt-get install -y -qq curl sudo openssh-server iproute2
ssh-keygen -A
mkdir -p /run/sshd
cp init.sh /tmp/init.sh && chmod +x /tmp/init.sh
/tmp/init.sh \
--user=deploy \
--port=2222 \
--key-raw="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPQQDejyTCPJO3Jyw5UX9jmojb/zjgcqVuRO28fmpWU5 ci@test" \
--no-update --no-bbr --delay-restart --no-ip-probe --yes --strict
# Log should NOT contain ipify lookup.
if grep -q "api.ipify.org" /var/log/server-init.log 2>/dev/null; then
echo "❌ --no-ip-probe failed: ipify URL leaked into log"
grep "api.ipify.org" /var/log/server-init.log
exit 1
fi
echo "✅ --no-ip-probe verified"
CONTAINER
# -------------------------------------------------------------------
# 9. ACCOUNT_LOCKED warning fires when key deployment fails (non-strict)
# -------------------------------------------------------------------
test-account-locked-path:
name: ACCOUNT_LOCKED fires when key deploy fails (non-strict)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Verify ACCOUNT_LOCKED_REASON path
run: |
chmod +x init.sh
docker run --rm --privileged -v "$(pwd)":/mnt:ro -w /mnt ubuntu:22.04 /bin/sh -s <<'CONTAINER'
set -e
apt-get update -qq
apt-get install -y -qq curl sudo openssh-server iproute2
ssh-keygen -A
mkdir -p /run/sshd
cp init.sh /tmp/init.sh && chmod +x /tmp/init.sh
# Deliberately use an unreachable URL so key deploy FAILS but in
# non-strict mode the script continues.
/tmp/init.sh \
--user=deploy_locked \
--port=2222 \
--key-url="https://127.0.0.1:1/nope" \
--no-update --no-bbr --delay-restart --no-ip-probe --yes \
|| { echo "non-strict run should not exit non-zero on key deploy failure"; exit 1; }
# Audit log should record the locked state.
grep -q "ACTION: ACCOUNT_KEPT_LOCKED" /var/log/server-init-audit.log \
|| { echo "❌ audit missing ACCOUNT_KEPT_LOCKED"; cat /var/log/server-init-audit.log; exit 1; }
# User should exist but be locked.
passwd -S deploy_locked | awk "{print \$2}" | grep -qE "^L\$|^LK\$" \
|| { echo "❌ deploy_locked is not locked"; passwd -S deploy_locked; exit 1; }
for lock in /run/server-init/script.lock /var/lib/server-init/script.lock; do
test ! -d "$lock" \
|| { echo "❌ script lock left behind after non-strict key failure: $lock"; exit 1; }
done
echo "✅ ACCOUNT_LOCKED path verified"
CONTAINER
# -------------------------------------------------------------------
# 10. restore.sh refuses when checksums.sha256 is missing; FORCE=1 overrides
# -------------------------------------------------------------------
test-restore-checksum-gate:
name: restore.sh refuses missing checksums; FORCE=1 overrides
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Verify checksum gate + FORCE=1
run: |
chmod +x init.sh
docker run --rm --privileged -v "$(pwd)":/mnt:ro -w /mnt ubuntu:22.04 /bin/sh -s <<'CONTAINER'
set -e
apt-get update -qq
apt-get install -y -qq curl sudo openssh-server iproute2
ssh-keygen -A
mkdir -p /run/sshd
cp init.sh /tmp/init.sh && chmod +x /tmp/init.sh
/tmp/init.sh \
--user=deploy \
--port=2222 \
--key-raw="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPQQDejyTCPJO3Jyw5UX9jmojb/zjgcqVuRO28fmpWU5 ci@test" \
--no-update --no-bbr --delay-restart --no-ip-probe --yes --strict
backup_dir=$(ls -dt /var/backups/ssh-config/*/ 2>/dev/null | head -n 1)
test -n "$backup_dir" || { echo "❌ no backup dir"; exit 1; }
test -x "$backup_dir/restore.sh" || { echo "❌ no restore.sh"; exit 1; }
# 1. Delete checksum file → restore.sh should REFUSE.
rm -f "$backup_dir/checksums.sha256"
if sh "$backup_dir/restore.sh" 2>&1; then
echo "❌ restore.sh did NOT refuse when checksums.sha256 was missing"
exit 1
fi
echo "✅ restore.sh refused missing checksums"
# 2. FORCE=1 should bypass.
if ! FORCE=1 sh "$backup_dir/restore.sh" 2>&1; then
echo "❌ FORCE=1 did NOT bypass the checksum gate"
exit 1
fi
echo "✅ FORCE=1 successfully bypassed"
CONTAINER