<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>CIS Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/cis/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/cis/</link>
	<description>Infrastructure security, from the kernel up.</description>
	<lastBuildDate>Mon, 27 Jul 2026 11:57:23 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.4</generator>

<image>
	<url>https://linuxcent.com/wp-content/uploads/2026/04/favicon-512x512-1-150x150.png</url>
	<title>CIS Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/cis/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<item>
		<title>Compliance Grading — Automated OpenSCAP with A-F Scores Before Deployment</title>
		<link>https://linuxcent.com/automated-compliance-scanning-openscap/</link>
					<comments>https://linuxcent.com/automated-compliance-scanning-openscap/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Fri, 15 May 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[OS Image Builder]]></category>
		<category><![CDATA[BakeX]]></category>
		<category><![CDATA[CIS]]></category>
		<category><![CDATA[Compliance]]></category>
		<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[OpenSCAP]]></category>
		<category><![CDATA[Security]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=1828</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 6</span> <span class="rt-label rt-postfix">minutes</span></span>Automated OpenSCAP scanning with A-F compliance grades before deployment. SARIF export, drift detection, and compliance metadata baked into every AMI.</p>
<p>The post <a href="https://linuxcent.com/automated-compliance-scanning-openscap/">Compliance Grading — Automated OpenSCAP with A-F Scores Before Deployment</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 6</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>OS Hardening as Code, Episode 4</em><br />
<em><a href="https://linuxcent.com/cloud-ami-security-risks-custom-os-images/">Cloud AMI Security Risks</a> · <a href="/linux-hardening-as-code/">Linux Hardening as Code</a> · <a href="/linux-hardening-multi-cloud/">Multi-Cloud OS Hardening</a> · </em><em>Automated OpenSCAP Compliance</em>**</p>
<blockquote>
<p><strong>Note:</strong> the tool in this series was released as <strong>Stratum</strong> and renamed to <strong>BakeX</strong> at<br />
v0.6.0 — same project, same license, same team. Commands below use the current <code class="" data-line="">bakex</code><br />
CLI. If you arrived here looking for <code class="" data-line="">stratum</code> or <code class="" data-line="">pip install stratumoss</code>, you&#8217;re in the<br />
right place: <a href="https://github.com/invicton/bakex">github.com/invicton/bakex</a>.</p>
</blockquote>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>&#8220;We use CIS L1&#8221; means nothing without a verified grade — automated OpenSCAP compliance provides one before any instance is deployed</li>
<li>BakeX runs OpenSCAP as a stage of every build, and the scan result carries a letter grade A–F</li>
<li>The grade is OpenSCAP&#8217;s own XCCDF score mapped to a letter: A ≥ 90, B ≥ 75, C ≥ 60, D ≥ 40, F below that</li>
<li>SARIF output is machine-readable — importable directly into GitHub Advanced Security, Jira, or any SIEM</li>
<li>Scanning and baseline comparison live in the web UI and HTTP API, not the CLI — the CLI is <code class="" data-line="">validate</code> and <code class="" data-line="">build</code></li>
<li>A build whose scan fails the blueprint&#8217;s threshold ends in <code class="" data-line="">Status: failed</code> with exit code 1, and no image is snapshotted</li>
</ul>
<hr />
<h2 id="the-problem-a-grade-thats-never-been-verified-is-not-a-grade">The Problem: A Grade That&#8217;s Never Been Verified Is Not a Grade</h2>
<pre><code class="" data-line="">Security audit request:
&quot;Provide CIS L1 compliance evidence for all production instances&quot;

Team response:
  Instance A: &quot;CIS L1 hardened&quot; — OpenSCAP last run: 4 months ago
  Instance B: &quot;CIS L1 hardened&quot; — OpenSCAP last run: never
  Instance C: &quot;CIS L1 hardened&quot; — OpenSCAP version: 1.2 (current: 1.3.8)
  Instance D: &quot;CIS L1 hardened&quot; — manual scan output: &quot;87% passing&quot;
  Instance E: &quot;CIS L1 hardened&quot; — manual scan output: &quot;91% passing&quot;

&quot;Which profile was used for D and E? Are they comparable?&quot;
&quot;Were they scanned before or after a recent kernel update?&quot;
&quot;Why is C running an old OpenSCAP version?&quot;
</code></pre>
<p>Automated OpenSCAP compliance means the grade is generated the same way, on every image, every time, before the image is ever deployed.</p>
<p>EP03 showed that the same HardeningBlueprint YAML builds consistent OS images across six cloud providers. What it left open is the question every auditor eventually asks: how do you know the Ansible hardening actually did what you think it did? Running Ansible-Lockdown successfully means the tasks ran. It does not mean every CIS control is satisfied — some controls can&#8217;t be applied by Ansible alone, some require manual verification, and some interact with the environment in unexpected ways.</p>
<hr />
<p>A compliance team requested CIS L2 evidence for a SOC 2 Type II audit. The security team had been running OpenSCAP scans — but manually, on-demand, using slightly different profiles across teams, with no standard for how to store or compare results.</p>
<p>The audit found four problems:<br />
1. Two instances had been scanned with CIS L1, not L2, despite being labeled &#8220;CIS L2&#8221;<br />
2. Three instances hadn&#8217;t been scanned in over six months<br />
3. The scan outputs from different teams were in different formats (HTML vs XML vs text)<br />
4. Two instances showed &#8220;91% passing&#8221; and &#8220;89% passing&#8221; — with no documentation of whether those were acceptable thresholds or what the failing controls were</p>
<p>The audit took two weeks to resolve. The finding wasn&#8217;t a security failure — it was a documentation and process failure. But it consumed two weeks of engineering time and appeared in the audit report as a gap.</p>
<p>The root cause: compliance scanning was a manual step that produced inconsistent output in an inconsistent format.</p>
<hr />
<h2 id="how-automated-openscap-compliance-works">How Automated OpenSCAP Compliance Works</h2>
<p>Scanning is a stage of the build, not an afterthought you remember to run:</p>
<pre><code class="" data-line="">bakex build blueprints/ubuntu/22.04/cis-l1-aws.yaml
      │
      ├─ Provisioning via aws
      │
      ├─ Applying pre-hardening system configuration
      │    (hostname, filesystem, users)
      │
      ├─ Applying Ansible-Lockdown hardening roles
      │
      ├─ Running OpenSCAP compliance scan
      │    ├── benchmark:  xccdf_org.ssgproject.content_benchmark_UBUNTU2204
      │    ├── profile:    ...content_profile_cis_level1_server
      │    └── datastream: ssg-ubuntu2204-ds.xml
      │
      ├─ Snapshotting golden image
      │
      └─ Image ready: ami-0a7f3c9e82d1b4c05
</code></pre>
<p>All three compliance identifiers come from the blueprint&#8217;s <code class="" data-line="">compliance</code> block, and they are full<br />
XCCDF strings rather than friendly names like <code class="" data-line="">cis-l1</code> — they&#8217;re handed to <code class="" data-line="">oscap</code> unmodified, so<br />
there is no name-mapping layer that can silently pick the wrong profile. That single detail<br />
answers the audit question &#8220;which profile was actually used?&#8221; without anyone having to remember.</p>
<p>Ubuntu is a special case worth knowing: it ships no SCAP content package in the archive, so BakeX<br />
downloads the matching datastream from a ComplianceAsCode release and checksum-verifies it rather<br />
than failing or silently scanning nothing.</p>
<hr />
<h2 id="the-a-f-grade-calculation">The A-F Grade Calculation</h2>
<p>The grade is deliberately boring, and that is the point. BakeX does not invent a scoring model —<br />
it takes OpenSCAP&#8217;s own XCCDF score and maps it to a letter:</p>
<pre><code class="" data-line="">def score_to_grade(score: float) -&gt; str:
    if score &gt;= 90: return &quot;A&quot;
    if score &gt;= 75: return &quot;B&quot;
    if score &gt;= 60: return &quot;C&quot;
    if score &gt;= 40: return &quot;D&quot;
    return &quot;F&quot;
</code></pre>
<table>
<thead>
<tr>
<th>Grade</th>
<th>Score</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td>A</td>
<td>≥ 90</td>
<td>Production-ready, minimal exceptions</td>
</tr>
<tr>
<td>B</td>
<td>≥ 75</td>
<td>Acceptable with documented exceptions</td>
</tr>
<tr>
<td>C</td>
<td>≥ 60</td>
<td>Below standard — deploy with caution</td>
</tr>
<tr>
<td>D</td>
<td>≥ 40</td>
<td>Significant gaps — do not deploy to production</td>
</tr>
<tr>
<td>F</td>
<td>&lt; 40</td>
<td>Hardening failed</td>
</tr>
</tbody>
</table>
<p>The thresholds are fixed, not per-blueprint tunables. That is a defensible choice: a grade you can<br />
adjust in the file being graded is not evidence, it&#8217;s decoration. If an A means ≥ 90 everywhere,<br />
two teams&#8217; grades are comparable without reading their blueprints — which was exactly the failure<br />
in the audit story above.</p>
<p>What <em>is</em> configurable is when the build refuses to continue:</p>
<pre><code class="" data-line="">compliance:
  benchmark: xccdf_org.ssgproject.content_benchmark_UBUNTU2204
  profile: xccdf_org.ssgproject.content_profile_cis_level1_server
  datastream: /usr/share/xml/scap/ssg/content/ssg-ubuntu2204-ds.xml
  fail_on_findings: true      # findings at/above the threshold fail the build
  severity_threshold: medium  # critical | high | medium | low
</code></pre>
<p><code class="" data-line="">fail_on_findings</code> with a <code class="" data-line="">severity_threshold</code> is severity-based rather than score-based, which<br />
tends to match how people actually reason about risk: one critical finding should block a release<br />
even when 94% of rules pass. When it trips, the build ends in <code class="" data-line="">Status: failed</code>, exit code 1, and<br />
no image is snapshotted.</p>
<hr />
<h2 id="where-the-scan-surface-actually-lives">Where the Scan Surface Actually Lives</h2>
<p>Worth being blunt about this, because it is the most common wrong assumption: <strong>there is no<br />
<code class="" data-line="">bakex scan</code> command.</strong> The CLI is two verbs — <code class="" data-line="">validate</code> and <code class="" data-line="">build</code>. Scanning, history, and<br />
baseline comparison live in the web app and its HTTP API, because scan results need somewhere to<br />
persist and something to render them.</p>
<p>Start the server and the whole surface is there:</p>
<pre><code class="" data-line="">bakex serve --port 8000
</code></pre>
<p>The auditor API is mounted at <code class="" data-line="">/api/auditor</code>:</p>
<table>
<thead>
<tr>
<th>Endpoint</th>
<th>What it does</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="" data-line="">POST /api/auditor/scan-image</code></td>
<td>Scan an image and return a job</td>
</tr>
<tr>
<td><code class="" data-line="">POST /api/auditor/scan-container</code></td>
<td>Same, for a container image</td>
</tr>
<tr>
<td><code class="" data-line="">GET  /api/auditor/jobs</code></td>
<td>List scan jobs</td>
</tr>
<tr>
<td><code class="" data-line="">GET  /api/auditor/jobs/{job_id}</code></td>
<td>One job, with grade and severity counts</td>
</tr>
<tr>
<td><code class="" data-line="">GET  /api/auditor/jobs/{job_id}/compare/{baseline_id}</code></td>
<td>Diff a scan against a baseline</td>
</tr>
<tr>
<td><code class="" data-line="">GET  /api/auditor/scan-image/{job_id}/report?fmt=…</code></td>
<td>Export the report</td>
</tr>
<tr>
<td><code class="" data-line="">GET  /api/auditor/scan-image/{job_id}/badge.svg</code></td>
<td>Grade badge for a README</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="sarif-export">SARIF Export</h2>
<p>The report endpoint speaks three formats, selected by query parameter:</p>
<pre><code class="" data-line=""># Human-readable — printable HTML, print-to-PDF from the browser
curl &quot;http://localhost:8000/api/auditor/scan-image/$JOB/report?fmt=html&quot;

# Machine-readable job dict
curl &quot;http://localhost:8000/api/auditor/scan-image/$JOB/report?fmt=json&quot;

# SARIF 2.1.0 — the one that matters for CI
curl -o scan.sarif.json \
  &quot;http://localhost:8000/api/auditor/scan-image/$JOB/report?fmt=sarif&quot;
</code></pre>
<p>SARIF 2.1.0 is the standard interchange format for security scan results, which means the OpenSCAP<br />
findings land wherever your other scanners&#8217; findings already land:</p>
<ul>
<li><strong>GitHub Advanced Security</strong> — upload with <code class="" data-line="">github/codeql-action/upload-sarif</code>; findings appear in the Security tab, annotated on the PR</li>
<li><strong>Azure DevOps</strong> — native SARIF viewer</li>
<li><strong>Splunk / SIEM</strong> — structured JSON, parseable as events</li>
<li><strong>AWS Security Hub</strong> — importable as findings via the Security Hub API</li>
</ul>
<p>For audit purposes the SARIF file <em>is</em> the evidence artifact: it carries every rule result, the<br />
profile that was used, and the timestamp. &#8220;91% passing&#8221; in a spreadsheet is a claim. A SARIF file<br />
in the Security tab is a record.</p>
<p>The badge endpoint is the small touch that gets used most — <code class="" data-line="">badge.svg</code> renders the letter grade,<br />
so a repo&#8217;s README can show the compliance grade of the image it builds, next to the CI badge.</p>
<hr />
<h2 id="drift-comparing-against-a-baseline">Drift: Comparing Against a Baseline</h2>
<p>The comparison endpoint takes two job IDs — a current scan and a stored baseline — and reports the<br />
delta, including the change in score:</p>
<pre><code class="" data-line="">curl &quot;http://localhost:8000/api/auditor/jobs/$CURRENT/compare/$BASELINE&quot;
</code></pre>
<p>That is the mechanism behind &#8220;what changed since we built this.&#8221; You scan the image at build time,<br />
keep that job as the baseline, and re-scan later; the comparison tells you which rules moved and<br />
which direction the score went. It is how you find the instance somebody modified &#8220;temporarily&#8221;<br />
and never reverted.</p>
<p>The honest limitation: this compares <em>scan jobs</em>, so drift detection is as good as your discipline<br />
about scanning on a schedule. Nothing re-scans your fleet for you.</p>
<hr />
<h2 id="what-controls-typically-block-an-a-grade">What Controls Typically Block an A Grade</h2>
<p>For Ubuntu 22.04 CIS L1 builds in most cloud environments, these are the controls that most commonly prevent an A grade:</p>
<table>
<thead>
<tr>
<th>Control</th>
<th>Why it often fails</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td>1.1.7 <code class="" data-line="">/var/log/audit</code> separate partition</td>
<td>Cloud images don&#8217;t have separate volumes at build time</td>
<td>Add EBS volume, configure at launch</td>
</tr>
<tr>
<td>1.6.1 AppArmor bootloader config</td>
<td>GRUB parameters not set correctly</td>
<td>Update <code class="" data-line="">/etc/default/grub</code>, run <code class="" data-line="">update-grub</code></td>
</tr>
<tr>
<td>3.1.1 Disable IPv6</td>
<td>Cloud networking sometimes requires IPv6</td>
<td>Override with documented reason if intentional</td>
</tr>
<tr>
<td>5.2.21 SSH MaxStartups</td>
<td>Default sshd_config not updated</td>
<td>Add <code class="" data-line="">MaxStartups 10:30:60</code> to sshd_config</td>
</tr>
<tr>
<td>6.1.10 World-writable files</td>
<td>Some package installations leave world-writable files</td>
<td>Post-install cleanup in Ansible role</td>
</tr>
</tbody>
</table>
<p>The first two (separate audit partition, AppArmor bootloader) are the most common A→B blockers and often require architecture decisions about how volumes are provisioned at launch versus build time.</p>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Automated OpenSCAP compliance means every image has a verified, reproducible grade generated by the same scanner with the same profile, before it&#8217;s ever deployed</li>
<li>The grade is OpenSCAP&#8217;s own XCCDF score mapped to a fixed scale (A ≥ 90, B ≥ 75, C ≥ 60, D ≥ 40) — fixed on purpose, so grades from two teams are comparable without reading their blueprints</li>
<li>The build gate is severity-based, not score-based: <code class="" data-line="">fail_on_findings</code> plus <code class="" data-line="">severity_threshold</code> blocks a release on one critical finding even when most rules pass</li>
<li>SARIF 2.1.0 export makes scan results importable into GitHub Advanced Security, Azure DevOps, SIEM, and audit tooling — the SARIF file is the evidence artifact</li>
<li>Scanning and baseline comparison are HTTP API surfaces, not CLI commands; the CLI is <code class="" data-line="">validate</code> and <code class="" data-line="">build</code></li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>Automated OpenSCAP compliance gives every image a verified grade before deployment. What EP04 left open is what happens after the grade is known — specifically, what prevents an engineer from deploying a C-grade image to production &#8220;just this once.&#8221;</p>
<p>The Pipeline API is the answer. EP05 covers the CI/CD compliance gate: <code class="" data-line="">POST /api/pipeline/scan</code> fails the build if the image grade is below threshold. The unhardened image never reaches production — not because engineers are disciplined, but because the pipeline won&#8217;t let it through.</p>
<p><em>Next: <a href="/hardened-image-cicd-pipeline-gate/">CI/CD compliance gate — block unhardened images before they reach production</a></em></p>
<p>Get EP05 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fautomated-compliance-scanning-openscap%2F&amp;linkname=Compliance%20Grading%20%E2%80%94%20Automated%20OpenSCAP%20with%20A-F%20Scores%20Before%20Deployment" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fautomated-compliance-scanning-openscap%2F&amp;linkname=Compliance%20Grading%20%E2%80%94%20Automated%20OpenSCAP%20with%20A-F%20Scores%20Before%20Deployment" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fautomated-compliance-scanning-openscap%2F&amp;linkname=Compliance%20Grading%20%E2%80%94%20Automated%20OpenSCAP%20with%20A-F%20Scores%20Before%20Deployment" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fautomated-compliance-scanning-openscap%2F&amp;linkname=Compliance%20Grading%20%E2%80%94%20Automated%20OpenSCAP%20with%20A-F%20Scores%20Before%20Deployment" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fautomated-compliance-scanning-openscap%2F&amp;linkname=Compliance%20Grading%20%E2%80%94%20Automated%20OpenSCAP%20with%20A-F%20Scores%20Before%20Deployment" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fautomated-compliance-scanning-openscap%2F&amp;linkname=Compliance%20Grading%20%E2%80%94%20Automated%20OpenSCAP%20with%20A-F%20Scores%20Before%20Deployment" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fautomated-compliance-scanning-openscap%2F&amp;linkname=Compliance%20Grading%20%E2%80%94%20Automated%20OpenSCAP%20with%20A-F%20Scores%20Before%20Deployment" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fautomated-compliance-scanning-openscap%2F&#038;title=Compliance%20Grading%20%E2%80%94%20Automated%20OpenSCAP%20with%20A-F%20Scores%20Before%20Deployment" data-a2a-url="https://linuxcent.com/automated-compliance-scanning-openscap/" data-a2a-title="Compliance Grading — Automated OpenSCAP with A-F Scores Before Deployment"></a></p><p>The post <a href="https://linuxcent.com/automated-compliance-scanning-openscap/">Compliance Grading — Automated OpenSCAP with A-F Scores Before Deployment</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/automated-compliance-scanning-openscap/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1828</post-id>	</item>
		<item>
		<title>Hardening Blueprint as Code — Declare Your OS Baseline in YAML</title>
		<link>https://linuxcent.com/linux-hardening-as-code/</link>
					<comments>https://linuxcent.com/linux-hardening-as-code/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Fri, 24 Apr 2026 04:43:36 +0000</pubDate>
				<category><![CDATA[OS Image Builder]]></category>
		<category><![CDATA[BakeX]]></category>
		<category><![CDATA[CIS]]></category>
		<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[Infrastructure as Code]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[OS Hardening]]></category>
		<category><![CDATA[Security]]></category>
		<guid isPermaLink="false">https://linuxcent.com/linux-hardening-as-code/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 8</span> <span class="rt-label rt-postfix">minutes</span></span>Stop relying on hardening runbooks that get skipped at 2am. Declare your Linux OS baseline as a YAML blueprint — and build images where skipping a step is structurally impossible.</p>
<p>The post <a href="https://linuxcent.com/linux-hardening-as-code/">Hardening Blueprint as Code — Declare Your OS Baseline in YAML</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 8</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>OS Hardening as Code, Episode 2</em><br />
<em><a href="https://linuxcent.com/cloud-ami-security-risks-custom-os-images/">Cloud AMI Security Risks</a> · </em><em>Linux Hardening as Code</em>**</p>
<blockquote>
<p><strong>Note:</strong> the tool in this series was released as <strong>Stratum</strong> and renamed to <strong>BakeX</strong> at<br />
v0.6.0 — same project, same license, same team. Commands below use the current <code class="" data-line="">bakex</code><br />
CLI. If you arrived here looking for <code class="" data-line="">stratum</code> or <code class="" data-line="">pip install stratumoss</code>, you&#8217;re in the<br />
right place: <a href="https://github.com/invicton/bakex">github.com/invicton/bakex</a>.</p>
</blockquote>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>A hardening runbook is a list of steps someone runs. A HardeningBlueprint YAML is a build artifact — if it wasn&#8217;t applied, the image doesn&#8217;t exist</li>
<li>Linux hardening as code means declaring your entire OS security baseline in a single YAML file and building it reproducibly across any provider</li>
<li><code class="" data-line="">bakex build blueprints/ubuntu/22.04/cis-l1-aws.yaml</code> either produces a hardened image or fails — there is no partial state</li>
<li>The blueprint includes: target OS/provider/base image, compliance benchmark, filesystem layout, users, and per-rule overrides with documented justifications</li>
<li>One blueprint file = one source of truth for your hardening posture, version-controlled and reviewable like any other infrastructure code</li>
<li>Post-build OpenSCAP scan runs automatically — the image only snapshots if it passes</li>
</ul>
<hr />
<h2 id="the-problem-a-runbook-that-gets-skipped-once-is-a-runbook-that-gets-skipped">The Problem: A Runbook That Gets Skipped Once Is a Runbook That Gets Skipped</h2>
<pre><code class="" data-line="">Hardening runbook
       │
       ▼
  Human executes
  steps manually
       │
       ├─── 47 deployments: followed correctly
       │
       └─── 1 deployment at 2am: step 12 skipped
                    │
                    ▼
           Instance in production
           without audit logging,
           SSH password auth enabled,
           unnecessary services running
</code></pre>
<p>Linux hardening as code eliminates the human decision point. If the blueprint wasn&#8217;t applied, the image doesn&#8217;t exist.</p>
<p>EP01 showed that default cloud AMIs arrive pre-broken — unnecessary services, no audit logging, weak kernel parameters, SSH configured for convenience not security. The obvious response is a hardening script. But a script run by a human is still a process step. It can be skipped. It can be done halfway. It can drift across different engineers who each interpret &#8220;run the hardening script&#8221; slightly differently.</p>
<hr />
<p>A production deployment last year. The platform team had a solid CIS L1 hardening runbook — 68 steps, well-documented, followed consistently. Then a critical incident at 2am required three new instances to be deployed on short notice. The engineer on call ran the provisioning script and, under pressure, skipped the hardening step with the intention of running it the next morning.</p>
<p>They didn&#8217;t. The three instances stayed in production unhardened for six weeks before an automated scan caught them. Audit logging wasn&#8217;t configured. SSH was accepting password authentication. Two unnecessary services were running that weren&#8217;t in the approved software list.</p>
<p>Nothing was breached. But the finding went into the next compliance report as a gap, the team spent a week remediating, and the post-mortem conclusion was &#8220;we need better runbook discipline.&#8221;</p>
<p>That&#8217;s the wrong conclusion. The runbook isn&#8217;t the problem. The problem is that hardening was a process step instead of a build constraint.</p>
<hr />
<h2 id="what-linux-hardening-as-code-actually-means">What Linux Hardening as Code Actually Means</h2>
<p>Linux hardening as code is the same principle as infrastructure as code applied to OS security posture: the desired state is declared in a file, the file is the source of truth, and the execution is deterministic and repeatable.</p>
<pre><code class="" data-line="">HardeningBlueprint YAML
         │
         ▼
  bakex build
         │
  ┌──────┴──────────────────┐
  │  Provider Layer          │
  │  (cloud-init, disk       │
  │   names, metadata        │
  │   endpoint per provider) │
  └──────┬──────────────────┘
         │
  ┌──────┴──────────────────┐
  │  Ansible-Lockdown        │
  │  (CIS L1/L2, STIG —      │
  │   the hardening steps)   │
  └──────┬──────────────────┘
         │
  ┌──────┴──────────────────┐
  │  OpenSCAP Scanner        │
  │  (post-build verify)     │
  └──────┬──────────────────┘
         │
         ▼
  Golden Image (AMI/GCP image/Azure image)
  + Compliance grade in image metadata
</code></pre>
<p>The YAML file is what you write. BakeX handles the rest.</p>
<hr />
<h2 id="the-hardeningblueprint-yaml">The HardeningBlueprint YAML</h2>
<p>The blueprint is the complete, auditable declaration of your OS security posture:</p>
<pre><code class="" data-line=""># blueprints/ubuntu/22.04/cis-l1-aws.yaml
bakex_version: &quot;0.6.0&quot;
kind: HardeningBlueprint

metadata:
  name: ubuntu22-cis-l1-aws
  version: &quot;1.0.0&quot;
  description: &gt;
    CIS Ubuntu Linux 22.04 LTS Benchmark — Level 1 Server profile for AWS.
  tags: [ubuntu, ubuntu22.04, cis, level1, server, aws]

target:
  os: ubuntu22.04
  arch: x86_64
  provider: aws
  base_image: ami-0c7217cdde317cfec   # Ubuntu 22.04 LTS, us-east-1 (x86_64)
  instance_type: t3.medium
  root_volume_size_gb: 20

system:
  hostname: hardened-node
  timezone: UTC
  locale: en_US.UTF-8
  selinux_mode: null              # Ubuntu uses AppArmor; null skips SELinux

filesystem:                       # CIS 1.1.x: separate mounts with noexec/nosuid/nodev
  - device: tmpfs
    mountpoint: /tmp
    fstype: tmpfs
    options: [rw, nosuid, nodev, noexec, relatime]
    size: 2G

  - device: tmpfs
    mountpoint: /dev/shm
    fstype: tmpfs
    options: [rw, nosuid, nodev, noexec, relatime]

users:
  root:
    lock: true                    # CIS 5.4.2: lock the root account
  accounts:
    - name: bakex-admin
      groups: [sudo]
      shell: /bin/bash
      ssh_authorized_keys: []     # Add: - &quot;ssh-ed25519 AAAA...&quot;

compliance:
  benchmark: xccdf_org.ssgproject.content_benchmark_UBUNTU2204
  profile: xccdf_org.ssgproject.content_profile_cis_level1_server
  datastream: /usr/share/xml/scap/ssg/content/ssg-ubuntu2204-ds.xml
  fail_on_findings: true
  severity_threshold: medium

controls:                         # per-rule overrides, keyed by XCCDF rule ID
  xccdf_org.ssgproject.content_rule_grub2_enable_selinux:
    enabled: false
    justification: &gt;
      Ubuntu Linux uses AppArmor as the mandatory access control framework.
      SELinux is not installed or applicable on this distribution.

  xccdf_org.ssgproject.content_rule_package_telnet_removed: true
</code></pre>
<p>That is not a simplified illustration — it is the shipped <code class="" data-line="">cis-l1-aws.yaml</code>, trimmed of comments. Each section is explicit:</p>
<p><strong><code class="" data-line="">target</code></strong> — which OS, which provider, and which base image to harden. This is the only provider-specific section. Everything below it is portable.</p>
<p><strong><code class="" data-line="">compliance</code></strong> — the OpenSCAP benchmark, profile, and datastream. These are full XCCDF identifiers, not friendly names like <code class="" data-line="">cis-l1</code>, because they&#8217;re passed straight to <code class="" data-line="">oscap</code>. Ubuntu ships no SCAP content package, so BakeX downloads the matching datastream from a ComplianceAsCode release and checksum-verifies it.</p>
<p><strong><code class="" data-line="">filesystem</code></strong> — a list of mounts, each with its CIS-required options.</p>
<p><strong><code class="" data-line="">users</code></strong> — root lock state and the service accounts baked into the image.</p>
<p><strong><code class="" data-line="">controls</code></strong> — documented exceptions, keyed by XCCDF rule ID. Not suppressions — overrides with a recorded justification. A bare <code class="" data-line="">true</code> enforces a rule; a map with <code class="" data-line="">enabled: false</code> disables it and demands a reason. This is the difference between &#8220;we turned off this control&#8221; and &#8220;this control is not applicable, documented here.&#8221;</p>
<hr />
<h2 id="building-the-image">Building the Image</h2>
<pre><code class="" data-line=""># Validate the blueprint before building — exit 0 if valid, 1 if not
bakex validate blueprints/ubuntu/22.04/cis-l1-aws.yaml

# Build — this will take 15-25 minutes
bakex build blueprints/ubuntu/22.04/cis-l1-aws.yaml
</code></pre>
<p>Validation is fast and offline:</p>
<pre><code class="" data-line="">OK    blueprints/ubuntu/22.04/cis-l1-aws.yaml  (HardeningBlueprint &#039;ubuntu22-cis-l1-aws&#039;)

1/1 blueprint(s) valid.
</code></pre>
<p>The build walks five stages, and the provider is read from the blueprint — there is no <code class="" data-line="">--provider</code> flag to get wrong:</p>
<pre><code class="" data-line="">Building &#039;ubuntu22-cis-l1-aws&#039; (aws) → job 7f3c9e82-4d1b-4c05-a7f3-c9e82d1b4c05
[2026-07-27T15:42:01] Provisioning via aws
[2026-07-27T15:42:45] Applying pre-hardening system configuration (hostname, filesystem, users)
[2026-07-27T15:43:12] Applying Ansible-Lockdown hardening roles
[2026-07-27T15:52:11] Running OpenSCAP compliance scan
[2026-07-27T15:54:10] Snapshotting golden image
[2026-07-27T15:54:47] Image ready: ami-0a7f3c9e82d1b4c05

Status: complete
</code></pre>
<p>Two things worth noticing. <code class="" data-line="">bakex build</code> also accepts a bundled profile name, so <code class="" data-line="">bakex build ubuntu22-cis-l1-aws</code> does the same thing without a path. And <code class="" data-line="">--json</code> emits the job record instead of the log, which is what you want in CI.</p>
<p>If the post-build scan comes back below the configured threshold, the build fails — no AMI is created. The instance is terminated. The image does not exist. Exit code is 1, and <code class="" data-line="">Status: failed</code> carries the reason.</p>
<p>That is the structural guarantee. You cannot skip a build step at 2am because at 2am you&#8217;re calling <code class="" data-line="">bakex build</code>, not running steps manually.</p>
<hr />
<h2 id="the-control-override-mechanism">The Control Override Mechanism</h2>
<p>The override mechanism is what separates this from checkbox compliance.</p>
<p>Every security benchmark has controls that conflict with how production environments actually work. CIS L1 recommends <code class="" data-line="">/tmp</code> on a separate partition. Many cloud instances use tmpfs with equivalent <code class="" data-line="">nodev, nosuid, noexec</code> mount options. The intent of the control is satisfied. The literal implementation differs.</p>
<p>Without an override mechanism, you have two bad options: fail the scan (noisy, meaningless), or configure the scanner to ignore the control (undocumented, invisible to auditors).</p>
<p>The blueprint&#8217;s <code class="" data-line="">controls</code> section gives you a third option: record the override and its justification in the same version-controlled artifact that produced the image.</p>
<pre><code class="" data-line="">controls:
  xccdf_org.ssgproject.content_rule_grub2_enable_selinux:
    enabled: false
    justification: &gt;
      Ubuntu Linux uses AppArmor as the mandatory access control framework.
      SELinux is not installed or applicable on this distribution.
</code></pre>
<p>Note that the key is the full XCCDF rule ID, not a CIS section number. That is deliberate — the rule ID is what OpenSCAP reports, so the override and the scanner finding line up exactly with no translation table in between.</p>
<p><strong>Be precise about what this does today.</strong> As of v0.6.0 the <code class="" data-line="">controls</code> block is <em>declarative</em>:<br />
it records intent alongside the blueprint, and the UI reads it back, but it is not yet compiled<br />
into an OpenSCAP tailoring file. The scan still evaluates the full profile, so an overridden<br />
rule still shows up as a finding and still counts against the score. What you get right now is<br />
provenance — the justification lives in git, next to the thing that built the image, reviewable<br />
in a pull request — not automatic score adjustment.</p>
<p>That is a smaller claim than &#8220;documented exceptions are counted as compliant,&#8221; and it&#8217;s the<br />
true one. Wiring overrides through to scan tailoring is the obvious next step, and it&#8217;s the<br />
kind of well-scoped gap that makes a good first contribution.</p>
<hr />
<h2 id="what-the-blueprint-gives-you-that-a-script-doesnt">What the Blueprint Gives You That a Script Doesn&#8217;t</h2>
<table>
<thead>
<tr>
<th></th>
<th>Hardening script</th>
<th>HardeningBlueprint YAML</th>
</tr>
</thead>
<tbody>
<tr>
<td>Version-controlled</td>
<td>Possible but not enforced</td>
<td>Always — it&#8217;s a file</td>
</tr>
<tr>
<td>Auditable exceptions</td>
<td>Typically not</td>
<td>Built-in override mechanism</td>
</tr>
<tr>
<td>Post-build verification</td>
<td>Manual or none</td>
<td>Automatic OpenSCAP scan</td>
</tr>
<tr>
<td>Image exists only if hardened</td>
<td>No</td>
<td>Yes — build fails if scan fails</td>
</tr>
<tr>
<td>Multi-cloud portability</td>
<td>Requires separate scripts</td>
<td>Swap the <code class="" data-line="">target</code> block; compliance sections stay identical</td>
</tr>
<tr>
<td>Drift detection</td>
<td>Not possible</td>
<td>Rescan instance against original grade</td>
</tr>
<tr>
<td>Skippable at 2am</td>
<td>Yes</td>
<td>No — you&#8217;d have to change the build process</td>
</tr>
</tbody>
</table>
<p>The last row is the one that matters. A script is skippable because there&#8217;s a human in the loop. A blueprint is a build artifact — you can&#8217;t deploy the image without the blueprint having been applied, because the image is what the blueprint produces.</p>
<hr />
<h2 id="validating-a-blueprint-before-building">Validating a Blueprint Before Building</h2>
<pre><code class="" data-line=""># Schema validation — one file
bakex validate blueprints/ubuntu/22.04/cis-l1-aws.yaml

# Or the whole library at once
bakex validate blueprints/**/*.yaml

# Machine-readable, for CI and agents
bakex validate blueprints/ubuntu/22.04/cis-l1-aws.yaml --json
</code></pre>
<p><code class="" data-line="">bakex validate</code> exits 0 when every file is valid and 1 when any file fails, which makes it a one-line CI gate. It never touches a cloud API — it is pure schema and cross-field checking, so it runs in your pipeline before you&#8217;ve paid for a build instance.</p>
<p>That cross-field part matters more than it sounds. Validation rejects OS/provider combinations the catalog doesn&#8217;t support, so a blueprint asking for an OS that provider can&#8217;t supply fails at validation time rather than fifteen minutes into a build.</p>
<p>If you want to generate or check blueprints from something other than the CLI, the format is published as a JSON Schema (Draft 2020-12) at <a href="https://github.com/invicton/bakex/blob/main/docs/schema/hardening-blueprint.schema.json"><code class="" data-line="">docs/schema/hardening-blueprint.schema.json</code></a>. Point your editor at it for autocomplete, or hand it to an LLM and let it write the blueprint.</p>
<hr />
<h2 id="production-gotchas">Production Gotchas</h2>
<p><strong>Build time is 15–25 minutes.</strong> Ansible-Lockdown applies 144+ tasks for CIS L1. Build this into your pipeline timing — don&#8217;t expect golden images in 3 minutes.</p>
<p><strong>Cloud-init ordering matters.</strong> On AWS, certain hardening steps (sysctl tuning, PAM configuration) interact with cloud-init. The BakeX provider layer handles sequencing — but if you add custom hardening roles, test the cloud-init interaction explicitly.</p>
<p><strong>Some CIS controls conflict with managed service requirements.</strong> AWS Systems Manager Session Manager requires specific SSH configuration. RDS requires specific networking settings. Use the <code class="" data-line="">controls</code> override section to document these — don&#8217;t suppress them silently.</p>
<p><strong>Kernel parameter hardening requires a reboot.</strong> Controls in the <code class="" data-line="">3.x</code> (network parameters) and <code class="" data-line="">1.5.x</code> (kernel modules) sections apply sysctl changes that take effect on reboot. The BakeX build process reboots the instance before the OpenSCAP scan — don&#8217;t skip the reboot if you&#8217;re building manually.</p>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Linux hardening as code means the blueprint YAML is the build artifact — the image either exists and is hardened, or it doesn&#8217;t exist</li>
<li>The <code class="" data-line="">controls</code> override mechanism is the difference between undocumented suppressions and auditable, reasoned exceptions</li>
<li>Post-build OpenSCAP scan runs automatically — a failing grade blocks image creation</li>
<li><code class="" data-line="">bakex validate</code> is an offline, exit-code-shaped CI gate — it catches unsupported OS/provider pairs before a build instance is ever launched</li>
<li>The compliance sections are portable across providers (EP03 covers this): swap the <code class="" data-line="">target</code> block, and the benchmark, filesystem, users, and control overrides stay byte-identical</li>
<li>Version-controlling the blueprint gives you a complete history of what your OS security posture was at any point in time — the same way Terraform state tracks infrastructure</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>One blueprint, one provider. EP02 showed that the skip-at-2am problem is solved when hardening is a build artifact rather than a process step.</p>
<p>What it didn&#8217;t address: what happens when you expand to a second cloud. GCP uses different disk names. Azure cloud-init fires in a different order. The AWS metadata endpoint IP is different from every other provider. If you maintain separate hardening scripts per cloud, they drift within a month.</p>
<p>EP03 covers multi-cloud OS hardening: one compliance posture, six providers, no drift — and it shows the diff that proves it.</p>
<p><em>Next: <a href="/linux-hardening-multi-cloud/">multi-cloud OS hardening — one blueprint for AWS, GCP, and Azure</a></em></p>
<p>Get EP03 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Flinux-hardening-as-code%2F&amp;linkname=Hardening%20Blueprint%20as%20Code%20%E2%80%94%20Declare%20Your%20OS%20Baseline%20in%20YAML" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Flinux-hardening-as-code%2F&amp;linkname=Hardening%20Blueprint%20as%20Code%20%E2%80%94%20Declare%20Your%20OS%20Baseline%20in%20YAML" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Flinux-hardening-as-code%2F&amp;linkname=Hardening%20Blueprint%20as%20Code%20%E2%80%94%20Declare%20Your%20OS%20Baseline%20in%20YAML" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Flinux-hardening-as-code%2F&amp;linkname=Hardening%20Blueprint%20as%20Code%20%E2%80%94%20Declare%20Your%20OS%20Baseline%20in%20YAML" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Flinux-hardening-as-code%2F&amp;linkname=Hardening%20Blueprint%20as%20Code%20%E2%80%94%20Declare%20Your%20OS%20Baseline%20in%20YAML" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Flinux-hardening-as-code%2F&amp;linkname=Hardening%20Blueprint%20as%20Code%20%E2%80%94%20Declare%20Your%20OS%20Baseline%20in%20YAML" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Flinux-hardening-as-code%2F&amp;linkname=Hardening%20Blueprint%20as%20Code%20%E2%80%94%20Declare%20Your%20OS%20Baseline%20in%20YAML" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Flinux-hardening-as-code%2F&#038;title=Hardening%20Blueprint%20as%20Code%20%E2%80%94%20Declare%20Your%20OS%20Baseline%20in%20YAML" data-a2a-url="https://linuxcent.com/linux-hardening-as-code/" data-a2a-title="Hardening Blueprint as Code — Declare Your OS Baseline in YAML"></a></p><p>The post <a href="https://linuxcent.com/linux-hardening-as-code/">Hardening Blueprint as Code — Declare Your OS Baseline in YAML</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/linux-hardening-as-code/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1548</post-id>	</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: linuxcent.com @ 2026-08-26 06:31:19 by W3 Total Cache
-->