Free DevSecOps on a private repo: the short version
Four long posts, one screen. If you’ve only got two minutes, read this one.
The setup: a Telegram bot that keeps track of the food in my kitchen, in a
private GitHub repo, with a pipeline of seven scanners around it (Semgrep,
gitleaks, pnpm audit and OSV-Scanner, hadolint and Trivy, and ZAP) that
need no account anywhere. There’s nowhere to upload results into on the free
plan, so the exit code is the control and the artifacts are the report. It
ran on GitHub’s free minutes until the 28th of the month, when the 2,000 of
them ran out; since then it runs on a 2-core box of my own.
The part I expected to be expensive, scanning the app while it runs, turned out to be free, and not because of anything I did for security. Months earlier I’d put fake adapters behind the ports so the tests would run on a fresh laptop, which means the app boots with no database, no API key and no bot token. ZAP attacks a throwaway copy inside the runner, and that’s it. Testability and scannability share a prerequisite.
It found things. A shell injection in my own deploy workflow, on the first run. Eleven HIGH and CRITICAL vulnerabilities inside the npm that ships with the Node base image, invisible to the lockfile, on the first merge. Two Debian CVEs the base image hadn’t picked up yet. And, once the scan finally reached the routes that mattered, an error handler answering 500 to requests that were the client’s fault.
My own mistakes are the part I’d read first. Two bugs in the gate script: one mistook warnings for failures, the other didn’t know Semgrep’s newer severity scale and quietly dropped three findings. A provenance step that had been green since day one and had never worked. A spider that reached six URLs while the report looked clean. And a record of what shipped, placed before the deploy, that stopped a release from shipping.
If I had to start again, this is what I’d keep. Scan the image before pushing it, because afterwards the gate is just a notification. Start every new gate in report-only mode and switch it on with a number. Read the report, not the summary. Count DAST coverage at the server, not in the scanner’s report: ten requests looked like a pass, and the API-definition scan made three thousand. Put the record of what shipped after the shipping, where a failure shows in red without stopping anything. And, learned last, count minutes as well as seconds: a scan that is free to build can still empty a monthly quota.
The posts, in order: the decision that made DAST cheap, three weeks with the gates, how little the scanner was actually reaching, and the month the free minutes ran out.