Quando un file sensibile cambia, la domanda utile non è soltanto “chi lo ha modificato?”. Prima ancora dobbiamo capire se quella modifica fosse prevista, se riguardasse contenuto o permessi, se altri elementi siano cambiati nello stesso intervallo di tempo e soprattutto se gli strumenti che avrebbero dovuto avvisarci fossero ancora vivi.
È da questo problema che nasce l’IR Stack. Non è un singolo programma e non vuole sostituire un SIEM. È piuttosto un insieme relativamente piccolo di componenti Linux già maturi, collegati in modo da coprire alcuni punti ciechi tipici di un server: modifiche agli account, variazioni di privilegi, chiavi SSH, accessi riusciti, alterazioni dello stesso stack di difesa e spegnimento dei servizi che dovrebbero sorvegliarlo.
Prevenzione e rilevamento sono due mestieri diversi
Un firewall può impedire una connessione. I permessi Unix possono impedire a un processo di leggere un file. SSH può rifiutare una password. Ma nessuno di questi strumenti, da solo, ci racconta bene che /etc/sudoers è stato modificato alle 03:14, che contemporaneamente è comparsa una nuova chiave in authorized_keys e che pochi secondi dopo il watcher è stato fermato.
Un sistema di incident response parte quindi da un principio diverso: assumiamo che qualcosa possa comunque cambiare e costruiamo abbastanza telemetria da accorgercene e ricostruirlo. Da qui l’uso contemporaneo di auditd, systemd path/timer, controlli logici sull’identità e watchdog sui guardian.
Perché non basta fare SHA256 di tutto
Il classico File Integrity Monitoring confronta una baseline con lo stato corrente. È utile, ma un hash da solo ci dice soltanto che due contenuti sono diversi. Non ci dice se la modifica sia avvenuta durante un aggiornamento autorizzato, chi abbia eseguito il comando, se il file abbia cambiato solo mode/owner oppure se l’evento faccia parte di una sequenza più ampia.
Nell’IR Stack usiamo quindi livelli differenti. auditd registra operazioni su file e comandi sensibili; systemd.path trasforma alcune modifiche in trigger immediati; un timer ripete comunque i controlli periodicamente; il checker ragiona sulla struttura degli account; il watcher protegge gli stessi script di difesa; il guardian verifica che path, timer e servizi non siano stati spenti.
Architettura dello stack
/etc/passwd, shadow, group, sudoers, sshd_config, authorized_keys
|
+------+------+
| |
auditd systemd.path
| |
+------+------+
|
identity checker
|
incident handler
|
+--------+---------+
| |
mail/log lockdown opzionale
|
rollback firewall
In parallelo:
journal SSH -> alert login riusciti
file stack -> watcher anti-tamper
unità IR -> guardian watchdog
1. auditd: la memoria forense
Auditd lavora a livello kernel e può registrare accessi o modifiche a file sensibili e l’esecuzione di determinati programmi. Le chiavi identity, privilege, sshcfg, sshkeys, user_mgmt e ir_stack permettono poi di isolare rapidamente gli eventi con ausearch.
auditctl -l
ausearch -k identity -ts recent -i
ausearch -k privilege -ts recent -i
ausearch -k ir_stack -ts recent -i
Il vantaggio rispetto a un semplice watcher è il contesto: non vediamo soltanto che il file è cambiato, ma possiamo spesso risalire al processo, all’utente e alla syscall coinvolta.
2. Il checker di identità
Alcune anomalie non sono “file cambiato = incidente”. Un secondo account con UID 0, invece, è quasi sempre qualcosa che merita attenzione. Lo stesso vale per un account di sistema che improvvisamente ottiene una shell interattiva reale. Il checker non si limita quindi agli hash: interpreta /etc/passwd, login.defs, le shell e una piccola whitelist locale per gli account di servizio legittimi.
È importante la whitelist. In un PBX, per esempio, l’utente asterisk può avere caratteristiche differenti da quelle attese da una regola generica. Un buon controllo non deve fingere che tutti i server siano uguali: deve consentire eccezioni documentate e specifiche.
3. Path e timer: immediatezza più ridondanza
systemd.path ci dà reazione quasi immediata: quando cambia uno dei file sorvegliati parte l’handler. Ma basarsi solo sugli eventi è fragile. Per questo esiste anche un timer periodico che ripete il controllo. Se un evento viene perso, se il watcher viene riavviato o se una condizione anomala non dipende da una singola scrittura, il controllo periodico ci offre una seconda possibilità.
4. Alert sugli accessi SSH riusciti
Molti sistemi avvisano sui tentativi falliti e ignorano il dato più interessante: un login riuscito. Lo script SSH conserva il cursor di journald e analizza soltanto le nuove righe Accepted publickey, Accepted password o equivalenti. L’alert contiene utente, IP, metodo e riga originale del log.
Il cursor è importante: senza stato, un parser lanciato periodicamente rischia di inviare più volte la stessa segnalazione. Al primo avvio lo script inizializza il cursor senza notificare eventi storici; da quel momento procede in avanti.
5. Chi sorveglia i sorveglianti?
Se un attaccante ottiene privilegi sufficienti, una delle prime mosse sensate è spegnere gli strumenti che potrebbero denunciarlo. Per questo lo stack controlla anche se stesso. Le modifiche a script, configurazioni e unità systemd generano eventi audit con chiave ir_stack e attivano un watcher dedicato.
Il guardian aggiunge un altro livello: verifica continuamente che path, timer e servizi fondamentali siano attivi. Nella versione produttiva alcuni guardian possono essere autoriparati, mentre servizi esterni importanti possono essere lasciati in modalità alert-only: riavviare automaticamente tutto ciò che cade sarebbe comodo, ma potrebbe anche nascondere la causa reale di un guasto.
6. Maintenance window
Il caso più fastidioso per un sistema del genere è l’aggiornamento legittimo. Un deploy o un upgrade può modificare proprio i file che stiamo proteggendo. La soluzione non è spegnere auditd: durante una maintenance window continuiamo a registrare gli eventi, ma sopprimiamo escalation, mail ripetitive e lockdown. In produzione usiamo un marker temporaneo con una scadenza; se il marker viene dimenticato, scade da solo.
7. Lockdown: separato e disabilitato di default
L’isolamento automatico è potente e pericoloso. Una falsa rilevazione non deve trasformarsi in un server che si taglia fuori dalla rete. L’installer pubblico crea gli strumenti di lockdown e rollback, ma ENABLE_LOCKDOWN parte su no. Prima di abilitarlo bisogna testare indirizzo amministrativo fidato, porta SSH, backup delle regole firewall e procedura di rollback da console.
Installer completo
Quello seguente deriva dall’installer realmente usato durante lo sviluppo dello stack. È stato reso standalone: non dipende dal setup NewVoip, usa valori neutri per IP/mail e non applica alcun lockdown durante l’installazione. Prima di usarlo in produzione va comunque letto e adattato ai servizi del server.
Nota importante: questa è la baseline standalone pubblicabile dello stack. La variante NewVoip attualmente in produzione aggiunge prefissi newvoip-*, maintenance window con marker temporaneo, guardian specifici per i servizi del PBX/Edge e integrazioni che non avrebbe senso imporre a un server Linux generico. L’architettura resta la stessa, ma la parte host-specific va sempre adattata.
Salviamolo come install-ir-stack.sh, rendiamolo eseguibile e lanciamolo impostando almeno mail di alert e, se vogliamo predisporre il lockdown, l’IP amministrativo fidato:
chmod 700 install-ir-stack.sh
TRUSTED_IPV4="192.0.2.10" ALERT_EMAIL="alert@example.net" ENABLE_LOCKDOWN="no" bash ./install-ir-stack.sh
#!/usr/bin/env bash
set -euo pipefail
# Installer standalone: nessuna dipendenza da librerie esterne
# ============================================================
# Incident Response / Identity Protection Stack Installer
# Debian / Raspberry / systemd / auditd / iptables
# ============================================================
TRUSTED_IPV4="${TRUSTED_IPV4:-}"
TRUSTED_IPV6="${TRUSTED_IPV6:-}"
SSH_PORT="${SSH_PORT:-22}"
ALERT_EMAIL="${ALERT_EMAIL:-alert@example.net}"
ENABLE_LOCKDOWN="${ENABLE_LOCKDOWN:-no}"
RELAYHOST="${RELAYHOST:-mail.example.net}"
RELAYPORT="${RELAYPORT:-25}"
if [[ -z "$TRUSTED_IPV4" ]]; then
echo "Errore: devi esportare TRUSTED_IPV4 prima di lanciare lo script."
echo 'Esempio: TRUSTED_IPV4="192.0.2.10" ALERT_EMAIL="alert@example.net" bash install-ir-stack.sh'
exit 1
fi
echo "[*] Preconfiguro Postfix..."
MAIL_FQDN="$(hostname -f 2>/dev/null || true)"
case "$MAIL_FQDN" in
""|localhost|localhost.localdomain) MAIL_FQDN="$(hostname)" ;;
esac
echo "$MAIL_FQDN" >/etc/mailname
export DEBIAN_FRONTEND=noninteractive
debconf-set-selections <<EOF
postfix postfix/mailname string ${MAIL_FQDN}
postfix postfix/main_mailer_type select Satellite system
postfix postfix/relayhost string [${RELAYHOST}]:${RELAYPORT}
EOF
echo "[*] Installo pacchetti..."
apt update
apt install -y auditd audispd-plugins iptables bsd-mailx postfix util-linux
echo "[*] Creo directory..."
install -d -m 700 /etc/incident-response
install -d -m 700 /root/firewall-backups
install -d -m 700 /var/lib/incident-response
install -d -m 700 /var/lib/incident-response/guardians-watchdog
echo "[*] Creo configurazione..."
cat >/etc/incident-response/lockdown.conf <<EOF
TRUSTED_IPV4="${TRUSTED_IPV4}"
TRUSTED_IPV6="${TRUSTED_IPV6}"
SSH_PORT="${SSH_PORT}"
ENABLE_LOCKDOWN="${ENABLE_LOCKDOWN}"
ALERT_EMAIL="${ALERT_EMAIL}"
EOF
chmod 600 /etc/incident-response/lockdown.conf
touch /etc/incident-response/identity-shell-whitelist
cat > /etc/incident-response/identity-shell-whitelist <<EOF
asterisk
EOF
chmod 600 /etc/incident-response/identity-shell-whitelist
echo "[*] Creo regole audit..."
cat >/etc/audit/rules.d/40-identity-watch.rules <<'EOF'
## File identità e privilegi
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/sudoers -p wa -k privilege
-w /etc/sudoers.d/ -p wa -k privilege
## SSH
-w /etc/ssh/sshd_config -p wa -k sshcfg
-w /root/.ssh/authorized_keys -p wa -k sshkeys
## Comandi sensibili
-w /usr/sbin/useradd -p x -k user_mgmt
-w /usr/sbin/usermod -p x -k user_mgmt
-w /usr/sbin/userdel -p x -k user_mgmt
-w /usr/bin/passwd -p x -k passwd_mgmt
-w /usr/sbin/visudo -p x -k sudo_change
EOF
cat >/etc/audit/rules.d/41-ir-stack.rules <<'EOF'
## Protezione stack di difesa
-w /usr/local/sbin/check-identity-anomalies.sh -p wa -k ir_stack
-w /usr/local/sbin/emergency-lockdown.sh -p wa -k ir_stack
-w /usr/local/sbin/emergency-lockdown-rollback.sh -p wa -k ir_stack
-w /usr/local/sbin/identity-incident-handler.sh -p wa -k ir_stack
-w /usr/local/sbin/ssh-login-alert.sh -p wa -k ir_stack
-w /usr/local/sbin/ir-stack-watch.sh -p wa -k ir_stack
-w /usr/local/sbin/ir-guardians-watchdog.sh -p wa -k ir_stack
-w /usr/local/sbin/irctl -p wa -k ir_stack
-w /etc/incident-response/lockdown.conf -p wa -k ir_stack
-w /etc/incident-response/identity-shell-whitelist -p wa -k ir_stack
-w /etc/systemd/system/identity-incident-from-path.service -p wa -k ir_stack
-w /etc/systemd/system/identity-incident-from-timer.service -p wa -k ir_stack
-w /etc/systemd/system/identity-incident-manual.service -p wa -k ir_stack
-w /etc/systemd/system/identity-incident-handler.path -p wa -k ir_stack
-w /etc/systemd/system/identity-incident-handler.timer -p wa -k ir_stack
-w /etc/systemd/system/ssh-login-alert.service -p wa -k ir_stack
-w /etc/systemd/system/ssh-login-alert.path -p wa -k ir_stack
-w /etc/systemd/system/ir-stack-watch.service -p wa -k ir_stack
-w /etc/systemd/system/ir-stack-watch.path -p wa -k ir_stack
-w /etc/systemd/system/ir-guardians-watchdog.service -p wa -k ir_stack
-w /etc/systemd/system/ir-guardians-watchdog.timer -p wa -k ir_stack
EOF
echo "[*] Creo checker identità..."
cat >/usr/local/sbin/check-identity-anomalies.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
PASSWD_FILE="/etc/passwd"
LOGIN_DEFS="/etc/login.defs"
SSHD_CFG="/etc/ssh/sshd_config"
LOCAL_WHITELIST="/etc/incident-response/identity-shell-whitelist"
UID_MIN=$(awk '/^[[:space:]]*UID_MIN[[:space:]]+/ {print $2}' "$LOGIN_DEFS" 2>/dev/null | tail -n1)
SYS_UID_MIN=$(awk '/^[[:space:]]*SYS_UID_MIN[[:space:]]+/ {print $2}' "$LOGIN_DEFS" 2>/dev/null | tail -n1)
SYS_UID_MAX=$(awk '/^[[:space:]]*SYS_UID_MAX[[:space:]]+/ {print $2}' "$LOGIN_DEFS" 2>/dev/null | tail -n1)
UID_MIN="${UID_MIN:-1000}"
SYS_UID_MIN="${SYS_UID_MIN:-101}"
SYS_UID_MAX="${SYS_UID_MAX:-$((UID_MIN-1))}"
ALERT=0
HOST="$(hostname -f 2>/dev/null || hostname)"
is_interactive_shell() {
local shell="${1:-}"
case "$shell" in
/bin/bash|/bin/sh|/bin/dash|/bin/zsh|/bin/ksh|/bin/tcsh|/bin/csh|/usr/bin/fish)
return 0
;;
*)
return 1
;;
esac
}
is_whitelisted_user() {
local user="${1:-}"
[ -f "$LOCAL_WHITELIST" ] || return 1
grep -qxF "$user" "$LOCAL_WHITELIST"
}
echo "[$(date --iso-8601=seconds)] host=$HOST"
echo "== Account con UID 0 =="
awk -F: '($3 == 0) {print}' "$PASSWD_FILE"
UID0_COUNT=$(awk -F: '($3 == 0) {c++} END {print c+0}' "$PASSWD_FILE")
if [ "$UID0_COUNT" -gt 1 ]; then
echo "ALERT: trovati $UID0_COUNT account con UID 0"
ALERT=1
fi
if ! awk -F: '($1=="root" && $3==0) {found=1} END {exit !found}' "$PASSWD_FILE"; then
echo "ALERT: account root mancante o con UID non 0"
ALERT=1
fi
echo
echo "== System account con shell interattiva reale =="
while IFS=: read -r user _ uid gid gecos home shell; do
if [ "$uid" -ge "$SYS_UID_MIN" ] && [ "$uid" -le "$SYS_UID_MAX" ]; then
if is_whitelisted_user "$user"; then
continue
fi
if is_interactive_shell "$shell"; then
echo "${user}\:x:${uid}:${gid}:${gecos}:${home}:${shell}"
echo "ALERT: system account '$user' ha shell interattiva reale ($shell)"
ALERT=1
fi
fi
done < "$PASSWD_FILE"
echo
echo "== Account sotto UID_MIN con shell interattiva reale (escluso root) =="
while IFS=: read -r user _ uid gid gecos home shell; do
if [ "$user" != "root" ] && [ "$uid" -lt "$UID_MIN" ]; then
if is_whitelisted_user "$user"; then
continue
fi
if is_interactive_shell "$shell"; then
echo "${user}\:x:${uid}:${gid}:${gecos}:${home}:${shell}"
echo "ALERT: account '$user' sotto UID_MIN con shell interattiva reale ($shell)"
ALERT=1
fi
fi
done < "$PASSWD_FILE"
echo
echo "== authorized_keys sensibili =="
for f in /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys; do
[ -f "$f" ] || continue
stat --printf '%n owner=%U group=%G mode=%a mtime=%y\n' "$f"
done
echo
echo "== sshd_config =="
if [ -f "$SSHD_CFG" ]; then
grep -Ei '^(PermitRootLogin|PasswordAuthentication|PubkeyAuthentication|AllowUsers|AllowGroups|Match|LogLevel)' "$SSHD_CFG" || true
fi
exit "$ALERT"
EOF
chmod 700 /usr/local/sbin/check-identity-anomalies.sh
echo "[*] Creo script lockdown..."
cat >/usr/local/sbin/emergency-lockdown.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
TRUSTED_IPV4="${1:-}"
TRUSTED_IPV6="${2:-}"
SSH_PORT="${SSH_PORT:-22}"
BACKUP_DIR="/root/firewall-backups"
TS="$(date +%F_%H%M%S)"
if [[ -z "$TRUSTED_IPV4" ]]; then
echo "Uso: $0 <trusted_ipv4> [trusted_ipv6]"
exit 1
fi
mkdir -p "$BACKUP_DIR"
iptables-save > "${BACKUP_DIR}/iptables_${TS}.rules"
ip6tables-save > "${BACKUP_DIR}/ip6tables_${TS}.rules"
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
iptables -t mangle -F
iptables -t mangle -X
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp -s "$TRUSTED_IPV4" --dport "$SSH_PORT" -m conntrack --ctstate NEW -j ACCEPT
iptables -A INPUT -p icmp -s "$TRUSTED_IPV4" -j ACCEPT
ip6tables -F
ip6tables -X
ip6tables -t mangle -F
ip6tables -t mangle -X
ip6tables -P INPUT DROP
ip6tables -P FORWARD DROP
ip6tables -P OUTPUT ACCEPT
ip6tables -A INPUT -i lo -j ACCEPT
ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
if [[ -n "$TRUSTED_IPV6" ]]; then
ip6tables -A INPUT -p tcp -s "$TRUSTED_IPV6" --dport "$SSH_PORT" -m conntrack --ctstate NEW -j ACCEPT
ip6tables -A INPUT -p ipv6-icmp -s "$TRUSTED_IPV6" -j ACCEPT
fi
logger -t emergency-lockdown "Lockdown applicato. SSH consentito solo da IPv4=${TRUSTED_IPV4} IPv6=${TRUSTED_IPV6:-none} porta=${SSH_PORT}"
echo "Lockdown applicato."
EOF
chmod 700 /usr/local/sbin/emergency-lockdown.sh
cat >/usr/local/sbin/emergency-lockdown-rollback.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
RULES_V4="${1:-}"
RULES_V6="${2:-}"
if [[ -z "$RULES_V4" || -z "$RULES_V6" ]]; then
echo "Uso: $0 <backup_ipv4.rules> <backup_ipv6.rules>"
exit 1
fi
iptables-restore < "$RULES_V4"
ip6tables-restore < "$RULES_V6"
logger -t emergency-lockdown "Firewall ripristinato da backup"
echo "Regole ripristinate."
EOF
chmod 700 /usr/local/sbin/emergency-lockdown-rollback.sh
echo "[*] Creo handler principale..."
cat >/usr/local/sbin/identity-incident-handler.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
CONF="/etc/incident-response/lockdown.conf"
LOGFILE="/var/log/identity-incident-handler.log"
SOURCE="${1:-unknown}"
[ -f "$CONF" ] && . "$CONF"
TRUSTED_IPV4="${TRUSTED_IPV4:-}"
TRUSTED_IPV6="${TRUSTED_IPV6:-}"
SSH_PORT="${SSH_PORT:-22}"
ENABLE_LOCKDOWN="${ENABLE_LOCKDOWN:-no}"
ALERT_EMAIL="${ALERT_EMAIL:-}"
HOST="$(hostname -f 2>/dev/null || true)"
case "$HOST" in
""|localhost|localhost.localdomain) HOST="$(hostname)" ;;
esac
send_mail() {
local subject="$1"
local body="$2"
if [[ -n "$ALERT_EMAIL" ]] && command -v mail >/dev/null 2>&1; then
printf '%s\n' "$body" | mail -s "$subject" "$ALERT_EMAIL" || true
fi
}
{
echo "============================================================"
echo "[$(date --iso-8601=seconds)] source=${SOURCE} avvio controllo identità"
} >> "$LOGFILE" 2>&1
if /usr/local/sbin/check-identity-anomalies.sh >> "$LOGFILE" 2>&1; then
logger -t identity-incident-handler "SOURCE=${SOURCE} Nessuna anomalia critica"
exit 0
fi
logger -t identity-incident-handler "SOURCE=${SOURCE} ANOMALIA CRITICA rilevata"
BODY="$(tail -n 120 "$LOGFILE")"
send_mail "[ALERT] Identity anomaly su ${HOST}" "$BODY"
if [[ "$ENABLE_LOCKDOWN" == "yes" && -n "$TRUSTED_IPV4" ]]; then
SSH_PORT="$SSH_PORT" /usr/local/sbin/emergency-lockdown.sh "$TRUSTED_IPV4" "$TRUSTED_IPV6" >> "$LOGFILE" 2>&1
logger -t identity-incident-handler "SOURCE=${SOURCE} Lockdown applicato"
send_mail "[LOCKDOWN] Server isolato ${HOST}" "$(tail -n 120 "$LOGFILE")"
else
logger -t identity-incident-handler "SOURCE=${SOURCE} Lockdown NON applicato: config incompleta o disabilitata"
fi
EOF
chmod 700 /usr/local/sbin/identity-incident-handler.sh
echo "[*] Creo script alert login SSH riusciti..."
cat >/usr/local/sbin/ssh-login-alert.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
CONF="/etc/incident-response/lockdown.conf"
STATE_DIR="/var/lib/ssh-login-alert"
STATE_FILE="${STATE_DIR}/last_cursor"
LOCK_FILE="/run/ssh-login-alert.lock"
TMP_FILE="$(mktemp)"
cleanup() {
rm -f "$TMP_FILE"
}
trap cleanup EXIT
exec 9>"$LOCK_FILE"
flock -n 9 || exit 0
[ -f "$CONF" ] && . "$CONF"
ALERT_EMAIL="${ALERT_EMAIL:-}"
mkdir -p "$STATE_DIR"
chmod 700 "$STATE_DIR"
HOST="$(hostname -f 2>/dev/null || true)"
case "$HOST" in
""|localhost|localhost.localdomain) HOST="$(hostname)" ;;
esac
# Piccolo ritardo per lasciare sedimentare tutte le righe del login
sleep 2
if [[ ! -f "$STATE_FILE" ]] || [[ ! -s "$STATE_FILE" ]]; then
journalctl _COMM=sshd -n 1 --show-cursor -o short-iso 2>/dev/null \\
| sed -n 's/^-- cursor: //p' > "$STATE_FILE" || true
chmod 600 "$STATE_FILE" 2>/dev/null || true
exit 0
fi
CURSOR_OPT=(--after-cursor "$(cat "$STATE_FILE")")
journalctl _COMM=sshd -o short-iso --show-cursor "${CURSOR_OPT[@]}" > "$TMP_FILE" 2>/dev/null || true
while read -r line; do
[ -n "$line" ] || continue
TS="$(echo "$line" | awk '{print $1" "$2}')"
METHOD="$(echo "$line" | sed -n 's/.*Accepted \\([^ ]*\\).*/\1/p')"
USERNAME="$(echo "$line" | sed -n 's/.* for \\([^ ]*\\) from .*/\1/p')"
IPADDR="$(echo "$line" | sed -n 's/.* from \\([^ ]*\\) port .*/\1/p')"
logger -t ssh-login-alert "login riuscito utente=${USERNAME} metodo=${METHOD} ip=${IPADDR}"
if [[ -n "${ALERT_EMAIL:-}" ]] && command -v mail >/dev/null 2>&1; then
cat <<MAIL | mail -s "[SSH LOGIN] ${HOST} utente=${USERNAME} ip=${IPADDR}" "$ALERT_EMAIL" || true
Host: ${HOST}
Data: ${TS}
Utente: ${USERNAME}
IP: ${IPADDR}
Metodo: ${METHOD}
Riga log:
${line}
MAIL
fi
done < <(grep -E 'Accepted (publickey|password|keyboard-interactive|keyboard-interactive/pam)' "$TMP_FILE" || true)
sed -n 's/^-- cursor: //p' "$TMP_FILE" | tail -n 1 > "$STATE_FILE" || true
chmod 600 "$STATE_FILE" 2>/dev/null || true
exit 0
EOF
chmod 700 /usr/local/sbin/ssh-login-alert.sh
echo "[*] Creo watcher stack IR..."
cat >/usr/local/sbin/ir-stack-watch.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
CONF="/etc/incident-response/lockdown.conf"
LOGFILE="/var/log/ir-stack-watch.log"
[ -f "$CONF" ] && . "$CONF"
ALERT_EMAIL="${ALERT_EMAIL:-}"
HOST="$(hostname -f 2>/dev/null || true)"
case "$HOST" in
""|localhost|localhost.localdomain) HOST="$(hostname)" ;;
esac
send_mail() {
local subject="$1"
local body="$2"
if [[ -n "${ALERT_EMAIL:-}" ]] && command -v mail >/dev/null 2>&1; then
printf '%s\n' "$body" | mail -s "$subject" "$ALERT_EMAIL" || true
fi
}
{
echo "============================================================"
echo "[$(date --iso-8601=seconds)] modifica rilevata nello stack IR su ${HOST}"
echo
echo "Ultimi eventi audit ir_stack:"
ausearch -k ir_stack -ts recent 2>/dev/null || true
} >> "$LOGFILE" 2>&1
logger -t ir-stack-watch "Modifica rilevata nello stack IR"
send_mail "[IR STACK] Modifica stack difesa su ${HOST}" "$(tail -n 200 "$LOGFILE")"
EOF
chmod 700 /usr/local/sbin/ir-stack-watch.sh
echo "[*] Creo watchdog dei guardiani..."
cat >/usr/local/sbin/ir-guardians-watchdog.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
CONF="/etc/incident-response/lockdown.conf"
STATE_DIR="/var/lib/incident-response/guardians-watchdog"
LOCK_FILE="/run/ir-guardians-watchdog.lock"
[ -f "$CONF" ] && . "$CONF"
ALERT_EMAIL="${ALERT_EMAIL:-}"
mkdir -p "$STATE_DIR"
chmod 700 "$STATE_DIR"
exec 9>"$LOCK_FILE"
flock -n 9 || exit 0
HOST="$(hostname -f 2>/dev/null || true)"
case "$HOST" in
""|localhost|localhost.localdomain) HOST="$(hostname)" ;;
esac
send_mail() {
local subject="$1"
local body="$2"
if [[ -n "${ALERT_EMAIL:-}" ]] && command -v mail >/dev/null 2>&1; then
printf '%s\n' "$body" | mail -s "$subject" "$ALERT_EMAIL" || true
fi
}
now="$(date +%s)"
GUARDIANS=(
"identity-incident-handler.path"
"identity-incident-handler.timer"
"ssh-login-alert.path"
"ir-stack-watch.path"
"ir-guardians-watchdog.timer"
)
handle_guardian() {
local unit="$1"
local safe="${unit//[^A-Za-z0-9_.-]/_}"
local down_since_file="${STATE_DIR}/${safe}.down_since"
local alert_sent_file="${STATE_DIR}/${safe}.alert_sent"
local active_state failed_state
active_state="$(systemctl is-active "$unit" 2>/dev/null || true)"
failed_state="$(systemctl is-failed "$unit" 2>/dev/null || true)"
if [[ "$active_state" == "active" && "$failed_state" != "failed" ]]; then
rm -f "$down_since_file" "$alert_sent_file"
return 0
fi
if [[ ! -f "$down_since_file" ]]; then
echo "$now" > "$down_since_file"
fi
systemctl reset-failed "$unit" >/dev/null 2>&1 || true
systemctl start "$unit" >/dev/null 2>&1 || true
sleep 2
local active_state2 failed_state2
active_state2="$(systemctl is-active "$unit" 2>/dev/null || true)"
failed_state2="$(systemctl is-failed "$unit" 2>/dev/null || true)"
if [[ "$active_state2" == "active" && "$failed_state2" != "failed" ]]; then
rm -f "$down_since_file" "$alert_sent_file"
return 0
fi
local down_since down_for
down_since="$(cat "$down_since_file" 2>/dev/null || echo "$now")"
down_for=$(( now - down_since ))
if [[ "$down_for" -ge 60 && ! -f "$alert_sent_file" ]]; then
local body
body="$(
{
echo "Host: $HOST"
echo "Guardian: $unit"
echo "active=$active_state2"
echo "failed=$failed_state2"
echo "Down da almeno ${down_for} secondi"
echo
echo "===== systemctl status $unit ====="
systemctl status "$unit" --no-pager -l || true
echo
echo "===== journalctl -u $unit -n 50 ====="
journalctl -u "$unit" -n 50 --no-pager || true
}
)"
send_mail "[IR] Guardian down oltre 60s su ${HOST}: ${unit}" "$body"
touch "$alert_sent_file"
fi
}
for unit in "${GUARDIANS[@]}"; do
handle_guardian "$unit"
done
exit 0
EOF
chmod 700 /usr/local/sbin/ir-guardians-watchdog.sh
echo "[*] Hardening sshd logging..."
if grep -qE '^[#[:space:]]*LogLevel[[:space:]]+' /etc/ssh/sshd_config; then
sed -i 's/^[#[:space:]]*LogLevel[[:space:]].*/LogLevel VERBOSE/' /etc/ssh/sshd_config
else
echo 'LogLevel VERBOSE' >> /etc/ssh/sshd_config
fi
echo "[*] Configuro Postfix per relay alert + consegna locale..."
MAIL_FQDN="$(hostname -f 2>/dev/null || true)"
case "$MAIL_FQDN" in
""|localhost|localhost.localdomain) MAIL_FQDN="$(hostname)" ;;
esac
echo "$MAIL_FQDN" >/etc/mailname
postconf -e "myhostname = ${MAIL_FQDN}"
postconf -e 'myorigin = /etc/mailname'
postconf -e 'inet_interfaces = loopback-only'
postconf -e 'mynetworks = 127.0.0.0/8 [::1]/128'
postconf -e 'relayhost = ['"${RELAYHOST}"']:'"${RELAYPORT}"
postconf -e 'mydestination = $myhostname, localhost.$mydomain, localhost, $myorigin'
postconf -X local_transport || true
touch /etc/aliases
grep -q '^root:' /etc/aliases 2>/dev/null || echo 'root: root' >> /etc/aliases
newaliases || true
systemctl enable postfix
systemctl restart postfix || true
echo "[*] Creo unità systemd..."
cat >/etc/systemd/system/identity-incident-from-path.service <<'EOF'
[Unit]
Description=Identity anomaly check triggered by path change
After=network-online.target auditd.service
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/identity-incident-handler.sh path
User=root
Group=root
NoNewPrivileges=no
PrivateTmp=yes
ProtectSystem=full
ProtectHome=read-only
ReadWritePaths=/var/log /var/spool/postfix /root/firewall-backups
ProtectControlGroups=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
EOF
cat >/etc/systemd/system/identity-incident-from-timer.service <<'EOF'
[Unit]
Description=Identity anomaly check triggered by periodic timer
After=network-online.target auditd.service
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/identity-incident-handler.sh timer
User=root
Group=root
NoNewPrivileges=no
PrivateTmp=yes
ProtectSystem=full
ProtectHome=read-only
ReadWritePaths=/var/log /var/spool/postfix /root/firewall-backups
ProtectControlGroups=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
EOF
cat >/etc/systemd/system/identity-incident-manual.service <<'EOF'
[Unit]
Description=Manual identity anomaly check
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/identity-incident-handler.sh manual
User=root
Group=root
NoNewPrivileges=no
PrivateTmp=yes
ProtectSystem=full
ProtectHome=read-only
ReadWritePaths=/var/log /var/spool/postfix /root/firewall-backups
ProtectControlGroups=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
EOF
cat >/etc/systemd/system/identity-incident-handler.path <<'EOF'
[Unit]
Description=Watch identity and ssh files for critical changes
StartLimitIntervalSec=300
StartLimitBurst=40
[Path]
PathModified=/etc/passwd
PathModified=/etc/shadow
PathModified=/etc/group
PathModified=/etc/gshadow
PathModified=/etc/sudoers
PathModified=/etc/sudoers.d
PathModified=/etc/ssh/sshd_config
PathModified=/root/.ssh/authorized_keys
Unit=identity-incident-from-path.service
[Install]
WantedBy=multi-user.target
EOF
cat >/etc/systemd/system/identity-incident-handler.timer <<'EOF'
[Unit]
Description=Periodic identity anomaly check
StartLimitIntervalSec=300
StartLimitBurst=20
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
Unit=identity-incident-from-timer.service
[Install]
WantedBy=timers.target
EOF
cat >/etc/systemd/system/ssh-login-alert.service <<'EOF'
[Unit]
Description=Parse successful SSH logins and send mail alerts
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/ssh-login-alert.sh
User=root
Group=root
NoNewPrivileges=no
PrivateTmp=yes
ProtectSystem=full
ProtectHome=read-only
StateDirectory=ssh-login-alert
ProtectControlGroups=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
EOF
cat >/etc/systemd/system/ssh-login-alert.path <<'EOF'
[Unit]
Description=Watch SSH auth logs and trigger login alert parser
StartLimitIntervalSec=300
StartLimitBurst=60
[Path]
PathModified=/var/log/auth.log
Unit=ssh-login-alert.service
[Install]
WantedBy=multi-user.target
EOF
cat >/etc/systemd/system/ir-stack-watch.service <<'EOF'
[Unit]
Description=IR stack modification alert
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/ir-stack-watch.sh
User=root
Group=root
NoNewPrivileges=no
PrivateTmp=yes
ProtectSystem=full
ProtectHome=read-only
ReadWritePaths=/var/log /var/spool/postfix
ProtectControlGroups=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
EOF
cat >/etc/systemd/system/ir-stack-watch.path <<'EOF'
[Unit]
Description=Watch IR ecosystem files for modifications
StartLimitIntervalSec=300
StartLimitBurst=40
[Path]
PathModified=/usr/local/sbin/check-identity-anomalies.sh
PathModified=/usr/local/sbin/emergency-lockdown.sh
PathModified=/usr/local/sbin/emergency-lockdown-rollback.sh
PathModified=/usr/local/sbin/identity-incident-handler.sh
PathModified=/usr/local/sbin/ssh-login-alert.sh
PathModified=/usr/local/sbin/ir-stack-watch.sh
PathModified=/usr/local/sbin/ir-guardians-watchdog.sh
PathModified=/usr/local/sbin/irctl
PathModified=/etc/incident-response/lockdown.conf
PathModified=/etc/incident-response/identity-shell-whitelist
PathModified=/etc/systemd/system/identity-incident-from-path.service
PathModified=/etc/systemd/system/identity-incident-from-timer.service
PathModified=/etc/systemd/system/identity-incident-manual.service
PathModified=/etc/systemd/system/identity-incident-handler.path
PathModified=/etc/systemd/system/identity-incident-handler.timer
PathModified=/etc/systemd/system/ssh-login-alert.service
PathModified=/etc/systemd/system/ssh-login-alert.path
PathModified=/etc/systemd/system/ir-stack-watch.service
PathModified=/etc/systemd/system/ir-stack-watch.path
PathModified=/etc/systemd/system/ir-guardians-watchdog.service
PathModified=/etc/systemd/system/ir-guardians-watchdog.timer
Unit=ir-stack-watch.service
[Install]
WantedBy=multi-user.target
EOF
cat >/etc/systemd/system/ir-guardians-watchdog.service <<'EOF'
[Unit]
Description=Incident Reporting guardians watchdog
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/ir-guardians-watchdog.sh
User=root
Group=root
NoNewPrivileges=no
PrivateTmp=yes
ProtectSystem=full
ProtectHome=read-only
ReadWritePaths=/var/lib/incident-response /var/spool/postfix /run
ProtectControlGroups=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
EOF
cat >/etc/systemd/system/ir-guardians-watchdog.timer <<'EOF'
[Unit]
Description=Run Incident Reporting guardians watchdog every 10 seconds
StartLimitIntervalSec=300
StartLimitBurst=20
[Timer]
OnBootSec=20s
OnUnitActiveSec=10s
AccuracySec=1s
Unit=ir-guardians-watchdog.service
[Install]
WantedBy=timers.target
EOF
echo "[*] Abilito auditd..."
systemctl enable --now auditd
echo "[*] Carico regole audit..."
augenrules --load || true
echo "[*] Ricarico systemd..."
systemctl daemon-reload
echo "[*] Rimuovo vecchia unità legacy se presente..."
systemctl disable --now identity-incident-handler.service 2>/dev/null || true
rm -f /etc/systemd/system/identity-incident-handler.service
echo "[*] Abilito unità nuove..."
systemctl enable --now identity-incident-handler.path
systemctl enable --now identity-incident-handler.timer
systemctl enable --now ssh-login-alert.path
systemctl enable --now ir-stack-watch.path
systemctl enable --now ir-guardians-watchdog.timer
echo "[*] Reload ssh..."
systemctl reload ssh 2>/dev/null || systemctl reload sshd 2>/dev/null || true
echo "[*] Creo utility irctl..."
cat >/usr/local/sbin/irctl <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
PATH_SERVICE="identity-incident-from-path.service"
TIMER_SERVICE="identity-incident-from-timer.service"
MANUAL_SERVICE="identity-incident-manual.service"
PATH_UNIT="identity-incident-handler.path"
TIMER_UNIT="identity-incident-handler.timer"
SSH_ALERT_PATH="ssh-login-alert.path"
SSH_ALERT_SERVICE="ssh-login-alert.service"
IR_STACK_PATH="ir-stack-watch.path"
IR_STACK_SERVICE="ir-stack-watch.service"
WATCHDOG_SERVICE="ir-guardians-watchdog.service"
WATCHDOG_TIMER="ir-guardians-watchdog.timer"
show_status() {
echo "==================== STATUS ===================="
systemctl status "$PATH_UNIT" --no-pager || true
echo
systemctl status "$TIMER_UNIT" --no-pager || true
echo
systemctl status "$PATH_SERVICE" --no-pager || true
echo
systemctl status "$TIMER_SERVICE" --no-pager || true
echo
systemctl status "$MANUAL_SERVICE" --no-pager || true
echo
systemctl status "$SSH_ALERT_PATH" --no-pager || true
echo
systemctl status "$SSH_ALERT_SERVICE" --no-pager || true
echo
systemctl status "$IR_STACK_PATH" --no-pager || true
echo
systemctl status "$IR_STACK_SERVICE" --no-pager || true
echo
systemctl status "$WATCHDOG_TIMER" --no-pager || true
echo
systemctl status "$WATCHDOG_SERVICE" --no-pager || true
}
show_audit() {
echo "==================== AUDIT RULES ===================="
auditctl -l || true
echo
echo "==================== AUSEARCH: identity ===================="
ausearch -k identity || true
echo
echo "==================== AUSEARCH: privilege ===================="
ausearch -k privilege || true
echo
echo "==================== AUSEARCH: sshkeys ===================="
ausearch -k sshkeys || true
echo
echo "==================== AUSEARCH: user_mgmt ===================="
ausearch -k user_mgmt || true
echo
echo "==================== AUSEARCH: passwd_mgmt ===================="
ausearch -k passwd_mgmt || true
echo
echo "==================== AUSEARCH: sudo_change ===================="
ausearch -k sudo_change || true
echo
echo "==================== AUSEARCH: ir_stack ===================="
ausearch -k ir_stack || true
}
show_logs() {
echo "==================== JOURNAL: identity-incident-handler ===================="
journalctl -t identity-incident-handler -n 50 --no-pager || true
echo
echo "==================== JOURNAL: ssh-login-alert ===================="
journalctl -t ssh-login-alert -n 50 --no-pager || true
echo
echo "==================== JOURNAL: ir-stack-watch ===================="
journalctl -t ir-stack-watch -n 50 --no-pager || true
echo
echo "==================== SERVICE LOG: PATH ===================="
journalctl -u "$PATH_SERVICE" -n 30 --no-pager || true
echo
echo "==================== SERVICE LOG: TIMER ===================="
journalctl -u "$TIMER_SERVICE" -n 30 --no-pager || true
echo
echo "==================== SERVICE LOG: MANUAL ===================="
journalctl -u "$MANUAL_SERVICE" -n 30 --no-pager || true
echo
echo "==================== SERVICE LOG: SSH ALERT ===================="
journalctl -u "$SSH_ALERT_SERVICE" -n 30 --no-pager || true
echo
echo "==================== SERVICE LOG: IR STACK ===================="
journalctl -u "$IR_STACK_SERVICE" -n 30 --no-pager || true
echo
echo "==================== SERVICE LOG: WATCHDOG ===================="
journalctl -u "$WATCHDOG_SERVICE" -n 30 --no-pager || true
}
run_checker() {
echo "==================== CHECK-IDENTITY ===================="
/usr/local/sbin/check-identity-anomalies.sh || true
}
test_path() {
echo "==================== TEST PATH ===================="
systemctl stop "$TIMER_UNIT" || true
touch /etc/passwd
sleep 2
journalctl -u "$PATH_SERVICE" -n 20 --no-pager || true
systemctl start "$TIMER_UNIT" || true
}
test_manual() {
echo "==================== TEST MANUAL ===================="
systemctl start "$MANUAL_SERVICE"
journalctl -u "$MANUAL_SERVICE" -n 20 --no-pager || true
}
test_ssh_alert() {
echo "==================== TEST SSH ALERT ===================="
systemctl start "$SSH_ALERT_SERVICE"
journalctl -u "$SSH_ALERT_SERVICE" -n 20 --no-pager || true
}
test_ir_stack() {
echo "==================== TEST IR STACK ===================="
touch /usr/local/sbin/identity-incident-handler.sh
sleep 2
journalctl -u "$IR_STACK_SERVICE" -n 20 --no-pager || true
}
test_watchdog() {
echo "==================== TEST WATCHDOG ===================="
systemctl start "$WATCHDOG_SERVICE"
journalctl -u "$WATCHDOG_SERVICE" -n 30 --no-pager || true
}
show_all() {
show_status
echo
run_checker
echo
show_logs
}
usage() {
cat <<'EOH'
Uso: irctl <comando>
Comandi:
status Mostra stato unità systemd principali
audit Mostra regole audit e ricerche ausearch
logs Mostra log utili
check Esegue check-identity-anomalies.sh
test-path Test rapido del trigger path
test-manual Test rapido del trigger manuale
test-ssh Test rapido parser alert login SSH
test-ir Test rapido watcher stack IR
test-watch Test rapido watchdog
all Mostra status + check + logs
help Mostra questo aiuto
EOH
}
cmd="${1:-help}"
case "$cmd" in
status) show_status ;;
audit) show_audit ;;
logs) show_logs ;;
check) run_checker ;;
test-path) test_path ;;
test-manual) test_manual ;;
test-ssh) test_ssh_alert ;;
test-ir) test_ir_stack ;;
test-watch) test_watchdog ;;
all) show_all ;;
help|-h|--help) usage ;;
*)
echo "Comando non riconosciuto: $cmd" >&2
usage
exit 1
;;
esac
EOF
chmod 700 /usr/local/sbin/irctl
echo "[*] Creo alias ir..."
cat >/etc/profile.d/irctl.sh <<'EOF'
alias ir='/usr/local/sbin/irctl'
EOF
chmod 644 /etc/profile.d/irctl.sh
echo "[*] Test iniziali..."
/usr/local/sbin/check-identity-anomalies.sh || true
mkdir -p /var/lib/ssh-login-alert
journalctl -u ssh -u sshd -n 1 --show-cursor -o short-iso 2>/dev/null | sed -n 's/^-- cursor: //p' > /var/lib/ssh-login-alert/last_cursor || true
chmod 700 /var/lib/ssh-login-alert
chmod 600 /var/lib/ssh-login-alert/last_cursor 2>/dev/null || true
systemctl start ssh-login-alert.service || true
systemctl start ir-stack-watch.service || true
systemctl start ir-guardians-watchdog.service || true
echo
echo "[OK] Installazione completata."
echo "Config: /etc/incident-response/lockdown.conf"
echo "Whitelist: /etc/incident-response/identity-shell-whitelist"
echo
echo "Verifiche utili:"
echo " auditctl -l"
echo " systemctl status identity-incident-handler.path"
echo " systemctl status identity-incident-handler.timer"
echo " systemctl status ssh-login-alert.path"
echo " systemctl status ir-guardians-watchdog.timer"
echo " journalctl -u identity-incident-from-path.service -n 20 --no-pager"
echo " journalctl -u identity-incident-from-timer.service -n 20 --no-pager"
echo " journalctl -u ssh-login-alert.service -n 20 --no-pager"
echo " journalctl -u ir-guardians-watchdog.service -n 20 --no-pager"
echo " ausearch -k identity"
echo " ausearch -k ir_stack"
Verifica dopo l’installazione
irctl status
irctl check
irctl audit
irctl logs
systemctl status identity-incident-handler.path
systemctl status identity-incident-handler.timer
systemctl status ssh-login-alert.path
systemctl status ir-stack-watch.path
systemctl status ir-guardians-watchdog.timer
auditctl -l
Test senza provocare un incidente vero
Lo stack contiene test espliciti proprio per evitare di “provare” la protezione creando un utente root fasullo o modificando chiavi reali. Possiamo forzare il controllo manuale, il path watcher, il parser SSH, il watcher dello stack e il guardian separatamente.
irctl test-manual
irctl test-path
irctl test-ssh
irctl test-ir
irctl test-watch
Come leggere un alert
Una segnalazione non è una sentenza. La sequenza corretta è: identificare il file o l’account interessato; cercare l’evento audit; verificare processi e utente; controllare se esisteva una manutenzione autorizzata; correlare SSH, sudo, package manager e deploy; solo a quel punto decidere se aggiornare la baseline, ripristinare un file oppure aprire un incidente.
Questo è il punto centrale dell’IR Stack: non cerca di rendere il server magicamente invulnerabile. Cerca di fare in modo che, quando qualcosa cambia, non dobbiamo ricostruire la storia partendo dal buio.

