Files
ja4sentinel/architecture.yml
Jacquin Antoine e862139fca fix: socket path to network.socket
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-01 02:14:32 +01:00

765 lines
32 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

version: 1
project:
name: ja4sentinel
description: >
Outil Go pour capturer le trafic réseau sur un serveur Linux,
extraire les handshakes TLS côté client, générer les signatures JA4
(via psanford/tlsfingerprint), enrichir avec des métadonnées IP/TCP,
et loguer les résultats (IP, ports, JA4, meta) vers une ou plusieurs
sorties configurables (socket UNIX, stdout, fichier, ...).
languages:
- go
goals:
- "Développement bloc par bloc avec interfaces simples et stables."
- "Focalisé sur JA4 client (le serveur est connu/local)."
- "Séparation claire des responsabilités (capture, parsing, fingerprint, output)."
- "Tests unitaires pour chaque fonction publique."
- "Tests dintégration dans des conteneurs Docker."
- "Commentaires standardisés, code évolutif avec changements minimaux."
modules:
- name: config
path: "internal/config"
description: "Chargement et validation de la configuration (fichier, env, CLI)."
responsibilities:
- "Lire le fichier de configuration (YAML par défaut)."
- "Fusionner avec les overrides env/CLI."
- "Construire une api.AppConfig cohérente."
allowed_dependencies: []
forbidden_dependencies:
- "capture"
- "tlsparse"
- "fingerprint"
- "output"
- name: capture
path: "internal/capture"
description: "Capture des paquets réseau (pcap/raw socket) sur Linux."
responsibilities:
- "Ouvrir linterface réseau configurée."
- "Appliquer les filtres (ports, BPF, protocole)."
- "Observer les flux TCP côté client vers les ports dintérêt."
- "Extraire les en-têtes IP/TCP utiles (IPMeta, TCPMeta)."
- "Convertir les paquets en objets RawPacket."
allowed_dependencies:
- "config"
- "api"
forbidden_dependencies:
- "tlsparse"
- "fingerprint"
- "output"
- name: tlsparse
path: "internal/tlsparse"
description: "Extraction des ClientHello TLS côté client à partir des paquets capturés."
responsibilities:
- "Décoder les couches IP/TCP jusquau payload TLS."
- "Identifier le ClientHello TLS du client sur les ports configurés."
- "Assembler les segments si nécessaire pour obtenir un ClientHello complet."
- "Produire des TLSClientHello enrichis avec IPMeta et TCPMeta."
allowed_dependencies:
- "config"
- "capture"
- "api"
forbidden_dependencies:
- "output"
- name: fingerprint
path: "internal/fingerprint"
description: "Génération des empreintes JA4 à partir des ClientHello TLS."
responsibilities:
- "Utiliser psanford/tlsfingerprint pour analyser le ClientHello."
- "Générer la chaîne JA4 (et éventuellement JA3) côté client."
- "Encapsuler les résultats dans un type Fingerprints."
allowed_dependencies:
- "config"
- "tlsparse"
- "api"
forbidden_dependencies:
- "capture"
- name: output
path: "internal/output"
description: "Sortie des résultats (JA4 + meta) vers différentes destinations."
responsibilities:
- "Prendre en entrée les Fingerprints et les métadonnées réseau."
- "Formater les données en enregistrements log (JSON ou autre format simple)."
- "Envoyer les enregistrements vers une ou plusieurs sorties (socket UNIX, stdout, fichier, ...)."
- "Gérer un MultiWriter pour combiner plusieurs outputs sans modifier le reste du code."
allowed_dependencies:
- "config"
- "api"
forbidden_dependencies:
- "capture"
- "tlsparse"
- "fingerprint"
- name: logging
path: "internal/logging"
description: "Logs structurés JSON pour le service (stdout/stderr)."
responsibilities:
- "Fournir une fabrique de loggers (LoggerFactory)."
- "Émettre des logs au format JSON lines sur stdout."
- "Supporter les niveaux : debug, info, warn, error."
- "Inclure timestamp, niveau, composant, message et détails optionnels."
allowed_dependencies:
- "api"
forbidden_dependencies:
- "config"
- "capture"
- "tlsparse"
- "fingerprint"
- "output"
- name: cmd_ja4sentinel
path: "cmd/ja4sentinel"
description: "Point d'entrée de l'application (main)."
responsibilities:
- "Charger la configuration via le module config."
- "Construire les instances des modules (capture, tlsparse, fingerprint, output, logging)."
- "Brancher les modules entre eux selon l'architecture pipeline."
- "Gérer les signaux système (arrêt propre)."
allowed_dependencies:
- "config"
- "capture"
- "tlsparse"
- "fingerprint"
- "output"
- "api"
- "logging"
forbidden_dependencies: []
api:
types:
- name: "api.ServiceLog"
description: "Log interne du service ja4sentinel (diagnostic)."
fields:
- { name: Level, type: "string", description: "niveau: debug, info, warn, error." }
- { name: Component, type: "string", description: "module concerné (capture, tlsparse, ...)." }
- { name: Message, type: "string", description: "texte du log." }
- { name: Details, type: "map[string]string", description: "infos additionnelles (erreurs, IDs...)." }
- { name: Timestamp, type: "int64", description: "Timestamp en nanosecondes (auto-rempli par le logger)." }
- { name: TraceID, type: "string", description: "ID de tracing distribué (optionnel)." }
- { name: ConnID, type: "string", description: "Identifiant de flux TCP (optionnel)." }
- name: "api.Config"
description: "Configuration réseau et TLS de base."
fields:
- { name: Interface, type: "string", description: "Nom de l'interface réseau (ex: eth0)." }
- { name: ListenPorts, type: "[]uint16", description: "Ports TCP à surveiller (ex: [443, 8443])." }
- { name: BPFFilter, type: "string", description: "Filtre BPF optionnel pour la capture." }
- { name: FlowTimeoutSec, type: "int", description: "Timeout en secondes pour l'extraction du handshake TLS (défaut: 30)." }
- { name: PacketBufferSize,type: "int", description: "Taille du buffer du canal de paquets (défaut: 1000). Pour les environnements à fort trafic." }
- name: "api.IPMeta"
description: "Métadonnées IP pour fingerprinting de stack."
fields:
- { name: TTL, type: "uint8", description: "TTL initial observé." }
- { name: TotalLength, type: "uint16", description: "Taille totale du paquet IP." }
- { name: IPID, type: "uint16", description: "Identifiant IP du paquet." }
- { name: DF, type: "bool", description: "Flag Don't Fragment." }
- name: "api.TCPMeta"
description: "Métadonnées TCP pour fingerprinting de stack."
fields:
- { name: WindowSize, type: "uint16", description: "Fenêtre initiale TCP." }
- { name: MSS, type: "uint16", description: "Maximum Segment Size (option TCP)." }
- { name: WindowScale, type: "uint8", description: "Facteur de scaling (option TCP)." }
- { name: Options, type: "[]string", description: "Liste ordonnée des options TCP (ex: [MSS, SACK, TS])." }
- name: "api.RawPacket"
description: "Paquet brut capturé sur le réseau (vue minimale)."
fields:
- { name: Data, type: "[]byte", description: "Contenu brut du paquet." }
- { name: Timestamp, type: "int64", description: "Timestamp (nanos / epoch) de capture." }
- name: "api.TLSClientHello"
description: "Représentation dun ClientHello TLS client, avec meta IP/TCP."
fields:
- { name: SrcIP, type: "string", description: "Adresse IP source (client)." }
- { name: SrcPort, type: "uint16", description: "Port source (client)." }
- { name: DstIP, type: "string", description: "Adresse IP destination (serveur)." }
- { name: DstPort, type: "uint16", description: "Port destination (serveur)." }
- { name: Payload, type: "[]byte", description: "Bytes correspondant au ClientHello TLS." }
- { name: IPMeta, type: "api.IPMeta", description: "Métadonnées IP observées côté client." }
- { name: TCPMeta, type: "api.TCPMeta", description: "Métadonnées TCP observées côté client." }
- name: "api.Fingerprints"
description: "Empreintes TLS pour un flux client."
fields:
- { name: JA4, type: "string", description: "Signature JA4 client." }
- { name: JA4Hash, type: "string", description: "Hash JA4 client." }
- { name: JA3, type: "string", description: "Signature JA3 (optionnel, si calculée)." }
- { name: JA3Hash, type: "string", description: "Hash JA3 (optionnel)." }
- name: "api.LogRecord"
description: "Enregistrement de log final, sérialisé en JSON objet plat."
json_object: true
fields:
- { name: SrcIP, type: "string", json_key: "src_ip" }
- { name: SrcPort, type: "uint16", json_key: "src_port" }
- { name: DstIP, type: "string", json_key: "dst_ip" }
- { name: DstPort, type: "uint16", json_key: "dst_port" }
# IPMeta flatten
- { name: IPTTL, type: "uint8", json_key: "ip_meta_ttl" }
- { name: IPTotalLen, type: "uint16", json_key: "ip_meta_total_length" }
- { name: IPID, type: "uint16", json_key: "ip_meta_id" }
- { name: IPDF, type: "bool", json_key: "ip_meta_df" }
# TCPMeta flatten
- { name: TCPWindow, type: "uint16", json_key: "tcp_meta_window_size" }
- { name: TCPMSS, type: "*uint16", json_key: "tcp_meta_mss", optional: true, description: "Pointeur (nil si non présent, 0 si absent)." }
- { name: TCPWScale, type: "*uint8", json_key: "tcp_meta_window_scale", optional: true, description: "Pointeur (nil si non présent, 0 si absent)." }
- { name: TCPOptions, type: "string", json_key: "tcp_meta_options" }
# Fingerprints
- { name: JA4, type: "string", json_key: "ja4" }
- { name: JA4Hash, type: "string", json_key: "ja4_hash" }
- { name: JA3, type: "string", json_key: "ja3" }
- { name: JA3Hash, type: "string", json_key: "ja3_hash" }
# Timestamp
- { name: Timestamp, type: "int64", json_key: "timestamp", description: "Wall-clock timestamp in nanoseconds since Unix epoch (auto-filled by NewLogRecord)." }
- name: "api.OutputConfig"
description: "Configuration dune sortie de logs."
fields:
- { name: Type, type: "string", description: "Type doutput (unix_socket, stdout, file, ...)." }
- { name: Enabled, type: "bool", description: "Active ou non cette sortie." }
- { name: Params, type: "map[string]string", description: "Paramètres spécifiques (socket_path, path, ...)." }
- name: "api.AppConfig"
description: "Configuration complète de ja4sentinel."
fields:
- { name: Core, type: "api.Config", description: "Paramètres réseau + TLS." }
- { name: Outputs, type: "[]api.OutputConfig", description: "Liste des outputs configurés." }
interfaces:
- name: "config.Loader"
description: "Charge la configuration (fichier + env + CLI)."
module: "config"
methods:
- name: "Load"
params: []
returns:
- { type: "api.AppConfig" }
- { type: "error" }
- name: "capture.Capture"
description: "Source de paquets réseau bruts côté client."
module: "capture"
methods:
- name: "Run"
params:
- { name: cfg, type: "api.Config" }
- { name: out, type: "chan<- api.RawPacket" }
returns:
- { type: "error" }
notes:
- "Doit respecter les filtres (ports, BPF) définis dans la configuration."
- "Ne connaît pas le format TLS ni JA4."
- name: "tlsparse.Parser"
description: "Transforme des RawPacket en TLSClientHello (côté client uniquement)."
module: "tlsparse"
methods:
- name: "Process"
params:
- { name: pkt, type: "api.RawPacket" }
returns:
- { type: "*api.TLSClientHello" }
- { type: "error" }
notes:
- "Retourne nil si le paquet ne contient pas (ou plus) de ClientHello."
- "Pour chaque flux, sarrête une fois le ClientHello complet obtenu."
- name: "fingerprint.Engine"
description: "Génère les empreintes JA4 (et JA3 éventuellement) à partir dun ClientHello."
module: "fingerprint"
methods:
- name: "FromClientHello"
params:
- { name: ch, type: "api.TLSClientHello" }
returns:
- { type: "*api.Fingerprints" }
- { type: "error" }
notes:
- "Utilise github.com/psanford/tlsfingerprint en interne."
- "Focalisé sur le JA4 client (le côté serveur est déjà connu)."
- name: "output.Writer"
description: "Interface générique pour écrire les résultats."
module: "output"
methods:
- name: "Write"
params:
- { name: rec, type: "api.LogRecord" }
returns:
- { type: "error" }
notes:
- "Ne connaît pas la capture ni les détails de parsing TLS."
- name: "output.UnixSocketWriter"
description: "Implémentation de Writer envoyant les logs sur une socket UNIX."
module: "output"
implements: "output.Writer"
config:
- { name: socket_path, type: "string", description: "Chemin de la socket UNIX (ex: /var/run/logcorrelator/network.socket)." }
- name: "output.MultiWriter"
description: "Combinaison de plusieurs Writer configurés."
module: "output"
implements: "output.Writer"
config:
- { name: writers, type: "[]output.Writer", description: "Liste de Writers concrets à appeler." }
- name: "output.Builder"
description: "Construit les Writers à partir de api.AppConfig."
module: "output"
methods:
- name: "NewFromConfig"
params:
- { name: cfg, type: "api.AppConfig" }
returns:
- { type: "output.Writer" }
- { type: "error" }
notes:
- "Doit supporter plusieurs outputs simultanés via un MultiWriter."
- name: "logging.LoggerFactory"
description: "Fabrique de loggers structurés JSON."
module: "logging"
methods:
- name: "NewLogger"
params:
- { name: level, type: "string" }
returns:
- { type: "api.Logger" }
- name: "NewDefaultLogger"
params: []
returns:
- { type: "api.Logger" }
notes:
- "Les logs sont émis en JSON lines sur stdout pour systemd/journald."
- name: "api.Logger"
description: "Interface de logging pour tous les modules."
module: "logging"
methods:
- name: "Debug"
params:
- { name: component, type: "string" }
- { name: message, type: "string" }
- { name: details, type: "map[string]string" }
- name: "Info"
params:
- { name: component, type: "string" }
- { name: message, type: "string" }
- { name: details, type: "map[string]string" }
- name: "Warn"
params:
- { name: component, type: "string" }
- { name: message, type: "string" }
- { name: details, type: "map[string]string" }
- name: "Error"
params:
- { name: component, type: "string" }
- { name: message, type: "string" }
- { name: details, type: "map[string]string" }
notes:
- "Tous les logs passent par stdout/stderr (pas de fichiers directs)."
architecture:
style: "pipeline"
flow:
- from: "capture.Capture"
to: "tlsparse.Parser"
via: "api.RawPacket"
- from: "tlsparse.Parser"
to: "fingerprint.Engine"
via: "api.TLSClientHello"
- from: "fingerprint.Engine"
to: "output.Writer"
via: "api.LogRecord"
constraints:
- id: "client_only"
description: "On ne calcule que les empreintes JA4 côté client (pas côté serveur)."
- id: "no_back_dependencies"
description: "Pas de dépendances en arrière (output ne dépend pas de fingerprint, etc.)."
- id: "simple_messages"
description: "Les communications entre blocs utilisent uniquement les types définis dans api.*."
- id: "no_global_state"
description: "Pas de variables globales partagées entre blocs pour la logique principale."
flow_control:
connection_states:
description: "États simplifiés dun flux TCP pour minimiser la capture."
states:
- name: "NEW"
description: "Observation dun SYN client sur un port surveillé, création dun état minimal (IP/TCP meta)."
- name: "WAIT_CLIENT_HELLO"
description: "Accumulation des segments TCP nécessaires pour extraire un ClientHello complet."
- name: "JA4_DONE"
description: "JA4 calculé et logué, on arrête de suivre ce flux."
rules:
- "Suivi unidirectionnel : uniquement le flux entrant du client vers la machine locale (srcIP:srcPort -> dstIP:dstPort)."
- "Pour chaque flux, on s'arrête dès que JA4 + IPMeta + TCPMeta sont obtenus et logués."
- "Un timeout par flux doit être défini pour éviter de garder un état si le ClientHello n'arrive jamais."
- "Le flow key est au format : `srcIP:srcPort->dstIP:dstPort` (pas de suivi bidirectionnel)."
testing:
policy:
description: >
Chaque fonction publique (exportée) doit avoir des tests unitaires,
et les principaux flux (capture -> tlsparse -> fingerprint -> output)
doivent être couverts par des tests dintégration automatisés.
applies_to:
- "toutes les méthodes des interfaces listées dans api.interfaces"
requirements:
- id: "test_skeletons"
description: "Pour chaque nouvelle fonction exportée, créer au minimum un squelette de test dans un fichier *_test.go."
- id: "error_cases"
description: "Les cas derreur doivent être couverts par au moins un test (inputs invalides, erreurs réseau, parsing raté, etc.)."
- id: "mock_dependencies"
description: "Les dépendances entre modules (Capture, Parser, Engine, Writer) doivent être mockables via interfaces."
levels:
- name: "unit"
description: "Tests sur chaque module isolé (config, capture, tlsparse, fingerprint, output)."
tools:
- "go test"
rules:
- "Pas de dépendance réseau réelle."
- "Les interactions entre modules sont mockées."
- name: "integration"
description: "Tests bout-à-bout dans Docker avec trafic TLS client simulé."
tools:
- "go test"
- "docker"
- "docker compose"
scenarios:
- id: "tls_client_ja4_to_outputs"
description: >
Injecter un flux TLS de test côté client,
vérifier que ja4sentinel capture les paquets client,
génère JA4, enrichit avec meta IP/TCP,
et écrit les enregistrements attendus vers les outputs configurés.
steps:
- "Démarrer un serveur TLS de test dans un conteneur."
- "Démarrer ja4sentinel dans un autre conteneur, connecté au même réseau."
- "Émettre une connexion TLS client de test."
- "Lire la socket UNIX et/ou les autres outputs et vérifier JA4, IP, ports, meta."
assertions:
- "Au moins un LogRecord est reçu."
- "Les champs JA4 ne sont pas vides."
- "Les IP/ports correspondent au flux client."
- "IPMeta et TCPMeta sont cohérents (TTL, window, options...)."
ci_cd:
goals:
- "Construire et tester ja4sentinel de façon reproductible dans des conteneurs Docker."
- "Fournir une commande unique pour lancer tous les tests (unitaires + intégration)."
docker:
images:
- name: "ja4sentinel-dev"
description: "Image de développement et de test (Go + outils réseau)."
base: "golang:1.23-alpine"
includes:
- "go toolchain"
- "libpcap et headers"
- "outils de test (go test, gotestsum, etc.)"
usage:
build_cmd: "docker build -t ja4sentinel-dev -f Dockerfile.dev ."
test_cmd: "docker run --rm -v $(pwd):/app -w /app ja4sentinel-dev make test"
- name: "ja4sentinel-runtime"
description: "Image minimale pour exécuter le binaire en production."
base: "alpine:latest"
includes:
- "binaire ja4sentinel"
- "libpcap runtime si nécessaire"
usage:
build_cmd: "docker build -t ja4sentinel-runtime -f Dockerfile ."
rules:
- "Le build de production doit partir du code testé."
- "Les tests dintégration réseau se font dans des conteneurs, pas directement sur la machine hôte."
make_targets:
- name: "build"
description: "Compile le binaire ja4sentinel pour la plateforme cible."
commands:
- "go build ./cmd/ja4sentinel"
- name: "test"
description: "Lance les tests unitaires Go."
commands:
- "go test ./..."
- name: "test-integration"
description: "Lance les tests d'intégration dans Docker (capture TLS client + outputs)."
commands:
- "docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from ja4sentinel-test"
- name: "lint"
description: "Lance les linters (go vet, staticcheck, etc.)."
commands:
- "go vet ./..."
config_guidelines:
sources:
- "Fichier de configuration YAML (par défaut config.yml)."
- "Variables denvironnement (JA4SENTINEL_*)."
- "Flags CLI."
rules:
- "Le module config est la seule source de vérité pour les paramètres (interface, ports, filtres, outputs)."
- "Les autres modules ne lisent jamais les variables denvironnement ni les fichiers de config directement."
code_style:
comments:
goals:
- "Commentaires standardisés, lisibles, utiles pour la génération de documentation."
- "Expliquer le pourquoi et les décisions, pas répéter le code."
rules:
- id: "godoc_exported"
description: >
Chaque fonction, type ou méthode exporté (nom capitalisé) doit avoir
un commentaire de documentation respectant le style GoDoc.
examples:
- "// FromClientHello génère les empreintes JA4 à partir dun ClientHello TLS client."
- id: "why_not_what"
description: "Les commentaires doivent expliquer le pourquoi plutôt que le quoi lorsque le code est clair."
- id: "no_duplicate_code"
description: "Un commentaire ne doit pas simplement répéter ce que le code dit déjà."
- id: "update_with_code"
description: "Si le code est modifié, les commentaires adjacents doivent être revus et mis à jour."
formatting:
- "Utiliser // pour les commentaires, pas /* ... */ sauf cas exceptionnel."
- "Commentaires de doc au-dessus des déclarations, sans ligne vide."
evolution:
api_stability:
strategy: "semver_like"
goals:
- "Éviter les changements majeurs dAPI une fois le projet stabilisé."
- "Limiter les modifications à ce qui est nécessaire (fixes, extensions compatibles)."
rules:
- id: "no_breaking_changes_without_reason"
description: >
Ne pas modifier les signatures des fonctions des interfaces définies dans api.interfaces
sans raison solide et documentée.
- id: "prefer_extension_over_modification"
description: >
Pour ajouter un comportement, préférer ajouter une nouvelle fonction ou un nouveau type,
plutôt que changer une signature existante.
- id: "document_changes"
description: >
Toute modification dAPI doit être reflétée dans architecture.yml
et, si nécessaire, dans un ADR séparé.
refactoring:
goals:
- "Autoriser le refactoring interne sans changer les contrats externes."
- "Limiter limpact des changements à lintérieur dun module."
guidelines:
- "Les interfaces de api.interfaces servent de contrat stable entre modules."
- "Les changements internes (optimisations, refactoring) ne doivent pas casser ces interfaces."
- "Lorsquun changement dAPI est inévitable, marquer lancienne API comme dépréciée avant de la supprimer."
dev_tools:
usage_guidelines:
- "Les outils IA doivent lire architecture.yml avant de générer ou modifier du code."
- "Les outils de test peuvent s'appuyer sur ci_cd.docker et ci_cd.make_targets pour générer des scripts / pipelines."
future_extensions:
- "Génération automatique de Dockerfile.dev et Dockerfile à partir de cette section."
- "Génération de fichiers docker-compose.test.yml pour les scénarios d'intégration."
packaging:
description: >
ja4sentinel est distribué sous forme de packages .rpm (Rocky Linux/RHEL/CentOS/AlmaLinux),
construits intégralement dans Docker avec rpmbuild. Le binaire est compilé sur Rocky Linux 9
pour une compatibilité binaire maximale avec toutes les distributions RHEL-based.
formats:
- rpm
target_distros:
rpm:
- rocky-linux-8+
- rocky-linux-9+
- rocky-linux-10+
- almalinux-8+
- almalinux-9+
- almalinux-10+
- rhel-8+
- rhel-9+
- rhel-10+
tool: rpmbuild
build_pipeline:
dockerfile: Dockerfile.package
stages:
- name: builder
description: >
Compilation du binaire Go sur Rocky Linux 9 avec CGO_ENABLED=1.
GOOS=linux GOARCH=amd64 pour un binaire compatible x86_64.
Le binaire est dynamiquement lié à libpcap pour une compatibilité maximale.
- name: rpm_builder
description: >
Image Rocky Linux 9 avec rpm-build. Setup de l'arborescence rpmbuild
(BUILD, RPMS, SOURCES, SPECS, SRPMS). Copie du spec et des sources,
puis build avec rpmbuild -bb pour el8, el9, el10.
- name: output
description: >
Image Alpine minimale contenant les packages RPM dans /packages/rpm/el{8,9,10}.
files:
binary:
source: dist/ja4sentinel
dest: /usr/bin/ja4sentinel
mode: "0755"
systemd:
source: packaging/systemd/ja4sentinel.service
dest: /usr/lib/systemd/system/ja4sentinel.service
mode: "0644"
config:
- source: packaging/systemd/config.yml
dest: /etc/ja4sentinel/config.yml.default
mode: "0640"
config_file: true
- source: packaging/systemd/config.yml
dest: /usr/share/ja4sentinel/config.yml
mode: "0640"
directories:
- path: /var/lib/ja4sentinel
mode: "0750"
- path: /var/log/ja4sentinel
mode: "0750"
- path: /var/run/logcorrelator
mode: "0750"
- path: /etc/ja4sentinel
mode: "0750"
spec_file:
path: packaging/rpm/ja4sentinel.spec
version_macro: "%{?build_version}%{!?build_version:1.0.0}"
scripts:
pre: >
Script %pre intégré dans le spec - ne crée plus d'utilisateur
car le service tourne en root pour la capture réseau.
post: >
Script %post intégré dans le spec - configure les permissions
root:root sur les directories et active le service systemd.
dependencies:
rpm:
- systemd
- libpcap >= 1.9.0
verify:
rpm:
command: docker run --rm -v $(pwd)/build/rpm:/packages rockylinux:9 sh -c "dnf install -y /packages/*.rpm"
service:
systemd:
unit_name: "ja4sentinel.service"
description: "JA4 client fingerprinting daemon"
wanted_by: "multi-user.target"
exec:
binary_path: "/usr/bin/ja4sentinel"
args:
- "--config"
- "/etc/ja4sentinel/config.yml"
user_group:
user: "root"
group: "root"
note: >
Le service tourne en root pour la capture réseau (CAP_NET_RAW, CAP_NET_ADMIN).
La création d'utilisateur ja4sentinel a été supprimée.
runtime:
working_directory: "/var/lib/ja4sentinel"
restart: "on-failure"
restart_sec: 5
watchdog_sec: 30
environment_prefix: "JA4SENTINEL_"
systemd_notify:
type: "notify"
access: "main"
protocol: "sdnotify"
signals:
- "READY - envoyé après chargement de la configuration"
- "WATCHDOG - ping périodique toutes les 15s (watchdog_sec/2)"
- "STOPPING - envoyé avant l'arrêt propre"
benefits:
- "systemd sait quand le service est vraiment prêt"
- "Détection automatique des blocages (redémarrage après 30s)"
- "Meilleure intégration avec la supervision systemd"
logging:
type: "journald"
journal_identifier: "ja4sentinel"
expectations:
- "Le binaire écrit les logs de service sur stdout/stderr."
- "Les messages doivent inclure au minimum un niveau (INFO/ERROR) et un composant."
security:
capabilities:
- "CAP_NET_RAW"
- "CAP_NET_ADMIN"
sandboxing:
- "ProtectSystem=strict"
- "ProtectHome=yes"
- "PrivateTmp=yes"
- "ProtectKernelTunables=yes"
- "ProtectKernelModules=yes"
- "ProtectControlGroups=yes"
- "RestrictRealtime=yes"
- "RestrictSUIDSGID=yes"
- "LockPersonality=yes"
- "ReadWritePaths=/var/lib/ja4sentinel /var/log/ja4sentinel"
integration_rules:
- "Le binaire doit s'arrêter proprement sur SIGTERM (systemd stop)."
- "Le module cmd_ja4sentinel gère les signaux et termine la capture proprement."
- "Les chemins (config, socket UNIX, logs) doivent être compatibles avec FHS (/etc, /var/run, /var/log)."
- "Le module cmd_ja4sentinel capture SIGTERM/SIGINT et déclenche un arrêt propre (stop capture, flush outputs, fermer socket UNIX)."
- "Le processus doit retourner un code de sortie non nul en cas d'erreur fatale au démarrage."
- "Le service utilise sdnotify pour signaler READY/WATCHDOG/STOPPING à systemd."
logging:
strategy:
description: >
ja4sentinel écrit ses logs techniques sur stdout/stderr au format JSON lines,
afin que systemd/journald puissent les collecter et les filtrer.
format: "json_lines"
fields:
- "timestamp"
- "level"
- "component" # capture, tlsparse, fingerprint, output, service, ...
- "message"
- "trace_id" # optionnel
- "conn_id" # optionnel (identifiant de flux TCP)
rules:
- "Pas décriture directe dans des fichiers de log techniques depuis le code (stdout/stderr uniquement)."
- "Les logs techniques du daemon passent par stdout/stderr (systemd/journald)."
- "Les outputs métiers (LogRecord JA4) sont gérés par le module output, vers socket UNIX et/ou fichier JSON."
json_format:
description: >
Les LogRecord métiers (JA4 + métadonnées) sont sérialisés en JSON objet plat,
avec des champs nommés explicitement pour ingestion dans ClickHouse.
rules:
- "Pas de tableaux imbriqués ni dobjets deeply nested."
- "Toutes les métadonnées IP/TCP sont flatten sous forme de champs scalaires nommés."
- "Les noms de champs suivent la convention: ip_meta_*, tcp_meta_*, ja4*."
logrecord_schema:
# Exemple de mapping pour api.LogRecord (résumé)
- "src_ip"
- "src_port"
- "dst_ip"
- "dst_port"
- "ip_meta_ttl"
- "ip_meta_total_length"
- "ip_meta_id"
- "ip_meta_df"
- "tcp_meta_window_size"
- "tcp_meta_mss"
- "tcp_meta_window_scale"
- "tcp_meta_options" # string joinée, ex: 'MSS,SACK,TS,NOP,WS'
- "ja4"
- "ja4_hash"
- "ja3"
- "ja3_hash"