Un server che gira da giorni senza che nessuno lo guardi è un server di cui non sai niente. Per non dover aprire il terminale ogni volta, ho scritto un piccolo plugin per il mio bot IRC: basta digitare !server o !log nel canale per vedere come sta la VPS e cosa sta succedendo ai suoi log di sicurezza. Ogni quattro ore, inoltre, il bot pubblica il report da solo. In questo articolo spiego come funziona, con gli estratti di codice più interessanti, e quali scelte ho fatto (e quali rifarei).

⚡ Cosa fa il plugin

  • !server: sistema operativo, uptime, CPU, carico, RAM e spazio su disco.
  • !log: eventi di accesso SSH falliti nelle ultime 24 ore e codici 403, 404 e 444 di Nginx nelle ultime 5.000 richieste.
  • Report automatico: ogni 4 ore il bot pubblica entrambi i messaggi nel canale.
  • Nessuna libreria esterna: legge tutto dal sistema Linux (/proc, journalctl e il log di Nginx).

1. Come appare nel canale

Ecco un esempio reale di cosa risponde il bot:

<ViRuS> !server
<ALTaiR> 📊 [VPS Status] OS: Linux | Uptime: 6g 8h | CPU: 9.5% | Load: 0.06 0.26 0.37 | RAM: 1.43GB/7.75GB (18.5%) | Disk: 13.06GB/144.26GB (9.1%)

<ViRuS> !log
<ALTaiR> 🔒 [Security Log] SSH Failures (24h): 4425 | Nginx (ultime 5k req) - 403: 318 | 404: 213 | 444: 87

In una sola riga si vede se il server è sano: carico basso, memoria e disco con molto margine, e il rumore di fondo di internet nei log.

2. Leggere le risorse senza installare nulla

Su Linux le informazioni sul sistema sono già disponibili come file di testo nella cartella /proc. Basta leggerli e fare due conti, senza librerie come psutil.

La CPU: due letture a confronto

Il file /proc/stat contiene contatori che crescono da quando il sistema è acceso. Un solo valore non dice nulla: serve leggerlo due volte a breve distanza e vedere quanto tempo è stato passato a riposo.

# versione semplificata
with open("/proc/stat") as f:
    values = [int(x) for x in f.readline().split()[1:]]
idle, total = values[3], sum(values)

time.sleep(0.1)          # una breve pausa

# ... seconda lettura: idle2, total2 ...
cpu_pct = (1.0 - (idle2 - idle) / (total2 - total)) * 100

La RAM: usare MemAvailable

Su Linux la memoria “libera” sembra sempre poca, perché il sistema usa quella inutilizzata come cache. Il valore giusto da guardare è MemAvailable in /proc/meminfo, che stima quanta memoria è davvero disponibile per i programmi.

used = total - available        # available = MemAvailable
ram_pct = used / total * 100

Il resto in poche righe

  • Uptime: il primo numero di /proc/uptime, convertito in giorni e ore.
  • Carico: os.getloadavg() restituisce la media a 1, 5 e 15 minuti.
  • Disco: shutil.disk_usage("/").

3. Contare gli attacchi dai log

SSH, con journalctl

Il plugin chiede a journalctl i messaggi del servizio SSH delle ultime 24 ore (provando prima ssh e poi sshd, perché il nome cambia da una distribuzione all’altra) e conta le righe che contengono segnali tipici di un accesso fallito.

res = subprocess.run(
    ["journalctl", "-u", service, "--since", "24 hours ago", "--no-pager"],
    capture_output=True, text=True, timeout=8)

for line in res.stdout.splitlines():
    if any(k in line for k in ("Failed password", "Invalid user",
                               "Authentication failure", "[preauth]")):
        count += 1

🔎 Un dettaglio importante: righe, non tentativi

Questo metodo conta le righe di log che corrispondono, non i tentativi singoli: un solo tentativo può generare più righe (per esempio “Invalid user”, poi “Failed password”, poi “Connection closed [preauth]”). Il numero che il bot mostra è quindi un indicatore dell’intensità del rumore, non il conteggio esatto dei tentativi. Per un controllo di sicurezza va benissimo, ma è giusto saperlo.

Nginx, leggendo solo la coda del file

Il log di Nginx può essere enorme, quindi il plugin ne legge solo le ultime 5.000 righe con una deque a lunghezza massima, che scarta man mano le righe vecchie senza caricare tutto in memoria. Poi estrae il codice di risposta, che nel formato standard compare subito dopo la richiesta tra virgolette.

with open(NGINX_LOG_PATH, "r", errors="ignore") as f:
    lines = deque(f, maxlen=5000)

for line in lines:
    parts = line.split('"')
    code = parts[2].strip().split()[0]    # es. "200", "404", "444"
    ...

4. Non bloccare il bot

Il bot è basato su Twisted, che gestisce tutto in un unico ciclo di eventi. Se un comando impiega qualche secondo (journalctl può essere lento, e il calcolo della CPU include una pausa), l’intero bot resterebbe fermo, senza rispondere né ricevere nulla. Per evitarlo, il lavoro pesante viene spostato in un thread separato e la risposta viene inviata solo quando è pronta:

d = threads.deferToThread(_async_server)   # lavoro in un thread
d.addCallback(_callback)                   # poi invia la risposta al canale

Per il report periodico c’è un thread dedicato che aspetta con Event.wait(timeout=...) invece di un semplice sleep: così, quando il plugin viene scaricato, il thread si ferma subito invece di restare bloccato fino alla fine dell’attesa. Per scrivere in canale da quel thread si usa reactor.callFromThread, l’unico modo sicuro per parlare con Twisted da fuori.

5. Le scelte di sicurezza

  • Chi può usare i comandi: c’è un’impostazione (RESTRICT_TO_ADMINS) per riservare !server e !log solo agli amministratori. Io li ho lasciati aperti, perché le informazioni mostrate non rivelano indirizzi né configurazioni, ma è una decisione da prendere in base al proprio canale.
  • Cosa NON mostrare mai: indirizzi IP del server o degli attaccanti, nomi utente validi, porte dei servizi o percorsi di configurazione. Il report contiene solo numeri aggregati.
  • Permessi minimi: per leggere i log il bot ha bisogno di permessi di lettura (per esempio l’appartenenza ai gruppi che accedono al journal e ai log di Nginx). Non serve, e non è consigliato, farlo girare come root.
  • Gli errori non fanno danni: se un dato non è leggibile, il plugin mostra “N/D” invece di bloccarsi.

6. Cosa migliorerei

  • Contare i tentativi, non le righe: raggruppando per indirizzo IP e per tentativo si avrebbe un numero più preciso, con in più il conteggio degli IP unici.
  • Leggere l’architettura vera: ora la riga del sistema operativo mostra un’architettura scritta a mano; si può sostituire con il valore reale che restituisce platform.uname().machine.
  • Una soglia d’allarme: avvisare nel canale solo se il carico, il disco o gli errori superano un limite, invece di mostrare sempre tutto.
  • Un controllo degli accessi riusciti: i fallimenti sono rumore, mentre un accesso riuscito da un indirizzo nuovo è la notizia vera.
  • Un grafico nel tempo: salvare i valori e mostrarli sul sito.

Conclusione

Un monitor di sistema non deve essere complicato: poche letture da /proc, un po’ di conteggi sui log e un bot che già c’è. Il risultato è un controllo quotidiano gratuito, visibile a colpo d’occhio dalla chat, che aiuta a notare subito se qualcosa cambia. E scrivere uno strumento su misura è anche il modo migliore per capire davvero cosa succede dentro un server.


Articolo a cura della Redazione di rfnode.net