diff --git a/Dockerfile.package b/Dockerfile.package index d85637e..ea4545d 100644 --- a/Dockerfile.package +++ b/Dockerfile.package @@ -35,7 +35,7 @@ COPY . . # Build binary for Linux # Binary will be dynamically linked but compatible with all RHEL-based distros -ARG VERSION=1.0.8 +ARG VERSION=1.1.0 ARG BUILD_TIME="" ARG GIT_COMMIT="" RUN mkdir -p dist && \ @@ -53,7 +53,7 @@ FROM rockylinux:9 AS rpm-builder WORKDIR /package # VERSION must be redeclared for each stage that needs it -ARG VERSION=1.0.8 +ARG VERSION=1.1.0 # Install rpm-build tools (Rocky Linux 9) RUN dnf install -y \ @@ -64,7 +64,8 @@ RUN dnf install -y \ && dnf clean all # Setup rpmbuild directory structure -RUN mkdir -p /root/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS} +RUN mkdir -p /root/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS} && \ + mkdir -p /root/rpmbuild/SOURCES/logrotate # Copy spec file COPY packaging/rpm/ja4sentinel.spec /root/rpmbuild/SPECS/ja4sentinel.spec @@ -72,11 +73,13 @@ COPY packaging/rpm/ja4sentinel.spec /root/rpmbuild/SPECS/ja4sentinel.spec # Copy binary from Go builder and other files to SOURCES COPY --from=builder /build/dist/ja4sentinel /root/rpmbuild/SOURCES/ja4sentinel COPY packaging/systemd/ja4sentinel.service /root/rpmbuild/SOURCES/ja4sentinel.service +COPY packaging/logrotate/ja4sentinel /root/rpmbuild/SOURCES/logrotate/ja4sentinel COPY config.yml.example /root/rpmbuild/SOURCES/config.yml # Set permissions RUN chmod 755 /root/rpmbuild/SOURCES/ja4sentinel && \ chmod 644 /root/rpmbuild/SOURCES/ja4sentinel.service && \ + chmod 644 /root/rpmbuild/SOURCES/logrotate/ja4sentinel && \ chmod 640 /root/rpmbuild/SOURCES/config.yml # Build RPM for Rocky Linux 8 (el8) diff --git a/api/types.go b/api/types.go index 7b8f630..1f5c26e 100644 --- a/api/types.go +++ b/api/types.go @@ -205,6 +205,13 @@ type Logger interface { Error(component, message string, details map[string]string) } +// Reopenable defines the interface for components that support log file rotation. +// Implementations must reopen their output files when receiving a SIGHUP signal. +// This is used by systemctl reload to switch to new log files after logrotate. +type Reopenable interface { + Reopen() error +} + // Helper functions for creating and converting records // NewLogRecord creates a flattened LogRecord from TLSClientHello and Fingerprints. diff --git a/architecture.yml b/architecture.yml index f9fdf7d..d408f7f 100644 --- a/architecture.yml +++ b/architecture.yml @@ -8,6 +8,9 @@ project: (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, ...). + Le service est géré par systemd avec support de rotation des logs via logrotate. + La commande `systemctl reload ja4sentinel` permet de réouvrir les fichiers de log + après rotation (signal SIGHUP). languages: - go goals: @@ -122,6 +125,7 @@ modules: - "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)." + - "Gérer le signal SIGHUP pour la rotation des logs (systemctl reload)." allowed_dependencies: - "config" - "capture" @@ -410,16 +414,16 @@ architecture: flow_control: connection_states: - description: "États simplifiés d’un flux TCP pour minimiser la capture." + description: "États simplifiés d'un flux TCP pour minimiser la capture." states: - name: "NEW" - description: "Observation d’un SYN client sur un port surveillé, création d’un état minimal (IP/TCP meta)." + description: "Observation d'un SYN client sur un port surveillé, création d'un é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: - - "Pas de tableaux imbriqués ni d’objets deeply nested." + - "Pas de tableaux imbriqués ni d'objets 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_*, ja*." - "Pas de champ ja4_hash : le format JA4 intègre déjà son propre hachage tronqué, la chaîne complète de 38 caractères suffit." @@ -447,3 +451,23 @@ flow_control: - "ja3" - "ja3_hash" +packaging: + rpm: + description: "Package RPM pour déploiement sur serveurs Linux." + files: + - path: "/etc/logrotate.d/ja4sentinel" + description: "Script logrotate pour la rotation des fichiers de log." + note: "Fourni par le RPM, configure la rotation quotidienne avec compression." + - path: "/etc/systemd/system/ja4sentinel.service" + description: "Unité systemd pour la gestion du service." + note: "Doit inclure Type=notify et ExecReload=/bin/kill -HUP $MAINPID pour supporter systemctl reload." + logrotate: + description: "Configuration logrotate pour la rotation des logs." + behavior: + - "Rotation quotidienne ou selon taille." + - "Compression des logs archivés." + - "Envoi du signal SIGHUP au service après rotation pour réouvrir les fichiers." + reload_mechanism: + - "systemctl reload ja4sentinel déclenche le handler SIGHUP." + - "Le service réouvre ses fichiers de log sans redémarrage complet." + diff --git a/cmd/ja4sentinel/main.go b/cmd/ja4sentinel/main.go index 68ba5e3..8ae7059 100644 --- a/cmd/ja4sentinel/main.go +++ b/cmd/ja4sentinel/main.go @@ -107,9 +107,9 @@ func main() { }() } - // Setup signal handling + // Setup signal handling for shutdown and log rotation sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) // Create pipeline components captureEngine := capture.New() @@ -202,19 +202,41 @@ func main() { }() // Wait for shutdown signal or capture error - select { - case sig := <-sigChan: - appLogger.Info("main", "Received shutdown signal", map[string]string{ - "signal": sig.String(), - }) - case err := <-captureErrChan: - if err != nil { - appLogger.Error("capture", "Capture engine failed", map[string]string{ - "error": err.Error(), - }) + for { + select { + case sig := <-sigChan: + switch sig { + case syscall.SIGHUP: + // Handle log rotation - reopen output files + appLogger.Info("main", "Received SIGHUP, reopening log files", nil) + if mw, ok := outputWriter.(api.Reopenable); ok { + if err := mw.Reopen(); err != nil { + appLogger.Error("main", "Failed to reopen log files", map[string]string{ + "error": err.Error(), + }) + } else { + appLogger.Info("main", "Log files reopened successfully", nil) + } + } else { + appLogger.Warn("main", "Output writer does not support log rotation", nil) + } + case syscall.SIGINT, syscall.SIGTERM: + appLogger.Info("main", "Received shutdown signal", map[string]string{ + "signal": sig.String(), + }) + goto shutdown + } + case err := <-captureErrChan: + if err != nil { + appLogger.Error("capture", "Capture engine failed", map[string]string{ + "error": err.Error(), + }) + } + goto shutdown } } +shutdown: // Graceful shutdown appLogger.Info("main", "Shutting down...", nil) diff --git a/internal/output/writers.go b/internal/output/writers.go index 625173f..22b3bbb 100644 --- a/internal/output/writers.go +++ b/internal/output/writers.go @@ -182,6 +182,28 @@ func (w *FileWriter) Close() error { return nil } +// Reopen reopens the log file (for logrotate support) +func (w *FileWriter) Reopen() error { + w.mutex.Lock() + defer w.mutex.Unlock() + + if err := w.file.Close(); err != nil { + return fmt.Errorf("failed to close file during reopen: %w", err) + } + + // Open new file + newFile, err := os.OpenFile(w.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + return fmt.Errorf("failed to reopen file %s: %w", w.path, err) + } + + w.file = newFile + w.encoder = json.NewEncoder(newFile) + w.currentSize = 0 + + return nil +} + // UnixSocketWriter writes log records to a UNIX socket with reconnection logic type UnixSocketWriter struct { socketPath string @@ -495,6 +517,23 @@ func (mw *MultiWriter) CloseAll() error { return lastErr } +// Reopen reopens all writers that support log rotation +func (mw *MultiWriter) Reopen() error { + mw.mutex.Lock() + defer mw.mutex.Unlock() + + var lastErr error + for _, w := range mw.writers { + if reopenable, ok := w.(api.Reopenable); ok { + if err := reopenable.Reopen(); err != nil { + lastErr = err + } + } + } + + return lastErr +} + // BuilderImpl implements the api.Builder interface type BuilderImpl struct{} diff --git a/packaging/logrotate/ja4sentinel b/packaging/logrotate/ja4sentinel new file mode 100644 index 0000000..f6f2de3 --- /dev/null +++ b/packaging/logrotate/ja4sentinel @@ -0,0 +1,17 @@ +# Logrotate configuration for ja4sentinel +# Install to: /etc/logrotate.d/ja4sentinel + +/var/log/ja4sentinel/*.log { + daily + missingok + rotate 7 + compress + delaycompress + notifempty + create 0600 root root + sharedscripts + postrotate + # Send SIGHUP to ja4sentinel to reopen log files + /bin/systemctl reload ja4sentinel 2>/dev/null || true + endscript +} diff --git a/packaging/rpm/ja4sentinel.spec b/packaging/rpm/ja4sentinel.spec index 7de19cb..2efc05c 100644 --- a/packaging/rpm/ja4sentinel.spec +++ b/packaging/rpm/ja4sentinel.spec @@ -3,7 +3,7 @@ %if %{defined build_version} %define spec_version %{build_version} %else -%define spec_version 1.0.9 +%define spec_version 1.1.0 %endif Name: ja4sentinel @@ -44,6 +44,7 @@ Features: %install mkdir -p %{buildroot}/usr/bin mkdir -p %{buildroot}/etc/ja4sentinel +mkdir -p %{buildroot}/etc/logrotate.d mkdir -p %{buildroot}/var/lib/ja4sentinel mkdir -p %{buildroot}/var/log/ja4sentinel mkdir -p %{buildroot}/var/run/logcorrelator @@ -56,6 +57,9 @@ install -m 755 %{_sourcedir}/ja4sentinel %{buildroot}/usr/bin/ja4sentinel # Install systemd service install -m 644 %{_sourcedir}/ja4sentinel.service %{buildroot}/usr/lib/systemd/system/ja4sentinel.service +# Install logrotate configuration +install -m 644 %{_sourcedir}/logrotate/ja4sentinel %{buildroot}/etc/logrotate.d/ja4sentinel + # Install default config install -m 640 %{_sourcedir}/config.yml %{buildroot}/etc/ja4sentinel/config.yml.default install -m 640 %{_sourcedir}/config.yml %{buildroot}/usr/share/ja4sentinel/config.yml @@ -109,6 +113,7 @@ fi %files /usr/bin/ja4sentinel /usr/lib/systemd/system/ja4sentinel.service +/etc/logrotate.d/ja4sentinel /usr/share/ja4sentinel/config.yml %config(noreplace) /etc/ja4sentinel/config.yml.default %dir /etc/ja4sentinel @@ -117,6 +122,18 @@ fi %dir /var/run/logcorrelator %changelog +* Mon Mar 02 2026 Jacquin Antoine - 1.1.0-1 +- Add logrotate configuration for automatic log file rotation +- Add SIGHUP signal handling for log file reopening (systemctl reload) +- Add ExecReload to systemd service for graceful log rotation +- Add Reopenable interface for output writers supporting log rotation +- Add FileWriter.Reopen() method for log file rotation support +- Add MultiWriter.Reopen() method to propagate rotation to all writers +- Update main.go to handle SIGHUP signal for log rotation +- Add packaging/logrotate/ja4sentinel configuration file +- Update architecture.yml with logrotate and reload documentation +- Update Dockerfile.package to include logrotate file in RPM build + * Mon Mar 02 2026 Jacquin Antoine - 1.0.9-1 - Add SNI (Server Name Indication) extraction from TLS ClientHello - Add ALPN (Application-Layer Protocol Negotiation) extraction diff --git a/packaging/systemd/ja4sentinel.service b/packaging/systemd/ja4sentinel.service index 7967163..dc1a748 100644 --- a/packaging/systemd/ja4sentinel.service +++ b/packaging/systemd/ja4sentinel.service @@ -10,6 +10,7 @@ User=root Group=root WorkingDirectory=/var/lib/ja4sentinel ExecStart=/usr/bin/ja4sentinel --config /etc/ja4sentinel/config.yml +ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=5 WatchdogSec=30