Die Anfrage, die Sie beantwortet haben, ist jetzt ein Datensatz, den Sie verwahren
Alles andere in dieser Serie richtet den Blick nach außen: den Ursprung verbergen, die Festplatte verschlüsseln, die Jurisdiktion wählen, verhindern, dass ein Fremder ein Log über Sie führt. Dieser Guide richtet den Blick auf die Maschine selbst, denn ein Server, der Anfragen beantwortet, schreibt auf, wer sie gestellt hat — standardmäßig, an mehreren Stellen gleichzeitig, mit einer Aufbewahrung, die niemand bewusst gewählt hat.
Schauen Sie sich an, was ein Standard-Image zwei Wochen nach dem Deploy vorhält. /var/log/nginx/access.log hat eine Zeile pro Anfrage: Adresse, Zeitstempel, Pfad, Referrer, User-Agent, täglich rotiert und vierzehn Tage aufbewahrt. /var/log/auth.log erfasst jede SSH-Sitzung mit Quelladresse — auch jede einzelne Ihrer eigenen. Das Journal enthält dieselben Ereignisse noch einmal, dazu alles, was Ihre Dienste nach stderr geschrieben haben. Läuft Docker, hat jeder Container eine JSON-Log-Datei, die von Haus aus kein Größenlimit kennt. Läuft fail2ban, führt es ein Log jeder Adresse, die es je gesperrt hat, sowie eine SQLite-Datenbank mit derselben Information, und in beiden sind die Adressen vollständig.
Nichts davon ist böswillig, und das meiste ist wirklich nützlich — für etwa eine Stunde, nachdem etwas schiefgegangen ist. Das Problem ist die Form: volle Detailtiefe, lange aufbewahrt, aus Versehen statt aus Entscheidung. Die Frage, die sich bei jeder dieser Dateien lohnt, ist nicht "ist das sensibel?", sondern "was würde ich mit der Zeile, die ich vor drei Wochen geschrieben habe, überhaupt anfangen?" Für die überwältigende Mehrheit der Zeilen auf der überwältigenden Mehrheit der Server lautet die Antwort: nichts, und ein Datensatz, den Sie nie lesen werden, ist reines Risiko — gegenüber einem Einbruch, gegenüber einem Backup, das die Maschine überlebt, gegenüber jedem, der irgendwann danach fragt.
Datenminimierung ist der offizielle Name für die Abhilfe, und sie ist die am wenigsten umstrittene Idee im Datenschutz: erfassen, was die Aufgabe braucht, behalten, solange die Aufgabe es braucht, dann aufhören. Was folgt, wendet das auf eine Box an, die Sie tatsächlich betreiben, unter der Annahme, dass Sie bereits die erste Stunde nach dem Deploy durchgearbeitet haben und dass auf der Maschine etwas ist, das Schutz verdient.
SP·02Sechs Logs, und die zwei, die Menschen beim Namen nennen
Bevor Sie irgendetwas ändern, kennen Sie das Inventar. Ein kleiner Debian- oder Ubuntu-VPS, der einen Webdienst betreibt, schreibt typischerweise sechs Streams, und sie überschneiden sich mehr, als man erwartet — dasselbe Ereignis landet oft in drei Dateien mit drei unterschiedlichen Aufbewahrungsfristen.
Das nginx-Zugriffslog nennt Ihre Besucher beim Namen. Eine Zeile pro Anfrage, mit Client-Adresse, exaktem Pfad, Referrer und einem User-Agent-String, der allein schon detailliert genug ist, um ein schwacher Fingerabdruck zu sein. Das nginx-Fehlerlog ist das, das man vergisst: Es protokolliert client: 203.0.113.9 bei jedem Upstream-Timeout, jedem 403, jeder fehlgeformten Anfrage — und anders als das Zugriffslog ist sein Format fest vorgegeben und lässt sich nicht per Template anpassen.
Das Auth-Log — /var/log/auth.log bei Debian-Derivaten, /var/log/secure bei RHEL-Derivaten — nennt Sie. Jede akzeptierte Publickey-Zeile trägt Ihre Quelladresse und den Fingerabdruck des Schlüssels, der die Sitzung geöffnet hat. Wer einen Monat davon liest, erfährt, aus welchen Netzwerken Sie administrieren, zu welchen Stunden, und wie viele unterschiedliche Schlüssel Sie besitzen. Auf einer anonym gehaltenen Box ist diese Datei oft aufschlussreicher als alles, was Ihre Besucher erzeugt haben.
Das Journal enthält eine Kopie der meisten der oben genannten Daten, dazu stdout und stderr jeder Unit, und darf in der Standardinstallation auf 10 % des Dateisystems wachsen, gedeckelt bei 4 GB, bevor die ältesten Einträge verworfen werden. Auf jeder Platte ab 40 GB liegt diese Grenze bei den vollen 4 GB, was bei den Datenmengen, die ein kleiner Server erzeugt, vielen Monaten entspricht.
Anwendungs-Logs sind der Joker. Ein Framework im Debug-Modus protokolliert vollständige URLs samt Query-Strings, und Query-Strings tragen routinemäßig Session-Tokens, Passwort-Reset-Links und Suchbegriffe. PHP-FPM lässt sich so konfigurieren, dass es sein eigenes Zugriffslog schreibt, das die Request-Zeile dupliziert, die nginx ohnehin schon geschrieben hat — aus einer Datei, die Ihre nginx-Einstellungen nie berühren.
Container-Logs sind der stille Fall. Der Standardtreiber json-file von Docker rotiert nicht, sofern Sie es nicht konfigurieren, also hält /var/lib/docker/containers/*/*-json.log alles fest, was der Container seit seiner Erstellung gesagt hat. So entdeckt man häufig, dass eine "14-Tage"-Aufbewahrungsrichtlinie in Wahrheit elf Monate umfasst — dieselbe Art von Überraschung wie bei den Firewall-Regeln, die Docker an ufw vorbei schreibt.
Von den sechs tragen zwei Identifikatoren über Menschen, die es sich lohnt, gezielt zu minimieren: das nginx-Zugriffslog (Ihre Besucher) und das Auth-Log (Sie). Der Rest braucht vor allem eine Größenbegrenzung und eine kürzere Uhr.
SP·03Kürzen beim Schreiben, nicht bei der Rotation
Der naheliegende Reflex ist, ganz normal weiterzuprotokollieren und später aufzuräumen — ein nächtlicher Cronjob, der die Datei von gestern neu schreibt und dabei die Adressen entfernt. Bauen Sie das nicht. Ein Bereinigungsjob bedeutet, dass die rohen Adressen bis zu einen Tag lang tatsächlich auf der Platte lagen, und in diesem Tag wurden sie von Ihrem Backup-Lauf erfasst, womöglich vom blockbasierten Snapshot Ihres Hosts kopiert, und blieben zurück in dem, was das Dateisystem mit den alten Blöcken auch immer getan hat. Schlimmer noch: Der Job ist ein bewegliches Teil — er scheitert lautlos in der Woche, in der die Platte volläuft, und nichts sagt Ihnen, dass die Datei von gestern noch vollständig ist.
Die einzige Reduktion, auf die Sie sich wirklich verlassen können, ist die, die passiert, bevor die Zeile geschrieben wird. In nginx ist das ein map-Block, der beim Loggen ausgewertet wird und die Adresse in eine neue Variable umschreibt, sowie ein log_format, das die neue Variable statt $remote_addr verwendet. Die vollständige Adresse wird nie serialisiert. Es gibt hinterher nichts aufzuräumen, nichts zu terminieren, und nichts, das an dem Tag schiefgehen kann, an dem Sie nicht hinschauen.
# /etc/nginx/conf.d/00-privacy-log.conf -- http context, loaded before the sites
map $remote_addr $ip_trunc {
# IPv4: keep the /24, zero the host part
~(?<v4>\d+\.\d+\.\d+)\. "${v4}.0";
# IPv6: keep the first two groups, drop the rest
~(?<v6>[^:]+:[^:]+): "${v6}::";
# anything the two patterns cannot parse -- including compressed forms
# like ::1 -- falls through here, i.e. fails closed rather than open
default "0.0.0.0";
}
log_format privacy '$ip_trunc - - [$time_local] "$request" $status '
'$body_bytes_sent "$http_referer" "$http_user_agent" '
'rid=$request_id rt=$request_time';
Drei Details entscheiden, ob das wirklich funktioniert. Erstens muss die map im http-Kontext liegen — innerhalb eines server-Blocks weigert sich nginx zu starten. Sie in conf.d/ abzulegen, mit einem Namen, der früh einsortiert wird, ist der einfachste Weg, sich dessen sicher zu sein.
Zweitens: Sitzen Sie hinter Cloudflare oder einem anderen Edge, prüfen Sie, was $remote_addr enthält. Das realip-Modul ersetzt sie früh in der Anfrageverarbeitung durch die echte Client-Adresse und hält die verbindende Adresse in $realip_remote_addr fest. Diese Reihenfolge spielt Ihnen in die Hände: Die map läuft beim Loggen, sie kürzt also den echten Besucher und nicht den Edge. Es bedeutet aber auch: Haben Sie realip nicht konfiguriert, kürzen Sie Cloudflares Adresse und lernen nichts, während die echte Adresse in einem Header steckt.
Drittens: Prüfen Sie den Rest des Format-Strings. Das Kürzen von $remote_addr bringt exakt nichts, wenn die Zeile weiterhin mit "$http_x_forwarded_for" endet oder $http_cf_connecting_ip mitführt — und sehr viele Standard- und Control-Panel-Formate enthalten eines von beiden. Die vollständige Adresse steckt in den Request-Headern; sie bleibt nur dann außen vor, wenn Sie jeden Header, der sie trägt, aus dem Format lassen.
Beachten Sie, wodurch sie ersetzt wurde: $request_id, eine zufällige 32-stellige Hex-Zeichenkette, die nginx pro Anfrage erzeugt. Das ist der Dreh- und Angelpunkt für den nächsten Abschnitt.
Was Kürzung kaputtmacht, und die drei Aufgaben, die ein Log wirklich erfüllt
Irgendjemand wendet immer ein, anonymisierte Logs seien nutzlos, und er hat halb recht — denn "die Logs" sind drei voneinander unabhängige Aufgaben unter einem gemeinsamen Dateinamen, und nur eine davon braucht die Adresse.
Aufgabe eins: wer Sie gerade unter Dauerbeschuss nimmt, blockieren. Das braucht die vollständige Adresse, und zwar innerhalb von Sekunden. Es braucht sie nicht morgen. fail2ban ist das übliche Werkzeug dafür, und es kann schlicht nicht mit einer gekürzten Datei arbeiten — eine Sperre gegen 203.0.113.0 sperrt einen unschuldigen Host aus und lässt den Angreifer weiterhin verbunden. Aber dieser Aufgabe genügt eine Datei, die einen Tag existiert, oder gar keine Datei: Die eigenen limit_req und limit_conn von nginx halten ihren Zustand im Shared Memory, reagieren in Mikrosekunden statt im Takt von fail2bans Polling-Intervall, und schreiben nichts auf die Platte.
Aufgabe zwei: herausfinden, warum diese Anfrage einen 500er zurückgegeben hat. Das braucht Korrelation, keine Identität. Eine Request-ID, die von nginx bis in die Anwendung durchgereicht wird, verknüpft die Zugriffszeile, den Upstream-Fehler und den Stacktrace der Anwendung zu einem Ereignis — genau das, was Sie eigentlich wollten, als Sie nach der Adresse gegriffen haben. In der Praxis ist die ID die bessere Wahl: Sie übersteht einen Client in einem Mobilfunknetz, dessen Adresse mitten in der Sitzung wechselt, und sie wird nicht ungültig, wenn sich vier Besucher eine CGNAT-Adresse teilen.
Aufgabe drei: den Traffic über die Zeit verstehen. Volumen, Mix der Statuscodes, welche Pfade heiß laufen, ob der Crawler außer Kontrolle ist. Eine gekürzte Adresse reicht hier völlig, und das /24, das die Kürzung übersteht, genügt, um zu erkennen, dass ein einziges Netzwerk für 40 % Ihrer Anfragen verantwortlich ist.
Das Design lautet also nicht "weniger loggen", sondern nach der Uhr trennen: ein Stream mit voller Detailtiefe, der einen Tag lebt und die Blocking-Tools speist, und ein gekürzter Stream, der so lange lebt, wie Sie Statistiken wollen. Beide werden von nginx im selben Moment geschrieben, es gibt also keinen Verarbeitungsschritt dazwischen und kein Zeitfenster, in dem das Falsche länger als beabsichtigt auf der Platte liegt.
# inside the server block, or in a snippet included by it
access_log /var/log/nginx/access.log privacy; # truncated, keep for weeks
access_log /var/log/nginx/security.log secip; # full address, keep for a day
# static assets are pure noise in a privacy log -- drop them entirely
location ~* \.(?:css|js|svg|png|jpe?g|webp|woff2?|ico)$ {
access_log off;
expires 30d;
}
Das Format secip ist im Vergleich zum anderen ein Einzeiler — $remote_addr, der Zeitstempel, die Anfrage und der Status, sonst nichts. Es ist die einzige Datei auf der Box, in der eine vollständige Besucheradresse liegen darf, was ihre Aufbewahrung zu einer einzigen Entscheidung an einer einzigen Stelle macht, statt zu einer Eigenschaft, über die Sie sich über sechs Dateien hinweg Gedanken machen müssen.
journald, auth.log, und die Spur, die zu Ihnen zurückführt
Über die Privatsphäre der Besucher wird geschrieben. Die Spur des Administrators ist der Teil, der auf einer Box zählt, deren ganzer Sinn ist, dass Ihr Name nicht daran hängt, und sie findet sich fast vollständig an zwei Stellen.
/var/log/auth.log protokolliert für jede Sitzung, die Sie öffnen, eine Zeile Accepted publickey, mit Ihrer Quelladresse und dem Fingerabdruck Ihres Schlüssels. Über einen Monat hinweg ergibt das einen Stundenplan Ihrer Arbeitsgewohnheiten und eine Liste der Netzwerke, die Sie nutzen. Verbinden Sie sich immer über denselben Tunnel, ist das eine einzige, sich wiederholende Adresse und relativ langweilig. Verbinden Sie sich von überall dort, wo Sie gerade sind, ist es eine Reisehistorie.
Das Journal enthält dieselben Ereignisse, dazu alles, was Ihre Units ausgegeben haben, und seine Standardwerte sind großzügig: SystemMaxUse= liegt bei 10 % des Dateisystems, und MaxRetentionSec= ist nicht gesetzt, was bedeutet, dass es überhaupt kein Zeitlimit gibt — nur ein Größenlimit. Auf einem ruhigen Server bewahrt diese Kombination Monate auf.
Hier gibt es einen echten Zielkonflikt, und der verdient es, klar benannt statt weggewischt zu werden. Die Logs, die Sie beschreiben, sind dieselben Logs, die Ihnen verraten, wie jemand eingedrungen ist. Setzen Sie Storage=volatile, lebt das Journal nur im RAM und verschwindet beim Neustart — wirklich privat, und an dem Morgen, an dem Sie einen Prozess finden, den Sie nicht gestartet haben, wirklich nutzlos, weil der erste Neustart Ihres Angreifers die Beweise gelöscht hat. Für die meisten ist die vernünftige Mitte persistenter Speicher mit einer harten Obergrenze und einer kurzen Uhr: lang genug, um einen Vorfall zu untersuchen, den Sie innerhalb einer Woche bemerken, kurz genug, dass die Datei kein Tagebuch ist.
Zwei Implementierungsdetails, die Leute reinlegen. Debian- und Ubuntu-Images unterscheiden sich darin, ob rsyslog installiert ist; existiert /var/log/auth.log auf Ihrer Box, schreibt rsyslog diese Datei, und journalds Obergrenzen regeln sie überhaupt nicht — das ist logrotates Aufgabe. Und ForwardToSyslog= ist es, was rsyslog aus dem Journal speist; es auf einer Box mit beidem abzuschalten, erspart Ihnen, zwei Kopien von allem unter zwei unterschiedlichen Aufbewahrungsrichtlinien zu halten.
Ihre Aufbewahrungsrichtlinie ist das, was Ihre Backups sagen
Das ist der Punkt, der die ganze sorgfältige Arbeit von oben zunichtemacht, und er ist unsichtbar, wenn man nicht gezielt danach sucht.
Nehmen wir an, logrotate hält vierzehn Tage nginx-Logs vor, und damit sind Sie zufrieden. Jetzt kommt das Offsite-Backup dazu, das einzurichten richtig war: ein täglicher Borg- oder restic-Lauf, mit einer Aufbewahrung von sieben täglichen, vier wöchentlichen und sechs monatlichen Archiven. Jedes dieser Archive enthält /var/log so, wie es am Tag des Laufs aussah. Das älteste monatliche Archiv ist sechs Monate alt und enthält die vierzehn Tage an Logs, die damals aktuell waren. Ihre effektive Log-Aufbewahrung beträgt nicht vierzehn Tage. Sie beträgt sechs Monate, in einem verschlüsselten Repository, das Sie ohne Restore nicht durchsuchen können, auf einer zweiten Maschine, in einem anderen Land.
Es gibt genau zwei ehrliche Lösungen. Die Log-Verzeichnisse vom Backup ausschließen — sie sind so ziemlich das Einzige auf einem Server, das Sie fast immer neu aufbauen oder entbehren können, und ein Restore, der /var/log ausspart, ist kein schlechterer Restore. Oder sie bewusst einschließen und akzeptieren, dass Ihre echte Aufbewahrung die des Repositorys ist — dann sagen Sie das auch so in jeder Richtlinie, die Sie veröffentlichen, denn die Alternative ist eine verkündete Richtlinie, der Ihre eigene Infrastruktur widerspricht.
# exclude logs from the backup, and prove it took
borg create --stats \
--exclude '/var/log' \
--exclude '/var/lib/docker/containers' \
::'{hostname}-{now:%Y-%m-%d}' /
# the check that matters: can you still find an address in the newest archive?
borg list ::"$(borg list --short --last 1)" | grep -c '^var/log/' || echo "clean"
Wo Sie schon dabei sind, zwei Nachbarn desselben Problems. Die Snapshots Ihres Hosts gehören nicht Ihnen. Ein Snapshot auf Hypervisor-Ebene erfasst die Platte so, wie sie war, einschließlich Logs, die Sie seither weggedreht haben, und er liegt auf dem Speicher des Providers, unter der Aufbewahrung des Providers — ein weiterer Grund, warum die Adressen nie vollständig hätten geschrieben werden sollen, und kein Grund, hinterher noch etwas Cleveres zu versuchen.
Und greifen Sie auf einem VPS nicht zu shred. Das Überschreiben einer Datei auf einer Thin-Provisioning-virtuellen Platte, über einem Copy-on-Write-Dateisystem, auf einer SSD, die zum Wear-Levelling Blöcke ummappt, überschreibt die physischen Zellen, die die Daten hielten, nicht zuverlässig. Sichere Löschung auf gemietetem, virtualisiertem Speicher ist Theater. Die Reduktion, die funktioniert, ist die, die Sie beim Schreiben vorgenommen haben; alles danach ist ein Best Effort, den Sie nicht verifizieren können. Die Verschlüsselung des Volumes ändert diese Gleichung — aber sie ändert sie bevor die Daten geschrieben werden, dieselbe Lektion noch einmal.
Die Kopien, die Sie nicht kontrollieren
Minimierung auf der eigenen Box ist eine von mehreren Schichten, und erst die Klarheit über die anderen verhindert, dass daraus ein falsches Gefühl von Vollständigkeit wird.
Ihr Hosting-Netzwerk sieht den Flow-Record. Quelle, Ziel, Ports, Bytes, Timing — für jede Verbindung in die Maschine hinein und aus ihr heraus, ganz gleich, ob Sie irgendetwas protokollieren. Keine Konfiguration auf dem Server ändert daran etwas. Das ist ein großer Teil des Grundes, warum die Jurisdiktion, in der die Box steht, eine echte Variable ist und keine aus dem Marketing; was vom Netzwerk rechtlich verlangt wird, unterscheidet sich enorm von Land zu Land.
Ihr CDN oder Edge protokolliert am Edge. Terminiert Cloudflare TLS für Sie, hat es die Request-Zeile und die Client-Adresse, bevor Ihr Server überhaupt beteiligt ist, nach seinem eigenen Aufbewahrungsplan, seinem eigenen Rechtsweg unterworfen. Das Kürzen Ihres Origin-Logs reicht nicht rückwirkend durch diese Schicht hindurch. Einen eigenen Edge zu betreiben ist die Version davon, die Sie tatsächlich konfigurieren können — und die Logging-Regeln in diesem Guide gelten dann zuerst für die Edge-Box, denn dort treffen die ungekürzten Adressen ein.
Error-Tracker und Analytics schaffen sie für Sie von der Box. Sentry und die meisten seiner Konkurrenten hängen standardmäßig die Client-IP an jedes Event; die Einstellung heißt meist so etwas wie send_default_pii, und es lohnt sich, das zu prüfen statt anzunehmen. Jedes gehostete Analytics-Tool ist, konstruktionsbedingt, eine Kopie des Zugriffslogs bei einem Dritten — genau des Logs, dessen Kürzung Sie gerade einen Nachmittag gekostet hat.
Mail ist von allem am undichtesten. Verschickt irgendetwas auf der Box E-Mails, tragen die Header den sendenden Host und die Adresse, und jedes Relay auf dem Weg behält eine Kopie des Envelopes mit Zeitstempeln. Ein Kontaktformular, das Ihnen eine Mail schickt, ist ein Log, das Sie nicht administrieren.
Nichts davon macht die lokale Arbeit sinnlos — die lokale Kopie ist diejenige, die mit der Maschine beschlagnahmt, bei einem Breach exfiltriert oder von Ihnen selbst herausgegeben wird. Sie ist einfach die eine Schicht, die Sie vollständig kontrollieren, und sie für das ganze Bild zu halten, ist der Fehler.
SP·08Minimierung by Design, nicht Löschung nach Zustellung
Hier lohnt sich Präzision, denn die beiden werden gern verwechselt, und der Unterschied ist der ganze Unterschied zwischen gängiger Ingenieurspraxis und etwas, das Sie nicht tun sollten.
Im Voraus festzulegen, was Ihr Dienst erfasst und wie lange er es behält, ist gängige, dokumentierte, ausdrücklich empfohlene Praxis. Unter der DSGVO sind das zwei der Kernprinzipien — Datenminimierung und Speicherbegrenzung —, und eine kürzere Log-Aufbewahrung ist eine Kontrolle, nach der Auditoren fragen, keine, gegen die sie sich wehren. Es gibt keine allgemeine Pflicht für Website-Betreiber oder Hosting-Kunden in der EU, Traffic-Logs aufzubewahren; die pauschale Vorratsdatenspeicherungs-Richtlinie, die einst das Gegenteil nahelegte, wurde 2014 vom Europäischen Gerichtshof gekippt, und die nationalen Gesetze, die sie überlebt haben, binden meist Telekommunikationsanbieter, nicht Menschen, die einen Webserver betreiben. Auch die Vereinigten Staaten kennen kein allgemeines Aufbewahrungsgebot für Website-Betreiber.
Bestimmte Aufzeichnungen zu vernichten, nachdem man Sie darüber in Kenntnis gesetzt hat, ist ein völlig anderer Akt. Eine Aufbewahrungsaufforderung, eine Litigation Hold, eine gerichtliche Anordnung oder eine polizeiliche Anfrage ändert, was Sie mit den in diesem Moment existierenden Daten tun dürfen, und "meine Aufbewahrungsrichtlinie hat es gelöscht" ist keine Verteidigung, wenn Sie die Löschung wegen dieser Benachrichtigung beschleunigt haben. Nichts in diesem Guide handelt davon. Eine Aufbewahrungsrichtlinie ist etwas, das Sie an einem ruhigen Dienstag festlegen und dann in Ruhe lassen; wird sie nur dann verkürzt, wenn etwas passiert, war sie keine Richtlinie.
Dieselbe Unterscheidung zieht sich durch den Rest dieser Site: Es gibt eine echte Linie zwischen Privacy Engineering und der "bulletproof"-Haltung, die sich selbst als Immunität vermarktet. Einen Dienst zu entwerfen, der nie einen Besucherverlauf anhäuft, liegt bequem auf der richtigen Seite davon, in derselben Schublade wie die eigenen Platten zu verschlüsseln und Nutzer nicht nach einer E-Mail-Adresse zu fragen, die man gar nicht braucht.
Zwei praktische Konsequenzen. Wenn Sie eine Datenschutzerklärung veröffentlichen, sorgen Sie dafür, dass ihre Aufbewahrungsangaben zu dem passen, was tatsächlich auf der Platte liegt — angegebene vierzehn Tage und echte sechs Monate in einem Backup-Repository sind genau die Art Lücke, die aus einem gutgläubigen Betreiber auf dem Papier einen bösgläubigen macht. Und halten Sie die Entscheidung schriftlich fest, und sei es nur als Kommentar am Kopf der logrotate-Datei, denn die Person, die diese Zahlen in achtzehn Monaten rechtfertigen muss, sind Sie, und die wird sich nicht mehr erinnern, warum die Zahl sieben war. Nichts von alledem ist Rechtsberatung; operieren Sie irgendwo mit branchenspezifischen Aufbewahrungspflichten, prüfen Sie diese gegen Ihre eigene Situation, bevor Sie irgendetwas verkürzen.
SP·09Schritt für Schritt
-
01
Herausfinden, was die Box schon aufbewahrt
Konfigurieren Sie nichts, bevor Sie gemessen haben. Der Sinn dieses Durchgangs ist es, die Datei zu finden, die Sie vergessen haben — auf den meisten Maschinen entweder ein Container-Log oder ein Anwendungslog, das seit dem Deploy niemand mehr angeschaut hat.
# biggest log files anywhere on the box, largest last sudo du -ah /var/log /var/lib/docker/containers 2>/dev/null \ | sort -h | tail -20 # how much disk the journal holds, and how far back it goes journalctl --disk-usage journalctl --output=short-iso | head -1 # how old is the oldest nginx line still on disk? zcat -f /var/log/nginx/access.log* 2>/dev/null | head -1
Schauen Sie sich dann eine Zeile aus jeder Datei an und fragen Sie, was sie identifiziert. Der folgende Befehl zählt, wie viele unterschiedliche vollständige Adressen sich aktuell aus Ihren Web-Logs auslesen lassen — meist ist das die Zahl, die den Rest dieses Guides rechtfertigt.
zcat -f /var/log/nginx/access.log* 2>/dev/null \ | grep -oE '^([0-9]{1,3}\.){3}[0-9]{1,3}' | sort -u | wc -lNotieren Sie sich, was Sie finden. Am Ende führen Sie dieselben Befehle noch einmal aus, um zu beweisen, dass die Änderung gewirkt hat.
-
02
Die Client-Adresse kürzen, bevor nginx die Zeile schreibt
Legen Sie die map und die beiden Formate in einer Datei an, die in den
http-Kontext geladen wird. Bei Debian und Ubuntu wird/etc/nginx/conf.d/aus dernginx.confheraus eingebunden, noch vor den Site-Configs — genau dorthin gehört das.sudo tee /etc/nginx/conf.d/00-privacy-log.conf > /dev/null <<'EOF' # The client address is reduced here, at log time, and never written in full # to the long-retention file. $realip_remote_addr still holds the connecting # address if you need it while debugging a proxy problem. map $remote_addr $ip_trunc { ~(?<v4>\d+\.\d+\.\d+)\. "${v4}.0"; ~(?<v6>[^:]+:[^:]+): "${v6}::"; # anything the two patterns cannot parse -- including compressed forms # like ::1 -- falls through here, i.e. fails closed rather than open default "0.0.0.0"; } # Long retention: no full address, no forwarded-for header, request id instead. log_format privacy '$ip_trunc - - [$time_local] "$request" $status ' '$body_bytes_sent "$http_referer" "$http_user_agent" ' 'rid=$request_id rt=$request_time'; # Short retention: the one file allowed to hold a complete address. log_format secip '$remote_addr [$time_local] "$request" $status'; EOF sudo nginx -t && sudo systemctl reload nginxRichten Sie die Site jetzt darauf aus. Ersetzen Sie in Ihrem server-Block die bestehende
access_log-Zeile durch das Paar, und schalten Sie, solange Sie dabei sind, das Logging für statische Assets ab.access_log /var/log/nginx/access.log privacy; access_log /var/log/nginx/security.log secip;
Laden Sie neu, rufen Sie eine Seite auf, und lesen Sie das Ergebnis. Das erste Feld sollte auf
.0enden, und die Zeile sollte einenrid=-Wert tragen.sudo nginx -t && sudo systemctl reload nginx curl -s -o /dev/null https://your-domain.example/ sudo tail -1 /var/log/nginx/access.log
Ist die Adresse weiterhin vollständig, überschreibt der server-Block das Format an einer Stelle weiter unten —
grep -rn access_log /etc/nginx/findet die Zeile, die gewinnt. -
03
Vollständige Adressen nur dort behalten, wo etwas damit geschieht
Läuft fail2ban, liest es gerade die Datei, die Sie eben gekürzt haben. Richten Sie es stattdessen auf das Sicherheits-Log aus, und geben Sie diesem Log eine Lebensdauer von einem Tag, sodass die vollständigen Adressen von selbst verfallen.
sudo tee /etc/fail2ban/jail.d/nginx-privacy.local > /dev/null <<'EOF' [nginx-http-auth] enabled = true logpath = /var/log/nginx/error.log [nginx-botsearch] enabled = true logpath = /var/log/nginx/security.log maxretry = 6 findtime = 10m bantime = 1h EOF sudo fail2ban-client reload
Kümmern Sie sich dann um fail2bans eigenes Gedächtnis, das die meisten Leute nie anfassen: Es führt sein eigenes Log der Sperren unter dem Standard
rotate 4 weekly, sowie eine SQLite-Datenbank jeder je verhängten Sperre.dbpurgeageist es, was Zeilen aus dieser Datenbank verfallen lässt — setzen Sie es auf etwas nahe an Ihrer längsten Sperre, statt es beim Standard zu belassen.sudo tee /etc/fail2ban/fail2ban.d/retention.local > /dev/null <<'EOF' [Definition] dbpurgeage = 2d loglevel = NOTICE EOF sudo systemctl restart fail2ban
Noch besser: Erledigen Sie es bei schlichten Fluten in nginx, wo überhaupt nichts aufgeschrieben wird.
limit_reqhält seine Zähler im Shared Memory, antwortet in Mikrosekunden statt im Takt eines Polling-Intervalls, und hinterlässt keinerlei Aufzeichnung, wer gedrosselt wurde — Größenrichtwerte für die Zonen unter echter Last finden Sie im DDoS-Runbook für die erste Stunde.# http context: keyed on the truncated address, so nothing complete is # held in memory either. 10m of shared state is plenty for a small site. limit_req_zone $ip_trunc zone=perip:10m rate=20r/s; # in the location you want protected limit_req zone=perip burst=40 nodelay;
Die Zone auf
$ip_truncstatt auf$binary_remote_addrzu schlüsseln, ist ein bewusster Kompromiss: Das Limit gilt jetzt für ein ganzes/24auf einmal, sodass sich ein belebtes Büro hinter einem Block ein Budget teilt. Für eine kleine Site ist das meist unproblematisch und gelegentlich sogar eine Verbesserung; falls nicht, schlüsseln Sie die Zone auf die vollständige Adresse — der Zustand eines Rate-Limiters lebt im Shared Memory und wird nie auf die Platte geschrieben, ist also gar nicht Teil dessen, was Sie hier minimieren. -
04
Das Journal begrenzen und die zweite Kopie stoppen
journald akzeptiert ein Drop-in, das Paket-Upgrades übersteht, wie das Bearbeiten von
journald.confes nicht tut. Die Werte unten bewahren etwa eine Woche auf — genug, um am Montag etwas zu untersuchen, das am Freitag begonnen hat — innerhalb einer harten Obergrenze von 200 MB.sudo mkdir -p /etc/systemd/journald.conf.d sudo tee /etc/systemd/journald.conf.d/retention.conf > /dev/null <<'EOF' [Journal] Storage=persistent SystemMaxUse=200M SystemMaxFileSize=20M MaxRetentionSec=7day MaxFileSec=1day ForwardToSyslog=no EOF sudo systemctl restart systemd-journald journalctl --disk-usage
Der Neustart wendet die Größenbegrenzung sofort an; die Aufbewahrungsuhr greift erst, wenn neue Dateien rotiert werden, sodass ein bestehendes, übergroßes Journal erst bei der nächsten Rotation schrumpft und nicht augenblicklich.
journalctl --vacuum-time=7derzwingt es sofort, falls Sie den Plattenplatz noch heute zurückwollen.ForwardToSyslog=noist auf jedem Image wichtig, das rsyslog mitbringt: Ohne diese Einstellung wird jeder Journal-Eintrag zusätzlich an/var/log/syslogangehängt, nach logrotates Zeitplan statt nach dem von journald, und Sie haben zwei Kopien mit zwei unterschiedlichen Verfallsdaten. Prüfen Sie, in welcher Situation Sie sich befinden, bevor Sie etwas annehmen:systemctl is-active rsyslog 2>/dev/null || echo "rsyslog not running" ls -la /var/log/auth.log /var/log/syslog 2>/dev/null
Existieren diese Dateien, gehören sie rsyslog, und Schritt sechs ist dort, wo deren Aufbewahrung festgelegt wird. Existieren sie nicht, ist das Journal die einzige Kopie, und Sie haben sie gerade begrenzt.
-
05
Verhindern, dass die Anwendung erneut loggt, was Sie entfernt haben
Nichts von oben berührt Ihre Anwendung, und eine Anwendung im falschen Modus schreibt bereitwillig die vollständige Adresse, die vollständige URL und das Session-Token in eine eigene Datei. Drei Dinge zu prüfen.
PHP-FPM liefert eine
access.log-Direktive in seiner Pool-Konfiguration mit, standardmäßig auskommentiert, aber von sehr vielen Control-Panels aktiviert. Ist sie aktiv, ist das eine zweite Kopie jeder Request-Zeile, aus einer Datei, die Ihre nginx-Arbeit nie berührt hat.grep -rn '^access.log' /etc/php/*/fpm/pool.d/ || echo "fpm access log off"
Das Log-Level Ihres Frameworks entscheidet, ob Query-Strings und Request-Bodies auf der Platte landen. Der Debug-Modus protokolliert in den meisten Frameworks die vollständige URL, und ein Passwort-Reset-Link ist eine vollständige URL. Setzen Sie die Produktionsstufe, und bestätigen Sie, dass tatsächlich diese geladen wird, statt der aus der Datei, von der Sie glauben, dass sie gelesen wird.
Der Standardtreiber von Docker rotiert nie. Beheben Sie das auf Daemon-Ebene, damit jeder künftige Container die Begrenzung erbt. Beachten Sie, dass dies für Container gilt, die nach dem Neustart erzeugt werden — bestehende behalten ihre aktuelle, unbegrenzte Datei, bis sie neu erzeugt werden.
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF' { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } } EOF sudo systemctl restart docker docker inspect --format '{{.HostConfig.LogConfig}}' $(docker ps -q) 2>/dev/nullHält ein Container bereits eine große Datei, kürzt erst das Neuerzeugen mit
docker compose up -d --force-recreatetatsächlich die Historie — ein bloßer Neustart behält dieselbe Log-Datei. -
06
Aufbewahrung bewusst festlegen, an einer Stelle pro Datei
In logrotate liegt die Uhr für alles, was rsyslog und nginx schreiben. Bearbeiten Sie die mitgelieferte Stanza, statt eine zweite hinzuzufügen: Zwei Stanzas, die denselben Pfad benennen, lassen logrotate mit einem Duplicate-Entry-Fehler scheitern und die Rotation dieser Datei komplett stoppen — die häufigste Art, wie aus einer Aufbewahrungsänderung still und leise eine unendliche Aufbewahrung wird.
# check for duplicates BEFORE editing, then again after sudo logrotate --debug /etc/logrotate.conf 2>&1 | grep -i 'duplicate\|error'
Bei nginx wollen die beiden Dateien unterschiedliche Uhren: Die gekürzte darf Wochen leben, die mit den vollständigen Adressen sollte den Tag nicht überleben. Fügen Sie eine eigene Stanza für das Sicherheits-Log hinzu — ein anderer Pfad, also kein Duplikat — und verkürzen Sie die mitgelieferte.
sudo tee /etc/logrotate.d/nginx-security > /dev/null <<'EOF' # The only file on this box that holds complete client addresses. # One day, uncompressed so fail2ban can read it. Do not lengthen without # a reason you would be happy to write down here. /var/log/nginx/security.log { daily rotate 1 maxage 1 missingok notifempty nocompress create 0640 www-data adm sharedscripts postrotate [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid` endscript } EOF sudo sed -i 's/^\trotate 14$/\trotate 7/' /etc/logrotate.d/nginx sudo logrotate --debug /etc/logrotate.d/nginx-securityMachen Sie dieselbe Rechnung für
/etc/logrotate.d/rsyslog, falls rsyslog installiert ist — dessen Standardwerte bewahren vier Wochenauth.logauf, also vier Wochen Ihrer eigenen SSH-Sitzungen. Und führen Sie den Debug-Durchlauf ein letztes Mal aus: Er zeigt genau, welche Dateien er rotieren und löschen würde, und das ist der einzige Weg, zu bestätigen, dass die Zahlen, die Sie gerade eingetippt haben, auch die geltenden Zahlen sind. -
07
Beweisen Sie es, auch durch das Backup hindurch
Wiederholen Sie die Messungen aus Schritt eins. Die Zahl unterschiedlicher vollständiger Adressen im Log mit langer Aufbewahrung sollte jetzt nicht mehr wachsen, und nach einem Rotationszyklus sollte sie null sein.
# should print 0 once the pre-change files have rotated out zcat -f /var/log/nginx/access.log* 2>/dev/null \ | grep -oE '^([0-9]{1,3}\.){3}[0-9]{1,3}' \ | grep -v '\.0$' | sort -u | wc -l # and the journal should now be bounded journalctl --disk-usageDann die Prüfung, die fast niemand durchführt: ins Backup schauen. Ein Repository, das weiterhin
/var/logmitführt, hält genau die Detailtiefe und die Aufbewahrung fest, die Sie sich gerade einen Nachmittag lang abgemüht haben zu entfernen, und es wird sie weiter festhalten, solange Ihr ältestes Archiv überlebt.borg list | head -3 # borg lists oldest first borg list ::"$(borg list --short | head -1)" 2>/dev/null \ | grep -c '^var/log/' || echo "no logs in oldest archive"
Ist die Zahl nicht null, fügen Sie entweder den Ausschluss von vorhin hinzu und lassen Sie die alten Archive auslaufen, oder bereinigen Sie sie gezielt. Bis eines von beidem geschieht, ist Ihre echte Aufbewahrung die des Repositorys — der mit Abstand nützlichste Satz in diesem Guide, und der, den man am leichtesten vergisst.
Schreiben Sie zuletzt die Zahlen dort auf, wo die nächste Person sie findet: die Aufbewahrung, für die Sie sich entschieden haben, den Grund, und das Datum. Ein Kommentar am Kopf der logrotate-Datei reicht. Die Konfiguration oben ist eine Entscheidung, und eine Entscheidung, die niemand in einem Jahr rekonstruieren kann, wird wieder zu einem Default.


