ICINGA2 - Installation, configuration et maintenance

L'outil exploitĂ© par Chezlepro.CA est ICINGA2, bonifiĂ© d'un module nommĂ© "Business Process Module" ou BPM.  Ce dernier permet de concevoir des vues orientĂ©es sur la prestation des services, ce qui compte le plus pour Chezlepro.CA, ses partenaires, ainsi que leurs clients.
apt-get update
apt-get -y install apt-transport-https wget gnupg
wget -O - https://packages.icinga.com/icinga.key | apt-key add -

. /etc/os-release; if [ ! -z ${UBUNTU_CODENAME+x} ]; then DIST="${UBUNTU_CODENAME}"; else DIST="$(lsb_release -c| awk '{print $2}')"; fi;  \
echo "deb https://packages.icinga.com/ubuntu icinga-${DIST} main" >  /etc/apt/sources.list.d/${DIST}-icinga.list
echo "deb-src https://packages.icinga.com/ubuntu icinga-${DIST} main" >>  /etc/apt/sources.list.d/${DIST}-icinga.list

apt-get update
apt-get install icinga2
icinga2 daemon -C
apt-get install monitoring-plugins

AJOUT DU REGISTRE UBUNTU ET INSTALLATION DU MODULE

apt-get update
apt-get -y install apt-transport-https wget gnupg
wget -O - https://packages.icinga.com/icinga.key | apt-key add -
. /etc/os-release; if [ ! -z ${UBUNTU_CODENAME+x} ]; then DIST="${UBUNTU_CODENAME}"; else DIST="$(lsb_release -c| awk '{print $2}')"; fi; \
 echo "deb https://packages.icinga.com/ubuntu icinga-${DIST} main" > \
 /etc/apt/sources.list.d/${DIST}-icinga.list
 echo "deb-src https://packages.icinga.com/ubuntu icinga-${DIST} main" >> \
 /etc/apt/sources.list.d/${DIST}-icinga.list
apt-get update

apt-get install icingadb

CRÉER UNE BASE DE DONNÉES

mysql -u root -p

CREATE DATABASE icingadb;
CREATE USER 'icingadb'@'localhost' IDENTIFIED BY 'CHANGEME';
GRANT ALL ON icingadb.* TO 'icingadb'@'localhost';

mysql -u root -p icingadb </usr/share/icingadb/schema/mysql/schema.sql

systemctl enable --now icingadb

Ref.: https://icinga.com/docs/icinga-db/latest/doc/02-Installation/06-Ubuntu/

INSTALLATION DU MODULE

apt-get install icingadb-web

Suivre ce lien pour les instructions :
PRÉ-REQUIS :


apt-get update
apt-get -y install apt-transport-https wget gnupg
wget -O - https://packages.icinga.com/icinga.key | apt-key add -
. /etc/os-release; if [ ! -z ${UBUNTU_CODENAME+x} ]; then DIST="${UBUNTU_CODENAME}"; else DIST="$(lsb_release -c| awk '{print $2}')"; fi; \
 echo "deb https://packages.icinga.com/ubuntu icinga-${DIST} main" > \
 /etc/apt/sources.list.d/${DIST}-icinga.list
 echo "deb-src https://packages.icinga.com/ubuntu icinga-${DIST} main" >> \
 /etc/apt/sources.list.d/${DIST}-icinga.list
apt-get update

apt-get install icingaweb2 libapache2-mod-php icingacli


Ref.: https://icinga.com/docs/icinga-web/latest/doc/02-Installation/02-Ubuntu/
Successfully connected to existing database "icingaweb2"...
Creating database schema...
Login "icingaweb2" already exists...
Required privileges were already granted to login "icingaweb2".
The database has been fully set up!
General configuration has been successfully written to: /etc/icingaweb2/config.ini
Authentication configuration has been successfully written to: /etc/icingaweb2/authentication.ini
Account "admin" has been successfully created.
Account "admin" has been successfully defined as initial administrator.
User Group Backend configuration has been successfully written to: /etc/icingaweb2/groups.ini
User Group "Administrators" has been successfully created.
Account "admin" has been successfully added as member to user group "Administrators".
Resource configuration has been successfully written to: /etc/icingaweb2/resources.ini
Monitoring backend configuration has been successfully written to: /etc/icingaweb2/modules/monitoring/backends.ini
Resource configuration has been successfully updated: /etc/icingaweb2/resources.ini
Command transport configuration has been successfully created: /etc/icingaweb2/modules/monitoring/commandtransports.ini
Monitoring security configuration has been successfully created: /etc/icingaweb2/modules/monitoring/config.ini
Module "doc" has been successfully enabled.
Module "monitoring" has been successfully enabled.

AJOUT DU REGISTRE UBUNTU ET INSTALLATION DU MODULE

apt update
apt -y install apt-transport-https wget gnupg
wget -O - https://packages.icinga.com/icinga.key | gpg --dearmor -o /usr/share/keyrings/icinga-archive-keyring.gpg
. /etc/os-release; if [ ! -z ${UBUNTU_CODENAME+x} ]; then DIST="${UBUNTU_CODENAME}"; else DIST="$(lsb_release -c| awk '{print $2}')"; fi; \
 echo "deb [signed-by=/usr/share/keyrings/icinga-archive-keyring.gpg] https://packages.icinga.com/ubuntu icinga-${DIST} main" > \
 /etc/apt/sources.list.d/${DIST}-icinga.list
 echo "deb-src [signed-by=/usr/share/keyrings/icinga-archive-keyring.gpg] https://packages.icinga.com/ubuntu icinga-${DIST} main" >> \
 /etc/apt/sources.list.d/${DIST}-icinga.list
apt update

apt install icinga-director


CRÉER UNE BASE DE DONNÉES

mysql -e "CREATE DATABASE director CHARACTER SET 'utf8';
  CREATE USER director@localhost IDENTIFIED BY 'CHANGEME';
  GRANT ALL ON director.* TO director@localhost;"



CONFIGURER LE MODULE DANS ICINGA2

Se connecter en admin sur l'interface de Icinga Web 2
Créez une nouvelle ressource pour la base de données Icinga Director :
Configuration → Application → Ressources (configurer utf8 comme encodage) SĂ©lectionner Icinga Director du menu principal pour ĂȘtre redirigĂ© vers l'assistant de dĂ©marrage
Suivre les instructions et le tour est joué !

Ref. : https://icinga.com/docs/icinga-director/latest/doc/02-Installation/Ubuntu/

SAUVEGARDES

MISES À JOUR

STOCKAGE

MÉMOIRE

CHARGE CPU

VÉRIFICATION DES MISES À JOUR

CERTIFICATS TLS

DHCP

DNS

FAIL2BAN

SSH




Les sondes

Objectif

Standardiser et alléger les sondes Icinga déployées sur toutes les VM en remplaçant les scripts Python par des binaires Zig statiques, rapides au démarrage, avec une approche hybride :

  • Multicall “core” : un seul binaire (znagios) sĂ©lectionne la sonde via argv[0] (symlink) ou la 1re sous-commande.

  • Binaires dĂ©diĂ©s : pour les sondes lourdes (TLS/HTTP complet, DNS, SNMP, DB) afin de ne pas enfler le core.


Pourquoi Zig ici

  • Cold‑start en ms, RSS minuscule, statique musl ⇒ pas de dĂ©pendances.

  • Pas de GC / pas de runtime ⇒ latence prĂ©visible.

  • Cross‑compile simple (x86_64/ARM, Linux musl).

  • Interop C si besoin (ABI stable pour extensions).

Note perf : en mode multicall, le noyau ne charge que les pages exĂ©cutĂ©es. Les autres sondes, bien que prĂ©sentes dans l’ELF, ne sont pas rĂ©sidentes.


Architecture

                          /opt/icinga-checks/
  znagios                 # binaire multicall (ReleaseSmall, statique musl)
  check_load  -> znagios  # symlink
  check_mem   -> znagios  # symlink
  check_disk  -> znagios  # symlink
  check_tcp   -> znagios  # symlink
  # lourds (si requis sur l’hîte)
  check_http              # binaire dédié (TLS, redirects, SNI
)
  check_dns               # binaire dédié

                        

Contrat Nagios/Icinga :

  • Exit codes : 0 OK · 1 WARNING · 2 CRITICAL · 3 UNKNOWN.

  • Sortie : une ligne + perfdata aprĂšs | (label=value[UOM];warn;crit;min;max).


Sondes “core” (dans znagios)

  • load : parse /proc/loadavg, seuils sur load1.

  • mem : parse /proc/meminfo ⇒ % used = Total − Free − Buffers − Cached − SReclaimable.

  • disk : statvfs() sur un mountpoint (MiB ou %).

  • tcp : connexion TCP avec timeout ms (latence + code).

  • file-age : Ăąge d’un fichier/journal en s (use-case : fraĂźcheur CRL, dumps, exports).

  • systemd-active : statut d’une unit (via systemctl is-active ou D‑Bus plus tard).

RĂšgle d’or : communes, courtes, sans piles rĂ©seau complexes ⇒ dans le multicall. RĂ©seaux/crypto/parsing riche ⇒ binaire dĂ©diĂ©.


Exemple de squelette multicall (Zig)

Minimum viable pour load et mem. Étendre avec disk, tcp, etc.

                          const std = @import("std");

const OK: u8 = 0; const WARN: u8 = 1; const CRIT: u8 = 2; const UNK: u8 = 3;
fn out(status: []const u8, code: u8, fmt: []const u8, args: anytype) noreturn {
    const w = std.io.getStdOut().writer();
    w.print("{s} - ", .{status}) catch {}; w.print(fmt, args) catch {}; w.print("\n", .{}) catch {};
    std.process.exit(code);
}

fn f64Or(s: []const u8, def: f64) f64 { return std.fmt.parseFloat(f64, s) catch def; }
fn u64Or(s: []const u8, def: u64) u64 { return std.fmt.parseUnsigned(u64, s, 10) catch def; }

fn check_load(args: [][]const u8) noreturn {
    var warn: f64 = 2.0; var crit: f64 = 4.0;
    for (args) |a| {
        if (std.mem.startsWith(u8, a, "--warn=")) warn = f64Or(a[7..], warn)
        else if (std.mem.startsWith(u8, a, "--crit=")) crit = f64Or(a[7..], crit)
        else if (std.mem.eql(u8, a, "--help")) out("UNKNOWN", UNK, "Usage: [--warn=2.0] [--crit=4.0]", .{});
    }
    var f = std.fs.cwd().openFile("/proc/loadavg", .{ .read = true }) catch { out("UNKNOWN", UNK, "cannot open /proc/loadavg", .{}); }; defer f.close();
    var buf: [128]u8 = undefined; const n = f.read(&buf) catch 0; const line = buf[0..n];
    var t = std.mem.tokenizeAny(u8, line, " \t\n");
    const l1 = std.fmt.parseFloat(f64, t.next() orelse "0") catch 0.0;
    const l5 = std.fmt.parseFloat(f64, t.next() orelse "0") catch 0.0;
    const l15= std.fmt.parseFloat(f64, t.next() orelse "0") catch 0.0;
    var code: u8 = OK; var txt: []const u8 = "OK";
    if (l1 >= crit) { code = CRIT; txt = "CRITICAL"; } else if (l1 >= warn) { code = WARN; txt = "WARNING"; }
    out(txt, code, "1m={d:.2}, 5m={d:.2}, 15m={d:.2} | load1={d:.2};{d:.2};{d:.2};0; load5={d:.2};{d:.2};{d:.2};0; load15={d:.2};{d:.2};{d:.2};0;",
        .{ l1, l5, l15, l1, warn, crit, l5, warn, crit, l15, warn, crit });
}

fn check_mem(args: [][]const u8) noreturn {
    var warn: u8 = 80; var crit: u8 = 90;
    for (args) |a| { if (std.mem.startsWith(u8,a,"--warn=")) warn = @intCast(u8, u64Or(a[7..], warn));
                      else if (std.mem.startsWith(u8,a,"--crit=")) crit = @intCast(u8, u64Or(a[7..], crit));
                      else if (std.mem.eql(u8, a, "--help")) out("UNKNOWN", UNK, "Usage: [--warn=80] [--crit=90]", .{}); }
    var f = std.fs.cwd().openFile("/proc/meminfo", .{ .read = true }) catch { out("UNKNOWN", UNK, "cannot open /proc/meminfo", .{}); }; defer f.close();
    var buf: [4096]u8 = undefined; const n = f.read(&buf) catch 0; const txt = buf[0..n];
    var total:u64=0; var free_:u64=0; var buffers:u64=0; var cached:u64=0; var srecl:u64=0;
    var it = std.mem.tokenizeAny(u8, txt, "\n");
    while (it.next()) |line| {
        if (std.mem.startsWith(u8, line, "MemTotal:"))      total   = u64Or(std.mem.trim(u8, line[9..],  " \tkBK:"), 0)
        else if (std.mem.startsWith(u8, line, "MemFree:"))   free_   = u64Or(std.mem.trim(u8, line[8..],  " \tkBK:"), 0)
        else if (std.mem.startsWith(u8, line, "Buffers:"))   buffers = u64Or(std.mem.trim(u8, line[8..],  " \tkBK:"), 0)
        else if (std.mem.startsWith(u8, line, "Cached:"))    cached  = u64Or(std.mem.trim(u8, line[7..],  " \tkBK:"), 0)
        else if (std.mem.startsWith(u8, line, "SReclaimable:")) srecl= u64Or(std.mem.trim(u8, line[13..], " \tkBK:"), 0);
    }
    if (total == 0) out("UNKNOWN", UNK, "no MemTotal in /proc/meminfo", .{});
    const used_kb = total - free_ - buffers - cached - srecl;
    const pct = @intCast(u8, @divTrunc(used_kb * 100, total));
    const used_mib: u64 = used_kb / 1024; const total_mib: u64 = total / 1024;
    var code: u8 = OK; var txt: []const u8 = "OK"; if (pct >= crit) { code = CRIT; txt = "CRITICAL"; } else if (pct >= warn) { code = WARN; txt = "WARNING"; }
    out(txt, code, "mem_used={d}% ({d}MiB/{d}MiB) | mem_used_pct={d}%;{d};{d};0;100 mem_used={d}MiB;;;0;{d}", .{ pct, used_mib, total_mib, pct, warn, crit, used_mib, total_mib });
}

fn dispatch(cmd: []const u8, args: [][]const u8) noreturn {
    if (std.mem.eql(u8, cmd, "check_load") or std.mem.eql(u8, cmd, "load")) check_load(args);
    if (std.mem.eql(u8, cmd, "check_mem")  or std.mem.eql(u8, cmd, "mem"))  check_mem(args);
    out("UNKNOWN", UNK, "unknown command '{s}' (try: load, mem)", .{cmd});
}

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit();
    const A = arena.allocator();
    var it = std.process.argsWithAllocator(A) catch @panic("args oom"); defer it.deinit();
    const argv0 = it.next() orelse "znagios"; const base = std.fs.path.basename(argv0);
    var args = std.ArrayList([]const u8).init(A); defer args.deinit();
    while (it.next()) |a| try args.append(a);
    if (std.mem.startsWith(u8, base, "check_")) dispatch(base, args.items);
    if (args.items.len == 0) out("UNKNOWN", UNK, "Usage: znagios <load|mem> [options]", .{});
    dispatch(args.items[0], args.items[1..]);
}

                        

Build & cross‑compile

Binaire statique musl, taille mini.

                          # Linux x86_64
zig build-exe -O ReleaseSmall -target x86_64-linux-musl -fstrip src/znagios.zig -o znagios

# ARM (Raspberry/edge)
zig build-exe -O ReleaseSmall -target aarch64-linux-musl -fstrip src/znagios.zig -o znagios-arm64

                        

Optionnel (profilage taille) : LTO est implicite dans ReleaseSmall ; garder le code simple (pas d’exceptions, pas d’alloc implicite).


Installation & symlinks

                          install -d /opt/icinga-checks
install -m 0755 znagios /opt/icinga-checks/
ln -sf /opt/icinga-checks/znagios /opt/icinga-checks/check_load
ln -sf /opt/icinga-checks/znagios /opt/icinga-checks/check_mem
# idem pour check_disk, check_tcp, file-age, systemd-active

                        

Intégration Icinga2

CheckCommand :

                          object CheckCommand "zig_check_load" {
  command = [ "/opt/icinga-checks/check_load" ]
  arguments = {
    "--warn=" = "$load_warn$"
    "--crit=" = "$load_crit$"
  }
  vars.load_warn = "2.0"
  vars.load_crit = "4.0"
}

object CheckCommand "zig_check_mem" {
  command = [ "/opt/icinga-checks/check_mem" ]
  arguments = {
    "--warn=" = "$mem_warn$"
    "--crit=" = "$mem_crit$"
  }
  vars.mem_warn = "80"
  vars.mem_crit = "90"
}

                        

Apply :

                          apply Service "load" {
  check_command = "zig_check_load"
  assign where host.vars.os == "Linux"
}

apply Service "mem" {
  check_command = "zig_check_mem"
  assign where host.vars.os == "Linux"
}

                        

Déploiement (Ansible)

                          - name: Deploy Zig Icinga checks
  hosts: linux_vms
  become: true
  tasks:
    - file: { path: /opt/icinga-checks, state: directory, mode: "0755" }
    - copy: { src: files/znagios, dest: /opt/icinga-checks/znagios, mode: "0755" }
    - file: { src: /opt/icinga-checks/znagios, dest: /opt/icinga-checks/check_load, state: link }
    - file: { src: /opt/icinga-checks/znagios, dest: /opt/icinga-checks/check_mem,  state: link }
    - command: systemctl reload icinga2

                        

Hardening (systemd, si un check tourne en daemon)

La plupart des checks sont one‑shot. Si un mini‑daemon est requis (ex. exporter Prometheus), durcir :

                          [Unit]
Description=Zig mini-agent (example)
After=network-online.target

[Service]
ExecStart=/opt/icinga-checks/znagios exporter --port=9100
User=agent
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
MemoryMax=32M
LockPersonality=yes
CapabilityBoundingSet=
AmbientCapabilities=
IPAddressDeny=any

[Install]
WantedBy=multi-user.target

                        

Pour une sonde ICMP (ping), préférer un binaire dédié avec AmbientCapabilities=CAP_NET_RAWseulement si nécessaire.


Packaging .deb (optionnel)

                          fpm -s dir -t deb -n zig-icinga-core -v 0.1.0 \
  --deb-no-default-config-files \
  /opt/icinga-checks/znagios

# post-install pour créer les symlinks
cat > postinst.sh <<'SH'
#!/bin/sh
set -e
ln -sf /opt/icinga-checks/znagios /opt/icinga-checks/check_load
ln -sf /opt/icinga-checks/znagios /opt/icinga-checks/check_mem
exit 0
SH
chmod +x postinst.sh
fpm -s dir -t deb -n zig-icinga-core -v 0.1.0 \
  --after-install postinst.sh \
  /opt/icinga-checks/znagios

                        

Mesurer & valider

  • Taille/RSS : \time -v /opt/icinga-checks/check_load ⇒ Maximum resident set size.

  • Perfdata : vĂ©rifier formats Nagios (UOM, ranges, warn/crit).

  • Charge : exĂ©cutions en boucle (for i in {1..1000}; do ...) pour valider latence/stabilitĂ©.


Migration depuis Python

  1. Mapping 1‑à‑1 : crĂ©er check_* Zig avec la mĂȘme sĂ©mantique (args, messages, perfdata).

  2. DĂ©ploiement progressif : conserver l’ancien script sous un autre nom (*.py.old) pendant une fenĂȘtre de rollback.

  3. Rollback : renommer symlink si besoin (Zig ↔ Python) ; pas de redĂ©ploiement.

  4. Désinstallation : supprimer les scripts Python une fois la stabilité confirmée.


Sécurité

  • ELF statique musl ⇒ moins de dĂ©pendances systĂšme ⇒ surface d’attaque rĂ©duite.

  • Principe du moindre privilĂšge : user dĂ©diĂ©, pas de network pour les checks qui n’en ont pas besoin (IPAddressDeny=any).

  • EntrĂ©es non fiables : toujours valider/parsers stricts si entrĂ©e externe (HTTP/DNS ⇒ binaire dĂ©diĂ©, tests).


Roadmap (suggestion)

  • Ajouter disk, tcp, file-age, systemd-active au core.

  • Binaire dĂ©diĂ© : check_http (TLS, SNI, redirects, cert-expiry), check_dns.

  • Option de build Ă  la carte : -Dmulti=true|false, -Denable_http=false, etc. (build.zig).

  • IntĂ©gration Prometheus (exporter lĂ©ger) si utile.


FAQ

Q : Le multicall charge‑t‑il tout en mĂ©moire ?

Non. Le noyau ne mappe que les pages exécutées. Impact mémoire comparable à un binaire par sonde.

Q : Pourquoi pas garder Python ?

Parfait pour la glue/API/ETL. Pour des checks fréquents et courts, le natif statique réduit latence, dépendances et taille.

Q : Et Rust ?

Excellente option pour des parseurs/protos complexes (sûreté mémoire). Ici, Zig offre un meilleur ratio simplicité/empreinte.


Annexes

  • RĂ©fĂ©rence perfdata : label=value[UOM];warn;crit;min;max (UOM: s,ms,%,B,KB,MB,GB,TB,c).

  • UOM recommandĂ©es : B/MiB pour mĂ©moire/disque, ms pour latences, % pour ratios.

  • Conventions : messages courts, verbes d’action, labels cohĂ©rents (load1, mem_used_pct, fs_used_pct, tcp_latency_ms).