← Back to blog

We audited our own server's security and found things we'd never let a client get away with

Daniel García·

We’d spent months adding security headers and CSP rules to client sites and never got around to doing the same for our own. You notice it the moment you finally sit down and look: we found things we’d have flagged on a client in the first review, no question asked.

The one that made me squirm the most was the SSH deploy step. The GitHub Actions workflow ran ssh-keyscan against the VPS’s IP on every deploy, without checking that key against anything at all. That’s blindly trusting whatever fingerprint the server happens to answer with at that moment: if someone intercepts the connection with a man-in-the-middle attack right as the pipeline runs, it swallows it without a peep. We fixed it by hardcoding the VPS’s real ED25519 public key into the workflow and dropping the StrictHostKeyChecking=no that had been sitting there since I set up the deploy, a couple of weeks back.

We were also missing headers we’ve been recommending to other people for months: X-Frame-Options, Strict-Transport-Security, an actual Content-Security-Policy. We added them in a 65-line nginx.conf that now gets copied into the Docker image at build time. Putting the CSP together was the tedious part, not because it’s hard but because you have to test what it breaks: Google Fonts needs its own origin in style-src and font-src, Google Analytics needs script-src and connect-src, and the contact form calls the Web3Forms API, so without adding it to connect-src the fetch was failing silently — nobody noticed until a lead said they’d filled in the form and never heard back. We had to leave unsafe-inline in script-src because of the scroll animations and the cookie notice, which are inline in the layout. It’s not the strictest policy out there, but it beats having none.

We also pinned the Node and nginx versions in the Dockerfile. Before that we were pulling plain alpine, which can bump a major version on you overnight and break the build without you having touched a thing.

There’s one thing you can’t fix with code, and it had completely slipped past me: the Web3Forms access key wasn’t restricted to our domain. Anyone who pulled it out of the JavaScript bundle, which is public by definition, could use it to send submissions from any other site and burn through our quota. That one only gets changed by hand in the Web3Forms dashboard, and it’s still sitting on my to-do list as I write this.

What bothers me most isn’t any single one of those gaps, it’s how long they’d been sitting there with nobody looking. We audit a client’s infrastructure the moment a new project starts, or the moment something smells off. Ours we set up once, it worked, and there it stayed. I doubt we’re the only small agency whose own house gets this treatment.

securityserverdeployment