feat(proxy): réception rsyslog générique pour les services exposés #25

Closed
Damien wants to merge 2 commits from feat/proxy-rsyslog into main
Owner

Contexte

Un jail fail2ban tournant dans le LXC d'un service applique ses règles sur des paquets dont l'IP source est celle du proxy sur le tailnet — un ban local bannirait donc le proxy et couperait le service. La détection doit avoir lieu où est le signal (le log applicatif), le bannissement là où arrivent les connexions publiques (le proxy). D'où la nécessité de faire remonter les logs applicatifs jusqu'ici.

Cette issue pose uniquement l'infrastructure générique — aucune mention d'un service en particulier (vérifié : git diff | grep -i gitea ne remonte rien).

Implémentation

  • /etc/rsyslog.d/10-remote-receiver.conf : listener imtcp sur RSYSLOG_PORT (défaut 5514), bind RSYSLOG_BIND_ADDR (défaut 0.0.0.0). Tout ce qui n'est pas repris par un 50-<service>.conf plus spécifique atterrit dans /var/log/remote/<hostname-émetteur>.log via un ruleset dédié ($RuleSet remoteLogs), pour ne jamais se mélanger au /var/log/syslog du proxy lui-même.
  • /etc/logrotate.d/remote-logs : rotation quotidienne, 7 jours, copytruncate (indispensable pour que fail2ban ne perde pas son descripteur de fichier).
  • proxy/README.md : nouvelle section documentant le pattern 50-<service>.conf (format attendu, exemple générique myservice), et le tradeoff sur le bind 0.0.0.0 (couvert par UFW default deny + allow in on tailscale0 ; alternative RSYSLOG_BIND_ADDR sur IP tailnet écartée pour ne pas complexifier l'enchaînement avec TS_AUTHKEY optionnel).

Aucune modification des règles UFW existantes : le port 5514 n'étant jamais dans les règles publiques (80/tcp, 443/tcp), la politique actuelle le couvre déjà sans ajout.

Vérification

  • Syntaxe rsyslog : validée avec rsyslogd -N1 dans un conteneur Debian 12 jetable — config OK.
  • Fonctionnel : dans le même conteneur, envoi d'un message syslog RFC5424 en TCP vers 127.0.0.1:5514 (nc) → confirmé atterrir dans /var/log/remote/testhost.log avec le contenu attendu.
  • Logrotate : logrotate -d /etc/logrotate.d/remote-logs sans erreur (conteneur jetable).
  • shellcheck proxy/install.sh : aucune nouvelle alerte (SC2006 préexistant en ligne 307, sur du code non touché par cette PR).

Idempotence

Écriture inconditionnelle des deux fichiers de conf à chaque run (même pattern que le bloc fail2ban existant) — un rejeu réapplique simplement la version courante, sans duplication. apt install rsyslog est un no-op si déjà présent. Aucune règle UFW n'est ajoutée donc rien à dédupliquer de ce côté.

Closes #20

## Contexte Un jail fail2ban tournant dans le LXC d'un service applique ses règles sur des paquets dont l'IP source est celle du proxy sur le tailnet — un ban local bannirait donc le proxy et couperait le service. La détection doit avoir lieu où est le signal (le log applicatif), le bannissement là où arrivent les connexions publiques (le proxy). D'où la nécessité de faire remonter les logs applicatifs jusqu'ici. Cette issue pose uniquement l'infrastructure générique — aucune mention d'un service en particulier (vérifié : `git diff | grep -i gitea` ne remonte rien). ## Implémentation - `/etc/rsyslog.d/10-remote-receiver.conf` : listener `imtcp` sur `RSYSLOG_PORT` (défaut `5514`), bind `RSYSLOG_BIND_ADDR` (défaut `0.0.0.0`). Tout ce qui n'est pas repris par un `50-<service>.conf` plus spécifique atterrit dans `/var/log/remote/<hostname-émetteur>.log` via un ruleset dédié (`$RuleSet remoteLogs`), pour ne jamais se mélanger au `/var/log/syslog` du proxy lui-même. - `/etc/logrotate.d/remote-logs` : rotation quotidienne, 7 jours, `copytruncate` (indispensable pour que fail2ban ne perde pas son descripteur de fichier). - `proxy/README.md` : nouvelle section documentant le pattern `50-<service>.conf` (format attendu, exemple générique `myservice`), et le tradeoff sur le bind `0.0.0.0` (couvert par UFW `default deny` + `allow in on tailscale0` ; alternative `RSYSLOG_BIND_ADDR` sur IP tailnet écartée pour ne pas complexifier l'enchaînement avec `TS_AUTHKEY` optionnel). Aucune modification des règles UFW existantes : le port 5514 n'étant jamais dans les règles publiques (`80/tcp`, `443/tcp`), la politique actuelle le couvre déjà sans ajout. ## Vérification - **Syntaxe rsyslog** : validée avec `rsyslogd -N1` dans un conteneur Debian 12 jetable — config OK. - **Fonctionnel** : dans le même conteneur, envoi d'un message syslog RFC5424 en TCP vers `127.0.0.1:5514` (`nc`) → confirmé atterrir dans `/var/log/remote/testhost.log` avec le contenu attendu. - **Logrotate** : `logrotate -d /etc/logrotate.d/remote-logs` sans erreur (conteneur jetable). - `shellcheck proxy/install.sh` : aucune nouvelle alerte (SC2006 préexistant en ligne 307, sur du code non touché par cette PR). ## Idempotence Écriture inconditionnelle des deux fichiers de conf à chaque run (même pattern que le bloc fail2ban existant) — un rejeu réapplique simplement la version courante, sans duplication. `apt install rsyslog` est un no-op si déjà présent. Aucune règle UFW n'est ajoutée donc rien à dédupliquer de ce côté. Closes #20
Damien added 1 commit 2026-07-31 15:53:10 +00:00
A fail2ban jail running inside a service's own LXC only ever sees this
proxy's tailnet IP as the connection source, so it would end up
banning the proxy itself. Detection needs to stay at the service's
application log; banning needs to happen here, at the edge where
public connections terminate.

Adds a generic imtcp listener (port RSYSLOG_PORT, default 5514) that
routes anything unclaimed by a later 50-<service>.conf into
/var/log/remote/<sender-hostname>.log, plus logrotate with
copytruncate so fail2ban never loses its file descriptor. No
service-specific routing yet — that's one 50-<service>.conf per
service, documented here for the next issue to follow.

Refs #20
Author
Owner

Le découpage est bon — listener générique, pattern 50-<service>.conf, aucune occurrence de « gitea », tradeoff sur RSYSLOG_BIND_ADDR correctement documenté, copytruncate bien justifié. Un problème de conception bloquant.

Bloquant — le catch-all du 10- s'exécute avant les règles des 50-

rsyslog charge /etc/rsyslog.d/*.conf par ordre alphabétique, et les règles d'un même ruleset sont évaluées dans l'ordre de chargement. Le fichier 10-remote-receiver.conf place un catch-all dans le ruleset remoteLogs :

$RuleSet remoteLogs
$template RemoteLogPath,"/var/log/remote/%HOSTNAME%.log"
*.* ?RemoteLogPath

Un 50-gitea.conf ajouté ensuite ajoute ses règles après ce catch-all. Résultat : chaque message part d'abord dans /var/log/remote/<host>.log, puis dans le fichier dédié. Double écriture, double volume, double rotation.

Ça rend aussi la ligne du README fausse :

Anything not claimed by a more specific routing file lands in /var/log/remote/<sender-hostname>.log

Tout y atterrit, y compris ce qui est réclamé — parce que le catch-all passe en premier.

Deux corrections possibles :

  • Renuméroter le catch-all en 90-remote-fallback.conf, en gardant 10- pour le seul module()/input(). Les 50- s'intercalent alors naturellement et leur stop fait son office. C'est ce qui correspond le mieux à l'intention décrite dans le README.
  • Ou garder un seul fichier et documenter que les services doivent s'insérer avec un préfixe < 10, ce qui est nettement moins lisible.

Dans les deux cas, l'exemple du README doit se terminer par stop — sans lui, le message continue vers les règles suivantes.

Secondaire — syntaxe legacy mélangée à la syntaxe moderne

$RuleSet et $template sont les directives legacy, ici mêlées à module(load=) / input(type=). C'est précisément ce mélange qui rend l'ordre d'évaluation difficile à raisonner. La forme moderne est explicite et supprime l'ambiguïté :

ruleset(name="remoteLogs") {
    action(type="omfile" dynaFile="RemoteLogPath")
}

Ça a aussi l'avantage de supprimer les \$ échappés du heredoc non quoté.

Vérification demandée

Après correction : envoyer un message taggé depuis un nœud du tailnet et confirmer qu'il apparaît dans un seul fichier, pas deux.

Le découpage est bon — listener générique, pattern `50-<service>.conf`, aucune occurrence de « gitea », tradeoff sur `RSYSLOG_BIND_ADDR` correctement documenté, `copytruncate` bien justifié. Un problème de conception bloquant. ## Bloquant — le catch-all du `10-` s'exécute avant les règles des `50-` rsyslog charge `/etc/rsyslog.d/*.conf` par ordre alphabétique, et les règles d'un même ruleset sont évaluées dans l'ordre de chargement. Le fichier `10-remote-receiver.conf` place un catch-all dans le ruleset `remoteLogs` : ``` $RuleSet remoteLogs $template RemoteLogPath,"/var/log/remote/%HOSTNAME%.log" *.* ?RemoteLogPath ``` Un `50-gitea.conf` ajouté ensuite ajoute ses règles **après** ce catch-all. Résultat : chaque message part d'abord dans `/var/log/remote/<host>.log`, puis dans le fichier dédié. Double écriture, double volume, double rotation. Ça rend aussi la ligne du README fausse : > Anything not claimed by a more specific routing file lands in `/var/log/remote/<sender-hostname>.log` Tout y atterrit, y compris ce qui est réclamé — parce que le catch-all passe en premier. Deux corrections possibles : - **Renuméroter le catch-all en `90-remote-fallback.conf`**, en gardant `10-` pour le seul `module()`/`input()`. Les `50-` s'intercalent alors naturellement et leur `stop` fait son office. C'est ce qui correspond le mieux à l'intention décrite dans le README. - Ou garder un seul fichier et documenter que les services doivent s'insérer avec un préfixe `< 10`, ce qui est nettement moins lisible. Dans les deux cas, l'exemple du README doit se terminer par `stop` — sans lui, le message continue vers les règles suivantes. ## Secondaire — syntaxe legacy mélangée à la syntaxe moderne `$RuleSet` et `$template` sont les directives legacy, ici mêlées à `module(load=)` / `input(type=)`. C'est précisément ce mélange qui rend l'ordre d'évaluation difficile à raisonner. La forme moderne est explicite et supprime l'ambiguïté : ``` ruleset(name="remoteLogs") { action(type="omfile" dynaFile="RemoteLogPath") } ``` Ça a aussi l'avantage de supprimer les `\$` échappés du heredoc non quoté. ## Vérification demandée Après correction : envoyer un message taggé depuis un nœud du tailnet et confirmer qu'il apparaît dans **un seul** fichier, pas deux.
Damien added 1 commit 2026-08-01 16:10:26 +00:00
Le catch-all du récepteur générique vivait dans 10-remote-receiver.conf,
chargé avant tout 50-<service>.conf (rsyslog charge /etc/rsyslog.d/*.conf
par ordre alphabétique, et les règles d'un ruleset s'exécutent dans
l'ordre de chargement). Chaque message tagué finissait donc écrit deux
fois : une fois par le catch-all générique, une fois par sa règle
dédiée.

10-remote-receiver.conf ne contient plus que module()/input(). Le
catch-all part dans 90-remote-fallback.conf, chargé après tout
50-<service>.conf, dont le `stop` empêche alors effectivement la
double écriture.

Gardé sur la syntaxe legacy $RuleSet plutôt que l'objet moderne
ruleset(name=...){...} suggéré en review : ce dernier refuse d'être
déclaré une seconde fois pour le même nom ("ruleset ... specified more
than once"), ce qui casserait dès qu'un 50-<service>.conf ajoute ses
propres règles au même ruleset remoteLogs — précisément le mécanisme
que ce découpage doit permettre. $RuleSet est un sélecteur de contexte,
pas une déclaration unique ; n'importe quel nombre de fichiers peut le
rouvrir pour y ajouter des règles. Détail dans le commentaire du script
et dans le README.

README : table mise à jour (3 fichiers au lieu de 2), exemple avec
`stop`, et la phrase sur ce qui atterrit dans /var/log/remote/ corrigée
(uniquement ce qui n'est pas réclamé, pas "tout").
Author
Owner

Corrigé pour le point bloquant (4744de1). Un point à signaler sur la syntaxe moderne suggérée : elle s'est révélée incompatible avec le mécanisme même que ce découpage doit permettre. Détail ci-dessous plutôt que de la contourner en silence.

Découpage 10-/50-/90- : fait

10-remote-receiver.conf ne contient plus que module()/input(). Le catch-all part dans 90-remote-fallback.conf, chargé après tout 50-<service>.conf. Vérifié avec les vrais binaires (Debian 12 jetable), configs telles qu'écrites par le script :

=== gitea.log ===
2026-08-01T16:06:54 somehost gitea Failed authentication attempt from 203.0.113.7
=== /var/log/remote/ ===
otherhost.log
2026-08-01T16:06:55 otherhost otherservice unrelated message

Un message tagué gitea (avec 50-gitea.conf de #21/#28 présent) n'apparaît plus que dans /var/log/gitea/gitea.log — plus de double écriture. Un message non tagué continue de tomber dans le catch-all générique, comme attendu.

Sur ruleset(name=...){...} — gardé sur $RuleSet, et voici pourquoi

En testant la syntaxe moderne suggérée, j'ai trouvé qu'elle ne peut pas être rouverte : rsyslog rejette catégoriquement une seconde déclaration ruleset(name="remoteLogs"){...} pour le même nom, avec ou sans mélange avec $RuleSet :

$ rsyslogd -N1 -f /etc/rsyslog.conf
rsyslogd: error: ruleset 'remoteLogs' specified more than once [v8.2302.0 try https://www.rsyslog.com/e/2306]

Testé dans trois configurations :

  • deux blocs ruleset(name="remoteLogs"){...} (un dans 10-/90-, un second imaginaire) → erreur.
  • un bloc moderne dans 90- + un $RuleSet remoteLogs legacy dans 50-gitea.conf (celui déjà ouvert dans #21/#28) → même erreur, et pire : le service continue de tourner (rsyslog traite l'erreur comme non fatale au démarrage) mais le catch-all de 90- ne reçoit plus rien du tout — la config moderne se fait silencieusement écraser/ignorer par la tentative de réouverture.
  • la syntaxe moderne seule, sans aucun 50-<service>.conf présent → valide parfaitement (rsyslogd -N1 propre).

Autrement dit : ruleset(name=...){...} fonctionne très bien pour cette PR isolée, mais casse dès que #21/#28 (déjà ouverte, 50-gitea.conf en $RuleSet legacy) est chargée en même temps — précisément le scénario que ce découpage 10-/50-/90- est censé rendre correct. $RuleSet <nom> est un sélecteur de contexte réouvrable, pas une déclaration unique ; c'est ce qui permet à n'importe quel nombre de 50-<service>.conf de s'ajouter au fil du temps sans toucher aux fichiers existants — l'exigence même de l'issue #20.

J'ai donc gardé $RuleSet/$template pour 90-remote-fallback.conf, avec un commentaire dans le script et dans le README expliquant pourquoi (avec ce test à l'appui). Si tu veux malgré tout la syntaxe moderne, il faudrait revoir l'architecture de fond (par ex. un ruleset séparé par service, ce qui ne colle pas avec un seul listener imtcp partagé) — je n'ai pas voulu trancher ça seul.

README

Table à 3 fichiers, exemple avec stop ajouté, phrase sur ce qui atterrit dans /var/log/remote/ corrigée (uniquement le non-réclamé, pas "tout").

shellcheck proxy/install.sh : aucune nouvelle alerte (SC2006 préexistant, non lié).

Corrigé pour le point bloquant (4744de1). Un point à signaler sur la syntaxe moderne suggérée : elle s'est révélée incompatible avec le mécanisme même que ce découpage doit permettre. Détail ci-dessous plutôt que de la contourner en silence. ## Découpage 10-/50-/90- : fait `10-remote-receiver.conf` ne contient plus que `module()`/`input()`. Le catch-all part dans `90-remote-fallback.conf`, chargé après tout `50-<service>.conf`. Vérifié avec les vrais binaires (Debian 12 jetable), configs telles qu'écrites par le script : ``` === gitea.log === 2026-08-01T16:06:54 somehost gitea Failed authentication attempt from 203.0.113.7 === /var/log/remote/ === otherhost.log 2026-08-01T16:06:55 otherhost otherservice unrelated message ``` Un message tagué `gitea` (avec `50-gitea.conf` de #21/#28 présent) n'apparaît plus que dans `/var/log/gitea/gitea.log` — plus de double écriture. Un message non tagué continue de tomber dans le catch-all générique, comme attendu. ## Sur `ruleset(name=...){...}` — gardé sur `$RuleSet`, et voici pourquoi En testant la syntaxe moderne suggérée, j'ai trouvé qu'elle ne peut pas être rouverte : rsyslog rejette catégoriquement une seconde déclaration `ruleset(name="remoteLogs"){...}` pour le même nom, avec ou sans mélange avec `$RuleSet` : ``` $ rsyslogd -N1 -f /etc/rsyslog.conf rsyslogd: error: ruleset 'remoteLogs' specified more than once [v8.2302.0 try https://www.rsyslog.com/e/2306] ``` Testé dans trois configurations : - deux blocs `ruleset(name="remoteLogs"){...}` (un dans `10-`/`90-`, un second imaginaire) → erreur. - un bloc moderne dans `90-` + un `$RuleSet remoteLogs` legacy dans `50-gitea.conf` (celui déjà ouvert dans #21/#28) → même erreur, et pire : le service continue de tourner (rsyslog traite l'erreur comme non fatale au démarrage) mais **le catch-all de `90-` ne reçoit plus rien du tout** — la config moderne se fait silencieusement écraser/ignorer par la tentative de réouverture. - la syntaxe moderne **seule**, sans aucun `50-<service>.conf` présent → valide parfaitement (`rsyslogd -N1` propre). Autrement dit : `ruleset(name=...){...}` fonctionne très bien pour cette PR *isolée*, mais casse dès que #21/#28 (déjà ouverte, `50-gitea.conf` en `$RuleSet` legacy) est chargée en même temps — précisément le scénario que ce découpage 10-/50-/90- est censé rendre correct. `$RuleSet <nom>` est un sélecteur de contexte réouvrable, pas une déclaration unique ; c'est ce qui permet à n'importe quel nombre de `50-<service>.conf` de s'ajouter au fil du temps sans toucher aux fichiers existants — l'exigence même de l'issue #20. J'ai donc gardé `$RuleSet`/`$template` pour `90-remote-fallback.conf`, avec un commentaire dans le script et dans le README expliquant pourquoi (avec ce test à l'appui). Si tu veux malgré tout la syntaxe moderne, il faudrait revoir l'architecture de fond (par ex. un ruleset séparé par service, ce qui ne colle pas avec un seul listener `imtcp` partagé) — je n'ai pas voulu trancher ça seul. ## README Table à 3 fichiers, exemple avec `stop` ajouté, phrase sur ce qui atterrit dans `/var/log/remote/` corrigée (uniquement le non-réclamé, pas "tout"). `shellcheck proxy/install.sh` : aucune nouvelle alerte (SC2006 préexistant, non lié).
Damien closed this pull request 2026-08-01 17:44:13 +00:00

Pull request closed

This pull request cannot be reopened because the branch was deleted.
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Damien/infra-scripts#25