From 9e8f024b896dc2dfa036318fb611fe9595811da2 Mon Sep 17 00:00:00 2001 From: ochenstarik-ui <267932263+ochenstarik-ui@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:31:21 +0700 Subject: [PATCH 1/2] fix: prepare sshd runtime directory before validation --- .github/workflows/bash-syntax.yml | 3 +++ ochenstarik-server-user-3.sh | 7 +++++++ tests/test-step3-runtime-directory.sh | 24 ++++++++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 tests/test-step3-runtime-directory.sh diff --git a/.github/workflows/bash-syntax.yml b/.github/workflows/bash-syntax.yml index 084e152..fcfea4b 100644 --- a/.github/workflows/bash-syntax.yml +++ b/.github/workflows/bash-syntax.yml @@ -59,3 +59,6 @@ jobs: - name: Test initial backup policy and upload retry run: bash tests/test-backup-initial-retry.sh + + - name: Test step 3 socket activation runtime directory + run: bash tests/test-step3-runtime-directory.sh diff --git a/ochenstarik-server-user-3.sh b/ochenstarik-server-user-3.sh index c557d42..a4bcad8 100644 --- a/ochenstarik-server-user-3.sh +++ b/ochenstarik-server-user-3.sh @@ -73,6 +73,12 @@ is_valid_port() { (( 10#$port >= 1 && 10#$port <= 65535 )) } +prepare_sshd_runtime_directory() { + # Socket-activated Ubuntu can have no active ssh.service after an upgrade. + # Its RuntimeDirectory then does not exist, but standalone sshd -t needs it. + install -d -o root -g root -m 755 /run/sshd +} + read_ip_mode() { local mode=both if [[ -e "$IP_FAMILY_CONFIG" ]]; then @@ -517,6 +523,7 @@ UsePAM yes EOF chmod 644 "$SSHD_DROPIN" +prepare_sshd_runtime_directory sshd -t || die "sshd syntax validation failed" # Capture the complete output once. With pipefail enabled, piping `sshd -T` # into an awk program that exits after the first match can terminate sshd with diff --git a/tests/test-step3-runtime-directory.sh b/tests/test-step3-runtime-directory.sh new file mode 100644 index 0000000..8c3bd7a --- /dev/null +++ b/tests/test-step3-runtime-directory.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." +source <(awk '/^\[\[ "\$EUID"/{exit} {print}' ochenstarik-server-user-3.sh) +test_dir="$(mktemp -d)" +trap 'rm -rf -- "$test_dir"' EXIT + +# Relocate just the privileged filesystem boundary; use the real install +# binary to check first creation, idempotency and repair of unsafe modes. +install() { + [[ "$*" == '-d -o root -g root -m 755 /run/sshd' ]] + /usr/bin/install -d -m 755 "$test_dir/sshd" +} +prepare_sshd_runtime_directory +[[ -d "$test_dir/sshd" && "$(stat -c %a "$test_dir/sshd")" == 755 ]] +chmod 777 "$test_dir/sshd" +prepare_sshd_runtime_directory +[[ "$(stat -c %a "$test_dir/sshd")" == 755 ]] + +# The runtime prerequisite must run before standalone configuration validation. +prepare_line="$(grep -n '^prepare_sshd_runtime_directory$' ochenstarik-server-user-3.sh | cut -d: -f1)" +validate_line="$(grep -n '^sshd -t ' ochenstarik-server-user-3.sh | cut -d: -f1)" +((prepare_line < validate_line)) +printf 'Step 3 socket-activation runtime directory tests passed.\n' -- 2.45.2 From e5b06793ec69dcbdf402b72b2e661453a7732569 Mon Sep 17 00:00:00 2001 From: ochenstarik-ui <267932263+ochenstarik-ui@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:26:10 +0700 Subject: [PATCH 2/2] Reject masked UFW boot service before changing firewall rules --- .github/workflows/bash-syntax.yml | 3 +++ ochenstarik-server-2.sh | 20 +++++++++++++-- tests/test-step2-ufw-boot.sh | 41 +++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 tests/test-step2-ufw-boot.sh diff --git a/.github/workflows/bash-syntax.yml b/.github/workflows/bash-syntax.yml index fcfea4b..a3d8776 100644 --- a/.github/workflows/bash-syntax.yml +++ b/.github/workflows/bash-syntax.yml @@ -62,3 +62,6 @@ jobs: - name: Test step 3 socket activation runtime directory run: bash tests/test-step3-runtime-directory.sh + + - name: Test step 2 UFW boot readiness + run: bash tests/test-step2-ufw-boot.sh diff --git a/ochenstarik-server-2.sh b/ochenstarik-server-2.sh index b58492a..aecdb19 100755 --- a/ochenstarik-server-2.sh +++ b/ochenstarik-server-2.sh @@ -17,6 +17,20 @@ require_command() { command -v "$1" >/dev/null 2>&1 || die "Не найдена команда: $1" } +ensure_ufw_boot_service() { + local state + state="$(systemctl is-enabled ufw.service 2>/dev/null || true)" + case "$state" in + masked|masked-runtime) + die "ufw.service замаскирована: сначала проверьте причину маски и включите службу вручную. Правила не изменены." + ;; + esac + systemctl enable ufw.service \ + || die "Не удалось включить автозапуск ufw.service; правила не изменены" + systemctl is-enabled --quiet ufw.service \ + || die "Автозапуск ufw.service не подтверждён; правила не изменены" +} + is_valid_port() { local port="$1" [[ "$port" =~ ^[0-9]{1,5}$ ]] || return 1 @@ -403,9 +417,10 @@ if [[ "$ACTION" == install ]]; then apt-get install -y \ sudo ufw fail2ban curl ca-certificates openssl openssh-server logrotate iproute2 - for command_name in ip sed sysctl ufw; do + for command_name in ip sed sysctl ufw systemctl; do require_command "$command_name" done + ensure_ufw_boot_service install -d -m 700 -o root -g root "$CONFIG_DIR" load_managed_rules PREVIOUS_MANAGED_RULES=("${MANAGED_RULES[@]}") @@ -415,9 +430,10 @@ if [[ "$ACTION" == install ]]; then save_configuration log "SSH-порт ${SSH_PORT} сохранён для этапа 3" else - for command_name in ip sed sysctl ufw; do + for command_name in ip sed sysctl ufw systemctl; do require_command "$command_name" done + ensure_ufw_boot_service install -d -m 700 -o root -g root "$CONFIG_DIR" SSH_PORT="$(read_saved_value "$SSH_PORT_CONFIG" SSH_PORT)" is_valid_port "$SSH_PORT" || SSH_PORT="$DEFAULT_SSH_PORT" diff --git a/tests/test-step2-ufw-boot.sh b/tests/test-step2-ufw-boot.sh new file mode 100644 index 0000000..32005b2 --- /dev/null +++ b/tests/test-step2-ufw-boot.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." +source <(awk '/^\[\[ "\$EUID"/{exit} {print}' ochenstarik-server-2.sh) +test_dir="$(mktemp -d)" +trap 'rm -rf -- "$test_dir"' EXIT + +systemctl() { + printf '%s\n' "$*" >> "$test_dir/calls" + case "$*" in + 'is-enabled ufw.service') cat "$test_dir/state"; return 1 ;; + 'enable ufw.service') + [[ "$scenario" != enable-failed ]] || return 1 + [[ "$scenario" == verify-failed ]] || printf enabled > "$test_dir/state" + ;; + 'is-enabled --quiet ufw.service') [[ "$(cat "$test_dir/state")" == enabled ]] ;; + *) return 99 ;; + esac +} + +for state in masked masked-runtime; do + printf '%s' "$state" > "$test_dir/state" + : > "$test_dir/calls" + scenario=masked + if (ensure_ufw_boot_service) > "$test_dir/output" 2>&1; then exit 1; fi + ! grep -q '^enable ' "$test_dir/calls" + [[ "$(cat "$test_dir/state")" == "$state" ]] +done + +for scenario in disabled enabled enable-failed verify-failed; do + printf '%s' "$scenario" > "$test_dir/state" + : > "$test_dir/calls" + if [[ "$scenario" == *-failed ]]; then + if (ensure_ufw_boot_service) > "$test_dir/output" 2>&1; then exit 1; fi + else + ensure_ufw_boot_service + [[ "$(cat "$test_dir/state")" == enabled ]] + grep -qx 'is-enabled --quiet ufw.service' "$test_dir/calls" + fi +done +printf 'Step 2 UFW boot readiness tests passed.\n' -- 2.45.2