Module examples: selinux¶
Scripts under examples/modules/selinux/ — one per fact or operation of the
selinux module. See the
Examples overview for the shared "connects for real"
warning and the error-reporting contract every script follows.
The read-only fact-gathering examples (selinux_se_boolean.py reading
SEBoolean, selinux_booleans.py reading Booleans,
selinux_file_context.py reading FileContext,
selinux_file_context_mapping.py reading FileContextMapping,
selinux_se_ports.py reading SEPorts — once unfiltered, once narrowed
with the protocol/se_type filter arguments — selinux_se_port.py
reading SEPort, selinux_mode.py reading Mode, selinux_se_login.py
reading SELogin, selinux_se_user.py reading SEUser,
selinux_modules.py reading Modules and selinux_permissive_domains.py
reading PermissiveDomains, all over gather_fact) are not rendered here
— they read a single fact, and the facts themselves are documented on the
selinux module page. Like the dnf examples
they read the shared project-root inventory.json and guard on the
"centos" group: is_centos filters the host list before the
gather_fact fan-out, and the operation scripts below carry
when=is_centos on every request. Every target is an SELinux
(RHEL-family) host, so the semanage/semodule/sestatus shells are
meant to run on all of them — point INVENTORY_PATH at your own file of
machines before executing.
Host-policy prerequisite for the mutating operations. The tools these
scripts drive (semanage, semodule, setsebool, chcon, restorecon
from policycoreutils/checkpolicy) are already installed on the demo
hosts and every request is correctly flagged sudo=True. What the
mutations additionally need is an unconfined sudo domain: they rewrite
the root-owned policy store under /var/lib/selinux and relabel root-owned
files. On the demo hosts sudo lands in the confined staff_t domain, which
cannot access that store, so semanage/semodule fail with "SELinux policy
is not managed or store cannot be accessed" and chcon/restorecon with
"Permission denied" — even though httpd (and thus /var/www/html) is
installed. This is a host-policy prerequisite, not a script gap: the host
admin must map the login's sudo role to an unconfined domain (e.g.
unconfined_t) or run the target domain permissive before these examples
can apply. The read-only fact gathers and selinux.mode (which only reads
or calls setenforce) are unaffected. selinux.module also needs its
operator-supplied compiled package at /tmp/myhttpd.pp (upload it with
files.put first — the library ships no on-host policy compiler).
Enabling a boolean persistently¶
The boolean operation: selinux.boolean sets one kernel security boolean
to on/off, idempotent on the
SEBoolean fact — the
check-before-executing the enhancement asked for falls out of the
fact-driven model, sparing the expensive setsebool -P policy rewrite on
an unchanged boolean. The script enables httpd_can_network_connect (the
switch that lets httpd open outbound connections) with persistent=True
and yields the same request twice — the second is a guaranteed no-op in
the apply phase, because the first set is visible to it through the fresh
per-step fact cache (in the prepare phase, where nothing is sent, both
plan as changes). setsebool writes kernel and policy state, so the
requests are flagged sudo=True.
selinux_boolean
¶
Enable an SELinux boolean persistently, with check-before-executing.
This is a usage example, not part of the library. It shows how an application drives reemote's two-phase execution model end to end over a real SSH transport (asyncssh):
- Read
inventory.jsonin the project root and parse it as JSON -- an inventory is just a list of dictionaries, which is exactly what JSON expresses -- then validate it up front withvalidate_inventoryso a malformed file fails with anInventoryErrornaming the offending entry, before any host is materialized or connected. - Build the deploy script as a generator yielding
OperationRequestobjects, each guarded by awhenpredicate. - Run the prepare phase to get a plan, and the apply phase to execute it.
Unlike the pytest suite -- which injects the test framework's SimulatedHost
via run's host_factory and never touches the network -- this example
uses the default factory (Host.from_inventory). Each plain Host
lazily opens an asyncssh connection from its connection data, gathers
facts and runs commands for real, and run closes those connections when
it finishes.
run is a coroutine (both phases are async), so main is async and
driven by asyncio.run.
selinux.boolean ports the baseline's operation of the same name: it sets one
kernel security boolean -- here httpd_can_network_connect, the switch
that lets httpd open outbound network connections (needed by every reverse
proxy and web app that talks to a backend) -- to on or off.
persistent=True writes the updated policy (setsebool -P) so the
value survives reboot; the default persistent=False changes only the
running policy.
The operation is idempotent on the SEBoolean fact: it reads the current
value word and yields a setsebool only when it differs -- the
"check if change is needed before executing" enhancement the boolean
proposal asked for falls out of the fact-driven model, sparing the
expensive -P policy rewrite on an unchanged boolean. The script yields
the same request twice to show that idempotence across the two phases:
the prepare phase plans both as changes when the boolean is off (nothing
is sent in prepare, so both compare against the same pre-deployment
state), while the apply phase sets it on the first and reports the second
as a no-op, because the fresh per-step fact cache now sees the value the
first step produced.
setsebool writes kernel and policy state, so both requests are flagged
sudo=True and the driver elevates their commands over SSH.
Every request carries when=is_centos, the declarative eligibility
guard: the predicate reads only static inventory data --
host.group, set by the "group" key of the inventory entry -- and
is a stable module-level function, so the prepare and apply phases reach
the same skip decision. The driver skips a rejected request without
materializing it, so on hosts outside the "centos" group neither the
commands nor the facts behind them run, and plan and log render the step
as [skipped: when guard is_centos] in its lockstep slot. The path is
resolved from __file__, so the example works from any working
directory; point INVENTORY_PATH at your own file to target your
machines.
Run it with::
uv run python examples/modules/selinux/selinux_boolean.py
Relabelling a file's context components¶
The file-context operation: selinux.file_context ports the baseline's
chcon -t scenario and extends it to the full context — each of
se_type, se_user, se_role, se_level is optional, at least one must
be given, and only the components differing from the
FileContext fact are
relabelled. The script labels httpd's index page httpd_sys_content_t
twice (the second a guaranteed no-op in apply), then asks for the whole
context on the same file — a component-wise no-op once the stock
user/role/level already match. chcon changes the runtime label only; a
relabel reverts it to the persistent database rule, which is the mapping
operation's job below. The requests are flagged sudo=True (the path is
root-owned).
selinux_op_file_context
¶
Relabel a file's SELinux context components with selinux.file_context.
This is a usage example, not part of the library. It shows how an application drives reemote's two-phase execution model end to end over a real SSH transport (asyncssh):
- Read
inventory.jsonin the project root and parse it as JSON -- an inventory is just a list of dictionaries, which is exactly what JSON expresses -- then validate it up front withvalidate_inventoryso a malformed file fails with anInventoryErrornaming the offending entry, before any host is materialized or connected. - Build the deploy script as a generator yielding
OperationRequestobjects, each guarded by awhenpredicate. - Run the prepare phase to get a plan, and the apply phase to execute it.
Unlike the pytest suite -- which injects the test framework's SimulatedHost
via run's host_factory and never touches the network -- this example
uses the default factory (Host.from_inventory). Each plain Host
lazily opens an asyncssh connection from its connection data, gathers
facts and runs commands for real, and run closes those connections when
it finishes.
run is a coroutine (both phases are async), so main is async and
driven by asyncio.run.
selinux.file_context ports the baseline's operation of the same name and
extends it to the full context: the baseline only sets the type (chcon -t),
while here each of se_type, se_user, se_role and se_level is
optional, at least one must be given, and only the components that differ
from the FileContext fact are relabelled. The first two requests are
the baseline's exact scenario -- label httpd's index page httpd_sys_content_t
so the daemon may serve it -- yielded twice to show idempotence: the prepare
phase plans both as changes when the type differs (nothing is sent in
prepare), while the apply phase relabels on the first and reports the second
as a no-op through the fresh per-step fact cache. The third request asks for
the whole context on the same file; after the first request the remaining
components already match the stock system_u:object_r:...:s0 label, so it
demonstrates the component-wise no-op ("already had the requested context").
chcon changes the runtime label of existing files only -- a relabel
(restorecon or a boot-time autorelabel) reverts it to whatever the
persistent fcontext database says. The durable rule is
selinux.file_context_mapping's job
(examples/modules/selinux/selinux_op_file_context_mapping.py). Relabelling
a root-owned system path needs root, so every request is flagged sudo=True
and the driver elevates its chcon command over SSH.
Every request carries when=is_centos, the declarative eligibility
guard: the predicate reads only static inventory data --
host.group, set by the "group" key of the inventory entry -- and
is a stable module-level function, so the prepare and apply phases reach
the same skip decision. The driver skips a rejected request without
materializing it, so on hosts outside the "centos" group neither the
commands nor the facts behind them run, and plan and log render the step
as [skipped: when guard is_centos] in its lockstep slot. The path is
resolved from __file__, so the example works from any working
directory; point INVENTORY_PATH at your own file to target your
machines.
Run it with::
uv run python examples/modules/selinux/selinux_op_file_context.py
Managing a persistent fcontext rule, then relabelling¶
The mapping operation: selinux.file_context_mapping manages one
persistent rule in the local fcontext database — present=True adds
(semanage fcontext -a) or modifies (-m) the rule for a target
expression, present=False deletes it (-d) — idempotent on the
FileContextMapping
fact. The script ensures /web(/.*)? carries httpd_sys_content_t twice
(the second a guaranteed no-op in apply), then composes the documented
remedy for the operation's own limitation: a database rule never touches
labels existing files carry, so a third request runs
selinux.restorecon over
/web after the mapping step, seeing the new rule through the fresh
per-step state — the fact-operation pairing the design recommends. The
/web tree must exist on the hosts. All requests are flagged sudo=True.
selinux_op_file_context_mapping
¶
Manage a persistent fcontext rule, then relabel the tree it covers.
This is a usage example, not part of the library. It shows how an application drives reemote's two-phase execution model end to end over a real SSH transport (asyncssh):
- Read
inventory.jsonin the project root and parse it as JSON -- an inventory is just a list of dictionaries, which is exactly what JSON expresses -- then validate it up front withvalidate_inventoryso a malformed file fails with anInventoryErrornaming the offending entry, before any host is materialized or connected. - Build the deploy script as a generator yielding
OperationRequestobjects, each guarded by awhenpredicate. - Run the prepare phase to get a plan, and the apply phase to execute it.
Unlike the pytest suite -- which injects the test framework's SimulatedHost
via run's host_factory and never touches the network -- this example
uses the default factory (Host.from_inventory). Each plain Host
lazily opens an asyncssh connection from its connection data, gathers
facts and runs commands for real, and run closes those connections when
it finishes.
run is a coroutine (both phases are async), so main is async and
driven by asyncio.run.
selinux.file_context_mapping ports the baseline's operation of the same
name: it manages one persistent rule in the local fcontext database --
here /web(/.*)? labelled httpd_sys_content_t, so a custom web root
and everything under it keeps the httpd type across relabels. With
present=True (the default) it adds the rule (semanage fcontext -a)
or changes an existing rule's type (-m); present=False deletes it
(-d). Idempotence reads the FileContextMapping fact: the first two
requests carry identical kwargs, so the prepare phase plans both as changes
when the rule is absent or mistyped, while the apply phase writes on the
first and reports the second as a no-op through the fresh per-step fact
cache.
The third request is the documented remedy for the operation's own
limitation: semanage fcontext changes the database, never the labels
existing files already carry. selinux.restorecon reapplies the database
to the tree (restorecon -R /web), and because it runs after the
mapping step in the same deploy, the fresh per-step state sees the new rule
-- the composition the fact-operation pairing table recommends. The /web
tree must exist on the hosts (create it with files.directory first);
restorecon is stateless and always reports a change, since reemote has
no fact that compares a whole tree against the database.
semanage and restorecon write root-owned policy and labels, so every
request is flagged sudo=True and the driver elevates its commands over
SSH.
Every request carries when=is_centos, the declarative eligibility
guard: the predicate reads only static inventory data --
host.group, set by the "group" key of the inventory entry -- and
is a stable module-level function, so the prepare and apply phases reach
the same skip decision. The driver skips a rejected request without
materializing it, so on hosts outside the "centos" group neither the
commands nor the facts behind them run, and plan and log render the step
as [skipped: when guard is_centos] in its lockstep slot. The path is
resolved from __file__, so the example works from any working
directory; point INVENTORY_PATH at your own file to target your
machines.
Run it with::
uv run python examples/modules/selinux/selinux_op_file_context_mapping.py
Labelling a network port with modify idempotence¶
The port operation: selinux.port labels a protocol/port pair and adds
the modify capability the baseline's add/remove-only operation lacked —
present=True adds an unlabelled port (semanage port -a) or changes a
differently-labelled one in place (-m), present=False deletes the
label (-d). Idempotence dispatches on tooling: the
SEPort fact when sepolicy
is installed, otherwise the
SEPorts table. The script
labels tcp/8080 http_port_t (httpd's alternate port) twice — the second
a guaranteed no-op in apply. semanage port writes the policy store, so
the requests are flagged sudo=True.
selinux_port
¶
Label a network port for SELinux, with add/modify/delete idempotence.
This is a usage example, not part of the library. It shows how an application drives reemote's two-phase execution model end to end over a real SSH transport (asyncssh):
- Read
inventory.jsonin the project root and parse it as JSON -- an inventory is just a list of dictionaries, which is exactly what JSON expresses -- then validate it up front withvalidate_inventoryso a malformed file fails with anInventoryErrornaming the offending entry, before any host is materialized or connected. - Build the deploy script as a generator yielding
OperationRequestobjects, each guarded by awhenpredicate. - Run the prepare phase to get a plan, and the apply phase to execute it.
Unlike the pytest suite -- which injects the test framework's SimulatedHost
via run's host_factory and never touches the network -- this example
uses the default factory (Host.from_inventory). Each plain Host
lazily opens an asyncssh connection from its connection data, gathers
facts and runs commands for real, and run closes those connections when
it finishes.
run is a coroutine (both phases are async), so main is async and
driven by asyncio.run.
selinux.port ports the baseline's operation of the same name and adds the
modify capability the port proposal asked for: it labels tcp/8080
http_port_t so httpd may bind the alternate port. With present=True
(the default) an unlabelled port is added (semanage port -a) and a port
already carrying a different type is modified in place (-m) -- the
update scenario the baseline's add/remove-only operation could not express;
present=False deletes the label (-d).
Idempotence dispatches on tooling: when sepolicy is installed the
operation compares the SEPort fact (the port's specific label), and
otherwise falls back to the SEPorts table (semanage port -ln) --
both answer "what type does this port carry now", so the check-before-
executing holds either way. The script yields the same request twice:
the prepare phase plans both as changes when the label is absent (nothing
is sent in prepare), while the apply phase labels on the first and reports
the second as a no-op through the fresh per-step fact cache.
semanage port writes the root-owned policy store, so both requests are
flagged sudo=True and the driver elevates their commands over SSH.
Every request carries when=is_centos, the declarative eligibility
guard: the predicate reads only static inventory data --
host.group, set by the "group" key of the inventory entry -- and
is a stable module-level function, so the prepare and apply phases reach
the same skip decision. The driver skips a rejected request without
materializing it, so on hosts outside the "centos" group neither the
commands nor the facts behind them run, and plan and log render the step
as [skipped: when guard is_centos] in its lockstep slot. The path is
resolved from __file__, so the example works from any working
directory; point INVENTORY_PATH at your own file to target your
machines.
Run it with::
uv run python examples/modules/selinux/selinux_port.py
Switching the runtime mode¶
The mode operation: selinux.mode is a reemote original — the baseline ships
no mode operation — setting the runtime enforcing state with
setenforce, idempotent on the
Mode fact. The script ensures
enforcing twice (both no-ops on an already-enforcing host; on a
permissive host the first switches and the second is the apply-phase
no-op). Two boundaries are deliberate: the boot-time mode in
/etc/selinux/config is the config operation's job below, and a disabled
subsystem raises ValueError — setenforce cannot enable SELinux, the
transition needs the config edit, autorelabel and a reboot. The
requests are flagged sudo=True.
selinux_op_mode
¶
Switch the SELinux runtime mode with selinux.mode.
This is a usage example, not part of the library. It shows how an application drives reemote's two-phase execution model end to end over a real SSH transport (asyncssh):
- Read
inventory.jsonin the project root and parse it as JSON -- an inventory is just a list of dictionaries, which is exactly what JSON expresses -- then validate it up front withvalidate_inventoryso a malformed file fails with anInventoryErrornaming the offending entry, before any host is materialized or connected. - Build the deploy script as a generator yielding
OperationRequestobjects, each guarded by awhenpredicate. - Run the prepare phase to get a plan, and the apply phase to execute it.
Unlike the pytest suite -- which injects the test framework's SimulatedHost
via run's host_factory and never touches the network -- this example
uses the default factory (Host.from_inventory). Each plain Host
lazily opens an asyncssh connection from its connection data, gathers
facts and runs commands for real, and run closes those connections when
it finishes.
run is a coroutine (both phases are async), so main is async and
driven by asyncio.run.
selinux.mode is a reemote original (the baseline ships no mode operation):
it sets the runtime enforcing state with setenforce -- here
enforcing, the secure default; permissive logs denials without
enforcing them, the troubleshooting half-step. The operation is idempotent
on the Mode fact: it compares the runtime mode and yields a
setenforce only when it differs. The script yields the same request
twice: on a permissive host the prepare phase plans both as changes, while
the apply phase switches on the first and reports the second as a no-op
through the fresh per-step fact cache; on an already-enforcing host both
are no-ops in both phases.
Two boundaries are deliberate. The runtime switch does not touch
/etc/selinux/config -- the boot-time mode is a file edit, managed by
the selinux.config spool-and-install operation in
examples/modules/selinux/selinux_config.py. And a disabled subsystem
cannot be enabled at runtime at all: setenforce refuses, so the
operation raises a ValueError pointing at that operation and at
selinux.autorelabel (a disabled-to-enforcing transition needs a full
relabel and a reboot). The Mode fact's config_mode field is how a
deploy tells "enforcing now but permissive after reboot" apart from a fully
enforcing host before deciding to run this operation.
setenforce writes kernel state, so both requests are flagged
sudo=True and the driver elevates their commands over SSH.
Every request carries when=is_centos, the declarative eligibility
guard: the predicate reads only static inventory data --
host.group, set by the "group" key of the inventory entry -- and
is a stable module-level function, so the prepare and apply phases reach
the same skip decision. The driver skips a rejected request without
materializing it, so on hosts outside the "centos" group neither the
commands nor the facts behind them run, and plan and log render the step
as [skipped: when guard is_centos] in its lockstep slot. The path is
resolved from __file__, so the example works from any working
directory; point INVENTORY_PATH at your own file to target your
machines.
Run it with::
uv run python examples/modules/selinux/selinux_op_mode.py
Confining a login to an SELinux user¶
The login operation: selinux.login is a reemote original managing the
Linux-user → SELinux-user mapping (semanage login -a/-m/-d), the core of
least-privilege confined-user management, idempotent on the
SELogin fact comparing both
the SELinux user and the MLS/MCS range. The script maps the login
kjarvis to staff_u at range s0 — the confined-administrator user
whose roles the SEUser fact reports — twice (the second a guaranteed
no-op in apply). The mapping takes effect at the user's next login.
semanage login writes the policy store, so the requests are flagged
sudo=True.
selinux_login
¶
Map a Linux login to a confined SELinux user with selinux.login.
This is a usage example, not part of the library. It shows how an application drives reemote's two-phase execution model end to end over a real SSH transport (asyncssh):
- Read
inventory.jsonin the project root and parse it as JSON -- an inventory is just a list of dictionaries, which is exactly what JSON expresses -- then validate it up front withvalidate_inventoryso a malformed file fails with anInventoryErrornaming the offending entry, before any host is materialized or connected. - Build the deploy script as a generator yielding
OperationRequestobjects, each guarded by awhenpredicate. - Run the prepare phase to get a plan, and the apply phase to execute it.
Unlike the pytest suite -- which injects the test framework's SimulatedHost
via run's host_factory and never touches the network -- this example
uses the default factory (Host.from_inventory). Each plain Host
lazily opens an asyncssh connection from its connection data, gathers
facts and runs commands for real, and run closes those connections when
it finishes.
run is a coroutine (both phases are async), so main is async and
driven by asyncio.run.
selinux.login is a reemote original (the baseline ships no login
operation): it manages the Linux-user -> SELinux-user mapping, the core of
least-privilege confined-user management. Here the login kjarvis is
mapped to staff_u with range s0 -- the confined-administrator user
whose roles (staff_r, sysadm_r) the SEUser fact reports -- so
the account stops landing in the unconfined domain the __default__
fallback assigns. With present=True (the default) an unmapped login is
added (semanage login -a) and an existing mapping is modified in place
(-m) when its user or range differs; present=False removes the
mapping (-d), returning the login to the fallback.
Idempotence reads the SELogin fact and compares both the SELinux user
and the MLS/MCS range (the Service column newer semanage releases print
is ignored -- the operation never manages it). The script yields the same
request twice: the prepare phase plans both as changes when the mapping is
absent or different, while the apply phase writes on the first and reports
the second as a no-op through the fresh per-step fact cache. The mapping
takes effect at the user's next login -- existing sessions keep the
context they started with.
semanage login writes the root-owned policy store, so both requests are
flagged sudo=True and the driver elevates their commands over SSH.
Every request carries when=is_centos, the declarative eligibility
guard: the predicate reads only static inventory data --
host.group, set by the "group" key of the inventory entry -- and
is a stable module-level function, so the prepare and apply phases reach
the same skip decision. The driver skips a rejected request without
materializing it, so on hosts outside the "centos" group neither the
commands nor the facts behind them run, and plan and log render the step
as [skipped: when guard is_centos] in its lockstep slot. The path is
resolved from __file__, so the example works from any working
directory; point INVENTORY_PATH at your own file to target your
machines.
Run it with::
uv run python examples/modules/selinux/selinux_login.py
Installing a custom policy module¶
The module operation: selinux.module is a reemote original managing
compiled policy modules through a four-state machine — present installs
(semodule -X <priority> -i <source>) or re-enables a disabled instance,
absent removes (-r), enabled/disabled flip an installed instance
(-e/-d) — idempotent on the
Modules fact. The parameter
is named module_name because op() reserves name for the request
label (the baseline's bool_name precedent). The script installs a custom
myhttpd package twice (the second a guaranteed no-op in apply); the
compiled .pp must already exist on the hosts at /tmp/myhttpd.pp
(upload it with files.put first). The absent/enabled/disabled
states belong in their own deploys against hosts where the module is
already installed: prepare compares every request against the
pre-deployment state, and flipping an uninstalled module raises
ValueError there. semodule rewrites the policy store, so the requests
are flagged sudo=True.
selinux_module
¶
Install a custom SELinux policy module with selinux.module.
This is a usage example, not part of the library. It shows how an application drives reemote's two-phase execution model end to end over a real SSH transport (asyncssh):
- Read
inventory.jsonin the project root and parse it as JSON -- an inventory is just a list of dictionaries, which is exactly what JSON expresses -- then validate it up front withvalidate_inventoryso a malformed file fails with anInventoryErrornaming the offending entry, before any host is materialized or connected. - Build the deploy script as a generator yielding
OperationRequestobjects, each guarded by awhenpredicate. - Run the prepare phase to get a plan, and the apply phase to execute it.
Unlike the pytest suite -- which injects the test framework's SimulatedHost
via run's host_factory and never touches the network -- this example
uses the default factory (Host.from_inventory). Each plain Host
lazily opens an asyncssh connection from its connection data, gathers
facts and runs commands for real, and run closes those connections when
it finishes.
run is a coroutine (both phases are async), so main is async and
driven by asyncio.run.
selinux.module is a reemote original (the baseline ships no module
operation): it manages compiled policy modules in the semodule store via a
four-state machine -- present installs (semodule -X <priority> -i
<source>) or re-enables a disabled instance, absent removes
(-r), enabled/disabled flip an installed instance (-e/
-d). Here a custom myhttpd package is installed at the default
priority 400. The parameter is named module_name rather than name
because op() reserves name for the request label (the baseline's
bool_name precedent).
Prerequisite: the compiled .pp package must already exist on the
host at MODULE_SOURCE -- upload it with files.put first (see
examples/modules/files/files_put.py), compiling it locally with
checkmodule -M -m and semodule_package. Idempotence reads the
Modules fact (semodule -lfull): an instance with the same name at
the same priority that is already enabled makes present a no-op. The
script yields the same install request twice: the prepare phase plans
both as changes when the module is absent, while the apply phase installs
on the first and reports the second as a no-op through the fresh per-step
fact cache.
The absent/enabled/disabled states belong in their own
deploys against hosts where the module is already installed: the prepare
phase compares every request against the pre-deployment state, and
enabling or disabling a module that is not yet installed raises a
ValueError there -- install first, then manage the state in a second
run (the two-deploy split examples/demonstrations/composition/two_deploy_split.py
explains).
semodule rewrites the root-owned policy store, so both requests are
flagged sudo=True and the driver elevates their commands over SSH.
Every request carries when=is_centos, the declarative eligibility
guard: the predicate reads only static inventory data --
host.group, set by the "group" key of the inventory entry -- and
is a stable module-level function, so the prepare and apply phases reach
the same skip decision. The driver skips a rejected request without
materializing it, so on hosts outside the "centos" group neither the
commands nor the facts behind them run, and plan and log render the step
as [skipped: when guard is_centos] in its lockstep slot. The path is
resolved from __file__, so the example works from any working
directory; point INVENTORY_PATH at your own file to target your
machines.
Run it with::
uv run python examples/modules/selinux/selinux_module.py
Carving a per-domain permissive exception¶
The permissive-domain operation: selinux.permissive_domain is a
reemote original adding or removing one domain from the permissive list
(semanage permissive -a/-d), idempotent on the
PermissiveDomains
fact — the safe alternative to global permissive mode when
troubleshooting one service: the exception is one domain wide, persistent
and auditable. The script runs a full add → no-op → remove cycle on
httpd_t, so the hosts are left exactly as they were found — and the two
phases tell the story differently: the plan, computed against the
pre-deployment snapshot, shows the removal as a no-op when the domain
started enforcing, while the apply log's fresh per-step fact cache sees
the exception the first step added and removes it. The requests are
flagged sudo=True.
selinux_permissive_domain
¶
Carve a per-domain SELinux exception with selinux.permissive_domain.
This is a usage example, not part of the library. It shows how an application drives reemote's two-phase execution model end to end over a real SSH transport (asyncssh):
- Read
inventory.jsonin the project root and parse it as JSON -- an inventory is just a list of dictionaries, which is exactly what JSON expresses -- then validate it up front withvalidate_inventoryso a malformed file fails with anInventoryErrornaming the offending entry, before any host is materialized or connected. - Build the deploy script as a generator yielding
OperationRequestobjects, each guarded by awhenpredicate. - Run the prepare phase to get a plan, and the apply phase to execute it.
Unlike the pytest suite -- which injects the test framework's SimulatedHost
via run's host_factory and never touches the network -- this example
uses the default factory (Host.from_inventory). Each plain Host
lazily opens an asyncssh connection from its connection data, gathers
facts and runs commands for real, and run closes those connections when
it finishes.
run is a coroutine (both phases are async), so main is async and
driven by asyncio.run.
selinux.permissive_domain is a reemote original (the baseline ships no
permissive-domain operation): it adds or removes one domain from the
permissive list -- here httpd_t, the web server's domain -- so its
denials are logged but not enforced while the rest of the system stays
enforcing. When troubleshooting a service this is the safe alternative to
selinux.mode(mode="permissive"), which disables enforcement
everywhere: the exception is one domain wide, persistent (it lives in
the policy store, not the runtime mode), and auditable through the
PermissiveDomains fact.
With present=True (the default) an enforcing domain is added
(semanage permissive -a); present=False removes it (-d),
returning the domain to full enforcement. Idempotence reads the
PermissiveDomains fact: the first two requests carry identical kwargs,
so the prepare phase plans both as changes when the domain is enforcing,
while the apply phase adds on the first and reports the second as a no-op
through the fresh per-step fact cache. The third request removes the
exception again, showing the bidirectional pair -- leave it out (or keep
only the first request) to keep the domain permissive while you work.
semanage permissive writes the root-owned policy store, so every
request is flagged sudo=True and the driver elevates its commands over
SSH.
Every request carries when=is_centos, the declarative eligibility
guard: the predicate reads only static inventory data --
host.group, set by the "group" key of the inventory entry -- and
is a stable module-level function, so the prepare and apply phases reach
the same skip decision. The driver skips a rejected request without
materializing it, so on hosts outside the "centos" group neither the
commands nor the facts behind them run, and plan and log render the step
as [skipped: when guard is_centos] in its lockstep slot. The path is
resolved from __file__, so the example works from any working
directory; point INVENTORY_PATH at your own file to target your
machines.
Run it with::
uv run python examples/modules/selinux/selinux_permissive_domain.py
Relabelling a tree (the stateless contract)¶
The restorecon operation: selinux.restorecon is a reemote original
reapplying the persistent fcontext database to existing files
(restorecon [-R] [-F] <path>) — the remedy for chcon labels being
runtime-only and database rules never touching existing labels. It is
deliberately stateless: no fact compares a whole tree's labels
against the database, so every request yields and reports a change, in
the plan and the log, on the first run and every re-run. The script
relabels /var/www/html twice to make that contract visible — unlike the
fact-driven operations, the second request is not a no-op; restorecon
itself is harmless to re-run (it only writes labels that differ). The
requests are flagged sudo=True.
selinux_restorecon
¶
Relabel a tree from the fcontext database with selinux.restorecon.
This is a usage example, not part of the library. It shows how an application drives reemote's two-phase execution model end to end over a real SSH transport (asyncssh):
- Read
inventory.jsonin the project root and parse it as JSON -- an inventory is just a list of dictionaries, which is exactly what JSON expresses -- then validate it up front withvalidate_inventoryso a malformed file fails with anInventoryErrornaming the offending entry, before any host is materialized or connected. - Build the deploy script as a generator yielding
OperationRequestobjects, each guarded by awhenpredicate. - Run the prepare phase to get a plan, and the apply phase to execute it.
Unlike the pytest suite -- which injects the test framework's SimulatedHost
via run's host_factory and never touches the network -- this example
uses the default factory (Host.from_inventory). Each plain Host
lazily opens an asyncssh connection from its connection data, gathers
facts and runs commands for real, and run closes those connections when
it finishes.
run is a coroutine (both phases are async), so main is async and
driven by asyncio.run.
selinux.restorecon is a reemote original (the baseline ships no restorecon
operation): it reapplies the persistent fcontext database to existing
files -- the documented remedy for selinux.file_context's limitation
that chcon labels are runtime-only and a relabel reverts them, and for
selinux.file_context_mapping's limitation that a database rule never
touches labels files already carry. Here /var/www/html is relabelled
recursively (restorecon -R, the default recurse=True);
force=True would add -F, resetting even the user-customizable
user/role/level components, not just the type.
The operation is deliberately stateless: reemote has no fact that
compares a whole tree's labels against the database (that would mean
walking every file), so every request yields its restorecon and
reports a change -- in the prepare plan and in the apply log, on the
first run and on every re-run. The script yields the request twice to
make that contract visible: unlike the fact-driven operations, the second
request is not a no-op. restorecon itself is harmless to re-run (it only
writes labels that differ), so the always-changed report is honest about
what the tool cannot know in advance. Pair it after a mapping step when
the relabel must reflect a rule the same deploy just wrote (see
examples/modules/selinux/selinux_op_file_context_mapping.py).
restorecon writes root-owned labels, so both requests are flagged
sudo=True and the driver elevates their commands over SSH.
Every request carries when=is_centos, the declarative eligibility
guard: the predicate reads only static inventory data --
host.group, set by the "group" key of the inventory entry -- and
is a stable module-level function, so the prepare and apply phases reach
the same skip decision. The driver skips a rejected request without
materializing it, so on hosts outside the "centos" group neither the
commands nor the facts behind them run, and plan and log render the step
as [skipped: when guard is_centos] in its lockstep slot. The path is
resolved from __file__, so the example works from any working
directory; point INVENTORY_PATH at your own file to target your
machines.
Run it with::
uv run python examples/modules/selinux/selinux_restorecon.py
Scheduling a full-system relabel¶
The autorelabel operation: selinux.autorelabel is a reemote original
creating the /.autorelabel marker (touch /.autorelabel) that tells the
next boot to relabel every file from the database — the heavy remedy
for a disabled-to-enabled transition (files created while SELinux was off
carry no labels) or severely corrupted contexts. Warning: the relabel
runs before services start, can take many minutes, and only happens at
the next reboot, which the operation does not trigger; for one tree,
restorecon is the surgical tool. Idempotent on the marker's existence,
the script schedules the relabel twice — the second a guaranteed no-op in
apply ("a full relabel is already pending"). Creating a file at / needs
root, so the requests are flagged sudo=True.
selinux_autorelabel
¶
Schedule a full-system SELinux relabel with selinux.autorelabel.
This is a usage example, not part of the library. It shows how an application drives reemote's two-phase execution model end to end over a real SSH transport (asyncssh):
- Read
inventory.jsonin the project root and parse it as JSON -- an inventory is just a list of dictionaries, which is exactly what JSON expresses -- then validate it up front withvalidate_inventoryso a malformed file fails with anInventoryErrornaming the offending entry, before any host is materialized or connected. - Build the deploy script as a generator yielding
OperationRequestobjects, each guarded by awhenpredicate. - Run the prepare phase to get a plan, and the apply phase to execute it.
Unlike the pytest suite -- which injects the test framework's SimulatedHost
via run's host_factory and never touches the network -- this example
uses the default factory (Host.from_inventory). Each plain Host
lazily opens an asyncssh connection from its connection data, gathers
facts and runs commands for real, and run closes those connections when
it finishes.
run is a coroutine (both phases are async), so main is async and
driven by asyncio.run.
selinux.autorelabel is a reemote original (the baseline ships no
autorelabel operation): it creates the /.autorelabel marker file
(touch /.autorelabel) that tells the next boot to relabel every file
on the system from the fcontext database. Warning -- this is a heavy,
disruptive remedy: the relabel runs before services start and can take
many minutes on a large filesystem, and it only happens at the next
reboot, which this operation does not trigger (reboot deliberately, or
with the server.reboot operation, when you are ready). Use it when
SELinux is being switched from disabled to enabled -- files created while
it was off carry no labels at all -- or when the filesystem's contexts are
severely corrupted; for one tree, selinux.restorecon is the surgical
tool instead.
The operation is idempotent on the marker's existence: when
/.autorelabel is already present a relabel is already pending and the
request is a no-op. The script yields the request twice to show that: the
prepare phase plans both as changes when the marker is absent (nothing is
sent in prepare), while the apply phase creates it on the first and
reports the second as a no-op ("a full relabel is already pending").
Creating a file at / needs root, so both requests are flagged
sudo=True and the driver elevates their commands over SSH. The
disabled-to-enabling flow this operation belongs to is completed by the
selinux.config operation in
examples/modules/selinux/selinux_config.py (set SELINUX=enforcing
for the next boot) followed by a reboot.
Every request carries when=is_centos, the declarative eligibility
guard: the predicate reads only static inventory data --
host.group, set by the "group" key of the inventory entry -- and
is a stable module-level function, so the prepare and apply phases reach
the same skip decision. The driver skips a rejected request without
materializing it, so on hosts outside the "centos" group neither the
commands nor the facts behind them run, and plan and log render the step
as [skipped: when guard is_centos] in its lockstep slot. The path is
resolved from __file__, so the example works from any working
directory; point INVENTORY_PATH at your own file to target your
machines.
Run it with::
uv run python examples/modules/selinux/selinux_autorelabel.py
Managing /etc/selinux/config at boot¶
The config operation: selinux.config is a reemote original — the baseline
ships no config operation — ensuring the boot-time settings in
/etc/selinux/config, and it supersedes the files.edit + files.move
two-run recipe this example used to demonstrate: the whole
spool-and-install dance is now inside
config, so one request in one
deploy replaces the snapshot gather, the controller-side gate and both
runs. The script ensures SELINUX=enforcing and SELINUXTYPE=targeted
(the runtime half is selinux.mode's job); each setting is optional —
None leaves that key untouched. Internally the operation reads the
file's text with the
SelinuxConfigFile
content fact, drops every existing assignment of each requested key
(commented ones included) and appends KEY=value unquoted; a file that
already holds exactly the wanted assignments is a no-op. When the content
differs, the request sends an unprivileged scp spooling the new text to
a deterministic /tmp path (transfers never elevate), then an elevated
sh -c that copies it over the file, chmods it 0644 and removes the
spool — the request's sudo=True covers the install only. The settings
take effect at the next reboot; switching a disabled subsystem to
enforcing additionally needs selinux.autorelabel before that reboot.
selinux_config
¶
Manage /etc/selinux/config with the selinux.config operation.
This is a usage example, not part of the library. It shows how an application drives reemote's two-phase execution model end to end over a real SSH transport (asyncssh):
- Read
inventory.jsonin the project root and parse it as JSON -- an inventory is just a list of dictionaries, which is exactly what JSON expresses -- then validate it up front withvalidate_inventoryso a malformed file fails with anInventoryErrornaming the offending entry, before any host is materialized or connected. - Build the deploy script as a generator yielding
OperationRequestobjects, each guarded by awhenpredicate. - Run the prepare phase to get a plan, and the apply phase to execute it.
selinux.config is a reemote original (the baseline ships no config
operation): it ensures the boot-time settings SELINUX=enforcing and
SELINUXTYPE=targeted in /etc/selinux/config. It supersedes the
files.edit + files.move two-run recipe this example used to
demonstrate -- the whole spool-and-install dance is now inside the
operation, so one request in one deploy replaces the snapshot gather, the
controller-side gate and both runs. Internally the operation reads the
file's current text with the SelinuxConfigFile content fact, drops
every existing assignment of each requested key (commented ones included)
and appends KEY=value unquoted, the stock convention; a file that
already holds exactly the wanted assignments is a no-op, so re-running
the script changes nothing. When the content differs, the request sends
two commands: an unprivileged scp spooling the new text to a
deterministic /tmp path (reemote's transfers never elevate, which is
why the root-owned file cannot be written directly), then an elevated
sh -c that copies it over /etc/selinux/config, chmods it 0644
and removes the spool. The request's sudo=True applies to the install
only. A None setting is left untouched: pass just SELINUX or just
SELINUXTYPE to manage one key alone.
The settings are read when the system boots, so the edit takes effect at
the next reboot -- the runtime half is selinux.mode's job
(setenforce, see examples/modules/selinux/selinux_op_mode.py), and
the Mode fact's config_mode field is how a deploy reads the
persistent setting back. Warning: switching a disabled subsystem to
enforcing additionally requires a full relabel -- request
selinux.autorelabel (examples/modules/selinux/selinux_autorelabel.py)
before the reboot, or the boot will relabel nothing and services will fail
on unlabelled files. Changing SELINUXTYPE requires the matching policy
package to be installed.
Every request carries when=is_centos, the declarative eligibility
guard: the predicate reads only static inventory data --
host.group, set by the "group" key of the inventory entry -- and
is a stable module-level function, so the prepare and apply phases reach
the same skip decision. The driver skips a rejected request without
materializing it, so on hosts outside the "centos" group neither the
commands nor the facts behind them run, and plan and log render the step
as [skipped: when guard is_centos] in its lockstep slot. The path is
resolved from __file__, so the example works from any working
directory; point INVENTORY_PATH at your own file of SELinux hosts
(RHEL-family) to target your machines.
Run it with::
uv run python examples/modules/selinux/selinux_config.py