FifeRouter

4 September 2026 · security ci

The security pipeline — what blocks a merge and what only reports

Two workflows with the same tools and opposite rules. A scanner that can fail your build on somebody else's disclosure is a scanner people learn to skip.

Two files, same toolchain:

security-scan.yml   report-only   Semgrep · Trivy · SBOM+Grype · ZAP baseline
security-gate.yml   blocking      secrets · diff-aware SAST · SCA · IaC

The split is the design. Everything else is configuration.

Why not one

A single blocking pipeline sounds stricter and is worse, because of what "blocking" means when the input is a moving target.

A dependency scanner reads a vulnerability database that changes without you. Somebody discloses a CVE overnight in a transitive dependency you have never heard of, and your build goes red on a pull request that changed a CSS file. Nothing you did caused it and nothing in your change can fix it.

Do that twice and people learn the workaround: re-run it, merge past it, or add a blanket ignore. The scanner has trained everyone to route around it, and the finding that actually mattered arrives into a process that no longer reads findings.

So visibility is unconditional and blocking is narrow.

What blocks

Secrets — any, zero tolerance. The one threshold that should never be relaxed. This repository's whole credential design rests on secrets living in GitHub and reaching the server only through a forced command; a key committed to the tree routes around all of it.

SAST, diff-aware. Only findings this pull request introduces fail. The existing backlog is surfaced by the report-only run instead. A gate that blocks every change until a legacy backlog is cleared is a gate that gets disabled in week two.

SCA — HIGH and CRITICAL, fixable only. --ignore-unfixed, because blocking on a vulnerability with no available patch is blocking on something nobody can act on. That is the difference between a gate and a wall.

IaC — HIGH and above. Dockerfiles and the deployment manifest.

What only reports

Everything above with broader rulesets, plus a CycloneDX SBOM with Grype over it, and an OWASP ZAP baseline against the live hosts.

ZAP is schedule and dispatch only. A baseline scan is passive — it crawls and reads responses rather than attacking — but it is still traffic against production, and running it per pull request would mean scanning the live site because somebody edited a comment.

Waivers carry a reason

.trivyignore starts empty, with the rule at the top:

a waiver carries a reason, an owner and a review date. A bare id with no note is indistinguishable from someone silencing a scanner to get a merge through, and in six months nobody can tell which it was.

That is the whole difference between a waiver file and a suppression file. Same syntax, opposite meaning, and the only thing separating them is a convention somebody has to hold.

The first run found something

KSV-0014 — the router container had no readOnlyRootFilesystem.

Fixed rather than waived, because nothing that process writes belongs in the image: the decision log already goes to a mounted volume, so the root filesystem can be immutable. allowPrivilegeEscalation: false and dropping all capabilities came along with it.

Worth saying that a clean first run would have been the worrying outcome. A scanner that finds nothing on a codebase that has never been scanned is usually a scanner that is not looking at it.

The adaptation worth noting

This is KibiPay's pipeline, ported so a finding means the same thing in both repositories. Three things changed, each stated rather than silently dropped.

The licence gate is absent. KibiPay fails on the GPL/AGPL family, which follows from a proprietary distribution model. This repository has no LICENSE and has not declared one, so the same gate would enforce a policy nobody has decided — and the first AGPL dependency would block a merge with a rule that cannot be pointed at. The scan inventories licences instead, so one arriving is visible.

Copying a security policy without copying the decision behind it is how a team ends up defending a rule nobody chose.


← All posts