Skip to main content
Version: 1.0.0

bitenforcer

bitenforcer reads a declarative hardening policy and answers one question completely:

"If I applied this to this machine, exactly what would change — and what could it do to my access?"

Release0.1.0-ga, commit 4c6ce93, built with go1.25.12
PlatformsLinux amd64/arm64 only — macOS and Windows are not published, and why
Supply chainReproducible build, CycloneDX SBOM, cosign signature, SBOM attestation, signed SHA256SUMS
ShapeOne-shot command-line tool. No daemon, no telemetry, no heartbeat
v1 does not change hosts

bitenforcer v1 ships validate and apply --dry-run. It plans every change, validates the content it would write, runs its lockout guards — and then stops. It does not write to the host, and no flag makes it.

This is a deliberate scope decision, not a bug, an outage, or a setting you are missing. Why is worth ten minutes of your time before you plan a rollout.

The guards themselves, and the full account of why enforcement is out of scope, are on Guards, and why v1 does not enforce.

What it does​

Point it at a YAML policy:

bitenforcer validate --config policy.yaml # is the policy well-formed?
bitenforcer apply --config policy.yaml --dry-run # what would it do to THIS host?

The pre-flight names every file it would write and the bytes it would write, every systemd unit it would enable, disable, mask or drop a hardening fragment into, every firewall rule, every sysctl, every account and permission — in the order they would be applied.

Where a real validator exists, the candidate content is run through it:

ArtefactValidator
sshd_configsshd -t -f <candidate>
sudoersvisudo -c
nftables rulesetnft -c -f <candidate>
iptables rulesetiptables-restore --test

So "this policy renders an sshd_config that sshd will reject" is caught before anyone is on the hook for it — not at 03:00 when the daemon fails to restart.

Alongside that, the lockout guards report what your policy would do to your own access, measured against the session you are actually running in.

What it will not do is guess. A module whose prerequisites are missing errors rather than assuming. A session that cannot be measured is reported as unmeasured, never as "nobody is at risk". A validator that cannot run says NOT VALIDATED per file, with the reason, rather than implying the content was checked.

Real pre-flight output​

This is an actual run of the shipped 0.1.0-ga binary against the standard template, on an ordinary Ubuntu host, as a non-root user. Only the operator's IP address has been replaced with a documentation address, and one very long nft error block trimmed.

$ bitenforcer generate --template standard --output-file policy.yaml
Example configuration written to policy.yaml

$ bitenforcer validate --config policy.yaml
Configuration 'standard-hardening' is valid.
Environment : staging
Compliance : [CIS-L1]
A valid policy is not a safe one: run `bitenforcer apply --dry-run` to see what it
would change and which guards fire.
(FR) Politique valide. Une politique valide n'est pas pour autant sûre : utilisez
`apply --dry-run`.

$ bitenforcer apply --config policy.yaml --dry-run

=== PRE-FLIGHT: every change this run would make ===
operator "ubuntu" is on an SSH session whose peer socket was NOT measured
(SSH_CONNECTION/SSH_CLIENT in this process's environment (an inherited, settable
environment variable -- a hint, NOT a measured socket)); any address in the
environment is a hint, not evidence

NOT EXAMINED on this host (the rest of the pre-flight below is unaffected):
selinux: SELinux prerequisites check failed: SELinux tools not found
(getenforce not in PATH)
These modules were SKIPPED, not found compliant — this run says nothing
about whether the host matches that part of the policy.
(FR) Modules NON EXAMINÉS sur cet hôte : ignorés, et non déclarés conformes.
1. [command] users /etc/passwd
chmod 0644 /etc/passwd
2. [command] users /etc/passwd
chown root:root /etc/passwd
3. [command] users /etc/shadow
chmod 0640 /etc/shadow
4. [command] users /etc/shadow
chown root:root /etc/shadow
5. [command] users /etc/group
chmod 0644 /etc/group
6. [command] users /etc/group
chown root:root /etc/group
7. [command] users /etc/gshadow
chmod 0640 /etc/gshadow
8. [command] users /etc/gshadow
chown root:root /etc/gshadow
9. [file-write] ssh /etc/ssh/sshd_config
1194 bytes, mode 0600, validated with sshd -t -f <candidate>; NOT VALIDATED in
this dry run (the validator could not be run in this context: sshd -t needs root
to read the host keys (sshd: no hostkeys available -- exiting.)) — re-run the
dry run as root to validate
10. [file-write] firewall /etc/nftables.conf
566 bytes, mode 0600, validated with nft -c -f <candidate>; NOT VALIDATED in
this dry run (nft -c needs root […]) — re-run the dry run as root to validate
11. [file-write] kernel /etc/sysctl.d/99-bitenforcer.conf
672 bytes, mode 0644, validated with sysctl key syntax check
12. [command] kernel /etc/sysctl.d/99-bitenforcer.conf
sysctl -p /etc/sysctl.d/99-bitenforcer.conf

=== PRE-FLIGHT ADVISORIES (the guards; they did NOT block this run) ===
These are the lockout guards. They are advisory: they report the shapes they
recognise, and seven independent reviewers each found one they did not — which is
why this release does not apply anything. Treat a finding as a reason to change the
policy, never as the full list of ways it could strand you. Re-run with
--guards=blocking to have findings exit non-zero, e.g. to gate a policy in CI.
1. [kernel-remote-access-loss] this run would set net.ipv4.conf.all.rp_filter=1,
which drops any packet whose reply route does not leave by the interface it
arrived on
evidence: bitenforcer measures the interface carrying your session (eth0)
but not this host's routing table, so it cannot show that the route back
to 198.51.100.24 leaves by that same interface. On a host with two
uplinks, a VPN, or asymmetric routing, strict reverse-path filtering
drops your session and every future connection from that address
remedy: run `ip route get 198.51.100.24` and confirm it leaves by eth0;
if it does, say so for this one parameter, and keep a second session
open while you apply
override: re-run with --acknowledge kernel-remote-access-loss (this guard only)
2. [kernel-remote-access-loss] this run would set net.ipv4.ip_forward=0, and
bitenforcer cannot establish whether this host routes the network your session
arrives through
…

DRY RUN: nothing above was applied and this host was not changed.
This is the only mode bitenforcer v1 runs in; enforcement is not part of this release.
(FR) SIMULATION : rien n'a été appliqué et cet hôte n'a pas été modifié.

Three things in that output are the whole product:

  1. NOT EXAMINED is an answer, not an omission. SELinux tooling is absent on stock Ubuntu, so the selinux module is reported as skipped, explicitly not found compliant, and the rest of the pre-flight carries on. A run that says nothing about a control is very different from a run that says the control is fine.
  2. NOT VALIDATED names why, per file. sshd -t needs root to read the host keys, so as a normal user the tool tells you the content was not checked and what to do about it — instead of quietly implying it was.
  3. The guard finding carries evidence, a remedy, and a per-guard override. It does not say "risky". It says which parameter, what it measured, what it could not measure, and the exact command you should run to settle the question.

What this is worth to you today​

It is easy to read "does not apply changes" as "does not do anything". Here is what a pre-flight-only tool actually buys:

  • A change-window artefact. Attach the pre-flight output to the change request. Every file, unit, rule and sysctl, in order, with the bytes.
  • A CI gate. --guards=blocking fails the pipeline when a baseline would sever access, before it reaches a host.
  • Catching broken policy content early. sshd -t and nft -c run against the candidate content, so a policy that renders an unparseable config is caught in review.
  • Drift you can act on, safely. Run the dry run on a fleet and read what each host reports it would need — with no possibility of changing anything by accident.
  • An honest coverage map. NOT EXAMINED tells you where the tool has nothing to say, so you do not mistake silence for compliance.

Policy as code​

bitenforcer generate --template standard --output-file policy.yaml

Templates: minimal, standard, strict. They are fixed starting points to edit — they do not read the host or derive a posture from the machine you ran them on. Generated policies round-trip: feed the output back into validate and it passes.

validate goes further than schema checking: a policy that sets a key nothing implements is refused at load, by name, with a statement of what does not happen. A setting that is parsed but inert is the defect these products spent a month removing from themselves.

A valid policy is not a safe policy

validate reads the policy file and nothing else — it does not look at this host. Run bitenforcer apply --config <file> --dry-run to find out what it would change here.

Configuration​

bitenforcer is configured as policy as code rather than through a settings file of toggles — the policy document is the configuration. See Policy as code for the format and what a policy may express.

Note that enforcement itself is not enabled in v1: bitenforcer reports what it would change without changing it. Why enforcement is not in v1 explains the reasoning, and The lockout guards covers the protections that exist before enforcement is ever switched on.

Platforms​

0.1.0-ga publishes two signed binaries, and the gap is deliberate:

PlatformPublishedWhy
Linux amd64Yes
Linux arm64Yes
macOS amd64/arm64NoWithheld — see below
Windows amd64NoWithheld — see below

bitenforcer is Linux-only because its entire subject matter is Linux. Every module it plans against is a Linux mechanism: iptables/nftables rulesets, sysctl keys under /proc/sys, systemd units, SELinux and AppArmor, and /etc/passwd, /etc/shadow, /etc/gshadow and sudoers with POSIX modes and ownership. So are the validators it runs the candidate content through — sshd -t, visudo -c, nft -c, iptables-restore --test.

A macOS or Windows build would compile. That is exactly the problem: it would start, accept a policy, and produce a pre-flight in which every module is NOT EXAMINED — a report that looks like a successful run and says nothing about the host. Given that this tool exists to tell you what a change would do before you make it, a binary whose honest output is "I have nothing to say about this machine" is worse than no binary. It is not shipped, so nobody can mistake it for coverage.

If you need hardening posture from a macOS host, that is bitcollector's posture collector, which ships on macOS and reports per control whether it was observed, not observed, or not supported by the agent there.

Next steps​

Was this page helpful?