#!/usr/bin/env bash set -Eeuo pipefail IFS=$'\n\t' # ============================================================================== # OTV 10.x Hardening Script # v1.0 - 2026-02-16: # - Robust logging: all command output (stdout/stderr) is tee'd into the log file. # - kube-apiserver-arg / kubelet-arg handling is surgical: # * preserves existing non-TLS args in the blocks # * removes any existing --tls-cipher-suites entries (unsupported) # * inserts the recommended --tls-cipher-suites entry # - Restart sequencing: # * rke2-server is restarted (if kube-apiserver/kubelet cipher config changes) # * etcd cipher configuration is applied after rke2 restart (to avoid regen/overwrite) # * etcd is restarted (container stop via crictl) when etcd cipher config changes # - Firewall remains surgical: # * only updates ports 6443, 9345, 10250, 2379, 2380 in the *filter INPUT chain # * preserves all other existing rules and the *nat table in /etc/iptables/rules.v4 # * inserts trusted CIDR allow + reject for each target port before the final INPUT reject # # Usage: # sudo ./tls_firewall_hardening.sh --trusted-cidr 10.0.0.0/8 [--dry-run] # # Rollback: # sudo ./tls_firewall_hardening.sh --rollback [--dry-run] # ============================================================================== SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" TS="$(date +%Y%m%d_%H%M%S)" LOG_FILE="${SCRIPT_DIR}/otv_hardening_${TS}.log" STATE_DIR="${SCRIPT_DIR}/.otv_hardening_state" mkdir -p "${STATE_DIR}" log() { printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "${LOG_FILE}" >&2; } rollback_guidance() { log "============================================================" log "Rollback guidance:" log " - Backups created by this script are recorded in: ${STATE_DIR}" log " - To rollback using this script: sudo $0 --rollback" log " - Or manually restore from the backup files noted in the state files." log "IMPORTANT: Validate services/connectivity before reboot." log "============================================================" } die() { log "ERROR: $*"; log "Log: ${LOG_FILE}"; rollback_guidance; exit 1; } on_err() { local exit_code=$? local line_no=$1 log "ERROR: Failed at line ${line_no} (exit code ${exit_code})." log "Last command: ${BASH_COMMAND}" rollback_guidance exit "${exit_code}" } trap 'on_err ${LINENO}' ERR # Robust command runner: prints command and captures all output into log + console run() { local cmd="$*" if [[ "${DRY_RUN}" == "1" ]]; then log "[DRY-RUN] ${cmd}" return 0 fi log "[RUN] ${cmd}" # shellcheck disable=SC2090 bash -o pipefail -c "${cmd}" 2>&1 | tee -a "${LOG_FILE}" >&2 } # --- Inputs ------------------------------------------------------------------- DRY_RUN="0" ROLLBACK="0" TRUSTED_CIDR="" # --- Paths -------------------------------------------------------------------- RKE2_CONFIG="/etc/rancher/rke2/config.yaml" ETCD_CONFIG="/opt/netapp/rancher/rke2/server/db/etcd/config" IPTABLES_RULES_FILE="/etc/iptables/rules.v4" SYSTEMD_UNIT="/etc/systemd/system/iptables-restore.service" CONTAINERD_RUNTIME="unix:///run/k3s/containerd/containerd.sock" # --- Ports to harden in rules.v4 --------------------------------------------- HARDEN_PORTS=(6443 9345 10250 2379 2380) # --- Recommended ciphers ------------------------------------------------------- APISERVER_CIPHERS="TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" KUBELET_CIPHERS="TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" ETCD_CIPHER_BLOCK=$'cipher-suites:\n - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\n - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\n - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256' # --- Arg parsing --------------------------------------------------------------- usage() { cat <<'USAGE' Usage: sudo ./otv_tls_firewall_hardening.sh --trusted-cidr [--dry-run] sudo ./otv_tls_firewall_hardening.sh --rollback [--dry-run] Required: --trusted-cidr Trusted CIDR allowed to access ports 6443,9345,10250,2379,2380 Options: --dry-run --rollback -h, --help USAGE } while [[ $# -gt 0 ]]; do case "$1" in --trusted-cidr) TRUSTED_CIDR="${2:-}"; shift 2 ;; --dry-run) DRY_RUN="1"; shift ;; --rollback) ROLLBACK="1"; shift ;; -h|--help) usage; exit 0 ;; *) usage; die "Unknown argument: $1" ;; esac done # --- Preflight ---------------------------------------------------------------- require_root() { [[ "${EUID}" -eq 0 ]] || die "Run as root (sudo)."; } check_commands() { local cmds=(date cp mv mkdir awk sed grep systemctl iptables iptables-restore ps uname cat mktemp chmod head diff bash tee) for c in "${cmds[@]}"; do command -v "${c}" >/dev/null 2>&1 || die "Missing command: ${c}"; done command -v crictl >/dev/null 2>&1 || die "'crictl' not found (required for optional etcd restart)." } check_systemd() { local pid1 pid1="$(ps -p 1 -o comm= || true)" [[ "${pid1}" == "systemd" ]] || die "systemd not PID 1 (found '${pid1}')." } validate_cidr_basic() { [[ -n "${TRUSTED_CIDR}" ]] || die "--trusted-cidr is required." [[ "${TRUSTED_CIDR}" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]] || die "Invalid CIDR format: ${TRUSTED_CIDR}" } print_environment_info() { log "Environment info:" run "uname -a" if [[ -f /etc/os-release ]]; then run "cat /etc/os-release" else log "WARN: /etc/os-release not found." fi run "iptables --version" if [[ -S "${CONTAINERD_RUNTIME#unix://}" ]]; then log "Containerd socket found: ${CONTAINERD_RUNTIME}" else die "Containerd socket not found at expected path: ${CONTAINERD_RUNTIME}" fi } precheck_services() { log "Service status: rke2-server" run "systemctl --no-pager -l status rke2-server.service || true" systemctl is-active --quiet rke2-server.service || die "rke2-server is not active. Fix service health before running this hardening." } # --- Confirmation gate --------------------------------------------------------- RESTART_ETCD="0" confirm_disclaimer() { if [[ "${DRY_RUN}" == "1" ]]; then log "[DRY-RUN] Skipping interactive confirmation." RESTART_ETCD="0" return 0 fi log "============================================================" log "============================================================" log "IMPORTANT / DOWNTIME NOTICE:" log "============================================================" log "============================================================" log " - THIS ACTIVITY MAY CAUSE A BRIEF SERVICE INTERRUPTION." log " - rke2-server and etcd may be restarted to apply TLS changes." log " - Run only during an approved maintenance window." log "============================================================" printf '%s\n' "Type 'Y' to proceed, anything else to abort: " | tee -a "${LOG_FILE}" >&2 read -r a if [[ "${a}" != "Y" && "${a}" != "y" ]]; then die "User aborted." fi RESTART_ETCD="1" log "============================================================" } confirm_rollback_disclaimer() { if [[ "${DRY_RUN}" == "1" ]]; then log "[DRY-RUN] Skipping interactive rollback confirmation." return 0 fi log "============================================================" log "============================================================" log "IMPORTANT / ROLLBACK NOTICE:" log "============================================================" log "============================================================" log " - THIS ACTIVITY WILL ATTEMPT TO ROLLBACK CHANGES MADE BY THIS SCRIPT." log " - It will restore configs from backups (state pointer first, then newest adjacent .bak_*)." log " - If at least one restore occurs, services may be restarted to apply restored settings." log " - If NO backups are found/restored, NO services will be restarted." log " - Run only during an approved maintenance window." log "============================================================" printf '%s\n' "Type 'Y' to proceed with rollback, anything else to abort: " | tee -a "${LOG_FILE}" >&2 read -r a if [[ "${a}" != "Y" && "${a}" != "y" ]]; then die "User aborted rollback." fi log "============================================================" } # --- State: backups ------------------------------------------------------------ record_backup() { printf '%s\n' "$2" > "${STATE_DIR}/$1.latest"; } latest_backup() { [[ -f "${STATE_DIR}/$1.latest" ]] && cat "${STATE_DIR}/$1.latest" || true; } # Finds newest adjacent backup file for a destination path. # This is a resilience fallback for rollback when the .latest pointer is missing or stale. # The timestamp format used by this script (YYYYMMDD_HHMMSS) sorts correctly lexicographically. find_latest_adjacent_backup() { local dest="$1" ls -1 "${dest}.bak_"* 2>/dev/null | sort | tail -n 1 || true } backup_file_mandatory() { local src="$1" key="$2" [[ -f "${src}" ]] || die "Required file not found: ${src}" local backup="${src}.bak_${TS}" run "cp -a -- '${src}' '${backup}'" record_backup "${key}" "${backup}" log "Backup created: ${backup}" } restore_from_latest_backup() { local dest="$1" key="$2" local backup backup="$(latest_backup "${key}")" # Primary: deterministic restore using the state pointer if [[ -n "${backup}" && -f "${backup}" ]]; then run "cp -a -- '${backup}' '${dest}'" log "Restored '${dest}' from '${backup}'" return 0 fi # Fallback: choose newest adjacent .bak_* if the pointer is missing/stale backup="$(find_latest_adjacent_backup "${dest}")" [[ -n "${backup}" && -f "${backup}" ]] || die "No backup found for '${key}'." run "cp -a -- '${backup}' '${dest}'" log "Restored '${dest}' from fallback newest backup '${backup}'" } # Best-effort restore: # - returns 0 if a restore happened # - returns 1 if no backup exists (pointer missing/stale AND no adjacent .bak_*) try_restore_from_backup() { local dest="$1" key="$2" local backup backup="$(latest_backup "${key}")" if [[ -n "${backup}" && -f "${backup}" ]]; then restore_from_latest_backup "${dest}" "${key}" return 0 fi backup="$(find_latest_adjacent_backup "${dest}")" if [[ -n "${backup}" && -f "${backup}" ]]; then restore_from_latest_backup "${dest}" "${key}" return 0 fi return 1 } do_rollback() { require_root check_commands check_systemd log "Rollback requested." confirm_rollback_disclaimer local restored_any="0" if try_restore_from_backup "${RKE2_CONFIG}" "rke2_config"; then restored_any="1" else log "No RKE2 backup found; skipping." fi if try_restore_from_backup "${ETCD_CONFIG}" "etcd_config"; then restored_any="1" else log "No etcd backup found; skipping." fi if try_restore_from_backup "${IPTABLES_RULES_FILE}" "iptables_rules"; then restored_any="1" run "iptables-restore < '${IPTABLES_RULES_FILE}'" else log "No iptables rules backup found; skipping." fi if try_restore_from_backup "${SYSTEMD_UNIT}" "systemd_unit"; then restored_any="1" else log "No systemd unit backup found; skipping." fi if [[ "${restored_any}" != "1" ]]; then log "No backups were restored. Skipping service restarts." log "Rollback completed. Validate services/connectivity." exit 0 fi run "systemctl daemon-reload" log "Restarting rke2-server after rollback." run "systemctl restart rke2-server || true" log "Service status: rke2-server (post-rollback)" run "systemctl --no-pager -l status rke2-server.service || true" log "Restarting iptables-restore.service after rollback." run "systemctl restart iptables-restore.service || true" log "Service status: iptables-restore (post-rollback)" run "systemctl --no-pager -l status iptables-restore.service || true" log "Container status: etcd (post-rollback)" run "crictl --runtime-endpoint '${CONTAINERD_RUNTIME}' ps --name etcd || true" log "Rollback completed. Validate services/connectivity." exit 0 } # --- File helpers -------------------------------------------------------------- ensure_file_exists() { local f="$1" run "mkdir -p -- '$(dirname -- "${f}")'" if [[ "${DRY_RUN}" == "1" ]]; then log "[DRY-RUN] Would ensure file exists: ${f}" return 0 fi [[ -f "${f}" ]] || { : > "${f}"; log "Created missing file: ${f}"; } } # --- RKE2 config: surgical TLS insertion -------------------------------------- # Returns 0 if block contains exactly one tls-cipher-suites with the specified value, else 1 rke2_block_has_exact_tls_value() { local file="$1" key="$2" desired="$3" local desired_line="--tls-cipher-suites=${desired}" awk -v k="${key}:" -v d="${desired_line}" ' BEGIN {inblock=0; found=0; extra=0} { if ($0 ~ ("^" k "$")) { inblock=1; next } if (inblock == 1 && $0 ~ "^[^[:space:]].*:$") { inblock=0 } if (inblock == 1 && $0 ~ /^[[:space:]]*-[[:space:]]*.*$/) { item=$0; sub(/^[[:space:]]*-[[:space:]]*/, "", item) if (item ~ /^--tls-cipher-suites=/) { if (item == d) { found++ } else { extra++ } } } } END { exit((found == 1 && extra == 0) ? 0 : 1) } ' "${file}" } # Ensures a block exists and contains exactly ONE tls-cipher-suites entry with the recommended value, # while preserving all other args in the block. # # Returns: # 0 => file changed # 1 => no change needed ensure_rke2_tls_cipher_in_block() { local file="$1" key="$2" desired="$3" local tmp tmp="$(mktemp)" local desired_line desired_line="--tls-cipher-suites=${desired}" if [[ "${DRY_RUN}" == "1" ]]; then log "[DRY-RUN] Would ensure ${key}: contains exactly '${desired_line}' (remove other tls-cipher-suites entries) in ${file}" rm -f "${tmp}" return 0 fi awk -v k="${key}:" -v d="${desired_line}" ' BEGIN {inblock=0; sawkey=0; changed=0; inserted=0} function print_desired() { print " - " d inserted=1 changed=1 } { line=$0 if (line ~ ("^" k "$")) { sawkey=1 inblock=1 print line next } if (inblock==1) { # Next top-level key ends the block if (line ~ "^[^[:space:]].*:") { if (inserted==0) { print_desired() } inblock=0 print line next } # Process list items if (line ~ "^[[:space:]]*-[[:space:]]*") { item=line sub(/^[[:space:]]*-[[:space:]]*/, "", item) # Drop any existing tls-cipher-suites entries that are NOT exactly desired if (item ~ /^--tls-cipher-suites=/) { if (item == d) { # Keep exactly one desired; drop duplicates if (inserted==0) { print line inserted=1 } else { changed=1 } } else { changed=1 } next } # Keep all other args unchanged print line next } # Keep any other lines in block (comments/blank) unchanged print line next } print line } END { if (sawkey==0) { print "" print k print " - " d changed=1 } else if (sawkey==1 && inserted==0) { # Block existed but file ended while still in block print " - " d changed=1 } exit(changed ? 0 : 1) } ' "${file}" > "${tmp}" if mv -- "${tmp}" "${file}"; then : fi } # --- etcd config --------------------------------------------------------------- etcd_has_recommended_cipher_block() { local file="$1" [[ -f "${file}" ]] || return 1 grep -Fq "${ETCD_CIPHER_BLOCK}" "${file}" } strict_replace_etcd_cipher_block() { local file="$1" local tmp tmp="$(mktemp)" if [[ "${DRY_RUN}" == "1" ]]; then log "[DRY-RUN] Would strictly replace cipher-suites block in ${file}" rm -f "${tmp}" return 0 fi awk ' BEGIN {skip=0} { if ($0 ~ /^cipher-suites:[[:space:]]*$/) { skip=1; next } if (skip==1) { if ($0 ~ /^[^[:space:]].*:[[:space:]]*$/) { skip=0; print $0 } else { next } } else { print $0 } } ' "${file}" > "${tmp}" { echo "" echo "cipher-suites:" echo " - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" echo " - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" echo " - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" } >> "${tmp}" mv -- "${tmp}" "${file}" } # --- Firewall: surgical update ------------------------------------------------- iptables_rules_needs_hardening_update() { local file="$1" [[ -f "${file}" ]] || return 0 local p for p in "${HARDEN_PORTS[@]}"; do grep -qE "^-A INPUT .* -p tcp .* --dport ${p} .* -s ${TRUSTED_CIDR} .* -j ACCEPT" "${file}" || return 0 grep -qE "^-A INPUT .* -p tcp .* --dport ${p} .* -j REJECT" "${file}" || return 0 done return 1 } surgically_update_rules_v4() { local file="$1" local tmp tmp="$(mktemp)" if [[ "${DRY_RUN}" == "1" ]]; then log "[DRY-RUN] Would surgically update ${file} for ports: ${HARDEN_PORTS[*]} (trusted CIDR: ${TRUSTED_CIDR})" rm -f "${tmp}" return 0 fi local ports_regex ports_regex="$(printf '%s|' "${HARDEN_PORTS[@]}")" ports_regex="${ports_regex%|}" local ports_list ports_list="$(printf '%s ' "${HARDEN_PORTS[@]}")" awk -v CIDR="${TRUSTED_CIDR}" -v PORTS_RE="${ports_regex}" -v PORTS_LIST="${ports_list}" ' BEGIN { in_filter=0 inserted=0 n=split(PORTS_LIST, P, /[[:space:]]+/) if (P[n] == "") n-- NPORTS=n } function is_target_port_rule(line) { return (line ~ /^-A INPUT/ && line ~ /-p tcp/ && line ~ ("--dport[[:space:]]+(" PORTS_RE ")")) } function emit_hardening_rules() { print "" print "# OTV hardening: restrict sensitive ports to trusted CIDR (managed by script)" for (i=1; i<=NPORTS; i++) { p=P[i] print "-A INPUT -p tcp --dport " p " -s " CIDR " -j ACCEPT" print "-A INPUT -p tcp --dport " p " -j REJECT" } print "" } { line=$0 if (line ~ /^\*filter[[:space:]]*$/) { in_filter=1; print line; next } if (in_filter==1 && line ~ /^COMMIT[[:space:]]*$/) { if (inserted==0) { emit_hardening_rules(); inserted=1 } in_filter=0 print line next } if (in_filter==1) { if (is_target_port_rule(line)) { next } if (line ~ /^-A INPUT -j REJECT[[:space:]]*$/ && inserted==0) { emit_hardening_rules() inserted=1 print line next } print line next } print line } ' "${file}" > "${tmp}" mv -- "${tmp}" "${file}" log "Surgically updated firewall rules for ports: ${HARDEN_PORTS[*]}" } validate_and_apply_iptables() { run "iptables-restore -t < '${IPTABLES_RULES_FILE}'" log "Validated ${IPTABLES_RULES_FILE} syntax." run "iptables-restore < '${IPTABLES_RULES_FILE}'" log "Applied ${IPTABLES_RULES_FILE} to running system." } ensure_iptables_restore_unit() { if [[ -f "${SYSTEMD_UNIT}" ]]; then log "iptables-restore unit already exists: ${SYSTEMD_UNIT} (not overwriting)." else log "Creating iptables-restore unit for persistence: ${SYSTEMD_UNIT}" if [[ "${DRY_RUN}" == "1" ]]; then log "[DRY-RUN] Would write systemd unit to ${SYSTEMD_UNIT}" else cat > "${SYSTEMD_UNIT}" <<'EOF' [Unit] Description=Restore iptables rules DefaultDependencies=no Before=network-pre.target Wants=network-pre.target [Service] Type=oneshot ExecStart=/sbin/iptables-restore /etc/iptables/rules.v4 RemainAfterExit=yes [Install] WantedBy=multi-user.target EOF chmod 644 "${SYSTEMD_UNIT}" fi fi run "systemctl daemon-reload" run "systemctl enable iptables-restore.service" run "systemctl restart iptables-restore.service" run "systemctl --no-pager -l status iptables-restore.service || true" } # --- Restarts ----------------------------------------------------------------- restart_rke2() { log "Restarting rke2-server." run "systemctl restart rke2-server" run "systemctl --no-pager -l status rke2-server.service || true" } restart_etcd_container() { log "Restarting etcd via crictl (runtime: ${CONTAINERD_RUNTIME})." if [[ "${DRY_RUN}" == "1" ]]; then log "[DRY-RUN] Would locate and stop etcd container." return 0 fi local cid cid="$(crictl --runtime-endpoint "${CONTAINERD_RUNTIME}" ps --name etcd -q | head -n 1 || true)" [[ -n "${cid}" ]] || die "Could not find an etcd container via crictl. Aborting." run "crictl --runtime-endpoint '${CONTAINERD_RUNTIME}' stop '${cid}'" log "Stopped etcd container ${cid}. It should be recreated automatically." } # --- Main --------------------------------------------------------------------- main() { log "Starting OTV hardening (v1.0 - 2026-02-16)." log "Script directory: ${SCRIPT_DIR}" log "Log file: ${LOG_FILE}" log "Dry-run: ${DRY_RUN}" require_root check_commands check_systemd print_environment_info if [[ "${ROLLBACK}" == "1" ]]; then do_rollback fi validate_cidr_basic log "Trusted CIDR: ${TRUSTED_CIDR}" precheck_services confirm_disclaimer # --------------------------------------------------------------------------- # Step 1: RKE2 config (kube-apiserver/kubelet) - surgical within blocks # --------------------------------------------------------------------------- log "Evaluating Kubernetes cipher settings in ${RKE2_CONFIG}" ensure_file_exists "${RKE2_CONFIG}" local_rke2_changed="0" # Determine if change needed before backing up if rke2_block_has_exact_tls_value "${RKE2_CONFIG}" "kube-apiserver-arg" "${APISERVER_CIPHERS}" \ && rke2_block_has_exact_tls_value "${RKE2_CONFIG}" "kubelet-arg" "${KUBELET_CIPHERS}"; then log "Kubernetes cipher settings already compliant. No change required." else backup_file_mandatory "${RKE2_CONFIG}" "rke2_config" if ensure_rke2_tls_cipher_in_block "${RKE2_CONFIG}" "kube-apiserver-arg" "${APISERVER_CIPHERS}"; then local_rke2_changed="1" log "Ensured kube-apiserver-arg uses recommended --tls-cipher-suites (removed unsupported entries)." fi if ensure_rke2_tls_cipher_in_block "${RKE2_CONFIG}" "kubelet-arg" "${KUBELET_CIPHERS}"; then local_rke2_changed="1" log "Ensured kubelet-arg uses recommended --tls-cipher-suites (removed unsupported entries)." fi fi if [[ "${local_rke2_changed}" == "1" ]]; then restart_rke2 else log "Skipping rke2-server restart because kube-apiserver/kubelet config did not change." fi # --------------------------------------------------------------------------- # Step 2: etcd cipher enforcement AFTER rke2 restart, then optional etcd restart # --------------------------------------------------------------------------- log "Evaluating etcd cipher settings in ${ETCD_CONFIG}" [[ -f "${ETCD_CONFIG}" ]] || die "etcd config not found: ${ETCD_CONFIG}" local_etcd_changed="0" if etcd_has_recommended_cipher_block "${ETCD_CONFIG}"; then log "etcd cipher settings already contain the recommended cipher-suites block. No change required." else backup_file_mandatory "${ETCD_CONFIG}" "etcd_config" strict_replace_etcd_cipher_block "${ETCD_CONFIG}" local_etcd_changed="1" log "Updated etcd cipher-suites to recommended values." fi if [[ "${RESTART_ETCD}" == "1" && "${local_etcd_changed}" == "1" ]]; then restart_etcd_container else log "Skipping etcd restart (no consent or no etcd change needed)." fi # --------------------------------------------------------------------------- # Step 3: Firewall surgical update and persistence # --------------------------------------------------------------------------- log "Evaluating firewall rules in ${IPTABLES_RULES_FILE}" [[ -f "${IPTABLES_RULES_FILE}" ]] || die "Firewall rules file not found: ${IPTABLES_RULES_FILE}" if iptables_rules_needs_hardening_update "${IPTABLES_RULES_FILE}"; then log "Firewall hardening required for target ports. Creating backup and applying surgical update." backup_file_mandatory "${IPTABLES_RULES_FILE}" "iptables_rules" surgically_update_rules_v4 "${IPTABLES_RULES_FILE}" validate_and_apply_iptables else log "Firewall rules already appear hardened for the target ports and trusted CIDR. No change required." fi ensure_iptables_restore_unit log "SUCCESS: Hardening completed." log "Backups/state pointers: ${STATE_DIR}" log "Rollback command: sudo $0 --rollback" } main