testifysec / hardener · Apache-2.0

SELinux confinement, compiled from behavior. Proven under Enforcing.

Give hardener a legacy RPM, a tarball, or a vendor binary — no source required. It derives the least privilege the application actually needs, generates a proper SELinux policy, packages it as an installable RPM, and verifies the app still works with enforcement on. The alternative it replaces is setenforce 0.

$ hardener run --vm selinux-verifier --out reports targets/plex.yaml
plex: 84 denials observed → 52 rules · 3 flagged for review · relabels: 0
plex: enforcing verification — domain system_u:system_r:plex_t:s0 · workload PASS · residual denials 0
plex: static cross-check — 6/6 predicted behaviors observed · coverage gaps 0
plex: ✔ PASS → plex-selinux-1.0.0.rpm + verdict attestation
13 / 13
corpus apps verified Enforcing — incl. Plex, Emby, Splunk UF
286 → 159
denials distilled into least-privilege rules
15
privileged rules routed to human review
0
residual denials at pass
How it works

Observe, classify, enforce, prove

1analyze artifact
2synthesize .te/.fc
3observe in permissive domain
4refine: relabel vs allow vs flag
5enforce + verify
6package RPM + verdict

The refine step is what separates this from audit2allow: a denial whose path falls under the app's own file-context claims but carries a generic label is a labeling problem — fixed with restorecon, never an allow rule. Privileged capabilities, sensitive targets (shadow_t), and broad shared types (var_log_t, tmp_t) are never granted silently; they route to a review gate.

What a pass proves

The workload succeeds with the domain Enforcing — real protocol round-trips, not process liveness.
The process provably runs in the generated domain (ps -o label=) — a failed transition cannot fake a pass with a clean log.
Zero residual denials, after bounded late-path refinement.
Static least-privilege negatives hold via sesearch; accepted exceptions are named in the verdict, never hidden.

Coverage honesty: static import analysis predicts what the binary can do (bind, drop privileges, exec); anything the exercise never drove is reported as a coverage gap instead of assumed safe.

Field-tested

Six ways real vendor software breaks naive policy generation

Each of these was found on a real artifact, and each is diagnosed by name instead of surfacing as a mystery:

Symlinked entrypointnats-server

The resolved inode decides the transition. Label the symlink and the service runs unconfined while looking healthy.

Mislabeled entrypointemby

The service can never start — with zero denials in its own domain. Caught by watching denials against owned types from every source domain.

NoNewPrivileges unitsplunk

The kernel permits only bounded transitions; no policy fixes it. Diagnosed up front with the systemd remediation named.

Base-policy collisionwebmin

Redeclaring a path the distro claims kills the whole module. Detected, deferred, reported.

Interpreter ExecStartplex

ExecStart=/bin/sh -c — labeling the shell would confine the entire OS. Guarded; the transition fires on the exec'd app binary.

App bug ≠ policy bugall

Failing in a permissive domain with zero denials is not SELinux. One round, honest diagnosis, stop.

Supply chain

One pipeline, three contracts

Third party

COTS/OSS — no counterparty holds a claim.

Observation is discovery; privileged rules go to review.

Second party

Supplier deliverable with a privilege declaration.

Observed-but-undeclared behavior fails acceptance — noncompliance or compromise.

First party

Your code, with a committed privilege baseline.

Drift fails the build until reviewed (--update-baseline).

$ hardener run --vm verifier targets/party-first-caddy-v2.yaml
caddy: FAIL — privilege drift from committed baseline: port-bind unreserved_port_t
# a developer added a listener; the build fails until someone reviews it
Evidence

The verdict is an attestation

Every run emits the verdict as an in-toto statement: subjects are the policy RPM and .te/.fc by digest; the predicate carries every gate, flagged rule, conformance outcome, coverage gap, and the exact verifier baseline. Signing (ed25519 DSSE) and Archivista upload are built in and strictly optional — both off by default. For keyless signing, timestamps, and full execution provenance, wrap the run with CI/Lock; the two compose.

Onboarding

Manifests are meant to be written by AI agents

The one input that takes judgment is the workload manifest — install, start, exercise, stop. The repo ships a skill that teaches an agent to inspect your artifact, write a scenario-based exercise, and iterate against the report's coverage gaps. Point your agent at an artifact; review the manifest like you'd review a test plan.