Safeguard
Engineering

Security Debt: Measuring and Paying It Down

Security debt is the gap between the risk you're carrying and the risk you've decided to carry. Here's how to measure it in vuln-days and pay it down without a heroic quarter.

Jonas Meyer
Site Reliability Engineer
7 min read

Security debt is every known, unresolved security deficiency you're carrying — old vulnerabilities, stale dependencies, expired exceptions, unpatched images — and like financial debt it has a principal (the fixes you owe), an interest rate (the growing cost and risk of deferral), and a minimum payment (the remediation rate below which the balance only grows). Veracode's 2024 State of Software Security put a number on how common the balance is: 42% of applications carry flaws that have been open for over a year, their working definition of debt. The useful move is treating yours as a measurable quantity instead of a mood.

Define the unit before you argue about the amount

Counting "open findings" is how debt conversations go nowhere — 500 findings might be 500 trivialities or 5 disasters and 495 rounding errors. Better units:

  • Vuln-days: for each open finding, days since it became fixable (a patched version existed), weighted by severity or exploitability. One critical open for 300 days = 300 critical vuln-days. This makes both volume and age visible, and it moves in the right direction when you fix old things, not just when you fix many things.
  • Libyears: the sum, across all dependencies, of how far behind the latest release each one sits. A service on django 3.2 when 4.2 is current contributes about two libyears. Computable with the libyear tooling for most ecosystems (pip install libyear, npx libyear). It measures the debt that hasn't turned into CVEs yet — the upgrade distance you'll have to cover at the worst possible moment when a critical lands.
  • Past-SLA count: findings that have outlived your own policy. This is the debt you've formally admitted to.
  • Exception decay: risk acceptances past their review date. An exception with no expiry isn't risk management, it's a permanent discount on your own policy.

A workable debt dashboard is those four numbers per service, trended weekly. Two afternoons of work against your scanner's API.

Where the interest gets charged

Deferred security work compounds through specific mechanisms, and knowing them tells you which debt to pay first:

Upgrade distance compounds worst. Skipping express 4.17 to 4.18 costs 20 minutes. Skipping four years of releases means breaking changes, transitive conflicts, and a migration project — under deadline pressure, because the trigger will be a critical CVE. Log4Shell in December 2021 was the industry-wide demonstration: teams one minor version behind patched in hours; teams on Log4j 1.x, which was EOL and architecturally different, were still remediating months later. The interest rate on staleness is nonlinear.

Debt attracts debt. A repo with 40 open findings gets a 41st with no ceremony; nobody notices one more broken window. Empirically, once scanner output stops fitting on one screen, developers stop reading it entirely, and your detection investment quietly becomes shelfware.

Audit surface grows. Every open finding is a line item somebody will eventually make you explain — a SOC 2 auditor, a customer questionnaire, an incident retro. Explanation cost scales with backlog size even when risk doesn't.

Paying it down: batch by cause, not by finding

The single highest-leverage tactic: group the backlog by remediation action instead of by finding. One jackson-databind bump in a shared build file can close dozens of findings across every service that inherits it; upgrading a golden base image closes OS-level findings fleet-wide in one PR. In most dependency backlogs, the top ten upgrade actions close 40–60% of open findings. Sort by findings-closed-per-PR, descending, and work the list.

Concretely:

# Which packages account for the most open findings?
jq -r '.findings[] | .package' findings.json | sort | uniq -c | sort -rn | head -20

Then structure the payments:

  1. Stop the bleeding first. Turn on automated dependency updates (Renovate or Dependabot) with auto-merge for patch-level bumps that pass CI. This caps new debt accumulation at near zero for the cheap category — and in mature setups, routine bumps silently close 40–60% of dependency findings before anyone triages them.
  2. Fixed-percentage capacity, not heroic quarters. A standing 10–15% of engineering capacity on debt paydown beats an annual "security sprint" that reliably gets cannibalized. Debt paydown as a utility bill, not a New Year's resolution.
  3. Boy-scout rule with teeth. Any PR touching a manifest file must leave dependencies no staler than it found them. Enforceable in CI by failing builds whose lockfile references versions with known criticals and an available patch — SCA gating at the PR stage makes the check automatic instead of aspirational.
  4. Big rocks get project treatment. The EOL framework migration doesn't fit in the 15% budget. Write it up with a cost, a risk statement, and a date; get it on the roadmap or get the risk formally accepted by someone with the authority to accept it. The worst outcome is the permanent limbo where it's neither funded nor accepted.

Keeping it down: the ratchet

Paydown without a ratchet is a sandcastle. After each debt milestone, tighten the policy so regression is mechanically blocked:

  • Freeze the vuln-days baseline: CI fails any PR that would push a service's weighted vuln-days above its current watermark.
  • Shrink SLA windows as the backlog shrinks — 90-day critical SLAs are for insolvent programs; solvent ones run 7–15 days because the queue is short enough to honor them.
  • Expire every exception. Default 90 days, renewable only by re-review. Calendar does the enforcement.
  • Report the trend, not the snapshot. "Vuln-days down 34% this quarter, arrival rate now below closure rate" is a story leadership funds. A raw count of open findings is a story leadership fears and defunds.

The dry truth from the SRE side of the fence: security debt behaves exactly like reliability debt. You don't get to zero, and chasing zero is its own pathology. You get to a balance you chose on purpose, with payments you can sustain, and alarms for when the interest rate spikes. If you want a structured curriculum for building this out, the Safeguard Academy material on vulnerability management program design covers the metrics and gating patterns in depth.

Frequently asked questions

What's the difference between security debt and a vulnerability backlog?

The backlog is the raw queue of open findings; debt is the subset you've known about and deferred past your own thresholds, plus the non-finding deficiencies — stale dependencies, EOL runtimes, expired exceptions — that no scanner files a ticket for. All debt eventually surfaces in the backlog, usually at the least convenient time.

Is some security debt acceptable?

Yes, deliberately and in writing. A low-exploitability finding in an internal tool, accepted with an owner, a rationale, and an expiry date, is sound engineering economics. The unacceptable kind is silent debt: deficiencies nobody decided to carry, which is most of what audits find.

What's a realistic paydown target for the first quarter?

Cutting weighted vuln-days 30–40% is achievable for most teams, because the first pass harvests the batch fixes — shared-dependency bumps and base image updates that close many findings per PR. The second quarter is harder and slower; that's when the fixed-capacity model matters more than the initial enthusiasm.

Which single metric should we start with?

Vuln-days, severity-weighted, per service. It's one query against your scanner data, it can't be gamed by mass-closing trivia (age matters) or by ignoring old findings (they accrue daily), and its trend line is legible to both engineers and executives.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.