EST. 2021  •  OSLO, NORWAY

Skui.io

Homelab & Self-Hosting

← Back
homelab

The CVEs You Didn't Write

I pointed a scanner at my own homelab and got a wall of red. Most of it wasn't mine to fix — and that turned out to be the whole point.

The CVEs You Didn't Write
Contents

Overview
#

I got a bit restless a while ago and built myself a small security dashboard. Nothing clever. A web app to keep reports in one place, and a little scanner that walks every container running on my server and checks each image against the known-vulnerability database.

Then I pointed it at my own stack.

First scan: thirteen criticals, eleven highs, a few hundred CVEs in total. A solid wall of red.

My first reaction was the obvious one.

How did it get this bad?

The answer turned out to be more interesting than the number.

A wall of red
#

When you run docker ps and see twenty-something containers, it’s easy to think of them as yours. You wrote the compose file. You picked the ports. You chose the image. It feels like your stack.

But almost none of that code is yours.

postgres, redis, traefik, nextcloud, vaultwarden, linkwarden, docmost — every one of those is software someone else wrote, baked into an image someone else built. You typed image: postgres:16-alpine and quietly adopted a few hundred thousand lines of C, a Go helper or two, and whatever happened to be in the base layer the day they pushed it.

And every CVE in all of that now lives in your docker ps.

The uncomfortable math
#

So I went through the list properly, and the pattern was hard to miss: most of the vulnerabilities weren’t mine to fix.

Take Postgres. The scanner flagged sixteen critical CVEs in postgres:16-alpine. Postgres is written in C. Every one of those sixteen was a Go standard-library CVE — coming from gosu, a tiny Go binary the official image bundles to drop root at startup. The vulnerable code lives in Go’s HTTP and crypto libraries. gosu calls none of it. It does one job: switch user, then exec.

Sixteen criticals. In code that never runs. In a binary I didn’t put there. In an image I didn’t build.

So what’s my move? Bump a package? I don’t build that image. Pull a newer one? I’m already on the latest they published. The only thing that clears those sixteen is the Postgres maintainers rebuilding with a newer Go — and then me pulling it.

That’s not a to-do. That’s a waiting room.

Yours, or theirs?
#

This is the part most vulnerability reports get wrong, and the part I ended up building into my scanner:

A CVE being “fixable” doesn’t mean you can fix it.

“Fixable” only means a patched version of the package exists somewhere out there. For an image you build, that’s a real task — bump the dependency, rebuild, gone. For an image you pull, the fix belongs to the maintainer. You’re a spectator. The most you can do is keep pulling and hope they shipped it.

So I split everything into two piles.

Mine. The handful of images I actually build — my CV site, this blog, a couple of small services. These had genuine, fixable findings: a stale Alpine base carrying an old OpenSSL, a Python dependency a few versions behind. A one-line bump and a rebuild, and they were gone. These are the ones worth an evening.

Theirs. Everything else. Postgres, Traefik, Nextcloud, the password manager, the bookmark app. Already on latest, vulnerable until upstream rebuilds. Nothing to do but watch.

Once the report was tagged that way, “118 critical” stopped being a panic number and became something I could read in one glance: one image you build — fix it; everything else — not your move.

What I actually fixed
#

The genuinely-mine pile was small, and that’s the good news. I rebuilt my own images so they pulled fresh, patched base layers. I recreated a few containers whose local copy had drifted behind what the maintainers had already published — that one’s pure laziness tax, and a pull fixes it. And I took Nextcloud through a major version jump, carefully, with a database dump first. Because that one actually was mine to break.

The rest — the long tail of upstream CVEs in other people’s software — I acknowledged.

Not ignored. Acknowledged. There’s a difference. I read them, I know who owns them, and they sit on the board as seen, tracking, waiting. If a fix lands upstream, I’ll pull it. If something genuinely new appears, it shows up fresh and loud, instead of hiding inside a number I’d already learned to ignore.

The actual point
#

Here’s the thing I didn’t fully appreciate until the numbers were in front of me:

Every container you run is a dependency you’ve adopted — and you adopt its entire security history along with it.

When you docker run someone’s image, you’re not just getting their app. You’re signing up for their base image, their build habits, their update cadence, and every CVE that lands in any of it for as long as you keep it running. One self-hosted bookmark manager on my box had over a hundred fixable criticals. None of them mine. All of them now technically running on my server.

That’s not an argument against self-hosting. I’m not stopping. But it did change how I think about adding things to the stack. “Is this app good?” is only half the question. The other half is: who maintains the image, and how often do they rebuild it? A project that ships a fresh, slim image every week is a very different roommate than one whose latest tag hasn’t moved in eight months.

You can’t code your way out of other people’s CVEs. But you can be aware of them. You can know which ones are actually yours. And you can choose your dependencies understanding that you’re not only picking a feature set — you’re picking whose security debt you’re willing to carry.

The board is green now. Most of it I fixed.

Most of it I never had to.

Both of those are the point.