[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fbbjpNnGswIE9QgnyAhiFcFc-o21BKY9jmnxalpEVMew":3},{"id":4,"title":5,"teaser":6,"body":7,"slug":8,"date":9,"coverImage":10,"tags":15},"b65fdd47-af28-4fd3-ab47-d4d481ad0a90","The webmail that wouldn't show mail","","\u003Cp>Last night I sat down to check an email. Simple. Open webmail, read inbox. Except Roundcube loaded a blank page and Firefox's console was screaming about Content-Security-Policy violations.\u003C\u002Fp>\u003Cp>The error was \u003Ccode>frame-ancestors 'none'\u003C\u002Fcode> - a CSP directive I had set myself, weeks ago, when I was hardening Traefik's security headers across every service. The logic seemed sound at the time: webmail should never be embedded in an iframe on another site. So I wrote \u003Ccode>frame-ancestors 'none'\u003C\u002Fcode> and moved on.\u003C\u002Fp>\u003Cp>What I forgot is that Roundcube \u003Cem>itself\u003C\u002Fem> uses an iframe. The message preview pane - the thing that shows you the actual email you clicked on - is an iframe. By setting \u003Ccode>frame-ancestors 'none'\u003C\u002Fcode>, I told the browser to block Roundcube from loading inside Roundcube. A webmail client that cannot display mail.\u003C\u002Fp>\u003Cp>The fix was one word: \u003Ccode>'none'\u003C\u002Fcode> to \u003Ccode>'self'\u003C\u002Fcode>. Same-origin iframes work, external embedding still blocked. But the story didn't end there.\u003C\u002Fp>\u003Ch2>When one fix breaks the next thing\u003C\u002Fh2>\u003Cp>With the iframe issue resolved, Roundcube loaded properly but threw a new error: \u003Ccode>call to eval() blocked by CSP\u003C\u002Fcode>. Roundcube's minified JavaScript uses \u003Ccode>eval()\u003C\u002Fcode> to parse AJAX responses. My CSP allowed \u003Ccode>'unsafe-inline'\u003C\u002Fcode> but not \u003Ccode>'unsafe-eval'\u003C\u002Fcode>. Another one-line fix, another deploy.\u003C\u002Fp>\u003Cp>Then the login stopped working entirely. 401 responses after 60-second timeouts. The IMAP connection from Roundcube to the mail server was dead. Ports were listening, containers were running, the network looked fine. But TCP connections from the Roundcube container to the mail server on port 993 just hung.\u003C\u002Fp>\u003Cp>The culprit was fail2ban. During the CSP fix cycle, I had also been fixing SMTP authentication - Roundcube 1.6 changed its config keys, and my setup was sending unauthenticated SMTP requests. Postfix rejected them. fail2ban saw the rejections, classified them as an attack, and banned the Roundcube container's IP address. A container on the same Docker network, banned by the service it exists to serve.\u003C\u002Fp>\u003Cp>So I unbanned the IP, added \u003Ccode>'unsafe-eval'\u003C\u002Fcode> to the CSP, fixed the SMTP auth config, and updated the fail2ban ignore list to not ban internal Docker network addresses. Four changes across three files to read an email.\u003C\u002Fp>\u003Ch2>This is normal\u003C\u002Fh2>\u003Cp>I run around 38 Docker containers across two Contabo VPS servers. Traefik routes everything. Grafana watches everything. And things break in ways that are never quite the same twice.\u003C\u002Fp>\u003Cp>There was the time MariaDB went into a crash loop because I set \u003Ccode>user: \"1000\"\u003C\u002Fcode> in a docker-compose file. Turns out uid 999 - which shows up as \u003Ccode>systemd-coredump\u003C\u002Fcode> on the host and looks deeply wrong - is actually the correct user. MariaDB's official image expects it. Setting uid 1000 breaks file permissions on the data directory and the container restarts forever.\u003C\u002Fp>\u003Cp>There was the Vaultwarden incident. I changed the \u003Ccode>ADMIN_TOKEN\u003C\u002Fcode> environment variable in the compose file, restarted the container, and the old token still worked. The new one didn't. Vaultwarden writes a \u003Ccode>\u002Fdata\u002Fconfig.json\u003C\u002Fcode> on first admin login that overrides the environment variable. The env var is a suggestion. The JSON file is the truth. I spent an embarrassing amount of time on that one before reading the docs more carefully.\u003C\u002Fp>\u003Cp>There was the Twig 3.19 regression in Drupal. A minor version bump in a template engine caused the site to white-screen if you cleared the database cache. Not the render cache, not the page cache - specifically the database cache. The workaround is to not clear it until a Drupal patch lands. I have a sticky note on my monitor.\u003C\u002Fp>\u003Cp>There was the week I spent hardening Traefik's security configuration - adding IP allowlists for admin services, rate limiting authentication endpoints, setting security headers globally via the websecure entrypoint. Everything went smoothly until I discovered that the global headers were fighting with per-service headers. Traefik's middleware chain applies both, and if your global config sets \u003Ccode>X-Frame-Options: SAMEORIGIN\u003C\u002Fcode> while your service-specific CSP sets \u003Ccode>frame-ancestors\u003C\u002Fcode>, the browser has to decide which one wins. The answer is CSP wins. Always. Even when CSP is the one that's wrong.\u003C\u002Fp>\u003Ch2>The monitoring that needed monitoring\u003C\u002Fh2>\u003Cp>I have a full Grafana, Prometheus, and Loki stack watching all of this. It has caught real problems - a mysterious SSL flood from a single IP that was filling up disk with TLS handshake errors in the Traefik logs. Prometheus flagged the log volume spike, I traced it to the IP, blocked it in the firewall. The system worked exactly as designed.\u003C\u002Fp>\u003Cp>But the monitoring stack itself has needed fixing more than once. Loki's log ingestion silently stopped working after an update. Prometheus's scrape targets drifted out of sync with the actual container names when I renamed services during the Traefik v2 to v3 migration. The certbot container that auto-renewed Let's Encrypt certificates was still running even though Traefik handles certificates natively now - burning CPU cycles and writing logs for certificates it could never deploy.\u003C\u002Fp>\u003Cp>Self-hosting means you are the SRE, the security team, and the customer all at the same time. When something breaks at 11pm, there is no escalation path. There is just you, a terminal, and \u003Ccode>docker logs\u003C\u002Fcode>.\u003C\u002Fp>\u003Ch2>Why do it then\u003C\u002Fh2>\u003Cp>Because my email is mine. My files are mine. My photos, my notes, my password vault - all sitting on hardware I pay for, in a data centre in Germany, behind configurations I wrote and can read.\u003C\u002Fp>\u003Cp>Because every outage teaches me something that years of managed cloud services never would. I understand TLS certificate chains because I've debugged them at midnight. I understand Postfix's relay restrictions because fail2ban taught me, forcefully, what happens when an internal service doesn't authenticate.\u003C\u002Fp>\u003Cp>And because honestly, after 15 years of building things for clients, there is something satisfying about building something for yourself that works. Even when it takes four config changes across three files just to read an email.\u003C\u002Fp>\u003Cp>The infrastructure is never finished. There's always another service to deploy, another security header to tighten, another fail2ban rule that's a little too eager. But it runs. Every site loads, every email arrives, every backup completes. And when something breaks, I fix it - usually before anyone else notices.\u003C\u002Fp>\u003Cp>Usually.\u003C\u002Fp>","webmail-wouldnt-show-mail","2026-04-07T18:23:33+00:00",{"id":11,"url":12,"alt":5,"width":13,"height":14},"698956fb-e3c3-4fd3-8be2-d90c18218ddf","\u002Fsites\u002Fdefault\u002Ffiles\u002F2026-04\u002FScreenshot%202026-04-03%20200612_0.png",1421,916,[]]