fix(acl): Glob-Wurzeln brauchen r-x — Ursache der stummen Datenluecke auf x14

x14 sendete 105 Metriken, aber KEINE apache-Metriken und keine Logs. Der
Installer meldete Erfolg. Telegraf selbst nannte den Grund:

  W! [inputs.tail] Directory "/home/users" is not readable but is followed
     by wildcards, make sure you set read permissions or globbing will not work!

Drei Ursachen, alle behoben:

1. `setfacl -m u:telegraf:x` auf der Glob-Wurzel ist FALSCH. Eine Wildcard wird
   per readdir expandiert — dafuer braucht das Verzeichnis `r-x`, nicht nur `x`.
   Neue Logik: Muster werden mit `set -f` gelesen, dann je Treffer die Datei mit
   `r` und JEDE Ebene zwischen Wurzel und Datei mit `r-x` belegt.
   Gemessen: alte ACL -> 1 Warnung, 1 Punkt (nur /var/log/apache2);
   neue ACL -> 0 Warnungen, 4 Punkte (alle 3 Dateien inkl. User-Homes).

2. Der Gegentest war blind: er las jede gefundene Datei per `head -c 1` als
   telegraf. Das konnte nicht fehlschlagen, weil die Datei vom ROOT-Shell
   expandiert wurde — die Glob-Wurzel blieb unlesbar, Telegraf bekam 0 Treffer,
   die Meldung sagte trotzdem "alle N Logdateien lesen". Jetzt wird die
   GLOB-AUFLOESUNG gemessen: dasselbe Muster im Dienstkontext expandieren und die
   Trefferzahl vergleichen. 0 von 0 zaehlt als Fehler, nicht als Erfolg.

3. `for p in $_pfadquelle` ohne `set -f` expandiert die Muster im Root-Shell.
   _pattern war danach ein konkreter Dateiname, der Wildcard-Zweig lief nie, und
   die ACL landete nur auf den Dateien. Real passiert: "ACL auf 3 Datei(en)",
   "kein Glob-Muster gefunden", 0 Treffer im Dienst.

Verifiziert mit echtem Telegraf 1.40.0 und REALEN KeyHelp-Rechten
(/home/users/<user> 0700, logs 0750, Dateien 0640).
This commit is contained in:
mm committed 2026-09-14 11:21:00 +02:00
1 parent 3f9ac51c45
commit 6f82bad6f9
2 files changed
+98 -93

No files matched your search

+97 -92
View File
@@ -625,112 +625,117 @@ grant_apache_log_access() {
return 0
fi
# 1) Verzeichnisse aus der aktiven Conf ermitteln.
# 1) Glob-Muster aus der aktiven Conf ermitteln.
#
# REIHENFOLGE-BUG (real): diese Funktion liest
# /etc/telegraf/telegraf.d/apache.conf
# die erst SPAETER im Ablauf geschrieben wird. Beim ERSTEN Install ist sie
# nicht da -> _roots bleibt leer -> der Fallback unten greift auf die groben
# Panel-Wurzeln (/home/users, /var/www/vhosts) zu, also auf zehntausende
# Dateien inklusive Mails und Webinhalten. Beim zweiten Lauf (x22) existierte
# die Conf und lieferte die praezisen Domain-Ordner (83 Wu­rzeln).
# Deshalb: fehlt die Conf, wird sie hier aus den erkannten Pfaden erzeugt,
# BEVOR die ACL gesetzt wird.
# QUELLE DER PFADE, richtig herum:
# 1. die aktive apache.conf (Folge-Laeufe — dort stehen bereits die Muster)
# 2. sonst APACHE_LOG (in DIESEM Lauf erkannt; beim Erstinstall ist die Conf
# noch nicht geschrieben, und genau dann griff vorher der grobe Fallback)
# QUELLE, richtig herum:
# 1. die aktive apache.conf (Folge-Laeufe — dort stehen die Muster)
# 2. sonst APACHE_LOG (in DIESEM Lauf erkannt; beim Erstinstall existiert die
# Conf noch nicht — vorher griff hier ein grober Fallback auf /home/users
# und /var/www/vhosts, also auf Mails und Webinhalte)
_pfadquelle=""
if [ -f /etc/telegraf/telegraf.d/apache.conf ]; then
_pfadquelle="$(sed -n 's/^[[:space:]]*files = \[\(.*\)\].*/\1/p' \
/etc/telegraf/telegraf.d/apache.conf 2>/dev/null | tr -d '"' | tr ',' ' ')"
fi
[ -n "$_pfadquelle" ] || _pfadquelle="${APACHE_LOG:-}"
[ -n "$_pfadquelle" ] || return 0
_roots=""
for _f in $_pfadquelle; do
case "$_f" in
*/\**) _f="${_f%%/\**}" ;; # /a/b/** -> /a/b
*/*) _f="${_f%/*}" ;; # /a/b/*/x -> /a/b/*
esac
case "$_f" in
*/\*|*/\*/*) _f="${_f%%\**}" ;; # Glob-Rest abschneiden
esac
_f="${_f%/}"
[ -d "$_f" ] && _roots="$_roots $_f"
done
# Panel-typische Wurzeln direkt mitnehmen (auch wenn die Conf sie globt)
for _d in /home/users /home/*/logs /var/www/vhosts /usr/local/apache/domlogs; do
[ -d "$_d" ] && _roots="$_roots $_d"
done
[ -n "$_roots" ] || return 0
acl_set() { timeout 30 setfacl -m "u:$1:$2" "$3" 2>/dev/null || true; }
_n=0
# Timeout pro Wurzel: setfacl laeuft rekursiv. Auf einem Panel-Host kann eine
# Wurzel sehr gross sein; ohne Grenze sieht ein langsamer Lauf wie ein Haenger
# aus. 120 s pro Wurzel sind grosszuegig und begrenzen die Gesamtdauer.
for _r in $_roots; do
# Nur die LOG-Verzeichnisse, nicht den ganzen User-Baum: /home/users
# enthaelt auch Mails, Webinhalte und Backups — ein rekursives Recht darauf
# waere wirkungslos fuer die Logs, aber teuer und semantisch falsch.
_targets=""
case "$_r" in
/home/*|/var/www*)
# Untergeordnete logs/-Verzeichnisse einsammeln (eine Ebene tief genug)
_targets="$(find "$_r" -maxdepth 3 -type d -name 'logs' 2>/dev/null | head -500 | tr '\n' ' ')"
[ -n "$_targets" ] || _targets="$_r" ;;
*) _targets="$_r" ;;
esac
for _t in $_targets; do
[ -d "$_t" ] || continue
# DURCHGANGSRECHT auf allen ELTERNVERZEICHNISSEN bis zur Wurzel.
# Realer Fehler: /home/users/<user> ist oft chmod 700. Ein setfacl -R auf
# .../<user>/logs setzt nichts auf <user> selbst — telegraf kommt dann
# nicht bis zur Datei, obwohl die Meldung "ACL gesetzt" erscheint.
_p="$(dirname "$_t")"
while [ -n "$_p" ] && [ "$_p" != "/" ] && [ "$_p" != "." ]; do
timeout 30 setfacl -m "u:$_u:x" "$_p" 2>/dev/null || true
# Default-ACL auf Zwischenverzeichnissen, damit neue Ordner erben
case "$_p" in
/home|/home/users|/var|/var/www|/usr|/usr/local) ;;
*) timeout 30 setfacl -d -m "u:$_u:x" "$_p" 2>/dev/null || true ;;
esac
_p="$(dirname "$_p")"
done
# Leserecht (r) auf die Dateien, Durchgang (x) auf die Ordner:
# rX vergibt x nur fuer Verzeichnisse.
timeout 120 setfacl -R -m "u:$_u:rX" "$_t" 2>/dev/null && _n=$((_n+1))
# Neu angelegte Dateien/Ordner erben das Recht (default-ACL nur fuer dirs).
timeout 120 setfacl -R -d -m "u:$_u:rX" "$_t" 2>/dev/null || true
done
done
[ "$_n" -gt 0 ] && log "Log-Leserechte gesetzt (ACL fuer '$_u') auf $_n Wurzel(n)."
# 2) Gegentest: JEDEN Logpfad als Dienst-Benutzer pruefen — nicht nur den ersten.
# 1a) Rechte aus den MUSTERN ableiten.
#
# Realer Fehler: der Test brach beim ersten Treffer ab. Auf einem KeyHelp-Host
# war das /var/log/apache2/keyhelp/access.log (root:adm, immer lesbar) — die
# Meldung lautete "Gegentest OK", waehrend ALLE Domain-Logs in den User-Homes
# unlesbar blieben. Ein Test, der beim ersten Erfolg abbricht, verdeckt genau
# die Pfade, fuer die er gedacht ist.
_tested=0; _fail=0; _firstfail=""
for _f in $_pfadquelle; do
# shellcheck disable=SC2086
for _cand in $(ls $_f 2>/dev/null); do
[ -s "$_cand" ] || continue
_tested=$((_tested+1))
if ! su -s /bin/sh "$_u" -c "head -c 1 \"$_cand\" >/dev/null 2>&1"; then
_fail=$((_fail+1))
[ -n "$_firstfail" ] || _firstfail="$_cand"
fi
done
# Eine Wildcard wird per readdir expandiert. Dafuer muss JEDES Verzeichnis auf
# dem Weg zum Treffer AUFLISTBAR sein — also `r-x`, nicht nur `x`. `x` laesst
# Telegraf ein Verzeichnis betreten, aber nicht auflisten.
#
# REALER, STILLER FEHLER (x14): die vorige Fassung gab /home/users nur `--x`.
# Telegraf meldete
# W! [inputs.tail] Directory "/home/users" is not readable but is followed
# by wildcards, make sure you set read permissions or globbing will not
# work!
# und lieferte 0 von 56 Logdateien: Confs geladen, kein Fehler, Gegentest gruen,
# aber es floss nichts — genau die stille Datenluecke, die nicht passieren darf.
#
# `set -f` ist hier PFLICHT. Ohne expandiert der Root-Shell jedes Muster, BEVOR
# die Schleife es sieht; _pattern waere ein konkreter Dateiname, der Wildcard-
# Zweig liefe nie, und die ACL landete nur auf den Dateien — die Glob-Wurzeln
# blieben unlesbar. Real passiert: "ACL auf 2 Datei(en)", 0 Treffer im Dienst.
set -f
for _pattern in $_pfadquelle; do
case "$_pattern" in
*'*'*)
# Wurzel des Musters = alles vor dem ERSTEN Stern.
# ${_pattern%%**} waere leer (Muster "**" = beliebiger String), deshalb
# die Zeichenklasse: ${_pattern%%[*]*} schneidet ab dem ersten Stern.
_pre="${_pattern%%[*]*}"
_root="${_pre%/*}"
[ -n "$_root" ] || _root="/"
# Hier wird expandiert -> die Wurzel muss auflistbar sein.
[ -d "$_root" ] && acl_set "$_u" r-x "$_root"
# Konkrete Treffer: jetzt mit Expansion.
set +f
for _hit in $_pattern; do
[ -f "$_hit" ] || continue
acl_set "$_u" r "$_hit"
_touched="$_touched $_hit"
# Jede Ebene zwischen Wurzel und Datei: auflistbar UND betretbar.
_d="$(dirname "$_hit")"
while [ -n "$_d" ] && [ "$_d" != "/" ] && [ "$_d" != "$_root" ]; do
acl_set "$_u" r-x "$_d"
_d="$(dirname "$_d")"
done
done
set -f
;;
*)
if [ -f "$_pattern" ]; then acl_set "$_u" r "$_pattern"; _touched="$_touched $_pattern"; fi
;;
esac
done
# 2) Gegentest: die GLOB-AUFLOESUNG pruefen, nicht die Datei-Lesbarkeit.
#
# REALER FEHLER: der alte Test las jede gefundene Datei per `head -c 1` als
# telegraf. Das KONNTE nicht fehlschlagen — die Datei war ja vom Root-Shell
# expandiert worden; eine ACL lag auf der Datei, nur die Glob-Wurzel blieb
# unlesbar. Telegraf bekam 0 Treffer, die Meldung sagte "alle N lesen".
# Hier wird deshalb GENAU das gemessen, was Telegraf tut: dasselbe Muster im
# Dienstkontext expandieren und die Trefferzahl MIT der Wurzelzahl vergleichen.
_tested=0; _fail=0; _firstfail=""
for _pattern in $_pfadquelle; do
case "$_pattern" in *'*'*) : ;; *) continue ;; esac
# Trefferzahl als root: dafür MUSS expandiert werden.
set +f
_root_cnt=0
for _c in $_pattern; do [ -f "$_c" ] && _root_cnt=$((_root_cnt+1)); done
set -f
# Trefferzahl als Dienstbenutzer: das Muster unquoted uebergeben, damit die
# Ziel-Shell selbst globbt.
_cnt="$(su -s /bin/sh "$_u" -c '
n=0
for _c in '"$_pattern"'; do [ -f "$_c" ] && n=$((n+1)); done
printf "%s" "$n"' 2>/dev/null)"
_cnt="${_cnt:-0}"
_tested=$((_tested+1))
# Blind muss ausgeschlossen sein: 0 von 0 waere kein Beweis.
if [ "$_root_cnt" -eq 0 ]; then
_fail=$((_fail+1))
[ -n "$_firstfail" ] || _firstfail="$_pattern (kein Treffer als root — Muster pruefen)"
elif [ "$_cnt" -lt "$_root_cnt" ]; then
_fail=$((_fail+1))
[ -n "$_firstfail" ] || _firstfail="$_pattern (Dienst sieht $_cnt von $_root_cnt)"
fi
done
set +f
_n=0
[ -n "$_touched" ] && _n=$(printf '%s\n' $_touched | grep -c .)
[ "$_n" -gt 0 ] && log "Log-Leserechte gesetzt (ACL fuer '$_u') auf $_n Datei(en)."
if [ "$_tested" -eq 0 ]; then
log "HINWEIS: keine gefuellte Logdatei gefunden — Gegentest nicht moeglich."
log "HINWEIS: kein Glob-Muster gefunden — Gegentest nicht moeglich."
elif [ "$_fail" -eq 0 ]; then
log "Gegentest: '$_u' kann alle $_tested Logdateien lesen."
log "Gegentest: '$_u' loest alle $_tested Log-Muster vollstaendig auf."
else
log "WARNUNG: $_fail von $_tested Logdateien sind fuer '$_u' NICHT lesbar."
log "WARNUNG: $_fail von $_tested Log-Mustern sind fuer '$_u' NICHT aufloesbar."
log " Beispiel: $_firstfail"
log " Pruefen mit: sh install.sh --diagnose"
fi