Compliance

Why you maintain twelve codebases to enforce two frameworks

Two frameworks, twelve codebases. Why compliance automation drifts and falls 6-18 months behind every new OS, and the design decision underneath all of it.

HHanalyx
July 5, 2026 · 6 min read
Why you maintain twelve codebases to enforce two frameworks

You run CIS and STIG on a RHEL and Ubuntu fleet. Two frameworks. Ask yourself how many separate automation artifacts you actually maintain to make that happen: repos, roles, datastreams, script bundles. Count them honestly.

For most teams the answer is somewhere north of ten. And almost none of that number is explained by the systems being different. It's explained by the way the industry decided to organize the code.

How the industry organizes the codebase

Here is the shape of it. Take one OS family, RHEL, and two frameworks:

image

That's six full codebases before you've added a single other distribution. Add Ubuntu 22.04, Ubuntu 24.04, and SUSE and the same two frameworks give you twelve. Each cell is a complete, independently maintained copy of the compliance logic for that one intersection of framework and OS. Add a PCI-DSS hardening guide or a NIST 800-171 overlay and the count keeps climbing.

This isn't a strawman. It's the model every serious project ships:

  • Ansible Lockdown publishes separate repositories per OS per benchmark: RHEL8-CIS, RHEL9-CIS, RHEL8-STIG, RHEL9-STIG. By the project's own structure those four repos share an estimated 70–80% of their logic and are maintained as four independent codebases.
  • ComplianceAsCode (the OpenSCAP upstream) uses shared Jinja2 content but renders down to per-OS datastreams (ssg-rhel8-ds.xml, ssg-rhel9-ds.xml), separate artifacts with separate rule sets.
  • DISA STIG Ansible ships as separate zip archives per OS version: near-identical playbooks in different files.
  • CIS hardening scripts and Puppet hardening modules: same pattern, every time.

These are good tools built by people who know the controls really well. The duplication isn't a skill problem. It's structural. And it produces four specific taxes you pay every quarter.

The four taxes

Drift. This is the one that bites in an audit. Say you find a bug in your RHEL 9 CIS role's SSH MaxAuthTries remediation (CIS RHEL 9 controls 5.1.16). The task set the value in the main sshd_config, but on your hosts a drop-in under /etc/ssh/sshd_config.d/ overrode it, so the box reported compliant while the effective value was still wrong. You fix it in the RHEL 9 CIS repo: move the setting into a managed drop-in, add a verify step that reads the effective config.

Now that same logical control lives in your RHEL 8 CIS role, your RHEL 9 STIG playbook, your Ubuntu roles: every artifact that touches MaxAuthTries. The fix has to be hand-ported to each. In practice it isn't. Someone gets to two of them before the sprint ends, and the rest keep shipping the old broken version. Six months later two "compliant" hosts behave differently on the exact same control, not because the systems differ, but because the automation drifted. The same control, forked N ways, is N places for the truth to diverge.

Delayed coverage. When RHEL 10 ships, every benchmark and every artifact has to be rebuilt. DISA takes an estimated 6–18 months to publish a new STIG; CIS runs a similar cadence; the automation projects then start a fresh repo and begin writing. So the team running RHEL 10 in production has no sanctioned automation for it, even though, by the source estimates, 85%+ of the controls are mechanically identical to RHEL 9. You wrote them two years ago. You simply wrote them in a file that says 9.

False complexity. Because every new OS or framework "requires" a full implementation effort, teams come to believe compliance automation is inherently expensive. It isn't. The security controls barely moved between RHEL 8 and RHEL 9. The architecture makes them look like a rewrite. That's accidental complexity dressed up as the cost of doing business.

Maintenance burden. N full codebases means N times the testing, N times the review, N times the release cycle. That math ends one way: playbooks go stale, people burn out, and controls that are supposed to be enforced quietly aren't, because the role that enforces them hasn't been touched in eighteen months.

The root cause: a document is not a program

None of these are separate problems. They're four symptoms of one design decision, made so uniformly across the industry that it reads like a law of nature:

Policy and mechanism are fused into a single artifact, organized by the structure of the benchmark document instead of the structure of the problem.

CIS publishes a PDF per OS. DISA publishes a STIG per OS. So we build a playbook per OS. The organizational structure of the document became the organizational structure of the code. That's Conway's Law pointed at compliance content: the artifact mirrors the org chart of the paperwork.

Here's why that's the wrong axis. The document structure exists to serve auditors: humans who need to look up "what does RHEL 9 require for SSH?" and find it in one place, numbered the way the benchmark numbers it. That's a real need. But it's a reporting need.

The automation serves a different question entirely. A machine doesn't need to know what section MaxAuthTries lives in. It needs to answer: "on this host, right now, is the effective SSH MaxAuthTries at the required value, and if not, how do I make it so — durably?" That question has nothing to do with which PDF the control came from. The desired state (limit SSH authentication attempts) is true on RHEL 8, RHEL 9, RHEL 10, and every future OpenSSH-shipping distro. The mechanism to enforce it varies a little. The control does not vary at all.

When you organize the code by document, you glue that one stable control to a dozen copies of a mechanism, then maintain all dozen by hand. The MaxAuthTries fix has to be ported N times precisely because you filed one control under N documents.

What the twelve-codebase problem really is

Notice what the twelve-codebase problem actually is. It's not that these tools are bad. It's that they answer the auditor's question ("what does this document require?") with a structure built for the machine's question ("how do I enforce this control here, now?"). Those are different questions. They were always going to demand different architectures. The industry standardized on the one that serves the paperwork.

So the drift, the lag, the burnout aren't bugs in your playbooks. They're the predictable output of filing a rule as a task, of treating "disable SSH root login" as an Ansible task in a RHEL-9-shaped repo instead of as a single statement of desired state that happens to have a RHEL 9 implementation, a RHEL 8 implementation, and ten framework labels hanging off it.

That distinction (a rule is not a task) is the hinge the whole industry skipped. It's also the way out of the matrix.