<?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>OS Image Builder Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/category/os-image-builder/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/category/os-image-builder/</link>
	<description>Infrastructure security, from the kernel up.</description>
	<lastBuildDate>Mon, 27 Jul 2026 11:57:39 +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>OS Image Builder Archives - Linuxcent</title>
	<link>https://linuxcent.com/category/os-image-builder/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<item>
		<title>BakeX — OS Hardening as a Platform</title>
		<link>https://linuxcent.com/stratum-os-hardening-platform/</link>
					<comments>https://linuxcent.com/stratum-os-hardening-platform/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Sun, 31 May 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[OS Image Builder]]></category>
		<category><![CDATA[BakeX]]></category>
		<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[Infrastructure as Code]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Open Source]]></category>
		<category><![CDATA[OS Hardening]]></category>
		<category><![CDATA[Security]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=1834</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>Stratum — open-core (Apache 2.0) OS hardening platform: declare baselines in YAML, build across six clouds, and gate CI/CD deployments on compliance grade.</p>
<p>The post <a href="https://linuxcent.com/stratum-os-hardening-platform/">BakeX — OS Hardening as a Platform</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 6</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> · <a href="/automated-compliance-scanning-openscap/">Automated OpenSCAP Compliance</a> · <a href="/hardened-image-cicd-pipeline-gate/">CI/CD Compliance Gate</a> · </em><em>BakeX Platform</em>**</p>
<blockquote>
<p><strong>Note:</strong> this series was written when the project was called <strong>Stratum</strong>. It was renamed to<br />
<strong>BakeX</strong> at v0.6.0 — same project, same Apache 2.0 license, same team. The old<br />
<code class="" data-line="">github.com/rrskris/Stratum</code> URL redirects here, and <code class="" data-line="">pip install stratumoss</code> is retired in<br />
favour of <code class="" data-line="">pip install bakex</code>. Current home:<br />
<a href="https://github.com/invicton/bakex">github.com/invicton/bakex</a>.</p>
</blockquote>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>BakeX is open-source under Apache 2.0 — the engine, blueprint format, scanner, and Pipeline API are all in the repository</li>
<li>Self-hostable end to end: nothing is locked to a hosted service, and there is no paid tier gating the pipeline</li>
<li>Two real extension points: <strong>provider plugins</strong> (drop-in <code class="" data-line="">.py</code> or a <code class="" data-line="">bakex.providers</code> entry point) and <strong>blueprints</strong> (pure YAML, no code)</li>
<li>Architecture: Blueprint YAML → Engine → Provider Layer → Ansible-Lockdown → OpenSCAP → Golden Image → Pipeline API</li>
<li>The series taught the user-facing interface for five episodes; EP06 covers what&#8217;s underneath and how to build on it</li>
<li>Installation is <code class="" data-line="">git clone</code> + <code class="" data-line="">docker compose up</code>, or <code class="" data-line="">pip install bakex</code> for the CLI and web app</li>
</ul>
<hr />
<h2 id="the-series-arc-inverted">The Series Arc, Inverted</h2>
<p>EP01 showed that default cloud AMIs arrive pre-broken. By the time you reach EP06, that problem has a complete solution:</p>
<pre><code class="" data-line="">EP01 — The problem:
  Default AMI → Production → Security audit finds gaps
  (unknown OS baseline, unverified hardening, no evidence)

EP06 — The solution:
  HardeningBlueprint YAML
           ↓
    bakex validate          ← EP02 (blueprint as code)
    bakex build             ← EP02
      one file per provider ← EP03 (multi-cloud)
           ↓
    OpenSCAP scan           ← EP04 (compliance grading)
    Grade: A (score 94)
           ↓
    POST /api/pipeline/scan ← EP05 (CI/CD gate)
    passed: true
           ↓
    Production deployment
    (Grade A, SARIF attached, blueprint version-controlled)
</code></pre>
<p>For five episodes, you&#8217;ve used BakeX as a user. This episode covers what it looks like to run it yourself, extend it, and build on it.</p>
<hr />
<p>I&#8217;ve spent years watching infrastructure teams solve the same OS hardening problem in slightly different ways. Custom scripts that drift. OpenSCAP runs that produce evidence no one reads. Compliance checklists completed by humans who have competing priorities.</p>
<p>The tools exist. <code class="" data-line="">ansible-lockdown</code> applies CIS controls reliably. OpenSCAP verifies them accurately. The CI/CD systems can enforce anything you can express as a pass/fail. The gap isn&#8217;t the tooling — it&#8217;s the integration layer that ties them together into a reproducible, auditable pipeline.</p>
<p>BakeX is that integration layer, open-sourced.</p>
<p>The philosophy is the same as Terraform applied to OS security posture: declare the desired state in a version-controlled file, apply it reproducibly, and verify it automatically. The skip-at-2am problem disappears not because engineers are more careful, but because there&#8217;s no step to skip.</p>
<hr />
<h2 id="the-architecture">The Architecture</h2>
<pre><code class="" data-line="">┌─────────────────────────────────────────────────────────┐
│                 HardeningBlueprint YAML                  │
│         (version-controlled, provider-agnostic)          │
└─────────────────────┬───────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────┐
│                    BakeX Engine                          │
│                  (Apache 2.0, OSS)                       │
│  ┌─────────────┐  ┌──────────────┐  ┌────────────────┐  │
│  │  Blueprint  │  │   Provider   │  │    Scheduler   │  │
│  │   Parser    │  │    Layer     │  │  (parallel     │  │
│  │             │  │  AWS  GCP    │  │   multi-cloud  │  │
│  │  Validates  │  │  Azure DO    │  │   builds)      │  │
│  │  schema +   │  │  Linode      │  │                │  │
│  │  overrides  │  │  Proxmox     │  │                │  │
│  └─────────────┘  └──────────────┘  └────────────────┘  │
└─────────────────────┬───────────────────────────────────┘
                      │
           ┌──────────┴──────────┐
           ▼                     ▼
  ┌─────────────────┐   ┌─────────────────┐
  │ Ansible-Lockdown │   │  OpenSCAP       │
  │  Runner          │   │  Scanner        │
  │                  │   │                 │
  │  UBUNTU22-CIS    │   │  A-F grade      │
  │  RHEL8-STIG      │   │  SARIF export   │
  │  Custom roles    │   │  Drift detect   │
  └────────┬─────────┘   └────────┬────────┘
           │                      │
           └──────────┬───────────┘
                      │
                      ▼
         ┌─────────────────────────┐
         │   Golden Image          │
         │   (AMI / GCP / Azure)   │
         │   + compliance metadata │
         └────────────┬────────────┘
                      │
                      ▼
         ┌─────────────────────────┐
         │   Pipeline API          │
         │   (Apache 2.0, OSS)     │
         │                         │
         │  POST /api/pipeline/scan │
         │  ← CI/CD gate           │
         └─────────────────────────┘
</code></pre>
<p>Every component is open-source under Apache 2.0. The engine, provider layer, Ansible runner, OpenSCAP scanner, and Pipeline API are all in the repository. Nothing is locked to a hosted service.</p>
<hr />
<h2 id="installation">Installation</h2>
<p>Three ways in, depending on how much you want installed on the host.</p>
<p><strong>Docker Compose — recommended, everything preinstalled:</strong></p>
<pre><code class="" data-line="">git clone https://github.com/invicton/bakex.git
cd bakex
docker compose up
</code></pre>
<p>Open <strong>http://localhost:8001</strong>. Log in with any username and the admin token as the password —<br />
it&#8217;s generated on first start and written to <code class="" data-line="">data/.admin_token</code>. Set <code class="" data-line="">BAKEX_ADMIN_TOKEN</code> and<br />
<code class="" data-line="">BAKEX_SECRET_KEY</code> in <code class="" data-line="">docker-compose.yml</code> if you want logins that survive a rebuild.</p>
<p>Compose mounts <code class="" data-line="">~/.aws</code>, <code class="" data-line="">~/.config/gcloud</code>, and <code class="" data-line="">~/.ssh</code> read-only, plus persistent <code class="" data-line="">./data</code>,<br />
<code class="" data-line="">./profiles</code>, and <code class="" data-line="">./plugins/providers</code>. That last mount is the one to notice — it&#8217;s the<br />
drop-in directory for provider plugins, which matters in the next section.</p>
<p><strong>Published image:</strong></p>
<pre><code class="" data-line="">docker run -p 8000:8000 rrskris/bakex:latest
</code></pre>
<p><strong>PyPI — CLI and web app:</strong></p>
<pre><code class="" data-line="">pip install &quot;bakex[all-providers]&quot;   # or pick extras: aws, gcp, azure, linode, digitalocean, proxmox
bakex serve --port 8000
</code></pre>
<p>One caveat worth stating plainly rather than letting you discover it: the extras install each<br />
provider&#8217;s cloud SDK, and Ansible and OpenSCAP must be present on the host for real builds. If you<br />
want the batteries-included path, use Compose. <code class="" data-line="">bakex validate</code> works anywhere with no host<br />
dependencies at all.</p>
<p>There is no Helm chart. BakeX is a build tool that talks to cloud APIs, not a cluster workload —<br />
it does not need to live in Kubernetes to harden images for it.</p>
<hr />
<h2 id="the-three-extension-points">The Three Extension Points</h2>
<h3 id="1-blueprints-the-extension-point-with-no-code-in-it">1. Blueprints — the extension point with no code in it</h3>
<p>The highest-leverage way to extend BakeX isn&#8217;t Python. It&#8217;s a YAML file.</p>
<p>A blueprint is a complete, self-contained description of a hardened OS on a specific provider,<br />
and the library ships 18 of them. Adding the nineteenth — say Ubuntu 24.04, or CIS Level 2 for a<br />
distro that only has Level 1 today — requires no engine changes, because the benchmark, profile,<br />
and datastream are just strings handed to <code class="" data-line="">oscap</code>.</p>
<p>The full format is published as a JSON Schema (Draft 2020-12) at<br />
<code class="" data-line="">docs/schema/hardening-blueprint.schema.json</code>. Point your editor at it for autocomplete and<br />
inline validation, or hand it to an LLM and let it draft the blueprint — the schema was published<br />
partly so that agents could write these correctly without reading the source.</p>
<p>The loop is short enough to run in a coffee break:</p>
<pre><code class="" data-line="">$EDITOR blueprints/ubuntu/24.04/cis-l1-aws.yaml
bakex validate blueprints/ubuntu/24.04/cis-l1-aws.yaml
</code></pre>
<p>Validation is offline and checks more than syntax — it rejects OS/provider combinations the<br />
catalog doesn&#8217;t support, so you find out that a distro isn&#8217;t available on your target cloud in<br />
milliseconds rather than fifteen minutes into a paid build.</p>
<h3 id="2-provider-plugins">2. Provider Plugins</h3>
<p>Adding a cloud means implementing four methods. That&#8217;s the whole interface<br />
(<code class="" data-line="">bakex/plugins/base_provider.py</code>):</p>
<pre><code class="" data-line=""># plugins/providers/my_cloud.py
from bakex.plugins.base_provider import BaseProvider, ProviderResult
from bakex.core.models import ComplianceProfile

class MyCloudProvider(BaseProvider):
    name = &quot;my-cloud&quot;          # matches target.provider in a blueprint

    def provision(self, profile: ComplianceProfile, **kwargs) -&gt; str:
        &quot;&quot;&quot;Launch a build instance; return its instance ID.&quot;&quot;&quot;
        ...

    def run_ansible(self, instance_id: str, profile: ComplianceProfile) -&gt; None:
        &quot;&quot;&quot;Apply the Ansible-Lockdown hardening roles.&quot;&quot;&quot;
        ...

    def snapshot(self, instance_id: str, profile: ComplianceProfile) -&gt; ProviderResult:
        &quot;&quot;&quot;Capture the golden image; return the artifact ID.&quot;&quot;&quot;
        ...

    def teardown(self, instance_id: str) -&gt; None:
        &quot;&quot;&quot;Destroy the ephemeral build instance.&quot;&quot;&quot;
        ...
</code></pre>
<p>There is no registration command. The loader (<code class="" data-line="">bakex/plugins/loader.py</code>) is hybrid and finds<br />
plugins two ways:</p>
<ol>
<li><strong>Drop-in</strong> — put the <code class="" data-line="">.py</code> file in <code class="" data-line="">plugins/providers/</code>. That directory is a Compose volume<br />
   mount, so a plugin dropped there is live in the container without rebuilding an image.</li>
<li><strong>Entry point</strong> — ship a pip-installable package declaring a <code class="" data-line="">bakex.providers</code> entry point.<br />
   This is how a third party distributes a provider without touching the BakeX repo.</li>
</ol>
<p>Entry points load first and drop-ins load second, so a local file deliberately shadows an<br />
installed package of the same name — which is exactly what you want when debugging someone<br />
else&#8217;s provider.</p>
<p>The plugin becomes usable by writing <code class="" data-line="">provider: my-cloud</code> in a blueprint&#8217;s <code class="" data-line="">target</code> block. There<br />
is no <code class="" data-line="">--provider</code> flag to pass, because there is no <code class="" data-line="">--provider</code> flag anywhere.</p>
<p>One honest note on the validation interaction from EP02: the compatibility check only objects<br />
when <em>both</em> the OS and the provider are in the catalog. An unknown provider is assumed to be a<br />
valid third-party plugin rather than an error — existence is the plugin registry&#8217;s call at build<br />
time, compatibility is validation&#8217;s. That&#8217;s what makes shipping a provider out-of-tree possible<br />
at all.</p>
<h3 id="3-pipeline-integrations">3. Pipeline Integrations</h3>
<p>Beyond the curl-based gate from EP05, BakeX has a webhook system. Webhooks are registered through<br />
the API rather than a config file, so they can be managed by the same automation that manages<br />
everything else:</p>
<pre><code class="" data-line="">curl -X POST http://localhost:8001/api/webhooks \
  -H &quot;Content-Type: application/json&quot; \
  -d &#039;{
    &quot;url&quot;: &quot;https://hooks.slack.com/services/…&quot;,
    &quot;events&quot;: [&quot;build.failed&quot;, &quot;scan.grade_change&quot;],
    &quot;label&quot;: &quot;platform-security alerts&quot;
  }&#039;
</code></pre>
<p>Five events fire: <code class="" data-line="">build.complete</code>, <code class="" data-line="">build.failed</code>, <code class="" data-line="">scan.complete</code>, <code class="" data-line="">scan.failed</code>, and<br />
<code class="" data-line="">scan.grade_change</code>. Registering an unknown event name is a 422 rather than a silent no-op —<br />
a small thing that saves an afternoon.</p>
<p><code class="" data-line="">scan.grade_change</code> is the one to wire up first. A build that fails is loud on its own; a base<br />
image that quietly slid from A to B between two scans is the signal nobody notices.</p>
<p>Deliveries are signed. The registration response returns a secret <strong>once</strong>, and every request<br />
carries an <code class="" data-line="">X-BakeX-Signature: sha256=…</code> HMAC so the receiver can verify the payload came from<br />
your BakeX instance and not from anyone who guessed the endpoint URL.</p>
<p>There&#8217;s a defensive detail here that&#8217;s worth calling out, because it&#8217;s the kind of thing that<br />
usually ships broken: webhook target URLs are checked against loopback, private, link-local, and<br />
reserved ranges — including <code class="" data-line="">169.254.169.254</code> — and they&#8217;re <strong>re-resolved at send time</strong>, not just<br />
at registration. A user-configurable URL that the server will fetch is a textbook SSRF into the<br />
cloud metadata endpoint, and on a tool that holds cloud credentials that would be a very bad day.</p>
<hr />
<h2 id="the-open-core-model">The Open-Core Model</h2>
<p>BakeX sits alongside the tools that became infrastructure standards by being genuinely usable<br />
before they were commercial:</p>
<table>
<thead>
<tr>
<th>Tool</th>
<th>Model</th>
</tr>
</thead>
<tbody>
<tr>
<td>Terraform / OpenTofu</td>
<td>Core OSS, enterprise features in paid tier</td>
</tr>
<tr>
<td>Cilium / Isovalent</td>
<td>Core OSS, enterprise support/features in paid tier</td>
</tr>
<tr>
<td>Vault / HCP Vault</td>
<td>Core OSS, hosted/enterprise in paid tier</td>
</tr>
<tr>
<td><strong>BakeX</strong></td>
<td>Engine + blueprint + scanner + Pipeline API: Apache 2.0, no paid tier today</td>
</tr>
</tbody>
</table>
<p>Everything taught in this series — the blueprint format, the build pipeline, the compliance<br />
grading, the CI/CD gate — is in the repository. There is no feature held back, because there is<br />
currently nothing to hold it back <em>for</em>. Self-host it, extend it, fork it.</p>
<p>Worth being straight about where the project actually is: BakeX is young. It has signed releases,<br />
SBOMs and provenance attestations, an OpenSSF Scorecard, a published JSON Schema, and over a<br />
thousand tests — the engineering is in good order. What it does not yet have is users. If you&#8217;re<br />
reading this and the shape of the tool fits your problem, you would be early, and early is when<br />
your opinion changes the design.</p>
<p>The repository is at: <strong><a href="https://github.com/invicton/bakex">github.com/invicton/bakex</a></strong></p>
<hr />
<h2 id="what-this-series-taught">What This Series Taught</h2>
<p>EP01 — EP06 in one view:</p>
<table>
<thead>
<tr>
<th>Episode</th>
<th>What you learned</th>
<th>What BakeX does</th>
</tr>
</thead>
<tbody>
<tr>
<td>EP01</td>
<td>Default AMIs are insecure by design</td>
<td>Replaces the default AMI with a hardened golden image</td>
</tr>
<tr>
<td>EP02</td>
<td>Blueprint as code — the 2am skip disappears</td>
<td>HardeningBlueprint YAML, <code class="" data-line="">bakex validate</code> / <code class="" data-line="">bakex build</code></td>
</tr>
<tr>
<td>EP03</td>
<td>One posture, six providers, no drift</td>
<td>18 shipped blueprints; only <code class="" data-line="">target</code> differs across providers</td>
</tr>
<tr>
<td>EP04</td>
<td>Automated OpenSCAP — grade at build time</td>
<td>A–F from the XCCDF score, SARIF 2.1.0 export, baseline compare</td>
</tr>
<tr>
<td>EP05</td>
<td>CI/CD gate — the unhardened image never deploys</td>
<td>Pipeline API: <code class="" data-line="">POST /api/pipeline/scan</code>, parse <code class="" data-line="">.passed</code></td>
</tr>
<tr>
<td>EP06</td>
<td>The platform — OSS, self-hostable, extendable</td>
<td>Apache 2.0, Compose install, blueprints + provider plugins</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>This series closes the OS hardening gap. The same principle — declare desired state, build<br />
reproducibly, verify automatically — applies to every layer of your infrastructure.</p>
<h3 id="write-the-next-blueprint">Write the next blueprint</h3>
<p>The most useful thing you can do with what this series taught is add a blueprint, and it is<br />
genuinely pure YAML — no Python, no engine changes, no build system to learn.</p>
<p>You&#8217;ve spent five episodes on Ubuntu 22.04 CIS Level 1. The natural next one is Level 2 for the<br />
same OS: <a href="https://github.com/invicton/bakex/issues/1"><strong>#1 — Ubuntu 22.04 CIS Level 2</strong></a>. The<br />
issue carries the acceptance criteria and the exact verify command, and the review loop is<br />
<code class="" data-line="">bakex validate</code> returning 0.</p>
<p>If a different distro is closer to what you actually run, the whole set is filed and labelled:<br />
<a href="https://github.com/invicton/bakex/issues?q=is%3Aissue+is%3Aopen+label%3Ablueprint"><strong>good first issues, <code class="" data-line="">blueprint</code> label</strong></a>.<br />
RHEL 9, AlmaLinux 9, Rocky 9, Debian 12, and Amazon Linux 2023 all have gaps. Each one is one<br />
file, and each is the sort of contribution that takes an evening.</p>
<p>GitHub: <a href="https://github.com/invicton/bakex">github.com/invicton/bakex</a></p>
<h3 id="elsewhere-on-the-blog">Elsewhere on the blog</h3>
<p>If you&#8217;ve been following the <a href="/ebpf-series/">eBPF: From Kernel to Cloud series</a>,<br />
EP10 covers what happens when you combine kernel-level observability with the hardened base BakeX<br />
produces: every connection, every process spawn, every file access — visible from the host kernel,<br />
on an OS baseline you can verify.</p>
<p>The next series is the <strong>Purple Team Playbook</strong> — real attack paths against cloud and Kubernetes<br />
infrastructure, how they&#8217;re detected, and how they&#8217;re closed.</p>
<p>Get new episodes in your inbox → <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%2Fstratum-os-hardening-platform%2F&amp;linkname=BakeX%20%E2%80%94%20OS%20Hardening%20as%20a%20Platform" 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%2Fstratum-os-hardening-platform%2F&amp;linkname=BakeX%20%E2%80%94%20OS%20Hardening%20as%20a%20Platform" 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%2Fstratum-os-hardening-platform%2F&amp;linkname=BakeX%20%E2%80%94%20OS%20Hardening%20as%20a%20Platform" 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%2Fstratum-os-hardening-platform%2F&amp;linkname=BakeX%20%E2%80%94%20OS%20Hardening%20as%20a%20Platform" 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%2Fstratum-os-hardening-platform%2F&amp;linkname=BakeX%20%E2%80%94%20OS%20Hardening%20as%20a%20Platform" 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%2Fstratum-os-hardening-platform%2F&amp;linkname=BakeX%20%E2%80%94%20OS%20Hardening%20as%20a%20Platform" 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%2Fstratum-os-hardening-platform%2F&amp;linkname=BakeX%20%E2%80%94%20OS%20Hardening%20as%20a%20Platform" 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%2Fstratum-os-hardening-platform%2F&#038;title=BakeX%20%E2%80%94%20OS%20Hardening%20as%20a%20Platform" data-a2a-url="https://linuxcent.com/stratum-os-hardening-platform/" data-a2a-title="BakeX — OS Hardening as a Platform"></a></p><p>The post <a href="https://linuxcent.com/stratum-os-hardening-platform/">BakeX — OS Hardening as a Platform</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/stratum-os-hardening-platform/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1834</post-id>	</item>
		<item>
		<title>The Pipeline Gate — Hardened Images as a CI/CD Build Constraint</title>
		<link>https://linuxcent.com/hardened-image-cicd-pipeline-gate/</link>
					<comments>https://linuxcent.com/hardened-image-cicd-pipeline-gate/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Sat, 23 May 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[OS Image Builder]]></category>
		<category><![CDATA[BakeX]]></category>
		<category><![CDATA[CI/CD]]></category>
		<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[GitOps]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Pipeline]]></category>
		<category><![CDATA[Security]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=1831</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"> 7</span> <span class="rt-label rt-postfix">minutes</span></span>Make hardened OS images a CI/CD build constraint: POST /api/pipeline/scan fails the build if grade < threshold. Unhardened images never reach production.
</p>
<p>The post <a href="https://linuxcent.com/hardened-image-cicd-pipeline-gate/">The Pipeline Gate — Hardened Images as a CI/CD Build Constraint</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"> 7</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 5</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> · <a href="/automated-compliance-scanning-openscap/">Automated OpenSCAP Compliance</a> · </em><em>CI/CD Compliance Gate</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 CI/CD compliance gate turns an OS hardening grade from a report into a build constraint — unhardened images fail the pipeline before they can be deployed</li>
<li><code class="" data-line="">POST /api/pipeline/scan</code> scores an image against a <code class="" data-line="">pass_threshold</code> and a <code class="" data-line="">severity_threshold</code>, and returns a <code class="" data-line="">passed</code> boolean</li>
<li><strong>The endpoint returns HTTP 200 even when the gate fails.</strong> <code class="" data-line="">curl -sf</code> will not catch it — you must parse <code class="" data-line="">.passed</code>. This is the single most important detail on this page</li>
<li>The gate is two-dimensional: a score floor <em>and</em> a severity ceiling, so one critical finding blocks a release that scores 94</li>
<li>GitHub Actions, GitLab CI, Jenkins, and Tekton integrations are one curl plus one <code class="" data-line="">jq</code></li>
<li>The structural guarantee: an image that doesn&#8217;t pass the gate doesn&#8217;t reach the deploy job</li>
</ul>
<hr />
<h2 id="the-problem-a-grade-no-one-checks-is-decoration">The Problem: A Grade No One Checks Is Decoration</h2>
<pre><code class="" data-line="">Pipeline without compliance gate:
  Build → Test → Security scan (results to dashboard) → Deploy

What actually happens:
  Build → Test → Security scan → &quot;C grade, but we need to ship&quot; → Deploy anyway
                                           │
                                           └─ Dashboard shows C grade
                                              Nobody is paged
                                              Deployment succeeds
</code></pre>
<p>A CI/CD compliance gate means the pipeline can&#8217;t continue if the grade is below threshold.</p>
<p>EP04 showed that automated OpenSCAP compliance gives every image a verified, reproducible grade before deployment. What it assumed is that someone checks the grade before deploying. They don&#8217;t — not under deadline pressure, not when the image has been &#8220;working fine for months,&#8221; not at 2am.</p>
<p>The same problem that made hardening runbooks skippable applies to compliance grades: if checking the grade is a discretionary step, it will be skipped.</p>
<hr />
<p>A new microservice was deployed from an unhardened base image. The team had built it quickly during a sprint, used a community AMI as the base, and planned to harden it &#8220;in the next sprint.&#8221;</p>
<p>Three weeks later, a penetration test found it. SSH password authentication enabled. Three unnecessary services running — one of them with a known CVE. The finding: the instance had full inbound access from the VPC and was reachable from a compromised adjacent instance.</p>
<p>The deployment had gone through the normal CI/CD pipeline. Unit tests passed. Integration tests passed. A vulnerability scan ran. The scan produced a report that went to a dashboard. Nobody had a gate set up to fail the build if the image was unhardened.</p>
<p>The hardening work from the &#8220;next sprint&#8221; plan would have taken four hours. The pentest remediation took a week, plus the time to investigate what had been exposed during the three weeks the instance was running.</p>
<p>The CI/CD pipeline had every check except the one that would have caught the base image problem before the first deployment.</p>
<hr />
<h2 id="the-pipeline-api">The Pipeline API</h2>
<p>The Pipeline API is a single HTTP endpoint that takes an image ID, scans it, and returns a verdict:</p>
<pre><code class="" data-line="">curl -s -X POST https://bakex.yourdomain.com/api/pipeline/scan \
  -H &quot;X-API-Key: ${BAKEX_TOKEN}&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#039;{
    &quot;image_id&quot;: &quot;ami-0a7f3c9e82d1b4c05&quot;,
    &quot;provider&quot;: &quot;aws&quot;,
    &quot;region&quot;: &quot;us-east-1&quot;,
    &quot;pass_threshold&quot;: 75.0,
    &quot;severity_threshold&quot;: &quot;high&quot;,
    &quot;wait&quot;: true
  }&#039;
</code></pre>
<p>Authentication takes either <code class="" data-line="">X-API-Key</code> or <code class="" data-line="">Authorization: Bearer</code>; keys are created at<br />
<code class="" data-line="">/settings/api-keys</code>. With <code class="" data-line="">wait: true</code> the request blocks until the scan completes — which is what<br />
you want in CI, where a job that returns before the answer exists is worse than a slow one. There&#8217;s<br />
a <code class="" data-line="">timeout_seconds</code> (default 900) for when it doesn&#8217;t.</p>
<p>The response is the same shape whether you passed or failed:</p>
<pre><code class="" data-line="">{
  &quot;job_id&quot;: &quot;7f3c9e82-4d1b-4c05-a7f3-c9e82d1b4c05&quot;,
  &quot;status&quot;: &quot;complete&quot;,
  &quot;passed&quot;: false,
  &quot;grade&quot;: &quot;C&quot;,
  &quot;score_pct&quot;: 72.0,
  &quot;severity_counts&quot;: { &quot;critical&quot;: 0, &quot;high&quot;: 2, &quot;medium&quot;: 5, &quot;low&quot;: 11 },
  &quot;threshold_violations&quot;: [&quot;high&quot;],
  &quot;pass_threshold&quot;: 75.0,
  &quot;severity_threshold&quot;: &quot;high&quot;,
  &quot;image_id&quot;: &quot;ami-0c9d5e3f81a2b6e07&quot;,
  &quot;sarif_url&quot;: &quot;.../api/auditor/scan-image/7f3c9e82.../report?fmt=sarif&quot;,
  &quot;html_report_url&quot;: &quot;.../api/auditor/scan-image/7f3c9e82.../report&quot;
}
</code></pre>
<h3 id="the-detail-that-will-silently-break-your-gate">The detail that will silently break your gate</h3>
<p><strong>A failed gate still returns HTTP 200.</strong> There is no 4xx on failure — the verdict is in the<br />
<code class="" data-line="">passed</code> field, not the status code.</p>
<p>That means the pattern everyone reaches for first is wrong:</p>
<pre><code class="" data-line=""># WRONG — this never fails. -f only reacts to HTTP &gt;= 400,
# and a failed gate returns 200.
curl -sf -X POST .../api/pipeline/scan -d &#039;...&#039; || exit 1
</code></pre>
<p>You have to read the body:</p>
<pre><code class="" data-line=""># RIGHT
RESULT=$(curl -s -X POST &quot;${BAKEX_URL}/api/pipeline/scan&quot; \
  -H &quot;X-API-Key: ${BAKEX_TOKEN}&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &quot;{\&quot;image_id\&quot;: \&quot;${AMI_ID}\&quot;, \&quot;pass_threshold\&quot;: 75.0, \&quot;severity_threshold\&quot;: \&quot;high\&quot;}&quot;)

echo &quot;$RESULT&quot; | jq -r &#039;&quot;grade=\(.grade) score=\(.score_pct) passed=\(.passed)&quot;&#039;

if [ &quot;$(echo &quot;$RESULT&quot; | jq -r &#039;.passed&#039;)&quot; != &quot;true&quot; ]; then
  echo &quot;Compliance gate failed — violations: $(echo &quot;$RESULT&quot; | jq -c &#039;.threshold_violations&#039;)&quot;
  echo &quot;Report: $(echo &quot;$RESULT&quot; | jq -r &#039;.html_report_url&#039;)&quot;
  exit 1
fi
</code></pre>
<p>A gate that reports failure and exits 0 is worse than no gate, because it produces a green<br />
pipeline and the belief that something was checked.</p>
<h3 id="two-thresholds-not-one">Two thresholds, not one</h3>
<p><code class="" data-line="">passed</code> is the AND of two independent conditions:</p>
<pre><code class="" data-line="">passed = (score_pct &gt;= pass_threshold) AND (no findings at or above severity_threshold)
</code></pre>
<p><code class="" data-line="">severity_threshold: &quot;high&quot;</code> means any <code class="" data-line="">critical</code> or <code class="" data-line="">high</code> finding fails the build regardless of<br />
score. An image can score 94 — a comfortable A — and still fail on a single critical finding. That<br />
is the right default: scores average away the thing that gets you breached.</p>
<hr />
<h2 id="github-actions-integration">GitHub Actions Integration</h2>
<pre><code class="" data-line=""># .github/workflows/deploy.yml

jobs:
  build-image:
    runs-on: ubuntu-latest
    outputs:
      ami_id: ${{ steps.build.outputs.ami_id }}
    steps:
      - name: Build hardened AMI
        id: build
        run: |
          AMI_ID=$(bakex build blueprints/ubuntu/22.04/cis-l1-aws.yaml --json \
            | jq -r &#039;.artifact_id&#039;)
          echo &quot;ami_id=${AMI_ID}&quot; &gt;&gt; $GITHUB_OUTPUT

  compliance-gate:
    runs-on: ubuntu-latest
    needs: build-image
    steps:
      - name: BakeX compliance gate
        run: |
          RESULT=$(curl -s -X POST ${{ vars.BAKEX_URL }}/api/pipeline/scan \
            -H &quot;X-API-Key: ${{ secrets.BAKEX_TOKEN }}&quot; \
            -H &quot;Content-Type: application/json&quot; \
            -d &quot;{\&quot;image_id\&quot;: \&quot;${{ needs.build-image.outputs.ami_id }}\&quot;,
                 \&quot;pass_threshold\&quot;: 75.0, \&quot;severity_threshold\&quot;: \&quot;high\&quot;}&quot;)

          echo &quot;$RESULT&quot; | jq -r &#039;&quot;grade=\(.grade) score=\(.score_pct)&quot;&#039;

          # Must check .passed — the endpoint returns 200 on failure
          if [ &quot;$(echo &quot;$RESULT&quot; | jq -r &#039;.passed&#039;)&quot; != &quot;true&quot; ]; then
            echo &quot;::error::Compliance gate failed: $(echo &quot;$RESULT&quot; | jq -c &#039;.threshold_violations&#039;)&quot;
            exit 1
          fi

      - name: Upload SARIF to code scanning
        if: always()
        run: |
          curl -s -o bakex.sarif &quot;$(echo &quot;$RESULT&quot; | jq -r &#039;.sarif_url&#039;)&quot;
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: bakex.sarif

  deploy:
    runs-on: ubuntu-latest
    needs: [build-image, compliance-gate]
    steps:
      - name: Deploy to staging
        run: |
          aws autoscaling update-auto-scaling-group \
            --auto-scaling-group-name my-asg \
            --launch-template &quot;ImageId=${{ needs.build-image.outputs.ami_id }}&quot;
</code></pre>
<p>The <code class="" data-line="">deploy</code> job only runs if <code class="" data-line="">compliance-gate</code> passes. The AMI doesn&#8217;t reach the autoscaling group if it doesn&#8217;t meet the grade threshold.</p>
<hr />
<h2 id="gitlab-ci-integration">GitLab CI Integration</h2>
<pre><code class="" data-line=""># .gitlab-ci.yml

stages:
  - build
  - compliance
  - deploy

build-image:
  stage: build
  script:
    - |
      AMI_ID=$(bakex build blueprints/ubuntu/22.04/cis-l1-aws.yaml --json \
        | jq -r &#039;.artifact_id&#039;)
      echo &quot;AMI_ID=${AMI_ID}&quot; &gt;&gt; build.env
  artifacts:
    reports:
      dotenv: build.env

compliance-gate:
  stage: compliance
  needs: [build-image]
  script:
    - |
      RESULT=$(curl -s -X POST ${BAKEX_URL}/api/pipeline/scan \
        -H &quot;X-API-Key: ${BAKEX_TOKEN}&quot; \
        -H &quot;Content-Type: application/json&quot; \
        -d &quot;{\&quot;image_id\&quot;: \&quot;${AMI_ID}\&quot;, \&quot;pass_threshold\&quot;: 75.0,
             \&quot;severity_threshold\&quot;: \&quot;high\&quot;}&quot;)
      echo &quot;$RESULT&quot; | jq -r &#039;&quot;grade=\(.grade) score=\(.score_pct) passed=\(.passed)&quot;&#039;
      test &quot;$(echo &quot;$RESULT&quot; | jq -r &#039;.passed&#039;)&quot; = &quot;true&quot;

deploy:
  stage: deploy
  needs: [build-image, compliance-gate]
  script:
    - ./deploy.sh ${AMI_ID}
</code></pre>
<hr />
<h2 id="what-the-failed-gate-tells-you">What the Failed Gate Tells You</h2>
<p>The value of the CI/CD compliance gate is not just that it blocks bad images — it&#8217;s that the failure output tells engineers what to fix.</p>
<p>The response carries three things an engineer can act on immediately:</p>
<pre><code class="" data-line="">$ echo &quot;$RESULT&quot; | jq &#039;{grade, score_pct, threshold_violations, severity_counts}&#039;
{
  &quot;grade&quot;: &quot;C&quot;,
  &quot;score_pct&quot;: 72.0,
  &quot;threshold_violations&quot;: [&quot;high&quot;],
  &quot;severity_counts&quot;: { &quot;critical&quot;: 0, &quot;high&quot;: 2, &quot;medium&quot;: 5, &quot;low&quot;: 11 }
}
</code></pre>
<p><code class="" data-line="">threshold_violations</code> names the severities that broke the gate — here, two <code class="" data-line="">high</code> findings, not the<br />
score. That distinction matters: an engineer who reads &#8220;grade C&#8221; starts a broad hardening project,<br />
while one who reads &#8220;two high findings&#8221; goes and fixes two things.</p>
<p>For the rule-level detail, follow <code class="" data-line="">sarif_url</code>. Pushing that SARIF into GitHub code scanning (as in<br />
the workflow above) puts each finding on the pull request diff, which is where someone will actually<br />
read it — a link to a dashboard in a CI log is a link nobody clicks.</p>
<hr />
<h2 id="thresholds-by-environment">Thresholds by Environment</h2>
<p>Not all environments need the same bar, and both dimensions are per-request — so the environment<br />
distinction lives in your pipeline, not in BakeX config:</p>
<pre><code class="" data-line=""># Production — high score floor, nothing high or above
PASS=90.0 ; SEV=high

# Staging — lower floor, still no criticals
PASS=75.0 ; SEV=critical

# Development — score only, severity effectively off
PASS=60.0 ; SEV=low

curl -s -X POST &quot;${BAKEX_URL}/api/pipeline/scan&quot; \
  -H &quot;X-API-Key: ${BAKEX_TOKEN}&quot; -H &quot;Content-Type: application/json&quot; \
  -d &quot;{\&quot;image_id\&quot;: \&quot;${AMI_ID}\&quot;, \&quot;pass_threshold\&quot;: ${PASS}, \&quot;severity_threshold\&quot;: \&quot;${SEV}\&quot;}&quot;
</code></pre>
<p>Note that <code class="" data-line="">severity_threshold</code> gets <em>stricter</em> as it goes down the list: <code class="" data-line="">low</code> fails on any finding<br />
at all, <code class="" data-line="">critical</code> fails only on criticals. It reads backwards the first time. Development wanting a<br />
permissive gate wants <code class="" data-line="">critical</code>, not <code class="" data-line="">low</code>.</p>
<hr />
<h2 id="production-gotchas">Production Gotchas</h2>
<p><strong>The 200-on-failure behaviour is the whole ballgame.</strong> Repeating it because it is the one thing that<br />
turns this page from useful to harmful if missed: check <code class="" data-line="">.passed</code>. Never rely on <code class="" data-line="">curl -f</code>, and never<br />
rely on the HTTP status.</p>
<p><strong>Scans take minutes, and <code class="" data-line="">wait: true</code> blocks.</strong> The endpoint provisions an instance from the image<br />
and scans it. With <code class="" data-line="">wait: true</code> your CI job blocks for the duration; <code class="" data-line="">timeout_seconds</code> defaults to<br />
900. Set your CI step timeout above that, or use <code class="" data-line="">wait: false</code> and poll <code class="" data-line="">GET /api/pipeline/scan/{job_id}</code>.</p>
<p><strong>Token rotation.</strong> The API key should rotate on the same schedule as other service credentials, and<br />
environments should use different keys — a leaked staging key must not be able to satisfy a<br />
production gate.</p>
<p><strong>The gate needs a reachable BakeX server.</strong> This is an HTTP API, not a self-contained action: the<br />
runner must reach the BakeX instance, and that instance needs cloud credentials for the provider<br />
whose image it is scanning.</p>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>A CI/CD compliance gate turns a compliance grade from a dashboard metric into a pipeline constraint — the image doesn&#8217;t deploy if it doesn&#8217;t pass</li>
<li><code class="" data-line="">POST /api/pipeline/scan</code> is a single HTTP call that any CI/CD system can make — no agent, no plugin, no SDK required</li>
<li><strong>The endpoint returns 200 even when the gate fails.</strong> Parse <code class="" data-line="">.passed</code>; <code class="" data-line="">curl -sf || exit 1</code> produces a green pipeline and a false sense of security</li>
<li>The verdict is two-dimensional — a score floor AND a severity ceiling — so a single critical finding blocks an image that scores 94</li>
<li><code class="" data-line="">threshold_violations</code> tells an engineer <em>why</em> it failed, which is the difference between &#8220;fix two high findings&#8221; and &#8220;start a hardening project&#8221;</li>
<li>Push the <code class="" data-line="">sarif_url</code> into GitHub code scanning so findings land on the pull request, not in a CI log</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>The CI/CD compliance gate closes the final gap: even if an unhardened image gets built, it can&#8217;t deploy. EP05 is the bookmark episode — this is the point where OS hardening becomes structurally enforced rather than procedurally expected.</p>
<p>EP06 is the series closer. For five episodes, you&#8217;ve been using BakeX as a user. What does it look like to run it yourself — extend it with a custom provider, deploy it in your own infrastructure, or contribute a blueprint back?</p>
<p>BakeX is Apache 2.0. EP06 is the architecture reveal, the deployment guide, and the extension points for everything the series taught.</p>
<p><em>Next: <a href="/stratum-os-hardening-platform/">BakeX — open-source OS hardening platform for multi-cloud infrastructure</a></em></p>
<p>Get EP06 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%2Fhardened-image-cicd-pipeline-gate%2F&amp;linkname=The%20Pipeline%20Gate%20%E2%80%94%20Hardened%20Images%20as%20a%20CI%2FCD%20Build%20Constraint" 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%2Fhardened-image-cicd-pipeline-gate%2F&amp;linkname=The%20Pipeline%20Gate%20%E2%80%94%20Hardened%20Images%20as%20a%20CI%2FCD%20Build%20Constraint" 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%2Fhardened-image-cicd-pipeline-gate%2F&amp;linkname=The%20Pipeline%20Gate%20%E2%80%94%20Hardened%20Images%20as%20a%20CI%2FCD%20Build%20Constraint" 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%2Fhardened-image-cicd-pipeline-gate%2F&amp;linkname=The%20Pipeline%20Gate%20%E2%80%94%20Hardened%20Images%20as%20a%20CI%2FCD%20Build%20Constraint" 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%2Fhardened-image-cicd-pipeline-gate%2F&amp;linkname=The%20Pipeline%20Gate%20%E2%80%94%20Hardened%20Images%20as%20a%20CI%2FCD%20Build%20Constraint" 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%2Fhardened-image-cicd-pipeline-gate%2F&amp;linkname=The%20Pipeline%20Gate%20%E2%80%94%20Hardened%20Images%20as%20a%20CI%2FCD%20Build%20Constraint" 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%2Fhardened-image-cicd-pipeline-gate%2F&amp;linkname=The%20Pipeline%20Gate%20%E2%80%94%20Hardened%20Images%20as%20a%20CI%2FCD%20Build%20Constraint" 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%2Fhardened-image-cicd-pipeline-gate%2F&#038;title=The%20Pipeline%20Gate%20%E2%80%94%20Hardened%20Images%20as%20a%20CI%2FCD%20Build%20Constraint" data-a2a-url="https://linuxcent.com/hardened-image-cicd-pipeline-gate/" data-a2a-title="The Pipeline Gate — Hardened Images as a CI/CD Build Constraint"></a></p><p>The post <a href="https://linuxcent.com/hardened-image-cicd-pipeline-gate/">The Pipeline Gate — Hardened Images as a CI/CD Build Constraint</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/hardened-image-cicd-pipeline-gate/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1831</post-id>	</item>
		<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>One Blueprint, Six Clouds — Multi-Provider OS Image Builds</title>
		<link>https://linuxcent.com/linux-hardening-multi-cloud/</link>
					<comments>https://linuxcent.com/linux-hardening-multi-cloud/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Sun, 26 Apr 2026 19:41:36 +0000</pubDate>
				<category><![CDATA[OS Image Builder]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[BakeX]]></category>
		<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[GCP]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Multi-Cloud]]></category>
		<category><![CDATA[OS Hardening]]></category>
		<guid isPermaLink="false">https://linuxcent.com/linux-hardening-multi-cloud/</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"> 7</span> <span class="rt-label rt-postfix">minutes</span></span>One HardeningBlueprint YAML, six cloud providers: AWS, GCP, Azure, DigitalOcean, Linode, Proxmox. How Stratum handles provider differences so your compliance intent stays portable.</p>
<p>The post <a href="https://linuxcent.com/linux-hardening-multi-cloud/">One Blueprint, Six Clouds — Multi-Provider OS Image Builds</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"> 7</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 3</em><br />
<em><a href="https://linuxcent.com/cloud-ami-security-risks-custom-os-images/">Cloud AMI Security Risks</a> · <a href="https://linuxcent.com/linux-hardening-as-code/">Linux Hardening as Code</a> · </em><em>Multi-Cloud OS Hardening</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>Multi-cloud OS hardening with separate scripts per provider means three scripts that drift within weeks</li>
<li>A HardeningBlueprint YAML separates compliance intent (portable) from provider details (handled by BakeX&#8217;s provider layer)</li>
<li>You keep one blueprint file per provider — and every section except <code class="" data-line="">target</code> stays byte-identical across all six. The diff below proves it</li>
<li>Provider-specific differences — disk names, cloud-init ordering, base image identifiers — are abstracted away from the blueprint author</li>
<li>The compliance posture becomes reviewable in a pull request: a control change touches six files identically, and a reviewer can see that at a glance</li>
<li>Six providers ship as working blueprints today: AWS, GCP, Azure, DigitalOcean, Linode, Proxmox</li>
</ul>
<hr />
<h2 id="the-problem-three-clouds-three-scripts-three-ways-to-drift">The Problem: Three Clouds, Three Scripts, Three Ways to Drift</h2>
<pre><code class="" data-line="">AWS hardening script          GCP hardening script          Azure hardening script
├── /dev/xvd* disk refs       ├── /dev/sda* disk refs       ├── /dev/sda* disk refs
├── 169.254.169.254 IMDS      ├── 169.254.169.254 IMDS      ├── 169.254.169.254 IMDS
├── cloud-init order A        ├── cloud-init order B        ├── cloud-init order C
└── Updated: Jan 2025         └── Updated: Aug 2024         └── Updated: Mar 2024
                                         │
                                         └─ 5 months behind
                                            on CIS updates
</code></pre>
<p>Multi-cloud OS hardening starts as a copy-paste of the AWS script. Within a month, the clouds diverge.</p>
<p>EP02 showed that a HardeningBlueprint YAML eliminates the skip-at-2am problem by making hardening a build artifact. What it assumed — quietly — is that you&#8217;re building for one provider. The moment you expand to a second cloud, the provider-specific details in the blueprint become a problem: disk names differ, cloud-init fires in a different order, and AWS-specific assumptions break silently on GCP.</p>
<hr />
<p>We expanded from AWS to GCP six months ago. The EC2 hardening script had been working reliably for over a year. The GCP engineer took the AWS script, made some quick changes, and started building images.</p>
<p>The first GCP images had a subtle problem: the <code class="" data-line="">/tmp</code> and <code class="" data-line="">/home</code> separate partition entries in <code class="" data-line="">/etc/fstab</code> referenced <code class="" data-line="">/dev/xvdb</code> — an AWS disk naming convention. GCP uses <code class="" data-line="">/dev/sdb</code>. The fstab entries were silently ignored. The mounts existed but weren&#8217;t restricted. The CIS controls for separate filesystem partitions were listed as passing in the scan output because the Ansible task had &#8220;run successfully&#8221; — it just hadn&#8217;t done what we thought.</p>
<p>It took a pentest three months later to catch it. The finding: six production GCP instances with <code class="" data-line="">/tmp</code> not mounted with <code class="" data-line="">noexec, nosuid, nodev</code> — despite our &#8220;CIS L1 hardened&#8221; label.</p>
<p>The root cause wasn&#8217;t the engineer. It was a hardening approach that required cloud-specific knowledge embedded in the script rather than in a provider abstraction layer.</p>
<hr />
<h2 id="how-bakex-separates-compliance-intent-from-provider-details">How BakeX Separates Compliance Intent from Provider Details</h2>
<p>Multi-cloud OS hardening works when the compliance intent and the provider details are kept strictly separate.</p>
<pre><code class="" data-line="">HardeningBlueprint YAML
(compliance intent — portable)
         │
         ▼
  BakeX Provider Layer
  ┌─────────────────────────────────────────────┐
  │  AWS         │  GCP         │  Azure        │
  │  /dev/xvd*   │  /dev/sda*   │  /dev/sda*    │
  │  IMDS v2     │  GCP IMDS    │  Azure IMDS   │
  │  cloud-init  │  cloud-init  │  waagent       │
  │  order A     │  order B     │  order C       │
  └─────────────────────────────────────────────┘
         │
         ▼
  Ansible-Lockdown + Provider-Aware Configuration
         │
         ▼
  OpenSCAP Scan
         │
         ▼
  Golden Image (AMI / GCP Image / Azure Image)
</code></pre>
<p>The blueprint author declares <strong>what</strong> should be true about the OS. BakeX&#8217;s provider layer handles <strong>how</strong> that&#8217;s achieved on each cloud.</p>
<p>The disk naming, cloud-init sequencing, metadata endpoint configuration, and provider-specific package repositories are all abstracted into the provider layer. They never appear in the blueprint file.</p>
<hr />
<h2 id="the-same-blueprint-across-six-providers">The Same Blueprint Across Six Providers</h2>
<p>Here is the part people expect to be a flag, and isn&#8217;t. There is no <code class="" data-line="">--provider</code> switch. The<br />
provider is a field <em>inside</em> the blueprint, so you keep one file per target:</p>
<pre><code class="" data-line="">$ ls blueprints/ubuntu/22.04/
cis-l1-aws.yaml           cis-l1-digitalocean.yaml  cis-l1-linode.yaml
cis-l1-azure.yaml         cis-l1-gcp.yaml           cis-l1-proxmox.yaml

# Validate all six at once — offline, no cloud API calls
$ bakex validate blueprints/ubuntu/22.04/*.yaml
OK    blueprints/ubuntu/22.04/cis-l1-aws.yaml  (HardeningBlueprint &#039;ubuntu22-cis-l1-aws&#039;)
...
6/6 blueprint(s) valid.

# Build one
$ bakex build blueprints/ubuntu/22.04/cis-l1-gcp.yaml
Building &#039;ubuntu22-cis-l1-gcp&#039; (gcp) → job 7f3c9e82-…
</code></pre>
<p>That design choice looks like more files, and it is. What you get for it is that a blueprint<br />
is completely self-describing: the file names its own cloud and its own base image, so it<br />
builds the same way on your laptop, in CI, and on a colleague&#8217;s machine with no flags to<br />
forget and no environment to match.</p>
<p><strong>The claim worth testing: how much actually differs between those six files?</strong></p>
<p>I parsed all six and compared every section except <code class="" data-line="">metadata</code> and <code class="" data-line="">target</code>:</p>
<pre><code class="" data-line="">compliance    identical across all 6
controls      identical across all 6   (same rules, same enable state)
filesystem    identical across all 6
users         identical across all 6
system        identical across all 6
</code></pre>
<p>Only the <code class="" data-line="">target</code> block changes, and it changes in exactly the way you&#8217;d expect:</p>
<table>
<thead>
<tr>
<th>Provider</th>
<th><code class="" data-line="">instance_type</code></th>
<th><code class="" data-line="">base_image</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>aws</td>
<td><code class="" data-line="">t3.medium</code></td>
<td><code class="" data-line="">ami-0c7217cdde317cfec</code></td>
</tr>
<tr>
<td>gcp</td>
<td><code class="" data-line="">e2-medium</code></td>
<td><code class="" data-line="">projects/ubuntu-os-cloud/global/images/family/ubuntu-2204-lts</code></td>
</tr>
<tr>
<td>azure</td>
<td><code class="" data-line="">Standard_B2s</code></td>
<td><code class="" data-line="">Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:latest</code></td>
</tr>
<tr>
<td>digitalocean</td>
<td><code class="" data-line="">s-2vcpu-4gb</code></td>
<td><code class="" data-line="">ubuntu-22-04-x64</code></td>
</tr>
<tr>
<td>linode</td>
<td><code class="" data-line="">g6-standard-2</code></td>
<td><code class="" data-line="">linode/ubuntu22.04</code></td>
</tr>
<tr>
<td>proxmox</td>
<td><code class="" data-line="">2c-4g</code></td>
<td><code class="" data-line="">9000</code> (VE template VMID)</td>
</tr>
</tbody>
</table>
<p>Six wildly different ways of naming &#8220;Ubuntu 22.04 LTS&#8221; and sizing a 2 vCPU / 4 GB box. That<br />
is the entire provider-specific surface. The CIS benchmark, the profile, the datastream, the<br />
mount options, the locked root account, the control overrides and their justifications — all<br />
byte-identical.</p>
<p>One honest caveat, because I checked rather than assumed: the six files are <em>semantically</em><br />
identical but not textually so. The justification string on one disabled SELinux rule is<br />
worded slightly differently between files — same rule, same <code class="" data-line="">enabled: false</code>, same meaning,<br />
different prose. It&#8217;s a cosmetic inconsistency in the shipped library, not a behavioural one,<br />
and it&#8217;s the kind of thing you only find by diffing rather than trusting the header comment.</p>
<p>If you change the compliance posture — add a control override, tighten a mount option — you<br />
change it identically in six files and rebuild. A reviewer sees six identical hunks in the<br />
diff. A sixth hunk that looks different is a bug, and it&#8217;s visible in code review rather than<br />
three months later in a pentest.</p>
<hr />
<h2 id="what-the-provider-layer-handles">What the Provider Layer Handles</h2>
<p>The provider layer is where the cloud-specific knowledge lives, so the blueprint author doesn&#8217;t have to carry it:</p>
<p><strong>Disk naming:</strong></p>
<table>
<thead>
<tr>
<th>Provider</th>
<th>OS disk</th>
<th>Ephemeral</th>
<th>Data</th>
</tr>
</thead>
<tbody>
<tr>
<td>AWS</td>
<td><code class="" data-line="">/dev/xvda</code></td>
<td><code class="" data-line="">/dev/xvdb</code></td>
<td><code class="" data-line="">/dev/xvdc+</code></td>
</tr>
<tr>
<td>GCP</td>
<td><code class="" data-line="">/dev/sda</code></td>
<td>—</td>
<td><code class="" data-line="">/dev/sdb+</code></td>
</tr>
<tr>
<td>Azure</td>
<td><code class="" data-line="">/dev/sda</code></td>
<td><code class="" data-line="">/dev/sdb</code> (temp disk)</td>
<td><code class="" data-line="">/dev/sdc+</code></td>
</tr>
<tr>
<td>DigitalOcean</td>
<td><code class="" data-line="">/dev/vda</code></td>
<td>—</td>
<td><code class="" data-line="">/dev/vdb+</code></td>
</tr>
</tbody>
</table>
<p>The CIS controls for separate <code class="" data-line="">/tmp</code> and <code class="" data-line="">/home</code> partitions reference disk paths that differ across these providers. The provider layer translates the blueprint&#8217;s <code class="" data-line="">filesystem.tmp</code> declaration into the correct fstab entries for the target cloud.</p>
<p><strong>Cloud-init ordering:</strong></p>
<p>Different providers initialize services in different orders. On AWS, the network is available before cloud-init runs most tasks. On GCP, some network configuration happens after cloud-init starts. On Azure, the waagent handles some configuration that cloud-init handles elsewhere.</p>
<p>The provider layer sequences the hardening steps to run in the correct order for each provider — specifically, it waits for network availability before applying network-level hardening, and ensures the package manager is configured before running Ansible roles that require package installation.</p>
<p><strong>Metadata endpoint configuration:</strong></p>
<p>CIS controls include restrictions on access to the instance metadata service (IMDSv2 enforcement on AWS, equivalent controls on GCP/Azure). The provider layer applies the correct restriction for each cloud — the blueprint just declares <code class="" data-line="">compliance: benchmark: cis-l1</code>.</p>
<hr />
<h2 id="building-every-provider">Building Every Provider</h2>
<p>There is no built-in fan-out flag, and honestly none is needed — the CLI is exit-code shaped,<br />
so the shell already does this well:</p>
<pre><code class="" data-line=""># Validate everything first; stop before spending money if anything is wrong
bakex validate blueprints/ubuntu/22.04/*.yaml || exit 1

# Then build each target
for bp in blueprints/ubuntu/22.04/cis-l1-*.yaml; do
  bakex build &quot;$bp&quot; --json &gt; &quot;builds/$(basename &quot;$bp&quot; .yaml).json&quot; &amp;
done
wait
</code></pre>
<p><code class="" data-line="">--json</code> emits the job record — id, profile name, provider, status, artifact ID, error — which<br />
is what you want when six builds are writing to six files at once. Every build either lands a<br />
<code class="" data-line="">complete</code> status with an artifact ID, or a <code class="" data-line="">failed</code> status with the reason. Nothing produces<br />
a half-hardened image.</p>
<p>The validate-then-build ordering matters more than it looks. Validation is offline and takes<br />
milliseconds; a build takes 15–25 minutes and costs money. Catching a malformed blueprint or an<br />
unsupported OS/provider pair in the first step means you never launch the instance.</p>
<hr />
<h2 id="blueprint-versioning-and-drift">Blueprint Versioning and Drift</h2>
<p>Version-controlling the blueprint file solves a problem multi-cloud environments hit<br />
consistently: knowing what your OS security posture was six months ago. The blueprint is the<br />
answer — it&#8217;s a file, in git, with a commit history and a reviewer&#8217;s name on every change.</p>
<p>Re-scanning a <em>running</em> instance against the posture that built it is a separate job, and it<br />
does not live in the CLI. <code class="" data-line="">bakex validate</code> and <code class="" data-line="">bakex build</code> are the two CLI verbs; scanning<br />
and drift comparison are in the web UI and the HTTP API, where the scan results have somewhere<br />
to live. EP04 covers that surface in detail — the A–F grade, the SARIF export, and comparing a<br />
current scan against a stored baseline.</p>
<p>The useful discipline in the meantime is unglamorous: rebuild from the blueprint rather than<br />
patching running instances. An instance that drifted is a symptom; the blueprint is the cure,<br />
and re-baking is cheaper than reconciling.</p>
<hr />
<h2 id="production-gotchas">Production Gotchas</h2>
<p><strong>Provider-specific CIS controls exist.</strong> CIS AWS Foundations Benchmark and CIS GCP Benchmark include cloud-specific controls (VPC flow logs, CloudTrail, etc.) that are separate from the OS-level CIS controls. The blueprint handles OS-level controls. Cloud-level controls (IAM, logging, network configuration) belong in your cloud security posture management tooling.</p>
<p><strong>Build costs vary by provider.</strong> On AWS, the build instance is a <code class="" data-line="">t3.medium</code> for 15–20 minutes (~$0.02). On GCP and Azure, equivalent pricing applies. For multi-provider builds, run them in regions close to your primary workloads to minimize image transfer time.</p>
<p><strong>Proxmox is a template VMID, not an image name.</strong> Every cloud provider names its base image with a string; Proxmox names it with a number — the VE template&#8217;s VMID (<code class="" data-line="">9000</code> in the shipped blueprint). The provider talks to the Proxmox API remotely via <code class="" data-line="">proxmoxer</code>, so no agent on the host is required, but it does need <code class="" data-line="">host</code> credentials and it discovers the built VM&#8217;s IP through the QEMU guest agent <em>inside</em> the VM. If the guest agent isn&#8217;t installed in your template, the build will provision and then hang waiting for an address.</p>
<p><strong>KVM and Proxmox are deliberately different providers.</strong> They look interchangeable and aren&#8217;t: a KVM target names a downloadable cloud image, a Proxmox target names a VE template that already exists on your host. Don&#8217;t assume a blueprint written for one works on the other.</p>
<p><strong>GCP image sharing across projects requires explicit IAM.</strong> GCP machine images aren&#8217;t automatically available to other projects in the organization. BakeX builds the image; sharing it is a GCP IAM operation you configure at the project or organization level — there&#8217;s no BakeX command that grants cross-project access for you.</p>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Multi-cloud OS hardening with separate scripts per provider creates inevitable drift; a provider-abstracted blueprint eliminates it</li>
<li>BakeX ships working blueprints for AWS, GCP, Azure, DigitalOcean, Linode, and Proxmox — one file per provider, with <code class="" data-line="">target</code> as the only section that differs</li>
<li>The provider is a field in the blueprint, not a CLI flag: every file is self-describing and builds identically in CI, locally, or on a teammate&#8217;s machine</li>
<li>Fan-out is a shell loop over exit codes, not a framework feature — validate all six offline first, then build in parallel with <code class="" data-line="">--json</code></li>
<li>Blueprint version control is the single source of truth for OS security posture history — and a compliance change that isn&#8217;t identical across all six providers shows up as an odd hunk in code review</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>Six providers, one compliance posture, and a diff that proves it. EP03 showed that the multi-cloud drift problem disappears when provider details are confined to a single block of the blueprint.</p>
<p>What neither EP02 nor EP03 answered is the auditor&#8217;s question: how do you know the image is actually compliant? &#8220;We ran CIS L1&#8221; is not an answer. &#8220;Grade A, 98/100 controls, SARIF export attached&#8221; is.</p>
<p>EP04 covers automated OpenSCAP compliance: the post-build scan in detail — how the A-F grade is calculated, what controls block an A grade, how SARIF exports work, and how drift detection catches what changed after deployment.</p>
<p><em>Next: <a href="/automated-compliance-scanning-openscap/">automated OpenSCAP compliance — CIS benchmark grading before deployment</a></em></p>
<p>Get EP04 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-multi-cloud%2F&amp;linkname=One%20Blueprint%2C%20Six%20Clouds%20%E2%80%94%20Multi-Provider%20OS%20Image%20Builds" 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-multi-cloud%2F&amp;linkname=One%20Blueprint%2C%20Six%20Clouds%20%E2%80%94%20Multi-Provider%20OS%20Image%20Builds" 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-multi-cloud%2F&amp;linkname=One%20Blueprint%2C%20Six%20Clouds%20%E2%80%94%20Multi-Provider%20OS%20Image%20Builds" 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-multi-cloud%2F&amp;linkname=One%20Blueprint%2C%20Six%20Clouds%20%E2%80%94%20Multi-Provider%20OS%20Image%20Builds" 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-multi-cloud%2F&amp;linkname=One%20Blueprint%2C%20Six%20Clouds%20%E2%80%94%20Multi-Provider%20OS%20Image%20Builds" 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-multi-cloud%2F&amp;linkname=One%20Blueprint%2C%20Six%20Clouds%20%E2%80%94%20Multi-Provider%20OS%20Image%20Builds" 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-multi-cloud%2F&amp;linkname=One%20Blueprint%2C%20Six%20Clouds%20%E2%80%94%20Multi-Provider%20OS%20Image%20Builds" 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-multi-cloud%2F&#038;title=One%20Blueprint%2C%20Six%20Clouds%20%E2%80%94%20Multi-Provider%20OS%20Image%20Builds" data-a2a-url="https://linuxcent.com/linux-hardening-multi-cloud/" data-a2a-title="One Blueprint, Six Clouds — Multi-Provider OS Image Builds"></a></p><p>The post <a href="https://linuxcent.com/linux-hardening-multi-cloud/">One Blueprint, Six Clouds — Multi-Provider OS Image Builds</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/linux-hardening-multi-cloud/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1774</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-25 04:06:27 by W3 Total Cache
-->