OS Hardening as Code, Episode 6
Cloud AMI Security Risks · Linux Hardening as Code · Multi-Cloud OS Hardening · Automated OpenSCAP Compliance · CI/CD Compliance Gate · BakeX Platform**
Note: this series was written when the project was called Stratum. It was renamed to
BakeX at v0.6.0 — same project, same Apache 2.0 license, same team. The old
github.com/rrskris/StratumURL redirects here, andpip install stratumossis retired in
favour ofpip install bakex. Current home:
github.com/invicton/bakex.
TL;DR
- BakeX is open-source under Apache 2.0 — the engine, blueprint format, scanner, and Pipeline API are all in the repository
- Self-hostable end to end: nothing is locked to a hosted service, and there is no paid tier gating the pipeline
- Two real extension points: provider plugins (drop-in
.pyor abakex.providersentry point) and blueprints (pure YAML, no code) - Architecture: Blueprint YAML → Engine → Provider Layer → Ansible-Lockdown → OpenSCAP → Golden Image → Pipeline API
- The series taught the user-facing interface for five episodes; EP06 covers what’s underneath and how to build on it
- Installation is
git clone+docker compose up, orpip install bakexfor the CLI and web app
The Series Arc, Inverted
EP01 showed that default cloud AMIs arrive pre-broken. By the time you reach EP06, that problem has a complete solution:
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)
For five episodes, you’ve used BakeX as a user. This episode covers what it looks like to run it yourself, extend it, and build on it.
I’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.
The tools exist. ansible-lockdown 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’t the tooling — it’s the integration layer that ties them together into a reproducible, auditable pipeline.
BakeX is that integration layer, open-sourced.
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’s no step to skip.
The Architecture
┌─────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────┘
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.
Installation
Three ways in, depending on how much you want installed on the host.
Docker Compose — recommended, everything preinstalled:
git clone https://github.com/invicton/bakex.git
cd bakex
docker compose up
Open http://localhost:8001. Log in with any username and the admin token as the password —
it’s generated on first start and written to data/.admin_token. Set BAKEX_ADMIN_TOKEN and
BAKEX_SECRET_KEY in docker-compose.yml if you want logins that survive a rebuild.
Compose mounts ~/.aws, ~/.config/gcloud, and ~/.ssh read-only, plus persistent ./data,
./profiles, and ./plugins/providers. That last mount is the one to notice — it’s the
drop-in directory for provider plugins, which matters in the next section.
Published image:
docker run -p 8000:8000 rrskris/bakex:latest
PyPI — CLI and web app:
pip install "bakex[all-providers]" # or pick extras: aws, gcp, azure, linode, digitalocean, proxmox
bakex serve --port 8000
One caveat worth stating plainly rather than letting you discover it: the extras install each
provider’s cloud SDK, and Ansible and OpenSCAP must be present on the host for real builds. If you
want the batteries-included path, use Compose. bakex validate works anywhere with no host
dependencies at all.
There is no Helm chart. BakeX is a build tool that talks to cloud APIs, not a cluster workload —
it does not need to live in Kubernetes to harden images for it.
The Three Extension Points
1. Blueprints — the extension point with no code in it
The highest-leverage way to extend BakeX isn’t Python. It’s a YAML file.
A blueprint is a complete, self-contained description of a hardened OS on a specific provider,
and the library ships 18 of them. Adding the nineteenth — say Ubuntu 24.04, or CIS Level 2 for a
distro that only has Level 1 today — requires no engine changes, because the benchmark, profile,
and datastream are just strings handed to oscap.
The full format is published as a JSON Schema (Draft 2020-12) at
docs/schema/hardening-blueprint.schema.json. Point your editor at it for autocomplete and
inline validation, or hand it to an LLM and let it draft the blueprint — the schema was published
partly so that agents could write these correctly without reading the source.
The loop is short enough to run in a coffee break:
$EDITOR blueprints/ubuntu/24.04/cis-l1-aws.yaml
bakex validate blueprints/ubuntu/24.04/cis-l1-aws.yaml
Validation is offline and checks more than syntax — it rejects OS/provider combinations the
catalog doesn’t support, so you find out that a distro isn’t available on your target cloud in
milliseconds rather than fifteen minutes into a paid build.
2. Provider Plugins
Adding a cloud means implementing four methods. That’s the whole interface
(bakex/plugins/base_provider.py):
# plugins/providers/my_cloud.py
from bakex.plugins.base_provider import BaseProvider, ProviderResult
from bakex.core.models import ComplianceProfile
class MyCloudProvider(BaseProvider):
name = "my-cloud" # matches target.provider in a blueprint
def provision(self, profile: ComplianceProfile, **kwargs) -> str:
"""Launch a build instance; return its instance ID."""
...
def run_ansible(self, instance_id: str, profile: ComplianceProfile) -> None:
"""Apply the Ansible-Lockdown hardening roles."""
...
def snapshot(self, instance_id: str, profile: ComplianceProfile) -> ProviderResult:
"""Capture the golden image; return the artifact ID."""
...
def teardown(self, instance_id: str) -> None:
"""Destroy the ephemeral build instance."""
...
There is no registration command. The loader (bakex/plugins/loader.py) is hybrid and finds
plugins two ways:
- Drop-in — put the
.pyfile inplugins/providers/. That directory is a Compose volume
mount, so a plugin dropped there is live in the container without rebuilding an image. - Entry point — ship a pip-installable package declaring a
bakex.providersentry point.
This is how a third party distributes a provider without touching the BakeX repo.
Entry points load first and drop-ins load second, so a local file deliberately shadows an
installed package of the same name — which is exactly what you want when debugging someone
else’s provider.
The plugin becomes usable by writing provider: my-cloud in a blueprint’s target block. There
is no --provider flag to pass, because there is no --provider flag anywhere.
One honest note on the validation interaction from EP02: the compatibility check only objects
when both the OS and the provider are in the catalog. An unknown provider is assumed to be a
valid third-party plugin rather than an error — existence is the plugin registry’s call at build
time, compatibility is validation’s. That’s what makes shipping a provider out-of-tree possible
at all.
3. Pipeline Integrations
Beyond the curl-based gate from EP05, BakeX has a webhook system. Webhooks are registered through
the API rather than a config file, so they can be managed by the same automation that manages
everything else:
curl -X POST http://localhost:8001/api/webhooks \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.slack.com/services/…",
"events": ["build.failed", "scan.grade_change"],
"label": "platform-security alerts"
}'
Five events fire: build.complete, build.failed, scan.complete, scan.failed, and
scan.grade_change. Registering an unknown event name is a 422 rather than a silent no-op —
a small thing that saves an afternoon.
scan.grade_change is the one to wire up first. A build that fails is loud on its own; a base
image that quietly slid from A to B between two scans is the signal nobody notices.
Deliveries are signed. The registration response returns a secret once, and every request
carries an X-BakeX-Signature: sha256=… HMAC so the receiver can verify the payload came from
your BakeX instance and not from anyone who guessed the endpoint URL.
There’s a defensive detail here that’s worth calling out, because it’s the kind of thing that
usually ships broken: webhook target URLs are checked against loopback, private, link-local, and
reserved ranges — including 169.254.169.254 — and they’re re-resolved at send time, not just
at registration. A user-configurable URL that the server will fetch is a textbook SSRF into the
cloud metadata endpoint, and on a tool that holds cloud credentials that would be a very bad day.
The Open-Core Model
BakeX sits alongside the tools that became infrastructure standards by being genuinely usable
before they were commercial:
| Tool | Model |
|---|---|
| Terraform / OpenTofu | Core OSS, enterprise features in paid tier |
| Cilium / Isovalent | Core OSS, enterprise support/features in paid tier |
| Vault / HCP Vault | Core OSS, hosted/enterprise in paid tier |
| BakeX | Engine + blueprint + scanner + Pipeline API: Apache 2.0, no paid tier today |
Everything taught in this series — the blueprint format, the build pipeline, the compliance
grading, the CI/CD gate — is in the repository. There is no feature held back, because there is
currently nothing to hold it back for. Self-host it, extend it, fork it.
Worth being straight about where the project actually is: BakeX is young. It has signed releases,
SBOMs and provenance attestations, an OpenSSF Scorecard, a published JSON Schema, and over a
thousand tests — the engineering is in good order. What it does not yet have is users. If you’re
reading this and the shape of the tool fits your problem, you would be early, and early is when
your opinion changes the design.
The repository is at: github.com/invicton/bakex
What This Series Taught
EP01 — EP06 in one view:
| Episode | What you learned | What BakeX does |
|---|---|---|
| EP01 | Default AMIs are insecure by design | Replaces the default AMI with a hardened golden image |
| EP02 | Blueprint as code — the 2am skip disappears | HardeningBlueprint YAML, bakex validate / bakex build |
| EP03 | One posture, six providers, no drift | 18 shipped blueprints; only target differs across providers |
| EP04 | Automated OpenSCAP — grade at build time | A–F from the XCCDF score, SARIF 2.1.0 export, baseline compare |
| EP05 | CI/CD gate — the unhardened image never deploys | Pipeline API: POST /api/pipeline/scan, parse .passed |
| EP06 | The platform — OSS, self-hostable, extendable | Apache 2.0, Compose install, blueprints + provider plugins |
What’s Next
This series closes the OS hardening gap. The same principle — declare desired state, build
reproducibly, verify automatically — applies to every layer of your infrastructure.
Write the next blueprint
The most useful thing you can do with what this series taught is add a blueprint, and it is
genuinely pure YAML — no Python, no engine changes, no build system to learn.
You’ve spent five episodes on Ubuntu 22.04 CIS Level 1. The natural next one is Level 2 for the
same OS: #1 — Ubuntu 22.04 CIS Level 2. The
issue carries the acceptance criteria and the exact verify command, and the review loop is
bakex validate returning 0.
If a different distro is closer to what you actually run, the whole set is filed and labelled:
good first issues, blueprint label.
RHEL 9, AlmaLinux 9, Rocky 9, Debian 12, and Amazon Linux 2023 all have gaps. Each one is one
file, and each is the sort of contribution that takes an evening.
GitHub: github.com/invicton/bakex
Elsewhere on the blog
If you’ve been following the eBPF: From Kernel to Cloud series,
EP10 covers what happens when you combine kernel-level observability with the hardened base BakeX
produces: every connection, every process spawn, every file access — visible from the host kernel,
on an OS baseline you can verify.
The next series is the Purple Team Playbook — real attack paths against cloud and Kubernetes
infrastructure, how they’re detected, and how they’re closed.
Get new episodes in your inbox → linuxcent.com/subscribe