Alerte d'expiration des certificats HTTPS du reverse proxy (sensor.evcs_certificats)
This commit is contained in:
84
tools/certificats.mjs
Normal file
84
tools/certificats.mjs
Normal file
@@ -0,0 +1,84 @@
|
||||
// =====================================================================
|
||||
// CERTIFICATS HTTPS DU REVERSE PROXY (13/10/2026, demande du proprietaire)
|
||||
// Le 12/10 a 15:52 UTC, le certificat de n8n.airtria.pro a expire sans que
|
||||
// personne ne le sache (MCP n8n et acces public en echec) ; gitlab et
|
||||
// open-webui le meme jour, openclaw le lendemain. Le DSM du .50 ne sait plus
|
||||
// joindre Let's Encrypt : le certificat est obtenu sur le .17 puis importe a la
|
||||
// main sur le .50, et rien ne relance cette copie. Ce module lit le certificat
|
||||
// REELLEMENT servi par le reverse proxy pour chaque nom (poignee de main TLS,
|
||||
// aucune requete HTTP) ; le service d'historisation publie
|
||||
// sensor.evcs_certificats et SPARK previent par WhatsApp.
|
||||
// `certificatsAAlerter` est pure (tests : tests/certificats/run.mjs).
|
||||
// =====================================================================
|
||||
import tls from 'node:tls';
|
||||
|
||||
export const PROXY_HOTE = '10.10.10.50';
|
||||
export const PROXY_PORT = 443;
|
||||
// Noms servis par le reverse proxy du .50 (verifie le 13/10 : airtria.fr et
|
||||
// airtria.com n'y sont pas, le proxy renvoie son certificat par defaut).
|
||||
export const DOMAINES = ['n8n.airtria.pro', 'openclaw.airtria.pro', 'gitlab.airtria.pro', 'open-webui.airtria.pro'];
|
||||
// Le .17 renouvelle a ~30 jours de l'echeance ; la copie vers le .50 est
|
||||
// manuelle : 14 jours laissent deux week-ends pour la faire.
|
||||
export const PREAVIS_J = 14;
|
||||
|
||||
// Lecture du certificat servi (non pure). Ne leve jamais : { domaine, expireMs,
|
||||
// noms, erreur }.
|
||||
export function lireCertificat(domaine, hote = PROXY_HOTE, port = PROXY_PORT, delaiMs = 8000) {
|
||||
return new Promise((resolve) => {
|
||||
let fini = false;
|
||||
const fin = (r) => { if (!fini) { fini = true; resolve({ domaine, ...r }); } };
|
||||
const s = tls.connect({ host: hote, port, servername: domaine, rejectUnauthorized: false, timeout: delaiMs }, () => {
|
||||
const c = s.getPeerCertificate();
|
||||
const noms = [c?.subject?.CN, ...String(c?.subjectaltname || '').split(',').map((x) => x.trim().replace(/^DNS:/, ''))]
|
||||
.filter(Boolean);
|
||||
fin({ expireMs: Date.parse(c?.valid_to), noms, erreur: null });
|
||||
s.end();
|
||||
});
|
||||
s.on('error', (e) => fin({ expireMs: null, noms: [], erreur: e.code || e.message }));
|
||||
s.on('timeout', () => { s.destroy(); fin({ expireMs: null, noms: [], erreur: 'delai depasse' }); });
|
||||
});
|
||||
}
|
||||
|
||||
const couvre = (noms, d) => noms.some((n) => n === d || (n.startsWith('*.') && d.endsWith(n.slice(1)) && d.split('.').length === n.split('.').length));
|
||||
const jour = (ms) => new Date(ms).toLocaleDateString('fr-FR', { timeZone: 'Europe/Paris', day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
|
||||
/**
|
||||
* @param {Array<{domaine, expireMs, noms, erreur}>} lectures
|
||||
* @returns {{ alertes, certificats, texte, prochaine }}
|
||||
* statut : expire | bientot | mauvais_nom | illisible | ok
|
||||
*/
|
||||
export function certificatsAAlerter(lectures, maintenantMs, preavisJ = PREAVIS_J) {
|
||||
const certificats = lectures.map((l) => {
|
||||
if (l.erreur || !Number.isFinite(l.expireMs)) {
|
||||
return { domaine: l.domaine, statut: 'illisible', expire_le: null, jours_restants: null,
|
||||
texte: `${l.domaine} : certificat illisible (${l.erreur || 'date absente'})` };
|
||||
}
|
||||
const j = Math.floor((l.expireMs - maintenantMs) / 86400000);
|
||||
const base = { domaine: l.domaine, expire_le: new Date(l.expireMs).toISOString(), jours_restants: j };
|
||||
if (!couvre(l.noms || [], l.domaine)) {
|
||||
return { ...base, statut: 'mauvais_nom',
|
||||
texte: `${l.domaine} : le proxy sert un certificat d'un autre nom (${(l.noms || []).join(', ') || 'inconnu'}) — certificat importe mais pas associe ?` };
|
||||
}
|
||||
if (l.expireMs <= maintenantMs) {
|
||||
return { ...base, statut: 'expire', texte: `${l.domaine} : certificat EXPIRE depuis le ${jour(l.expireMs)}` };
|
||||
}
|
||||
if (j < preavisJ) {
|
||||
return { ...base, statut: 'bientot', texte: `${l.domaine} : certificat expire le ${jour(l.expireMs)} (dans ${j} j)` };
|
||||
}
|
||||
return { ...base, statut: 'ok', texte: `${l.domaine} : valable jusqu'au ${jour(l.expireMs)}` };
|
||||
});
|
||||
const ordre = { expire: 0, mauvais_nom: 1, illisible: 2, bientot: 3, ok: 4 };
|
||||
certificats.sort((a, b) => ordre[a.statut] - ordre[b.statut] || (a.jours_restants ?? 0) - (b.jours_restants ?? 0));
|
||||
const alertes = certificats.filter((c) => c.statut !== 'ok');
|
||||
const valides = certificats.filter((c) => Number.isFinite(c.jours_restants) && c.statut !== 'mauvais_nom' && c.statut !== 'expire');
|
||||
const prochaine = valides.length ? valides.reduce((a, b) => (a.jours_restants <= b.jours_restants ? a : b)) : null;
|
||||
const geste = 'Renouveler sur le .17 (Let\'s Encrypt), exporter, importer sur le .50 et associer au reverse proxy.';
|
||||
return {
|
||||
alertes,
|
||||
certificats,
|
||||
prochaine: prochaine ? { domaine: prochaine.domaine, expire_le: prochaine.expire_le, jours_restants: prochaine.jours_restants } : null,
|
||||
texte: alertes.length
|
||||
? `${alertes.map((a) => `- ${a.texte}`).join('\n')}\n${geste}`
|
||||
: `Certificats du reverse proxy valables (prochaine echeance : ${prochaine ? `${prochaine.domaine} dans ${prochaine.jours_restants} j` : 'inconnue'}).`,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user