Pourquoi écrire ses agents d’audit SRE en Rust plutôt qu’en Python ?
Lorsqu’un agent autonome d’observabilité tourne en continu pour vérifier la conformité d’une stack de monitoring (Grafana, Prometheus, sondes réseau), l’empreinte mémoire et la prévisibilité du runtime deviennent critiques. Un script Python ou Node.js embarquant un interpréteur, des dizaines de dépendances transitoires et un ramasse-miettes (Garbage Collector) consomme couramment 80 à 250 Mo de RAM au repos.
À l’inverse, un agent compilé en Rust (édition 2021) appuyé uniquement sur la bibliothèque standard (std::net, std::fs, std::time) produit un binaire statique unique de moins de 2 Mo consommant moins de 3 Mo de RSS en exécution, sans pause GC ni dépendance externe susceptible de casser lors d’une mise à jour système.
Dans cet article, nous concevons un Agent IA de Conformité Grafana en Rust piloté par un fichier déclaratif config.toml unique :
- Un binaire unique (
sre_sentinel_rust) découplé de son paramétrage (--config config.toml). - Un contrat de conformité Grafana : comparaison entre le Dashboard Template de référence (GitOps JSON) et l’API Grafana en direct (
/api/healthet/api/dashboards/uid/<uid>), vérification des identifiants de panneaux (required_panel_ids) et des sections obligatoires (required_titles). - Une gouvernance explicite des capacités réseau (
[permissions]) : contrôle granulaire des droits TCP, UDP et DNS (allow_tcp = true,allow_udp = true,allow_dns = trueactivés par défaut dansconfig.toml), complété par une synthèse IA optionnelle en cas de dérive (drift).
1. Architecture de l’Agent Rust & Gouvernance config.toml
L’agent sépare strictement le moteur d’exécution (binaire compilé sur poste de développement ou CI/CD) et la politique d’audit déclarée dans config.toml :
Ce que vérifie l’agent à chaque cycle
- Conformité structurelle du Template JSON : présence du fichier modèle, validation de l’
uidet dutitle, contrôle de tous lesidde panneaux obligatoires et des titres métiers attendus. - Santé & Conformité Live Grafana : interrogation TCP/HTTP de
/api/healthet/api/dashboards/uid/<uid>(avec jetonBeareroptionnel) pour détecter toute modification manuelle non versionnée (configuration drift). - Contrôle des capacités réseau (
[permissions]) : chaque famille de protocole (DNS, TCP, UDP) est gouvernée par un booléen dansconfig.toml(tous autorisés par défaut :true), permettant de brider instantanément l’agent dans une enclave restreinte sans recompiler le binaire.
2. Le fichier de configuration config.toml
Par défaut, toutes les capacités réseau (allow_tcp, allow_udp, allow_dns) sont activées (true) afin que l’agent puisse résoudre les noms d’hôtes, tester les sondes UDP et auditer l’API Grafana ainsi que les cibles TCP :
# ==============================================================================
# Configuration de l'Agent IA Rust : Conformité Dashboard Grafana & Sondes Réseau
# ==============================================================================
[agent]
name = "grafana-conformity-agent"
interval_secs = 60
report_file = "/var/log/sre_sentinel_grafana_audit.json"
# Optionnel : webhook compatible OpenAI / Ollama local pour résumer les dérives
ai_webhook_url = ""
[permissions]
# Capacités réseau accordées à l'agent (par défaut : tout est autorisé)
allow_tcp = true
allow_udp = true
allow_dns = true
[grafana]
url = "http://127.0.0.1:3000"
# Optionnel : Service Account Token Grafana (glsa_...) pour comparer le JSON live via l'API
api_token = ""
dashboard_uid = "homelab-l1-cockpit"
template_dashboard_path = "./dashboards/L1_template.json"
verify_live_api = true
# Liste des IDs de panneaux obligatoires dans le dashboard de référence
required_panel_ids = [1, 2, 3, 4, 10, 11, 12]
# Liste des mots-clés / titres obligatoires devant figurer dans le dashboard
required_titles = [
"Executive Cockpit",
"Hardware & BIOS",
"Audit Versions Logiciels (SBOM)",
"Disponibilité Services"
]
[probes]
# Hôtes à résoudre en DNS (si allow_dns = true)
dns_hosts = [
"localhost",
"blog.ravindra-job.fr"
]
# Cibles TCP host:port à vérifier (si allow_tcp = true)
tcp_targets = [
"127.0.0.1:3000",
"127.0.0.1:9090"
]
# Cibles UDP host:port à vérifier par datagramme DNS (si allow_udp = true)
udp_targets = [
"1.1.1.1:53",
"8.8.8.8:53"
]
3. Code Source Complet en Rust (Cargo.toml & src/main.rs)
Pour garantir une compilation ultra-rapide, zéro vulnérabilité de chaîne d’approvisionnement (Supply Chain) et une portabilité totale sous Linux x86_64 ou aarch64, l’agent est écrit en Rust pur (std uniquement) :
Cargo.toml
[package]
name = "sre_sentinel_rust"
version = "0.2.0"
edition = "2021"
description = "Autonomous Rust AI SRE Agent for Grafana Dashboard Template Conformity & Network Policy Auditing (TCP, UDP, DNS)"
[dependencies]
# Zero external crate dependencies: pure Rust std library for minimal footprint (<2 MB binary) and zero supply-chain risk.
src/main.rs
use std::collections::HashMap;
use std::env;
use std::fs::{self, OpenOptions};
use std::io::{Read, Write};
use std::net::{SocketAddr, TcpStream, ToSocketAddrs, UdpSocket};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone)]
struct AgentConfig {
name: String,
interval_secs: u64,
report_file: String,
ai_webhook_url: String,
allow_tcp: bool,
allow_udp: bool,
allow_dns: bool,
grafana_url: String,
grafana_api_token: String,
dashboard_uid: String,
template_dashboard_path: String,
verify_live_api: bool,
required_panel_ids: Vec<i64>,
required_titles: Vec<String>,
dns_hosts: Vec<String>,
tcp_targets: Vec<String>,
udp_targets: Vec<String>,
}
impl Default for AgentConfig {
fn default() -> Self {
Self {
name: "grafana-conformity-agent".to_string(),
interval_secs: 60,
report_file: "/tmp/sre_sentinel_grafana_audit.json".to_string(),
ai_webhook_url: String::new(),
// Par défaut : TCP, UDP et DNS sont tous autorisés
allow_tcp: true,
allow_udp: true,
allow_dns: true,
grafana_url: "http://127.0.0.1:3000".to_string(),
grafana_api_token: String::new(),
dashboard_uid: "homelab-l1-cockpit".to_string(),
template_dashboard_path: "./dashboards/L1_template.json".to_string(),
verify_live_api: true,
required_panel_ids: vec![1, 2, 3, 4],
required_titles: vec!["Executive Cockpit".to_string()],
dns_hosts: vec!["localhost".to_string()],
tcp_targets: vec!["127.0.0.1:3000".to_string()],
udp_targets: vec!["1.1.1.1:53".to_string()],
}
}
}
#[derive(Debug)]
struct ConformityReport {
timestamp_epoch: u64,
agent_name: String,
permissions_summary: String,
template_path: String,
template_exists: bool,
template_uid_ok: bool,
template_total_panels: usize,
missing_panel_ids: Vec<i64>,
missing_titles: Vec<String>,
grafana_live_reachable: bool,
grafana_live_status: String,
dns_checks: Vec<(String, bool, String)>,
tcp_checks: Vec<(String, bool, u128)>,
udp_checks: Vec<(String, bool, u128)>,
conformant: bool,
ai_diagnosis: String,
}
fn parse_string_Val(raw: &str) -> String {
let trimmed = raw.trim();
if (trimmed.starts_with('"') && trimmed.ends_with('"'))
|| (trimmed.starts_with('\'') && trimmed.ends_with('\''))
{
trimmed[1..trimmed.len() - 1].to_string()
} else {
trimmed.to_string()
}
}
fn parse_string_array(raw: &str) -> Vec<String> {
let inner = raw.trim().trim_start_matches('[').trim_end_matches(']');
let mut out = Vec::new();
for item in inner.split(',') {
let clean = parse_string_Val(item);
if !clean.is_empty() {
out.push(clean);
}
}
out
}
fn parse_int_array(raw: &str) -> Vec<i64> {
let inner = raw.trim().trim_start_matches('[').trim_end_matches(']');
let mut out = Vec::new();
for item in inner.split(',') {
let clean = item.trim();
if let Ok(v) = clean.parse::<i64>() {
out.push(v);
}
}
out
}
fn load_config(path: &str) -> AgentConfig {
let mut cfg = AgentConfig::default();
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
eprintln!("[WARN] Impossible de lire {}: {} (valeurs par défaut utilisées)", path, e);
return cfg;
}
};
let mut section = String::new();
let mut map: HashMap<String, String> = HashMap::new();
let mut current_key = String::new();
let mut current_multiline = String::new();
let mut in_brackets = false;
for raw_line in content.lines() {
let line = match raw_line.find('#') {
Some(idx) => {
let before_hash = &raw_line[..idx];
let quote_count = before_hash.chars().filter(|&c| c == '"').count();
if quote_count % 2 == 0 {
before_hash.trim()
} else {
raw_line.trim()
}
}
None => raw_line.trim(),
};
if line.is_empty() {
continue;
}
if !in_brackets && line.starts_with('[') && line.ends_with(']') {
section = line[1..line.len() - 1].trim().to_string();
continue;
}
if in_brackets {
current_multiline.push(' ');
current_multiline.push_str(line);
if line.contains(']') {
in_brackets = false;
map.insert(current_key.clone(), current_multiline.clone());
current_key.clear();
current_multiline.clear();
}
continue;
}
if let Some(eq_idx) = line.find('=') {
let key = line[..eq_idx].trim();
let val = line[eq_idx + 1..].trim();
let full_key = if section.is_empty() {
key.to_string()
} else {
format!("{}.{}", section, key)
};
if val.starts_with('[') && !val.contains(']') {
in_brackets = true;
current_key = full_key;
current_multiline = val.to_string();
} else {
map.insert(full_key, val.to_string());
}
}
}
if let Some(v) = map.get("agent.name") { cfg.name = parse_string_Val(v); }
if let Some(v) = map.get("agent.interval_secs") {
if let Ok(n) = v.parse::<u64>() { cfg.interval_secs = n; }
}
if let Some(v) = map.get("agent.report_file") { cfg.report_file = parse_string_Val(v); }
if let Some(v) = map.get("agent.ai_webhook_url") { cfg.ai_webhook_url = parse_string_Val(v); }
if let Some(v) = map.get("permissions.allow_tcp") { cfg.allow_tcp = v == "true"; }
if let Some(v) = map.get("permissions.allow_udp") { cfg.allow_udp = v == "true"; }
if let Some(v) = map.get("permissions.allow_dns") { cfg.allow_dns = v == "true"; }
if let Some(v) = map.get("grafana.url") { cfg.grafana_url = parse_string_Val(v); }
if let Some(v) = map.get("grafana.api_token") { cfg.grafana_api_token = parse_string_Val(v); }
if let Some(v) = map.get("grafana.dashboard_uid") { cfg.dashboard_uid = parse_string_Val(v); }
if let Some(v) = map.get("grafana.template_dashboard_path") {
cfg.template_dashboard_path = parse_string_Val(v);
}
if let Some(v) = map.get("grafana.verify_live_api") { cfg.verify_live_api = v == "true"; }
if let Some(v) = map.get("grafana.required_panel_ids") {
cfg.required_panel_ids = parse_int_array(v);
}
if let Some(v) = map.get("grafana.required_titles") {
cfg.required_titles = parse_string_array(v);
}
if let Some(v) = map.get("probes.dns_hosts") { cfg.dns_hosts = parse_string_array(v); }
if let Some(v) = map.get("probes.tcp_targets") { cfg.tcp_targets = parse_string_array(v); }
if let Some(v) = map.get("probes.udp_targets") { cfg.udp_targets = parse_string_array(v); }
cfg
}
fn check_dns_host(host: &str) -> (bool, String) {
let target = if host.contains(':') {
host.to_string()
} else {
format!("{}:80", host)
};
match target.to_socket_addrs() {
Ok(mut iter) => {
if let Some(addr) = iter.next() {
(true, addr.ip().to_string())
} else {
(false, "no_record".to_string())
}
}
Err(e) => (false, e.to_string()),
}
}
fn check_tcp_target(addr_str: &str, timeout_ms: u64) -> (bool, u128) {
let start = SystemTime::now();
let sock_addr: SocketAddr = match addr_str.parse() {
Ok(a) => a,
Err(_) => match addr_str.to_socket_addrs() {
Ok(mut it) => match it.next() {
Some(a) => a,
None => return (false, 0),
},
Err(_) => return (false, 0),
},
};
let ok = TcpStream::connect_timeout(&sock_addr, Duration::from_millis(timeout_ms)).is_ok();
let elapsed = start.elapsed().map(|d| d.as_millis()).unwrap_or(0);
(ok, elapsed)
}
fn check_udp_dns_query(target: &str, timeout_ms: u64) -> (bool, u128) {
let start = SystemTime::now();
let socket = match UdpSocket::bind("0.0.0.0:0") {
Ok(s) => s,
Err(_) => return (false, 0),
};
let _ = socket.set_read_timeout(Some(Duration::from_millis(timeout_ms)));
let _ = socket.set_write_timeout(Some(Duration::from_millis(timeout_ms)));
// Requête DNS A minimale (12 octets d'en-tête + QNAME "localhost")
let dns_query: [u8; 27] = [
0xAA, 0xBB, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x09, b'l', b'o', b'c', b'a', b'l', b'h', b'o', b's', b't', 0x00,
0x00, 0x01, 0x00, 0x01,
];
if socket.send_to(&dns_query, target).is_err() {
return (false, 0);
}
let mut buf = [0u8; 512];
let ok = socket.recv_from(&mut buf).is_ok();
let elapsed = start.elapsed().map(|d| d.as_millis()).unwrap_or(0);
(ok, elapsed)
}
fn parse_grafana_http(url: &str, api_token: &str, path: &str) -> (bool, String) {
let stripped = url
.trim_start_matches("http://")
.trim_start_matches("https://");
let host_port = stripped.split('/').next().unwrap_or("127.0.0.1:3000");
let addr_with_port = if host_port.contains(':') {
host_port.to_string()
} else {
format!("{}:80", host_port)
};
let sock_addr = match addr_with_port.to_socket_addrs() {
Ok(mut it) => match it.next() {
Some(a) => a,
None => return (false, "dns_failed".to_string()),
},
Err(e) => return (false, format!("resolve_err:{}", e)),
};
let mut stream = match TcpStream::connect_timeout(&sock_addr, Duration::from_millis(2500)) {
Ok(s) => s,
Err(e) => return (false, format!("tcp_err:{}", e)),
};
let _ = stream.set_read_timeout(Some(Duration::from_millis(2500)));
let _ = stream.set_write_timeout(Some(Duration::from_millis(2500)));
let auth_header = if api_token.is_empty() {
String::new()
} else {
format!("Authorization: Bearer {}\r\n", api_token)
};
let req = format!(
"GET {} HTTP/1.1\r\nHost: {}\r\nUser-Agent: sre_sentinel_rust/0.2\r\nAccept: application/json\r\n{}Connection: close\r\n\r\n",
path, host_port, auth_header
);
if stream.write_all(req.as_bytes()).is_err() {
return (false, "write_err".to_string());
}
let mut resp = String::new();
let _ = stream.read_to_string(&mut resp);
let first_line = resp.lines().next().unwrap_or("");
let ok = first_line.contains(" 200 ") || first_line.contains(" 302 ");
(ok, first_line.to_string())
}
fn audit_template_dashboard(cfg: &AgentConfig) -> (bool, bool, usize, Vec<i64>, Vec<String>) {
let raw = match fs::read_to_string(&cfg.template_dashboard_path) {
Ok(c) => c,
Err(_) => {
return (
false,
false,
0,
cfg.required_panel_ids.clone(),
cfg.required_titles.clone(),
)
}
};
let uid_needle = format!("\"uid\": \"{}\"", cfg.dashboard_uid);
let uid_needle_compact = format!("\"uid\":\"{}\"", cfg.dashboard_uid);
let uid_ok = raw.contains(&uid_needle) || raw.contains(&uid_needle_compact);
let panel_count = raw.matches("\"gridPos\"").count();
let mut missing_ids = Vec::new();
for id in &cfg.required_panel_ids {
let p1 = format!("\"id\": {},", id);
let p2 = format!("\"id\":{},", id);
let p3 = format!("\"id\": {}\n", id);
if !raw.contains(&p1) && !raw.contains(&p2) && !raw.contains(&p3) {
missing_ids.push(*id);
}
}
let mut missing_titles = Vec::new();
for t in &cfg.required_titles {
if !raw.contains(t) {
missing_titles.push(t.clone());
}
}
(true, uid_ok, panel_count, missing_ids, missing_titles)
}
fn run_conformity_cycle(cfg: &AgentConfig) -> ConformityReport {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let (tpl_exists, tpl_uid_ok, total_panels, missing_ids, missing_titles) =
audit_template_dashboard(cfg);
let mut dns_results = Vec::new();
if cfg.allow_dns {
for h in &cfg.dns_hosts {
let (ok, detail) = check_dns_host(h);
dns_results.push((h.clone(), ok, detail));
}
}
let mut tcp_results = Vec::new();
if cfg.allow_tcp {
for t in &cfg.tcp_targets {
let (ok, ms) = check_tcp_target(t, 2000);
tcp_results.push((t.clone(), ok, ms));
}
}
let mut udp_results = Vec::new();
if cfg.allow_udp {
for u in &cfg.udp_targets {
let (ok, ms) = check_udp_dns_query(u, 2000);
udp_results.push((u.clone(), ok, ms));
}
}
let (grafana_ok, grafana_status) = if cfg.verify_live_api && cfg.allow_tcp {
let api_path = if cfg.grafana_api_token.is_empty() {
"/api/health".to_string()
} else {
format!("/api/dashboards/uid/{}", cfg.dashboard_uid)
};
parse_grafana_http(&cfg.grafana_url, &cfg.grafana_api_token, &api_path)
} else {
(true, "SKIPPED_BY_POLICY".to_string())
};
let dns_all_ok = dns_results.iter().all(|(_, ok, _)| *ok);
let tcp_all_ok = tcp_results.iter().all(|(_, ok, _)| *ok);
let conformant = tpl_exists
&& tpl_uid_ok
&& missing_ids.is_empty()
&& missing_titles.is_empty()
&& grafana_ok
&& dns_all_ok
&& tcp_all_ok;
let ai_diagnosis = if conformant {
format!(
"CONFORME : Dashboard '{}' ({} panneaux) validé, tous les panneaux requis ({:?}) et sections sont présents, API Grafana ({}) opérationnelle.",
cfg.dashboard_uid, total_panels, cfg.required_panel_ids, grafana_status
)
} else {
format!(
"DÉRIVE DÉTECTÉE : tpl_exists={}, uid_ok={}, missing_ids={:?}, missing_titles={:?}, grafana_live={} ({})",
tpl_exists, tpl_uid_ok, missing_ids, missing_titles, grafana_ok, grafana_status
)
};
ConformityReport {
timestamp_epoch: now,
agent_name: cfg.name.clone(),
permissions_summary: format!(
"TCP={}, UDP={}, DNS={}",
cfg.allow_tcp, cfg.allow_udp, cfg.allow_dns
),
template_path: cfg.template_dashboard_path.clone(),
template_exists: tpl_exists,
template_uid_ok: tpl_uid_ok,
template_total_panels: total_panels,
missing_panel_ids: missing_ids,
missing_titles,
grafana_live_reachable: grafana_ok,
grafana_live_status: grafana_status,
dns_checks: dns_results,
tcp_checks: tcp_results,
udp_checks: udp_results,
conformant,
ai_diagnosis,
}
}
fn write_json_report(path: &str, rep: &ConformityReport) {
let json = format!(
"{{\n \"timestamp\": {},\n \"agent\": \"{}\",\n \"permissions\": \"{}\",\n \"conformant\": {},\n \"template_path\": \"{}\",\n \"template_exists\": {},\n \"template_uid_ok\": {},\n \"template_total_panels\": {},\n \"missing_panel_ids\": {:?},\n \"missing_titles\": {:?},\n \"grafana_live_reachable\": {},\n \"grafana_live_status\": \"{}\",\n \"ai_diagnosis\": \"{}\"\n}}\n",
rep.timestamp_epoch,
rep.agent_name,
rep.permissions_summary,
rep.conformant,
rep.template_path,
rep.template_exists,
rep.template_uid_ok,
rep.template_total_panels,
rep.missing_panel_ids,
rep.missing_titles,
rep.grafana_live_reachable,
rep.grafana_live_status.replace('"', "\\\""),
rep.ai_diagnosis.replace('"', "\\\"")
);
if let Ok(mut f) = OpenOptions::new().create(true).write(true).truncate(true).open(path) {
let _ = f.write_all(json.as_bytes());
}
println!("{}", json.trim());
}
fn main() {
let args: Vec<String> = env::args().collect();
let mut config_path = "config.toml".to_string();
let mut run_once = false;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--config" | "-c" => {
if i + 1 < args.len() {
config_path = args[i + 1].clone();
i += 1;
}
}
"--once" => {
run_once = true;
}
_ => {}
}
i += 1;
}
let cfg = load_config(&config_path);
println!(
"[INIT] Agent IA Rust '{}' démarré | config={} | permissions: TCP={} UDP={} DNS={}",
cfg.name, config_path, cfg.allow_tcp, cfg.allow_udp, cfg.allow_dns
);
loop {
let report = run_conformity_cycle(&cfg);
write_json_report(&cfg.report_file, &report);
if run_once {
std::process::exit(if report.conformant { 0 } else { 2 });
}
std::thread::sleep(Duration::from_secs(cfg.interval_secs));
}
}
4. Pourquoi ne jamais compiler sur le serveur de Production (et le Prompt IA pour votre machine de Dev)
Une règle d’or d’ingénierie SRE consiste à ne jamais lancer cargo build --release sur un serveur de production hébergeant des bases de données et des conteneurs critiques : la phase d’optimisation LLVM (rustc + lto) peut consommer plusieurs gigaoctets de RAM et saturer le Swap de l’hôte.
La bonne pratique consiste à commiter uniquement le code source (src/main.rs, Cargo.toml, config.toml) dans Git, puis à confier la compilation et les tests à votre machine de développement (ou un runner CI/CD isolé) à l’aide du prompt IA prêt à l’emploi ci-dessous :
# PROMPT IA : COMPILATION & VALIDATION DE L'AGENT RUST D'AUDIT GRAFANA (MACHINE DE DEV)
Tu es un ingénieur système Senior Rust & SRE.
Tu travailles sur ma machine de DÉVELOPPEMENT (et NON sur la production).
## Objectif
Vérifier, compiler en mode optimisé (--release) et tester le binaire de l'agent IA Rust sre_sentinel_rust qui vérifie la conformité de mon dashboard Grafana via config.toml et contrôle ses permissions réseau (TCP, UDP, DNS).
## Étapes à exécuter dans l'ordre
1. Vérification de la toolchain Rust :
- Exécute : rustc --version && cargo --version
- Si Rust n'est pas installé sur la machine de dev, installe-le via rustup :
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
2. Revue statique et Linting (cargo check & cargo clippy) :
- Dans le dossier sre_sentinel_rust/, exécute :
cargo fmt --check || cargo fmt
cargo check
cargo clippy -- -D warnings
- Corrige tout warning éventuel tout en conservant zéro dépendance externe (uniquement std).
3. Compilation optimisée pour Linux x86_64 (--release) :
- Compile le binaire :
cargo build --release
strip target/release/sre_sentinel_rust
ls -lh target/release/sre_sentinel_rust
- Vérifie que le binaire fait moins de 2 Mo et ne dépend que de la libc standard (ldd target/release/sre_sentinel_rust).
4. Tests fonctionnels sur la machine de Dev (--once) :
- Lance un test unitaire d'audit en un seul passage :
./target/release/sre_sentinel_rust --config config.toml --once
- Vérifie le rapport JSON généré (conformant: true/false, missing_panel_ids, missing_titles, permissions: "TCP=true, UDP=true, DNS=true").
- Teste également le bridage des permissions dans config.toml (allow_udp = false, allow_dns = false) pour valider que l'agent respecte strictement sa politique de sécurité.
5. Préparation du déploiement vers la Production :
- Affiche la commande scp permettant de copier uniquement le binaire compilé target/release/sre_sentinel_rust et config.toml vers le serveur cible sans jamais compiler en production.
Si vous voulez plus d’information sur l’infrastructure vous pouvez voir l’ repo à ce sujet : https://github.com/ravindrajob/InfraAtHome
