<?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>Infrastructure as Code Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/infrastructure-as-code/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/infrastructure-as-code/</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>Infrastructure as Code Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/infrastructure-as-code/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<item>
		<title>Crossplane vs Terraform: Composition vs HCL for Infrastructure as Code</title>
		<link>https://linuxcent.com/crossplane-vs-terraform/</link>
					<comments>https://linuxcent.com/crossplane-vs-terraform/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Thu, 23 Jul 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[Kubernetes Ecosystem]]></category>
		<category><![CDATA[Crossplane]]></category>
		<category><![CDATA[Infrastructure as Code]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Terraform]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=2255</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"> 5</span> <span class="rt-label rt-postfix">minutes</span></span>Compare Crossplane's Kubernetes-native composition model to Terraform's HCL state model — and get a clear recommendation, not a neutral list.</p>
<p>The post <a href="https://linuxcent.com/crossplane-vs-terraform/">Crossplane vs Terraform: Composition vs HCL for Infrastructure as Code</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"> 5</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>Kubernetes Ecosystem: From User to Contributor, Episode 7</em><br />
<a href="/crossplane-universal-control-plane/">← EP06: Crossplane</a> · <strong>EP07: Crossplane vs Terraform</strong> · <a href="/karpenter-node-provisioning/">EP08: Karpenter →</a></p>
<p><strong>11 min read</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Crossplane vs Terraform is fundamentally a continuous-reconciliation model against a plan/apply model — not just two different syntaxes for the same idea</li>
<li>Crossplane needs a live Kubernetes cluster to run at all; Terraform needs nothing but a state file and network access to the providers it&#8217;s calling</li>
<li>Terraform&#8217;s provider registry is a decade deep and covers services Crossplane&#8217;s younger ecosystem hasn&#8217;t reached yet — SaaS tools, monitoring platforms, and services with no cloud-infrastructure angle at all</li>
<li>Crossplane&#8217;s Compositions give app teams a genuinely self-service, in-cluster API; Terraform&#8217;s modules give infra teams reusable code, but consuming a module still means running Terraform yourself</li>
<li>Recommendation: many real platform teams use both — Terraform (or CAPI, EP05) to bootstrap the cluster and its surrounding VPC/networking, then Crossplane running inside that cluster for the self-service, app-team-facing layer</li>
<li>Contribution opportunity: Crossplane&#8217;s provider coverage gap against Terraform&#8217;s registry is real, specific, and a legitimate place to build a brand-new provider</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">TERRAFORM                                   CROSSPLANE
──────────                                   ──────────
terraform plan                               kubectl apply -f resource.yaml
  │  (shows what WOULD change)                    │
  ▼                                                ▼
terraform apply                              Crossplane controller reconciles
  │  (changes happen once, here)                   │  (continuously, forever,
  ▼                                                 │   not just at apply time)
State file (local or remote backend)               ▼
tracks what Terraform created                 Kubernetes etcd IS the state —
                                                the CR&#039;s status field tracks
No live cluster or control                     sync state
plane required to run this
                                               Requires a running Kubernetes
                                               cluster as the control plane
</code></pre>
<p>Crossplane vs Terraform is best understood through that control-flow difference first, before comparing any specific feature: Terraform changes things at discrete moments you trigger; Crossplane&#8217;s controllers are always watching, always correcting drift, for as long as the cluster runs.</p>
<hr />
<h2 id="the-fundamental-model-difference-continuous-reconciliation-vs-planapply">The Fundamental Model Difference: Continuous Reconciliation vs Plan/Apply</h2>
<p>Terraform&#8217;s model gives you an explicit review step — <code class="" data-line="">terraform plan</code> shows exactly what will change before anything does, and nothing changes again until you run <code class="" data-line="">apply</code> a second time. Crossplane&#8217;s model (covered in EP06) has no equivalent pause: once a Managed Resource or Composition claim exists, Crossplane&#8217;s controllers reconcile it toward the desired state continuously, including reverting manual out-of-band changes automatically.</p>
<p>Neither is objectively better — they&#8217;re suited to different operating assumptions. Terraform&#8217;s model fits teams who want a deliberate, reviewed change process. Crossplane&#8217;s fits teams who want infrastructure to behave like every other Kubernetes-native resource: self-healing, always converging, no separate &#8220;did anyone remember to re-apply&#8221; step.</p>
<hr />
<h2 id="state-management-etcd-crds-vs-terraform-state-files">State Management: etcd + CRDs vs Terraform State Files</h2>
<pre><code class="" data-line=""># Terraform: state lives in a file (local or remote — S3, Terraform Cloud, etc.)
$ terraform state list
aws_s3_bucket.uploads
aws_db_instance.main

# Crossplane: &quot;state&quot; is just the live cluster&#039;s etcd — the CR&#039;s own status
$ kubectl get bucket uploads -o jsonpath=&#039;{.status.conditions}&#039;
[{&quot;type&quot;:&quot;Ready&quot;,&quot;status&quot;:&quot;True&quot;},{&quot;type&quot;:&quot;Synced&quot;,&quot;status&quot;:&quot;True&quot;}]
</code></pre>
<p>Terraform&#8217;s state file is a single point of coordination that has to be locked correctly for concurrent runs to be safe — a well-understood but real operational concern (remote state backends, state locking, occasional manual state surgery after a botched apply). Crossplane sidesteps a separate state file entirely, but that means the health of your Kubernetes cluster&#8217;s etcd <em>is</em> the health of your infrastructure&#8217;s state — a different, not smaller, operational responsibility.</p>
<hr />
<h2 id="composition-vs-modules-reusable-infrastructure-patterns-compared">Composition vs Modules: Reusable Infrastructure Patterns Compared</h2>
<p>Terraform modules are reusable code that whoever runs Terraform includes in their own configuration — genuinely reusable, but still something each consumer runs themselves. Crossplane Compositions (EP06) are reusable <em>inside the cluster</em> — an app team doesn&#8217;t run anything, they just create a custom resource the platform team already defined, and Crossplane&#8217;s controllers do the rest without the app team ever touching Terraform or Crossplane&#8217;s own tooling directly.</p>
<p>That&#8217;s the real practical difference for organizational self-service: Compositions remove the &#8220;app team needs to know how to run our IaC tool&#8221; step entirely. Modules still require the consumer to run Terraform, even if they didn&#8217;t write the module.</p>
<hr />
<h2 id="ecosystem-maturity-terraforms-decade-deep-provider-registry-vs-crossplanes-younger-one">Ecosystem Maturity: Terraform&#8217;s Decade-Deep Provider Registry vs Crossplane&#8217;s Younger One</h2>
<p>Terraform&#8217;s provider registry covers not just the major clouds but a long tail of SaaS platforms, monitoring tools, DNS providers, and internal enterprise systems that have no &#8220;cloud infrastructure&#8221; angle at all — a decade of community and vendor-contributed providers. Crossplane&#8217;s provider ecosystem, while actively growing and covering the major clouds thoroughly, has real, documented gaps once you look past core compute/storage/networking/database resources into more specialized or less common services.</p>
<hr />
<h2 id="the-recommendation-which-one-and-when-to-use-both">The Recommendation: Which One, and When to Use Both</h2>
<p><strong>If your platform team is Kubernetes-native and wants to offer app teams a true self-service infrastructure API without teaching them a separate IaC tool:</strong> Crossplane. That&#8217;s the specific problem its Composition model solves better than anything Terraform offers.</p>
<p><strong>If you need broad provider coverage beyond core cloud infrastructure, or you don&#8217;t want infrastructure lifecycle tied to a live Kubernetes control plane&#8217;s uptime:</strong> Terraform. Its registry depth and its independence from any running cluster are real advantages Crossplane doesn&#8217;t currently match.</p>
<p><strong>The honest answer for a lot of real platform teams is both, at different layers.</strong> Use Terraform (or Cluster API, EP05) to bootstrap the Kubernetes cluster itself and its surrounding cloud networking — the layer that has to exist before Crossplane can run at all — then run Crossplane inside that cluster for the ongoing, self-service, app-team-facing infrastructure requests. This isn&#8217;t a compromise; it&#8217;s matching each tool to the layer it&#8217;s actually better suited for.</p>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>Don&#8217;t manage the same cloud resource with both Terraform and Crossplane simultaneously.</strong> Both tools will detect the other&#8217;s changes as drift and fight to revert them — pick one owner per resource, even when both tools are in use across your stack at different layers.</p>
<p><strong>Terraform&#8217;s plan/apply gives you a review window Crossplane doesn&#8217;t — build your own review gate if you need one with Crossplane</strong> (a PR-based GitOps flow with required approval before a claim manifest merges is the common substitute).</p>
<p><strong>Crossplane&#8217;s continuous reconciliation means a broken provider or a cloud API outage shows up as a stuck <code class="" data-line="">Synced: False</code> condition, not a failed one-time command</strong> — monitoring needs to watch for stuck conditions over time, not just command exit codes the way Terraform CI pipelines typically do.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<table>
<thead>
<tr>
<th></th>
<th>Terraform</th>
<th>Crossplane</th>
</tr>
</thead>
<tbody>
<tr>
<td>Change model</td>
<td>Plan → Apply (explicit)</td>
<td>Continuous reconciliation</td>
</tr>
<tr>
<td>Requires a live cluster</td>
<td>No</td>
<td>Yes</td>
</tr>
<tr>
<td>State</td>
<td>State file (local/remote)</td>
<td>Kubernetes etcd + CR status</td>
</tr>
<tr>
<td>Reusable patterns</td>
<td>Modules (you still run them)</td>
<td>Compositions (app team just creates a claim)</td>
</tr>
<tr>
<td>Provider breadth</td>
<td>Very broad, decade-deep</td>
<td>Growing, strong on core cloud, gaps elsewhere</td>
</tr>
<tr>
<td>Manual drift</td>
<td>Detected at next <code class="" data-line="">plan</code>, not auto-reverted</td>
<td>Auto-reverted on next reconcile</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="contribution-opportunity-building-a-missing-crossplane-provider">Contribution Opportunity: Building a Missing Crossplane Provider</h2>
<p><strong>The limitation:</strong> For a meaningful number of services Terraform has supported for years — smaller SaaS platforms, specialized monitoring tools, niche infrastructure services — there&#8217;s no Crossplane provider equivalent yet. Anyone wanting to manage that service the Crossplane way currently can&#8217;t, full stop.</p>
<p><strong>Why it&#8217;s hard to fix:</strong> Building a new provider means implementing a real API client, defining CRD schemas that faithfully map the service&#8217;s actual parameters, and maintaining it as that service&#8217;s API evolves — real, ongoing engineering commitment, not a one-time script. That&#8217;s exactly why the ecosystem&#8217;s provider list still trails Terraform&#8217;s, despite Crossplane&#8217;s core reconciliation engine being mature: the core is one thing to maintain, but each provider is its own ongoing surface area.</p>
<p><strong>What a contribution-shaped fix looks like:</strong> Crossplane&#8217;s <code class="" data-line="">provider-template</code> repository exists specifically to make starting a new provider tractable — it scaffolds the boilerplate (code generation, CRD structure, controller wiring) so a new provider author focuses on the actual API mapping, not plumbing. Picking one service you already use via Terraform that has no Crossplane equivalent, and building a minimal provider covering just the 2-3 resource types you actually need, is a real, bounded, achievable contribution — and one the Crossplane community actively wants, given how directly it grows the ecosystem.</p>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Crossplane&#8217;s continuous reconciliation and Terraform&#8217;s plan/apply are different operating models, not different syntaxes for the same thing — pick based on which review/change process fits your team</li>
<li>Crossplane requires a live cluster to function at all; Terraform doesn&#8217;t, which matters for bootstrapping order</li>
<li>Compositions remove the &#8220;app team has to run our IaC tool&#8221; step that Terraform modules still require</li>
<li>Terraform&#8217;s provider registry breadth remains a real advantage for anything beyond core cloud infrastructure</li>
<li>Many real platform teams run both at different layers — Terraform/CAPI to bootstrap the cluster, Crossplane inside it for self-service — and that&#8217;s a legitimate architecture, not indecision</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>Everything so far in this series has been about provisioning clusters and the infrastructure around them. EP08 shifts to what happens inside an already-running cluster when pods can&#8217;t be scheduled: Karpenter&#8217;s just-in-time node provisioning, and why it replaced the node-group model most teams started with.</p>
<p><em>Next: <a href="/karpenter-node-provisioning/">EP08 — Karpenter: Just-in-Time Node Provisioning for Kubernetes</a></em></p>
<p>Get EP08 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%2Fcrossplane-vs-terraform%2F&amp;linkname=Crossplane%20vs%20Terraform%3A%20Composition%20vs%20HCL%20for%20Infrastructure%20as%20Code" 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%2Fcrossplane-vs-terraform%2F&amp;linkname=Crossplane%20vs%20Terraform%3A%20Composition%20vs%20HCL%20for%20Infrastructure%20as%20Code" 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%2Fcrossplane-vs-terraform%2F&amp;linkname=Crossplane%20vs%20Terraform%3A%20Composition%20vs%20HCL%20for%20Infrastructure%20as%20Code" 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%2Fcrossplane-vs-terraform%2F&amp;linkname=Crossplane%20vs%20Terraform%3A%20Composition%20vs%20HCL%20for%20Infrastructure%20as%20Code" 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%2Fcrossplane-vs-terraform%2F&amp;linkname=Crossplane%20vs%20Terraform%3A%20Composition%20vs%20HCL%20for%20Infrastructure%20as%20Code" 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%2Fcrossplane-vs-terraform%2F&amp;linkname=Crossplane%20vs%20Terraform%3A%20Composition%20vs%20HCL%20for%20Infrastructure%20as%20Code" 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%2Fcrossplane-vs-terraform%2F&amp;linkname=Crossplane%20vs%20Terraform%3A%20Composition%20vs%20HCL%20for%20Infrastructure%20as%20Code" 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%2Fcrossplane-vs-terraform%2F&#038;title=Crossplane%20vs%20Terraform%3A%20Composition%20vs%20HCL%20for%20Infrastructure%20as%20Code" data-a2a-url="https://linuxcent.com/crossplane-vs-terraform/" data-a2a-title="Crossplane vs Terraform: Composition vs HCL for Infrastructure as Code"></a></p><p>The post <a href="https://linuxcent.com/crossplane-vs-terraform/">Crossplane vs Terraform: Composition vs HCL for Infrastructure as Code</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/crossplane-vs-terraform/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2255</post-id>	</item>
		<item>
		<title>Crossplane: Kubernetes as the Universal Control Plane</title>
		<link>https://linuxcent.com/crossplane-universal-control-plane/</link>
					<comments>https://linuxcent.com/crossplane-universal-control-plane/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Tue, 21 Jul 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[Kubernetes Ecosystem]]></category>
		<category><![CDATA[Crossplane]]></category>
		<category><![CDATA[Infrastructure as Code]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Platform Engineering]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=2252</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"> 5</span> <span class="rt-label rt-postfix">minutes</span></span>See how Crossplane turns Kubernetes into a control plane for any cloud resource — and exactly what that composition model costs you operationally.</p>
<p>The post <a href="https://linuxcent.com/crossplane-universal-control-plane/">Crossplane: Kubernetes as the Universal Control Plane</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"> 5</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>Kubernetes Ecosystem: From User to Contributor, Episode 6</em><br />
<a href="/cluster-api-declarative-lifecycle/">← EP05: Cluster API</a> · <strong>EP06: Crossplane</strong> · <a href="/crossplane-vs-terraform/">EP07: Crossplane vs Terraform →</a></p>
<p><strong>12 min read</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Crossplane extends the exact reconciliation pattern EP05 covered for cluster infrastructure to <em>any</em> cloud resource — an S3 bucket, an RDS instance, a DNS record all become Kubernetes CRDs, continuously reconciled</li>
<li><strong>Managed Resources</strong> represent one real cloud resource each; <strong>Compositions</strong> bundle several Managed Resources behind a single, simpler custom API a platform team defines and app teams consume</li>
<li><strong>Composition Functions</strong> are Crossplane&#8217;s newer, more flexible replacement for its older YAML-based patch-and-transform templating — real code (Go, Python, or others) instead of declarative patches</li>
<li>Crossplane continuously reconciles like any Kubernetes controller — a manual change to a cloud resource outside Crossplane gets reverted on the next reconcile loop, which is a real surprise for teams used to Terraform&#8217;s plan/apply model</li>
<li>Provider CRD counts can bloat a cluster&#8217;s etcd significantly — this drove the ecosystem&#8217;s move toward smaller, split &#8220;provider families&#8221; instead of one monolithic provider per cloud</li>
<li>Contribution opportunity: several providers still haven&#8217;t migrated to the family-split pattern — a real, currently-tracked, achievable upstream contribution</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">App team writes:                    Platform team defined this Composition
                                     once, behind the scenes:
apiVersion: platform.example.com/v1
kind: Database                       XRD &quot;Database&quot; ─── composes ───┐
metadata:                                                             │
  name: my-app-db                                                     ▼
spec:                                                        ┌────────────────┐
  size: small                                                │ RDSInstance    │
                                                                │ SecurityGroup  │
   │                                                            │ ParameterGroup │
   │ app team never sees                                        └────────────────┘
   │ or touches these three                                     each a real Managed
   ▼                                                            Resource, a real
Crossplane reconciles all three,                                cloud API call
continuously, forever
</code></pre>
<p>Crossplane&#8217;s pitch as a universal control plane is literal: instead of app teams filing tickets or writing their own Terraform for a database, they request a <code class="" data-line="">Database</code> — a custom API the platform team designed — and Crossplane&#8217;s controllers translate that into the actual RDS instance, security group, and parameter group underneath, then keep reconciling all three toward the declared state indefinitely.</p>
<hr />
<h2 id="managed-resources-cloud-infrastructure-as-kubernetes-crds">Managed Resources: Cloud Infrastructure as Kubernetes CRDs</h2>
<pre><code class="" data-line="">$ kubectl apply -f - &lt;&lt;EOF
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
  name: app-uploads-prod
spec:
  forProvider:
    region: us-east-1
  providerConfigRef:
    name: aws-prod
EOF

$ kubectl get bucket app-uploads-prod
NAME               READY   SYNCED   AGE
app-uploads-prod   True    True     30s
#                  ^^^^    ^^^^^^ — READY: resource exists and is healthy
#                          SYNCED: Crossplane&#039;s last reconcile succeeded
</code></pre>
<p>Every field under <code class="" data-line="">forProvider</code> maps directly to that cloud API&#8217;s actual parameters — this is a thin, honest translation layer, not an abstraction hiding what&#8217;s actually being created. <code class="" data-line="">READY</code>/<code class="" data-line="">SYNCED</code> becoming <code class="" data-line="">True</code> means an actual S3 bucket now exists in that AWS account, exactly as declared.</p>
<hr />
<h2 id="compositions-and-xrds-building-your-own-abstract-platform-api">Compositions and XRDs: Building Your Own Abstract Platform API</h2>
<p>This is Crossplane&#8217;s real differentiator over just using individual Managed Resources directly:</p>
<pre><code class="" data-line=""># The platform team defines the abstract API app teams will see
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xdatabases.platform.example.com
spec:
  group: platform.example.com
  names:
    kind: XDatabase
    plural: xdatabases
  claimNames:
    kind: Database        # ← this is what app teams actually create
    plural: databases
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        properties:
          spec:
            properties:
              size: {type: string, enum: [&quot;small&quot;, &quot;medium&quot;, &quot;large&quot;]}
</code></pre>
<p>App teams interact only with the simple <code class="" data-line="">Database</code> claim shown in the Big Picture diagram above. The <code class="" data-line="">Composition</code> resource (not shown here for brevity) is what actually maps <code class="" data-line="">size: small</code> to a specific RDS instance class, storage size, and backup configuration — the platform team&#8217;s opinions, encoded once, consumed self-service by every app team afterward.</p>
<hr />
<h2 id="composition-functions-crossplanes-newer-more-flexible-approach">Composition Functions: Crossplane&#8217;s Newer, More Flexible Approach</h2>
<p>Older Crossplane Compositions used a YAML-based &#8220;patch and transform&#8221; templating language to map the abstract API&#8217;s fields onto Managed Resource fields — functional, but limited for anything beyond straightforward field mapping. <strong>Composition Functions</strong> replace that with actual executable code:</p>
<pre><code class="" data-line="">$ crossplane beta render xr.yaml composition.yaml functions.yaml
---
apiVersion: rds.aws.upbound.io/v1alpha1
kind: Instance
metadata:
  name: my-app-db-instance
spec:
  forProvider:
    instanceClass: db.t3.micro   # ← computed by real Go logic based on
                                  #   spec.size, not a static YAML patch
    engine: postgres
</code></pre>
<p>Composition Functions run as small, packaged pieces of logic (often distributed as OCI images) that Crossplane&#8217;s engine invokes during reconciliation — giving platform teams real conditionals, loops, and validation instead of the older templating language&#8217;s more limited patch syntax.</p>
<hr />
<h2 id="providers-and-the-provider-ecosystem">Providers and the Provider Ecosystem</h2>
<p>Each cloud&#8217;s resources are supplied by a separate <strong>provider</strong> — <code class="" data-line="">provider-aws</code>, <code class="" data-line="">provider-gcp</code>, <code class="" data-line="">provider-azure</code>, and increasingly split into smaller <strong>provider families</strong> (<code class="" data-line="">provider-aws-s3</code>, <code class="" data-line="">provider-aws-rds</code>, etc.) rather than one enormous provider per cloud:</p>
<pre><code class="" data-line="">$ kubectl get providers
NAME                   INSTALLED   HEALTHY   AGE
provider-aws-s3        True        True      10d
provider-aws-rds       True        True      10d
#         ^^^^^^ — installing only the families you actually use, instead
#                  of one monolithic provider-aws with every AWS service&#039;s
#                  CRDs installed regardless of whether you use them
</code></pre>
<p>The family split exists specifically because a single monolithic cloud provider can register thousands of CRDs — a real, measurable strain on a cluster&#8217;s etcd and API server that the ecosystem is still in the process of migrating away from.</p>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>Crossplane reconciles continuously — a manual change to a cloud resource outside Crossplane gets reverted on the next loop.</strong> Teams coming from Terraform&#8217;s plan/apply model, where nothing changes until you explicitly run <code class="" data-line="">apply</code> again, are frequently surprised the first time a manual &#8220;quick fix&#8221; in the AWS console gets silently undone minutes later.</p>
<p><strong>Monolithic providers can register thousands of CRDs, and that has a real, measurable etcd and API-server cost.</strong> If you&#8217;re on an older, non-family provider version and seeing API server memory pressure, check CRD count before assuming it&#8217;s an unrelated capacity issue.</p>
<p><strong>Deleting a Composition&#8217;s underlying claim doesn&#8217;t always tear down cleanly if finalizers on the Managed Resources are stuck</strong> — a Managed Resource that failed to delete cleanly from the cloud side (a non-empty S3 bucket, for instance) will block the whole claim&#8217;s deletion until that&#8217;s resolved manually.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="" data-line="">kubectl get managed                        # every Managed Resource, all providers
kubectl get compositeresourcedefinitions   # XRDs — the abstract APIs defined
kubectl get compositions                   # the mapping logic behind each XRD
kubectl get providers                       # installed providers + health
crossplane beta render &lt;xr&gt; &lt;comp&gt; &lt;fns&gt;    # render a Composition locally, no cluster needed
kubectl describe &lt;managed-resource-kind&gt; &lt;name&gt;   # sync status + underlying cloud errors
</code></pre>
<hr />
<h2 id="contribution-opportunity-migrating-providers-to-the-family-pattern">Contribution Opportunity: Migrating Providers to the Family Pattern</h2>
<p><strong>The limitation:</strong> Not every Crossplane provider has migrated from the older, monolithic-per-cloud model to the smaller &#8220;provider family&#8221; pattern that registers only the CRDs for services actually in use. Clusters running an un-migrated provider carry the etcd and API-server overhead of thousands of unused CRDs, and this is a known, actively-discussed problem in the Crossplane community — not a hypothetical one.</p>
<p><strong>Why it&#8217;s hard to fix:</strong> Splitting a monolithic provider into families isn&#8217;t a mechanical find-and-replace — it means restructuring code generation, versioning, and release processes for every resource type the provider covers, while keeping a migration path that doesn&#8217;t break existing users who depend on the old provider&#8217;s CRDs. It&#8217;s real, unglamorous engineering work that has to happen provider-by-provider, cloud-by-cloud, and each provider&#8217;s maintainer bandwidth varies.</p>
<p><strong>What a contribution-shaped fix looks like:</strong> The Crossplane and Upbound-maintained provider repositories publicly track which providers still need family-splitting — this is documented, wanted work, not a gap you&#8217;d have to go discover yourself. A concrete starting contribution: pick one still-monolithic provider (checking the project&#8217;s own tracking issues for an unclaimed one), and work through the documented family-split process the already-migrated providers (like <code class="" data-line="">provider-aws</code>) used as a reference implementation. This is real upstream OSS work with an existing template to follow, not a design problem you have to solve from scratch.</p>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Crossplane&#8217;s Managed Resources make individual cloud resources real Kubernetes CRDs, continuously reconciled rather than applied once</li>
<li>Compositions and XRDs are the actual value proposition: platform teams define a simple, opinionated API once; app teams self-serve against it without needing to know what&#8217;s underneath</li>
<li>Composition Functions replace older YAML patch-and-transform templating with real executable logic — a genuinely evolving, more flexible part of the project</li>
<li>Continuous reconciliation means manual out-of-band changes get reverted — a real behavioral difference from Terraform&#8217;s plan/apply model, not just a implementation detail</li>
<li>The provider family migration is documented, wanted, achievable contribution work — not a gap you&#8217;d need to discover on your own</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>Crossplane&#8217;s composition model and Terraform&#8217;s HCL module model solve the same underlying problem — reusable, parameterized infrastructure definitions — from genuinely different architectural starting points. EP07 puts them side by side and gives a clear recommendation for which fits which team.</p>
<p><em>Next: <a href="/crossplane-vs-terraform/">EP07 — Crossplane vs Terraform: Composition vs HCL for Infrastructure as Code</a></em></p>
<p>Get EP07 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%2Fcrossplane-universal-control-plane%2F&amp;linkname=Crossplane%3A%20Kubernetes%20as%20the%20Universal%20Control%20Plane" 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%2Fcrossplane-universal-control-plane%2F&amp;linkname=Crossplane%3A%20Kubernetes%20as%20the%20Universal%20Control%20Plane" 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%2Fcrossplane-universal-control-plane%2F&amp;linkname=Crossplane%3A%20Kubernetes%20as%20the%20Universal%20Control%20Plane" 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%2Fcrossplane-universal-control-plane%2F&amp;linkname=Crossplane%3A%20Kubernetes%20as%20the%20Universal%20Control%20Plane" 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%2Fcrossplane-universal-control-plane%2F&amp;linkname=Crossplane%3A%20Kubernetes%20as%20the%20Universal%20Control%20Plane" 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%2Fcrossplane-universal-control-plane%2F&amp;linkname=Crossplane%3A%20Kubernetes%20as%20the%20Universal%20Control%20Plane" 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%2Fcrossplane-universal-control-plane%2F&amp;linkname=Crossplane%3A%20Kubernetes%20as%20the%20Universal%20Control%20Plane" 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%2Fcrossplane-universal-control-plane%2F&#038;title=Crossplane%3A%20Kubernetes%20as%20the%20Universal%20Control%20Plane" data-a2a-url="https://linuxcent.com/crossplane-universal-control-plane/" data-a2a-title="Crossplane: Kubernetes as the Universal Control Plane"></a></p><p>The post <a href="https://linuxcent.com/crossplane-universal-control-plane/">Crossplane: Kubernetes as the Universal Control Plane</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/crossplane-universal-control-plane/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2252</post-id>	</item>
		<item>
		<title>Cloud-Native Hardening: Securing the AWS Identity Perimeter</title>
		<link>https://linuxcent.com/cloud-native-hardening-aws-identity/</link>
					<comments>https://linuxcent.com/cloud-native-hardening-aws-identity/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Mon, 06 Jul 2026 21:31:37 +0000</pubDate>
				<category><![CDATA[Security Architecture]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[IMDSv2]]></category>
		<category><![CDATA[Infrastructure as Code]]></category>
		<guid isPermaLink="false">https://linuxcent.com/cloud-native-hardening-aws-identity/</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>Apply cloud native infrastructure hardening to the AWS identity perimeter: enforce IMDSv2, scope IAM least privilege, and gate IaC scans before merge.</p>
<p>The post <a href="https://linuxcent.com/cloud-native-hardening-aws-identity/">Cloud-Native Hardening: Securing the AWS Identity Perimeter</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>Zero to Hero: Cybersecurity Architecture Masterclass, Module 3</em><br />
<a href="/stride-threat-modeling/">← Module 2: Proactive Design</a> · <strong>Module 3: Cloud-Native Hardening</strong> · <a href="/immutable-data-architecture-worm/">Module 4: Resilience &amp; Survival →</a></p>
<p><strong>12 min read</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Cloud native infrastructure hardening starts from a different assumption than on-prem hardening: there is no network perimeter, only an identity perimeter — every AWS API call is the boundary</li>
<li>IMDSv1 (the EC2 metadata service without a token) is the single highest-leverage cloud-native hardening fix available — it turned an SSRF bug into the Capital One breach</li>
<li>IAM policy design is architecture, not IT administration: least privilege, permission boundaries, and SCPs compose into the actual perimeter</li>
<li>Infrastructure-as-code scanning (<code class="" data-line="">checkov</code>, <code class="" data-line="">tfsec</code>) catches identity-perimeter mistakes in a pull request instead of in an incident</li>
<li><code class="" data-line="">aws iam simulate-principal-policy</code> answers &#8220;can this role actually do that?&#8221; definitively, without waiting to find out in production</li>
<li>Recommendation: treat IMDSv2 enforcement and IAM least-privilege review as pipeline gates, not periodic audits — the same &#8220;build constraint, not process step&#8221; principle from the OS Hardening series</li>
</ul>
<hr />
<h2 id="the-big-picture-the-perimeter-moved-to-the-api-call">The Big Picture: The Perimeter Moved to the API Call</h2>
<pre><code class="" data-line="">ON-PREM MODEL                          CLOUD-NATIVE MODEL
──────────────                          ──────────────────
Firewall at network edge                No fixed network edge
        │                                        │
Trusted internal subnet                 Every API call carries its
        │                                 own identity + policy
Server assumed safe if                          │
inside the firewall                     IAM evaluates: who is this,
                                          what can they do, right now
                                                 │
                                          Perimeter = the IAM policy
                                          attached to the caller
</code></pre>
<p>Cloud-native infrastructure hardening means accepting that the network no longer defines what&#8217;s trusted — the AWS identity perimeter, enforced entirely through IAM policy evaluation on every single API call, is the only perimeter that actually exists. Module 1 called this the shift from network-centric to identity-centric trust; this module makes it concrete with the two failures that actually break it in production: a leaky metadata service and an over-permissioned role.</p>
<hr />
<h2 id="the-breach-that-made-imdsv2-mandatory">The Breach That Made IMDSv2 Mandatory</h2>
<p>In 2019, a misconfigured WAF in front of a bank&#8217;s application allowed a Server-Side Request Forgery (SSRF) — an attacker convinced the application server to make an HTTP request to <code class="" data-line="">http://169.254.169.254</code>, the EC2 instance metadata endpoint. IMDSv1 answered with no authentication required at all: temporary IAM credentials for the role attached to that instance, handed to anyone who could make the server issue that one request.</p>
<p>Those credentials had read access to S3. The attacker used them to exfiltrate over 100 million customer records. This is the Capital One breach — <a href="https://linuxcent.com/ssrf-cloud-metadata-imds-capital-one/">covered in full in the Purple Team series</a> — and it is the single clearest illustration in cloud history of why &#8220;the perimeter is the identity, not the network&#8221; isn&#8217;t a slogan — it&#8217;s a description of exactly where that breach actually happened. The WAF misconfiguration was the entry point. The metadata service handing out credentials with zero verification was the architectural failure that turned an SSRF bug into a 100-million-record breach.</p>
<p><strong>IMDSv2 closes this specific gap</strong> by requiring a session token, fetched via a PUT request, before any metadata GET request is honored — and that PUT request cannot be replayed through a typical SSRF, because SSRF vulnerabilities almost always only allow GET-style requests to be forged. This single setting is the highest-leverage cloud-native hardening control available, and it should be enforced at the account level, not left as an opt-in per instance:</p>
<pre><code class="" data-line=""># Check whether IMDSv2 is enforced (HttpTokens: required) on an instance
$ aws ec2 describe-instances --instance-ids i-0abc123 \
    --query &#039;Reservations[].Instances[].MetadataOptions&#039;
{
    &quot;HttpTokens&quot;: &quot;required&quot;,
    &quot;HttpPutResponseHopLimit&quot;: 1,
    &quot;HttpEndpoint&quot;: &quot;enabled&quot;
}
# &quot;required&quot; = IMDSv2 only. &quot;optional&quot; = IMDSv1 still works — the gap.
</code></pre>
<pre><code class="" data-line=""># Enforce it account-wide for all new instances
$ aws ec2 modify-instance-metadata-defaults \
    --http-tokens required --http-put-response-hop-limit 1
</code></pre>
<hr />
<h2 id="iam-policy-design-is-architecture">IAM Policy Design Is Architecture</h2>
<p>If the metadata service is one way the identity perimeter leaks, an over-permissioned IAM policy is the other — and it&#8217;s far more common, because it doesn&#8217;t require a bug at all. It only requires a policy written with <code class="" data-line="">&quot;Resource&quot;: &quot;*&quot;</code> because scoping it felt like it would slow down a deploy.</p>
<p><strong>Least privilege</strong> means a role can do exactly what its function requires and nothing else — not &#8220;read-only across the account,&#8221; but &#8220;read this specific S3 prefix, write to this specific queue.&#8221;</p>
<p><strong>Permission boundaries</strong> cap what a role can ever be granted, even by someone with <code class="" data-line="">iam:CreatePolicy</code> access — a safety rail against exactly the kind of <a href="https://linuxcent.com/cloud-iam-privilege-escalation/"><code class="" data-line="">iam:PassRole</code> privilege escalation</a> covered in the Cloud IAM series, not just against the policy as originally written.</p>
<p><strong>Service Control Policies (SCPs)</strong> apply at the AWS Organization level, capping what any role in an account can do regardless of how permissive that account&#8217;s own IAM policies are — the outermost layer of the identity perimeter, and the one that survives a single account being compromised.</p>
<pre><code class="" data-line="">{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [{
    &quot;Effect&quot;: &quot;Allow&quot;,
    &quot;Action&quot;: [&quot;s3:GetObject&quot;],
    &quot;Resource&quot;: &quot;arn:aws:s3:::billing-invoices/tenant-4471/*&quot;
  }]
}
</code></pre>
<p>That policy can only ever read one tenant&#8217;s invoice prefix. Compare it to <code class="" data-line="">&quot;Resource&quot;: &quot;arn:aws:s3:::billing-invoices/*&quot;</code> — functionally identical for the one use case the developer was testing, and catastrophically different the day this role&#8217;s credentials leak.</p>
<hr />
<h2 id="quick-check-can-this-role-actually-do-that">Quick Check: Can This Role Actually Do That?</h2>
<p>Don&#8217;t wait to find out in production. <code class="" data-line="">aws iam simulate-principal-policy</code> evaluates a specific action against a role&#8217;s actual attached and inline policies — including SCPs and permission boundaries — and gives you a definitive allow/deny before anything runs:</p>
<pre><code class="" data-line="">$ aws iam simulate-principal-policy \
    --policy-source-arn arn:aws:iam::123456789012:role/billing-api-role \
    --action-names s3:GetObject \
    --resource-arns arn:aws:s3:::billing-invoices/tenant-9982/*

{
  &quot;EvaluationResults&quot;: [{
    &quot;EvalActionName&quot;: &quot;s3:GetObject&quot;,
    &quot;EvalResourceName&quot;: &quot;arn:aws:s3:::billing-invoices/tenant-9982/*&quot;,
    &quot;EvalDecision&quot;: &quot;explicitDeny&quot;,     # ← the answer you needed before deploying
    &quot;MatchedStatements&quot;: [...]
  }]
}
</code></pre>
<p><code class="" data-line="">explicitDeny</code> here means some policy statement — the role&#8217;s own policy, a permission boundary, or an SCP — explicitly blocks the action, and that takes precedence over any <code class="" data-line="">Allow</code> anywhere else in the policy chain (Module 1&#8217;s deny-by-default evaluation model, in practice). Run this simulation as part of code review for any new IAM policy, not after the role is already attached to a running service.</p>
<hr />
<h2 id="catching-this-before-it-ships-cloud-native-hardening-via-iac-scanning">Catching This Before It Ships: Cloud-Native Hardening via IaC Scanning</h2>
<p>Manually reviewing every Terraform IAM policy in every pull request doesn&#8217;t scale past a handful of engineers. <code class="" data-line="">checkov</code> and <code class="" data-line="">tfsec</code> scan infrastructure-as-code for exactly the patterns above — wildcard resources, IMDSv1 left enabled, public S3 buckets — as a CI step, before <code class="" data-line="">terraform apply</code> ever runs:</p>
<pre><code class="" data-line="">$ checkov -d ./terraform --check CKV_AWS_79,CKV_AWS_8

Check: CKV_AWS_79: &quot;Ensure Instance Metadata Service Version 1 is not enabled&quot;
    FAILED for resource: aws_instance.billing_api
    File: main.tf:14-22

Check: CKV_AWS_8: &quot;Ensure IAM policies do not allow full administrative privileges&quot;
    FAILED for resource: aws_iam_role_policy.billing_api_policy
    File: iam.tf:8-15
        Resource: &quot;*&quot;
</code></pre>
<p>A failed <code class="" data-line="">checkov</code> check blocking a pull request is the identity-perimeter equivalent of Stratum&#8217;s pipeline gate refusing to snapshot an unhardened image — the unsafe configuration never reaches an account where it can be exploited, because the check runs before merge, not after an audit finds it months later.</p>
<hr />
<h2 id="production-gotchas">Production Gotchas</h2>
<p><strong>IMDSv2 enforcement can break old SDKs and tools silently.</strong> Some older AWS SDK versions and third-party agents assume IMDSv1 and simply fail to fetch credentials once <code class="" data-line="">HttpTokens: required</code> is set — test in staging before enforcing account-wide.</p>
<p><strong><code class="" data-line="">iam simulate-principal-policy</code> doesn&#8217;t account for resource-based policies on the target.</strong> It evaluates the <em>principal&#8217;s</em> policies correctly, but if the target (an S3 bucket, a KMS key) has its own resource policy denying access, you need <code class="" data-line="">simulate-custom-policy</code> with both policies supplied to get the full picture.</p>
<p><strong>SCPs fail closed in a way that&#8217;s easy to misdiagnose.</strong> An SCP deny produces the same <code class="" data-line="">AccessDenied</code> error as a missing IAM permission — check the SCP layer explicitly before assuming the role&#8217;s own policy is the problem, or you&#8217;ll spend an hour widening a policy that was never the actual blocker.</p>
<p><strong><code class="" data-line="">checkov</code>/<code class="" data-line="">tfsec</code> false positives erode trust in the gate fast.</strong> Suppress specific, documented exceptions inline (<code class="" data-line="">#checkov:skip=CKV_AWS_79:reason</code>) rather than disabling the check account-wide the first time it blocks something legitimate.</p>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th style="text-align: left;">Framework</th>
<th style="text-align: left;">Control / ID</th>
<th style="text-align: left;">Architectural Mapping</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;"><strong>NIST CSF 2.0</strong></td>
<td style="text-align: left;">PR.AA-05</td>
<td style="text-align: left;">Access permissions are managed, incorporating least privilege and separation of duties.</td>
</tr>
<tr>
<td style="text-align: left;"><strong>NIST SP 800-207</strong></td>
<td style="text-align: left;">Zero Trust</td>
<td style="text-align: left;">The identity perimeter, enforced per-API-call, is the direct implementation of continuous verification.</td>
</tr>
<tr>
<td style="text-align: left;"><strong>ISO 27001:2022</strong></td>
<td style="text-align: left;">8.2</td>
<td style="text-align: left;">Privileged access rights are restricted and managed.</td>
</tr>
<tr>
<td style="text-align: left;"><strong>SOC 2</strong></td>
<td style="text-align: left;">CC6.3</td>
<td style="text-align: left;">The entity authorizes, modifies, or removes access based on roles and responsibilities.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>The identity perimeter, not the network, is what cloud-native hardening actually secures — every IAM policy evaluation is a perimeter check</li>
<li>IMDSv2 enforcement is the single highest-leverage fix available and should be an account-wide default, not an opt-in</li>
<li>Least privilege, permission boundaries, and SCPs are three layers of the same perimeter — design all three deliberately, don&#8217;t rely on one</li>
<li><code class="" data-line="">aws iam simulate-principal-policy</code> gives a definitive answer before deployment instead of an incident after</li>
<li>IaC scanning turns identity-perimeter mistakes into blocked pull requests instead of production findings</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>Module 3 hardened the identity perimeter against external and lateral threats. Module 4 asks what happens after a perimeter fails anyway — specifically, how immutable, WORM-locked data architecture makes ransomware and mass-deletion attacks survivable even when an attacker has already gotten past every control this module covers.</p>
<p><em>Next: <a href="/immutable-data-architecture-worm/">Module 4: Resilience &amp; Survival — Immutable Data Architecture and Surviving Ransomware via WORM</a></em></p>
<p>Get the full masterclass 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%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" 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%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" 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%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" 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%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" 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%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" 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%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" 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%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" 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%2Fcloud-native-hardening-aws-identity%2F&#038;title=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" data-a2a-url="https://linuxcent.com/cloud-native-hardening-aws-identity/" data-a2a-title="Cloud-Native Hardening: Securing the AWS Identity Perimeter"></a></p><p>The post <a href="https://linuxcent.com/cloud-native-hardening-aws-identity/">Cloud-Native Hardening: Securing the AWS Identity Perimeter</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/cloud-native-hardening-aws-identity/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2195</post-id>	</item>
		<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>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-27 00:15:15 by W3 Total Cache
-->