blastwall is a proof of concept for using SELinux user confinement as an
exploit firewall for privileged automation.
This follows from the argument I made in
privileged-automation-security:
I do not want automation to inherit the same assumptions as an unconfined human
operator and then move faster. I want automation to start constrained. I want
movement out of that constraint to be narrow, deliberate, and visible.
The first concrete target is the Copy Fail exploit path. This project is
inspired by Anthony Green's
block-copyfail PoC. Anthony's
project uses BPF LSM to block only AF_ALG binds for authencesn. That is
the right shape when the mitigation needs kernel argument precision.
Blastwall asks a different question. If the vulnerable path should be blocked
for privileged automation identities, can I make that part of the same
SELinux, IdM, AAP, and content-delivery model I already expect RHEL operators
to understand? SELinux cannot inspect the authencesn algorithm string, so
this PoC blocks the broader surface for mapped automation sessions:
blastwall_u domains are denied alg_socket access entirely.
I do not think FreeIPA should author SELinux policy. I think FreeIPA should map named identities into SELinux users and roles that already exist on the endpoint. I also do not think AAP should discover halfway through a play that the target host was not in the right confinement state.
The model has three parts:
- Local policy on each managed RHEL host defines
blastwall_u,blastwall_r, and the confined automation domainblastwall_root_local_t. - FreeIPA/IdM maps AAP automation identities into
blastwall_uwith an HBAC-linked SELinux user map. - AAP uses
eigenstate.ipabefore running jobs to select eligible hosts and fail closed when SELinux map, HBAC, sudo, or optional host policy markers do not match expectations.
The point is not to replace kernel patches. The point is to make privileged automation land inside a narrow domain where known exploit surfaces can be blocked quickly while the real fix is still moving through the fleet.
block-copyfail is the reference point for this work because it shows the
kernel-native shape of the mitigation:
- hook the relevant LSM decision point;
- inspect the syscall argument that contains the algorithm name;
- deny only the vulnerable
authencesnAF_ALG bind; - leave unrelated AF_ALG users alone.
That precision is the strength of BPF LSM. Blastwall does not compete with it on per-argument filtering. It borrows the same practical lesson, which is that the exploit should be stopped before it reaches the vulnerable kernel path, and then moves the control boundary into the automation trust model.
flowchart LR
COPY["Copy Fail mitigation idea"] --> BPF["block-copyfail<br/>BPF LSM precision"]
COPY --> BW["Blastwall<br/>SELinux automation boundary"]
BPF --> BPFHOOK["socket_bind hook<br/>inspect authencesn"]
BPFHOOK --> BPFDENY["Deny exact algorithm"]
BW --> IDM["IdM maps identity<br/>to blastwall_u"]
IDM --> AAP["AAP preflight<br/>chooses safe hosts"]
AAP --> SELINUX["SELinux denies<br/>alg_socket for automation"]
I would build this because the logistics matter.
In a RHEL automation estate, a versioned SELinux policy RPM is often easier to stage, promote, audit, and roll back than a new set of eBPF probes. That does not make SELinux more precise. It makes the mitigation fit the machinery that already exists around RHEL operations.
If the exploit can be mitigated by denying a broad surface for automation identities, this is the kind of workflow I want:
flowchart TB
subgraph SIGNAL["1. Convert the exploit into managed policy"]
EXP["New exploit surface"] --> POLICY["Update Blastwall policy"]
end
subgraph ARTIFACT["2. Give the mitigation an artifact lifecycle"]
RPM["Build/sign policy RPM"] --> SAT["Publish via Satellite<br/>or content pipeline"]
end
subgraph ROLLOUT["3. Roll out and prove host-local enforcement"]
AAP["AAP rollout workflow"] --> VERIFY["Local verification<br/>expected denial works"]
end
subgraph INVENTORY["4. Feed verified state back into automation"]
IDM["Update IdM host marker"] --> EIGEN["eigenstate.ipa inventory"]
EIGEN --> PREFLIGHT["AAP preflight<br/>select current hosts"]
end
POLICY --> RPM
SAT --> AAP
VERIFY --> IDM
That path is operationally ordinary:
- RPM versioning gives the mitigation a concrete artifact name.
- Satellite can stage, promote, pin, report, and roll back the package.
- AAP can install the package with normal
dnfworkflows. - IdM can expose which identities and hosts are in scope.
eigenstate.ipacan make AAP aware of SELinux maps, HBAC access, sudo policy, and optional host coverage markers before a job starts.- Audit and change control can reason about
blastwall-selinux-0.2.0-1more easily than a dynamically attached probe.
The tradeoff is precision. BPF LSM can inspect kernel hook arguments and block
one exact shape, such as authencesn in socket_bind. Blastwall blocks
broader SELinux surfaces for a specific subject: privileged automation mapped
into blastwall_u.
That is often acceptable for automation. Many AAP jobs should not need
AF_ALG, BPF, perf, raw sockets, user namespace creation, or ptrace. Blocking
those surfaces for automation identities is part of the point.
The short version:
BPF LSM is the precision tool.
Blastwall is the identity, policy, and delivery model.
The important question is not only "can this exploit be blocked?" It is also:
- which automation identities are allowed to run on this host;
- whether those identities land in a confined SELinux user;
- whether sudo reaches root without escaping confinement;
- whether AAP can tell which hosts have the required mitigation before running;
- whether policy coverage can be versioned, rolled out, and audited through IdM-visible host state.
That is where the combination of
eigenstate.ipa, AAP, and IdM
matters. IdM is the source of truth for identity, host scope, HBAC, sudo,
SELinux user mapping, and
optional host policy markers. AAP consumes that truth before execution.
SELinux provides the host-local enforcement once the automation session lands.
flowchart TB
IDM["IdM / FreeIPA<br/>identity + host + policy truth"] --> EIGEN["eigenstate.ipa<br/>read policy state"]
EIGEN --> AAP["AAP preflight<br/>gate and target selection"]
AAP --> SSH["SSH to managed host"]
SSH --> PAM["SSSD + pam_selinux<br/>apply SELinux user map"]
PAM --> BW["blastwall_u confined session"]
BW --> LOCAL["Local root work<br/>inside SELinux boundary"]
BW --> BLOCK["Exploit surfaces denied<br/>by versioned policy"]
The host is not merely patched or unpatched. For a given job, it is suitable or unsuitable based on current IdM state and local policy coverage.
I would treat Blastwall as a small policy product, not a one-off rule. If another exploit surface is identified before Copy Fail is fixed everywhere, the workflow is:
- Define the new exploit surface in terms SELinux can enforce.
- Add a new policy scope to the Blastwall policy source.
- Generate and review the updated policy module/RPM.
- Bump the Blastwall policy version.
- Roll out the policy to eligible hosts.
- Update the IdM host marker with the new version and coverage claims.
- Let AAP inventory and preflight prefer only hosts with the required coverage.
That keeps emergency mitigations from becoming untracked local state. Each new rule has a version, a stated scope, and an inventory-visible rollout state.
flowchart TB
subgraph SCOPE_STAGE["1. Scope the new surface"]
EXP["New exploit surface"] --> SCOPE["Define enforceable scope<br/>class, permission, type, or transition"]
end
subgraph POLICY_STAGE["2. Productize the policy change"]
POLICY["Add Blastwall policy scope"] --> REVIEW["Review impact<br/>normal automation still works"]
REVIEW --> VERSION["Bump policy version"]
end
subgraph SHIP_STAGE["3. Ship coverage through normal RHEL operations"]
RPM["Build policy RPM/module"] --> ROLLOUT["Roll out to managed hosts"]
end
subgraph SELECT_STAGE["4. Make coverage selectable before jobs run"]
MARK["Update IdM host marker"] --> INV["AAP inventory sync"]
INV --> PREFLIGHT["Preflight selects hosts<br/>with required coverage"]
end
SCOPE --> POLICY
VERSION --> RPM
ROLLOUT --> MARK
flowchart LR
AAP["AAP job<br/>automation identity"] --> INV["eigenstate.ipa.idm<br/>IdM-backed inventory"]
INV --> HBAC["HBAC-scoped hosts<br/>blastwall-automation-ssh"]
INV --> MARKERS["Host markers<br/>current or stale policy"]
HBAC --> PREFLIGHT["Preflight gate"]
MARKERS --> PREFLIGHT
PREFLIGHT --> MAP["SELinux user map<br/>blastwall-root-local-map"]
PREFLIGHT --> SUDO["Sudo policy<br/>blastwall-root-local-sudo"]
PREFLIGHT --> RUN["Run job on selected hosts"]
RUN --> LOGIN["SSH login via SSSD + pam_selinux"]
LOGIN --> DOMAIN["blastwall_u:blastwall_r:blastwall_t<br/>alias: blastwall_root_local_t"]
DOMAIN --> DENY["CIL deny<br/>no alg_socket access"]
policy/- SELinux reference-policy module and login context template.scripts/- local install/cleanup helpers, IdM command emitters, and the asciinema PoC demo bundle.idm/- IdM object creation example for group, HBAC, sudo, and user map.inventory/-eigenstate.ipa.idminventory source for AAP.playbooks/- AAP/controller preflight, policy deployment, and host checks.tests/- safe AF_ALG/authencesn trigger used to verify denial.
flowchart TB
GROUP["IdM group<br/>blastwall-automation"] --> HBAC["HBAC rule<br/>blastwall-automation-ssh"]
HOSTGROUP["IdM hostgroup<br/>blastwall-managed-hosts"] --> HBAC
SERVICE["HBAC service<br/>sshd"] --> HBAC
HBAC --> MAP["SELinux user map<br/>blastwall-root-local-map"]
MAP --> SEUSER["blastwall_u:s0"]
GROUP --> SUDO["Sudo rule<br/>blastwall-root-local-sudo"]
HOSTGROUP --> SUDO
SUDO --> ROOT["RunAs root<br/>no unconfined role/type option"]
Managed RHEL hosts need:
- SELinux enforcing.
- SELinux userspace 3.6 or newer for CIL
denyrules. selinux-policy-develcheckpolicypolicycoreutils-python-utilsmake
AAP execution environments that run the preflight need:
eigenstate.ipapython3-ipalibpython3-ipaclientkrb5-workstation- a mounted IdM keytab and CA certificate
Install Ansible collection dependencies with:
ansible-galaxy collection install -r requirements.ymlOn each managed host:
sudo ./scripts/install-local.shIn IdM, create the demo group, HBAC rule, sudo rule, and SELinux user map:
./scripts/configure-idm-map.sh --printThe script prints the ipa commands by default. Review them, then run them
with --apply from a host with an admin ticket.
In AAP, sync inventory from:
inventory/blastwall-idm.yml
Before any mutation job, run:
playbooks/preflight.yml
Then run the managed-host verification:
playbooks/verify-managed-host.yml
sequenceDiagram
participant A as AAP
participant I as IdM / FreeIPA
participant S as SSSD + pam_selinux
participant H as Managed RHEL host
participant K as SELinux kernel policy
A->>I: Read inventory, HBAC, SELinux map, sudo policy
I-->>A: Eligible hosts + confinement policy state
A->>I: hbactest automation identity against host:sshd
I-->>A: access granted or denied
A->>H: SSH as automation identity
H->>S: Resolve SELinux user map at login
S-->>H: Launch session as blastwall_u:blastwall_r:blastwall_t
A->>H: Run sudo for local-root task
H->>K: Enforce blastwall_t permissions
K-->>H: Deny alg_socket create/bind/send/recv
eigenstate.ipa.idm can
export IdM host attributes into inventory hostvars.
This PoC uses idm_description as a simple marker carrier because it is already
available from normal host records.
The policy deployment play writes markers like:
blastwall_policy_rpm=blastwall-selinux-0.1.0-6
blastwall_policy_state=active
blastwall_policy_alg_socket=denied
When coverage expands, add a marker for each enforced surface rather than overloading a single boolean. For example:
blastwall_policy_rpm=blastwall-selinux-0.2.0-1
blastwall_policy_state=active
blastwall_policy_alg_socket=denied
blastwall_policy_userns_create=denied
blastwall_policy_perf_event_open=denied
Inventory groups then split eligible hosts into:
blastwall_policy_currentblastwall_policy_stale
These markers are supplementary. They help AAP choose the best candidates when several hosts are HBAC-eligible. They do not replace live SELinux, HBAC, sudo, and runtime verification.
flowchart TB
subgraph LOCAL["1. Host-local proof"]
DEPLOY["Policy deployment workflow"] --> RPM["Install/update<br/>blastwall-selinux RPM"]
RPM --> VERIFY["Verify local policy<br/>alg_socket denied"]
end
subgraph IDM_STATE["2. Inventory-visible claim"]
IDM["Update IdM host description marker"] --> INV["eigenstate.ipa.idm inventory sync"]
end
subgraph AAP_DECISION["3. AAP target decision"]
CURRENT{"Marker matches<br/>required policy?"}
CURRENT -->|yes| GOOD["blastwall_policy_current<br/>preferred candidates"]
CURRENT -->|no| STALE["blastwall_policy_stale<br/>report/remediate"]
end
VERIFY --> IDM
INV --> CURRENT
When two hosts are both reachable and mapped into blastwall_u, AAP should
prefer the host with the newest policy and the coverage required by the job. A
job that only needs the Copy Fail mitigation can accept alg_socket=denied; a
job responding to a newer exploit can require both the older and newer markers.
flowchart TD
JOB["AAP job declares required coverage"] --> REQ["Required markers<br/>policy version + scopes"]
REQ --> H1{"Host A markers<br/>0.1.0 alg_socket"}
REQ --> H2{"Host B markers<br/>0.2.0 alg_socket + userns"}
H1 -->|missing new scope| STALE["Eligible but stale<br/>skip or remediate"]
H2 -->|all required scopes| SELECT["Preferred candidate"]
STALE --> REMEDIATE["Run policy update workflow"]
REMEDIATE --> MARKER["Refresh IdM marker"]
MARKER --> SELECT
The markers are not proof by themselves. The rollout workflow should update the marker only after local verification confirms the policy is installed and the expected denial is observable.
The reference-policy user template creates the concrete domain blastwall_t.
The policy also defines blastwall_root_local_t as a type alias so the public
PoC context names describe the root-local automation intent. Some SELinux
tools canonicalize aliases back to blastwall_t; the verification play accepts
both names as long as the SELinux user and role remain blastwall_u and
blastwall_r.
SELinux can block alg_socket use for a confined automation domain. It cannot
match authencesn inside struct sockaddr_alg. If the requirement is
algorithm-level matching while preserving other AF_ALG use, keep using an
eBPF LSM mitigation like Anthony Green's block-copyfail.
flowchart TD
EXPLOIT["Copy Fail path"] --> SOCK["AF_ALG socket"]
SOCK --> BIND["bind(authencesn...)"]
BIND --> VULN["vulnerable kernel path"]
BPF["block-copyfail<br/>BPF LSM approach"] --> PRECISE["Inspect sockaddr_alg<br/>deny only authencesn"]
SELINUX["Blastwall<br/>SELinux approach"] --> BROAD["Deny alg_socket class<br/>for automation domain"]
PRECISE --> SOCK
BROAD --> SOCK
This PoC uses a CIL deny module because current distribution policy may grant
socket permissions through inherited user-domain attributes. The deny rule
subtracts alg_socket access from blastwall_t; the paired neverallow keeps
future policy changes from silently restoring the permission.
I would use block-copyfail-style BPF LSM when I need the narrowest runtime
mitigation and I want to preserve unrelated AF_ALG use. It is shaped around
the vulnerable kernel argument, which is exactly why it is useful.
I would use Blastwall when the mitigation should be tied to automation identity, host scope, and the way RHEL estates are actually operated. The advantage is not finer kernel inspection. The advantage is that the same control plane answers:
- who may automate this host;
- what SELinux user they receive;
- what sudo policy they inherit;
- what exploit surfaces the host policy claims to cover;
- whether AAP should run, skip, or remediate the host.
The two approaches compose well. BPF LSM gives me precision at the kernel hook. Blastwall gives me an identity-scoped automation boundary and a rollout record that AAP can use before it touches the host.