| | 229 | |
| | 230 | == Content-Security-Policy fuer den Shop == |
| | 231 | |
| | 232 | Status: noch nicht aktiv. Die Konfiguration steht auskommentiert in |
| | 233 | `conf.d/shop-headers.inc`. |
| | 234 | |
| | 235 | === Wer sammelt die Reports? === |
| | 236 | |
| | 237 | Der Browser erzeugt sie, legt sie aber nirgends ab. Ohne Reporting-Direktive |
| | 238 | erscheinen Verstoesse ausschliesslich in der Entwicklerkonsole des Browsers -- |
| | 239 | NGINX bekommt davon nichts mit, es gibt kein Log. Mit Reporting-Direktive |
| | 240 | schickt der Browser einen POST mit JSON-Body an eine URL, die wir angeben. |
| | 241 | Den Empfaenger muessen wir selbst stellen. |
| | 242 | |
| | 243 | Zwei Mechanismen: |
| | 244 | |
| | 245 | * `report-uri` -- der alte, CSP-eigene Weg, inzwischen deprecated |
| | 246 | * `report-to` plus den Header `Reporting-Endpoints` -- der moderne Weg ueber |
| | 247 | die allgemeine Reporting-API |
| | 248 | |
| | 249 | Browser mit `report-to`-Unterstuetzung ignorieren `report-uri` vollstaendig. |
| | 250 | Lange war `report-uri` unverzichtbar, weil Firefox und Safari die |
| | 251 | Reporting-API nicht umgesetzt hatten; seit Maerz 2026 unterstuetzen alle |
| | 252 | aktuellen Browser `report-to` (Firefox seit Version 149). Wir verwenden |
| | 253 | daher nur noch `report-to`. |
| | 254 | |
| | 255 | === Dreistufiges Vorgehen === |
| | 256 | |
| | 257 | 1. '''Entwicklerkonsole.''' Solange der Shop nicht oeffentlich ist, nichts |
| | 258 | aktivieren. Shop selbst durchklicken -- Katalog, Warenkorb, Checkout, |
| | 259 | Zahlungsmodule, Admin -- und die Verstoesse in der Konsole abarbeiten. |
| | 260 | Die zeigt sie sofort und mit Zeilennummer, ein Report-Endpunkt liefert |
| | 261 | gebuendelt und mit bis zu einer Minute Verzoegerung. |
| | 262 | 2. '''Report-Only mit Endpunkt.''' Erst wenn der Shop oeffentlich ist und |
| | 263 | Felddaten aus echten Browsern gebraucht werden. Blockiert nichts. |
| | 264 | 3. '''Scharf schalten.''' `Content-Security-Policy-Report-Only` durch |
| | 265 | `Content-Security-Policy` ersetzen, Endpunkt beibehalten. |
| | 266 | |
| | 267 | === Endpunkt in NGINX === |
| | 268 | |
| | 269 | In `nginx.conf`, http-Kontext: |
| | 270 | |
| | 271 | {{{#!nginx |
| | 272 | log_format csp escape=json |
| | 273 | '{"time":"$time_iso8601","ua":"$http_user_agent",' |
| | 274 | '"report":$request_body}'; |
| | 275 | |
| | 276 | limit_req_zone $binary_remote_addr zone=csp:1m rate=10r/m; |
| | 277 | |
| | 278 | # Dummy-Backend, das den POST quittiert |
| | 279 | server { |
| | 280 | listen 127.0.0.1:8099; |
| | 281 | location / { return 204; } |
| | 282 | } |
| | 283 | }}} |
| | 284 | |
| | 285 | Im server-Block des Shops: |
| | 286 | |
| | 287 | {{{#!nginx |
| | 288 | location = /_csp-report { |
| | 289 | access_log /var/log/nginx/csp-report.jsonl csp; |
| | 290 | |
| | 291 | client_body_buffer_size 64k; |
| | 292 | client_max_body_size 64k; |
| | 293 | limit_req zone=csp burst=5 nodelay; |
| | 294 | |
| | 295 | proxy_pass http://127.0.0.1:8099; |
| | 296 | } |
| | 297 | }}} |
| | 298 | |
| | 299 | Und im Shop-Header: |
| | 300 | |
| | 301 | {{{#!nginx |
| | 302 | add_header Reporting-Endpoints 'csp="https://shop.vpn21.net/_csp-report"' always; |
| | 303 | add_header Content-Security-Policy-Report-Only "default-src 'self'; report-to csp" always; |
| | 304 | }}} |
| | 305 | |
| | 306 | '''Fallstricke:''' |
| | 307 | |
| | 308 | * Der Umweg ueber `proxy_pass` ist noetig. NGINX liest den Request-Body nur |
| | 309 | ein, wenn es ihn weiterreicht; bei einem schlichten `return 204;` bleibt |
| | 310 | `$request_body` leer. |
| | 311 | * `$request_body` ist nur gefuellt, wenn der Body in den Speicher passt. |
| | 312 | Groesserer Body wandert in eine temporaere Datei, die Variable bleibt |
| | 313 | leer -- daher `client_body_buffer_size` passend setzen. |
| | 314 | * Die Endpunkt-URL muss HTTPS sein. Ueber HTTP verwirft der Browser sie |
| | 315 | kommentarlos. |
| | 316 | * `escape=json` im `log_format` gibt es ab NGINX 1.11.8. |
| | 317 | |
| | 318 | === Logrotation === |
| | 319 | |
| | 320 | '''NGINX kann nicht selbst rotieren.''' Es gibt keine Groessen- oder |
| | 321 | Zeitbegrenzung in der Konfiguration; NGINX schreibt in ein offenes |
| | 322 | File-Handle, bis jemand von aussen eingreift. Der vorgesehene Weg ist: |
| | 323 | Datei umbenennen, dann `SIGUSR1` an den Master-Prozess, woraufhin NGINX die |
| | 324 | Logs neu oeffnet. Genau das macht logrotate. |
| | 325 | |
| | 326 | Begrenzen laesst sich in der NGINX-Konfiguration nur der Zufluss -- |
| | 327 | `client_max_body_size` pro Eintrag, `limit_req` pro Client. Beides ist oben |
| | 328 | gesetzt. |
| | 329 | |
| | 330 | Der Dateiname lautet bewusst `csp-report.jsonl` und nicht `.log`: die |
| | 331 | mitgelieferte logrotate-Konfiguration der Distribution greift ueblicherweise |
| | 332 | auf `/var/log/nginx/*.log` zu. Zwei Stanzas fuer dieselbe Datei quittiert |
| | 333 | logrotate mit "duplicate log entry" und bricht ab. Die eigene Endung haelt |
| | 334 | die CSP-Logs aus dem Standard-Glob heraus und erlaubt eine eigene Retention. |
| | 335 | |
| | 336 | `/etc/logrotate.d/nginx-csp`: |
| | 337 | |
| | 338 | {{{ |
| | 339 | /var/log/nginx/csp-report.jsonl { |
| | 340 | size 10M |
| | 341 | rotate 5 |
| | 342 | compress |
| | 343 | delaycompress |
| | 344 | missingok |
| | 345 | notifempty |
| | 346 | create 0640 www-data adm |
| | 347 | sharedscripts |
| | 348 | postrotate |
| | 349 | [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid` |
| | 350 | endscript |
| | 351 | } |
| | 352 | }}} |
| | 353 | |
| | 354 | Zur Wirkung von `size`: die Direktive schaltet die zeitbasierte Rotation ab |
| | 355 | und rotiert nur noch bei Ueberschreiten der Groesse -- aber geprueft wird |
| | 356 | das erst, wenn logrotate laeuft, ueblicherweise einmal taeglich per |
| | 357 | systemd-Timer. Eine harte Obergrenze ist das also nicht; bei einem Ausbruch |
| | 358 | kann die Datei zwischen zwei Laeufen deutlich groesser werden. Wer das enger |
| | 359 | braucht, laesst logrotate oefter laufen (eigener Timer oder Cron-Eintrag mit |
| | 360 | `logrotate /etc/logrotate.d/nginx-csp`). |
| | 361 | |
| | 362 | Alternative `maxsize` statt `size`: rotiert nach Zeitplan '''oder''' bei |
| | 363 | Ueberschreiten der Groesse, je nachdem was zuerst eintritt. Sinnvoll, wenn |
| | 364 | die Datei auch bei wenig Verkehr regelmaessig umgebrochen werden soll. |
| | 365 | |
| | 366 | Benutzer und Gruppe in `create` an die Distribution anpassen -- auf einigen |
| | 367 | Systemen `nginx:nginx` statt `www-data:adm`. |
| | 368 | |
| | 369 | === Rauschfilter -- noch zu entwickeln === |
| | 370 | |
| | 371 | Sobald der Endpunkt oeffentlich erreichbar ist, besteht ein erheblicher Teil |
| | 372 | der Reports aus Muell: Browser-Erweiterungen, die Skripte injizieren, |
| | 373 | Virenscanner mit TLS-Interception, von Providern eingeschleuste Inhalte. |
| | 374 | Die Trennung von relevanten und irrelevanten Reports ist eine eigene |
| | 375 | Aufgabe. |
| | 376 | |
| | 377 | Prompt zum Kopieren, wenn es soweit ist: |
| | 378 | |
| | 379 | {{{ |
| | 380 | Ich betreibe einen modified-eCommerce-Shop unter shop.vpn21.net hinter |
| | 381 | NGINX (Version siehe unten). Der Shop laeuft seit einiger Zeit mit |
| | 382 | Content-Security-Policy-Report-Only und einem Report-Endpunkt |
| | 383 | /_csp-report, der die Reports per report-to einsammelt. |
| | 384 | |
| | 385 | NGINX loggt die Reports mit log_format csp escape=json als eine JSON-Zeile |
| | 386 | pro Report nach /var/log/nginx/csp-report.jsonl, Format: |
| | 387 | {"time":"...","ua":"...","report":<Reporting-API-Payload>} |
| | 388 | |
| | 389 | Ich haenge einen Auszug der Logdatei an. |
| | 390 | |
| | 391 | Ich moechte mit dir zusammen einen Filter entwickeln, der die relevanten |
| | 392 | Reports -- also echte Verstoesse meines eigenen Shop-Codes und seiner |
| | 393 | Module -- von Rauschen trennt. Als Rauschen erwarte ich vor allem: |
| | 394 | Browser-Erweiterungen, Virenscanner mit TLS-Interception, von Providern |
| | 395 | eingeschleuste Inhalte, Bots. |
| | 396 | |
| | 397 | Bitte gehe so vor: |
| | 398 | 1. Analysiere den Auszug und zeige mir, welche Report-Klassen tatsaechlich |
| | 399 | vorkommen und wie haeufig. Rate nicht, zaehle. |
| | 400 | 2. Schlage Unterscheidungskriterien vor und begruende sie. Nenne mir bei |
| | 401 | jedem Kriterium, welche echten Verstoesse es faelschlich ausfiltern |
| | 402 | koennte. |
| | 403 | 3. Erst danach: ein Auswertungsskript. Sprache nach deinem Vorschlag, |
| | 404 | begruende die Wahl. Es soll die Logdatei einlesen, Rauschen abtrennen |
| | 405 | und die verbleibenden Verstoesse nach Haeufigkeit gruppiert ausgeben. |
| | 406 | 4. Sag mir, welche CSP-Direktiven ich aus den verbleibenden Verstoessen |
| | 407 | ableiten muesste, damit ich von Report-Only auf scharf umstellen kann. |
| | 408 | |
| | 409 | Frag nach, wenn etwas unklar ist, statt auf Annahmen aufzubauen. |
| | 410 | Code-Policy fuer C++ gilt hier vermutlich nicht, aber falls doch Code in |
| | 411 | C++ entsteht, halte dich daran. |
| | 412 | }}} |
| | 413 | |
| | 414 | Mitzubringen, wenn wir das angehen: ein Auszug aus `csp-report.jsonl` von |
| | 415 | einigen Tagen bis Wochen, moeglichst ungefiltert, und die dann aktive |
| | 416 | Report-Only-Policy. |