SELinux: strongholdd in a domain of its own¶
D3: SELinux enforcing, always, and Stronghold ships its own policy
module. Until October 2026 strongholdd ran as unconfined_service_t; now
it has its own domain, stronghold_t, from the stronghold module
(stronghold/dist/selinux, the stronghold-selinux package).
What is confined how¶
| What | Domain | Notes |
|---|---|---|
| strongholdd | stronghold_t |
this module |
| The service functions (Unbound, Knot, Kea, radvd, Suricata, FRR, headscale, ddclient, lego, Squid, HAProxy) | container_t |
each in its container (functions.md) |
| keepalived, conntrackd, firewalld, NetworkManager, certmonger, SSSD, realmd | their own Fedora domains | strongholdd drives them over D-Bus and their sockets |
What strongholdd runs: nft |
iptables_t |
reads its rule files in /run/stronghold |
ip, tc, ethtool |
ifconfig_t |
|
ping, tracepath, tcpdump (the tools) |
ping_t, traceroute_t, netutils_t |
|
modprobe |
kmod_t |
|
restorecon |
setfiles_t |
|
ssh-keygen |
ssh_keygen_t |
the host's public keys, for the pair's peer |
bootc (updates) |
install_t |
with podman and skopeo under it |
journalctl |
journalctl_t |
|
PAM's unix_chkpwd |
chkpwd_t |
password logins never read /etc/shadow themselves |
Its types¶
stronghold_exec_t:/usr/bin/strongholdd.stronghold_var_lib_t:/var/lib/stronghold: the config repository, history, backups, alerts.stronghold_secret_t:/var/lib/stronghold/secrets: only strongholdd reads them.stronghold_var_run_t:/run/stronghold: the API's socket, the nft rule files.- Left alone:
/var/lib/stronghold/functions(podman labels the functions' volumes for their containers). stronghold_cert_t:/var/lib/stronghold/certmonger, where certmonger writes the certificates FreeIPA issues; strongholdd manages it too, becausegetcert(run as strongholdd) checks it may write there first (ascert_tit said "insufficient permissions", found with a real FreeIPA server).
Choices¶
- The programs it runs go to their own domains, which have the right
rules already; the module doesn't give strongholdd what
bootc,podmanortcpdumpneed. - No policy changes at run time: strongholdd used to turn on
keepalived_connect_anyitself, for the load balancer's health checks; the module now lets keepalived connect to any port (not bind any, as the boolean would). Not a boolean the package or the image sets: a box whose/etc/selinuxhas local changes (a boolean set when it joined IdM, say) keeps its ownbooleans.localacross image updates, and the image's booleans never reach it; the modules do.chconbecame the module's file context andrestorecon. - The host's SSH keys: public and private keys share
sshd_key_t, so strongholdd doesn't read them; it asksssh-keygen -y, inssh_keygen_t, for the public ones (the pair's known hosts). - The functions' files go through their agents: strongholdd reads
the containers' volumes (
container_file_t, Suricata's alerts) but writes them only through the function's agent, the blocklists' zones too; the module doesn't let it write other containers' files. - The CLI connects to
/run/stronghold/api.sock; the module'sstronghold_stream_connectinterface lets other domains do so.
How the rules were learned¶
The domain was permissive (only it: permissive stronghold_t) while every
feature ran on the lab's boxes; the denials, from the journal (the image
has no auditd: ausearch finds nothing, journalctl _TRANSPORT=audit
does), were turned into the reference policy's interfaces, and into
transitions where audit2allow proposed giving strongholdd what its child
programs did. Then enforcing, and the lab's checks again.
The functions on the host¶
A box installed from packages runs the service functions as the host's
own programs (functions.md, "Enabling a function"). strongholdd writes
their configs and runs their own tools as itself:
- Unbound (
named_t): its config in/etc/unbound/conf.d(named_conf_t), the blocklists' zones in/var/lib/unbound(named_cache_t),unbound-checkconf(which reads the trust anchors),unbound-controlon its control socket. - radvd (
radvd_t):/etc/radvd.conf(radvd_etc_t),radvd -c. - The DHCP relay (
dnsmasq_t, its own unit): its config in/etc/stronghold(etc_t, the package's directory),dnsmasq --test. - Dynamic DNS (ddclient):
ddclient.conf(ddclient_etc_t), and its cache for the status (ddclient_var_t). - The web proxy (Squid,
squid_t):squid.conf(squid_conf_t),squid -k parse; Squid may listen on unreserved ports (the transparent proxy's 3129) and reach any port, forCONNECT(notsquid_connect_any, which Fedora has on, for the same reason as keepalived's above). - The reverse proxy (HAProxy,
haproxy_t): its config and certificates in/etc/haproxy(etc_t),haproxy -c; HAProxy may listen on and connect to any port, its sites' and their servers' (nothaproxy_connect_any). strongholdd makes/etc/haproxy/certs(directories inetc_t);haproxy -ckeeping its startup messages in/dev/shmis denied and not audited. - IPsec (libreswan,
ipsec_t): Fedora's policy confines it. strongholdd writes/etc/ipsec.d(ipsec_key_file_t, soipsec_manage_key_file) and runsipsec(ipsec_mgmt_t, entered fromunconfined_service_tand allowed the pipes strongholdd gives it). With certificates it also runsopenssl pkcs12,pk12util,certutilandcrlutilon libreswan's NSS database (/var/lib/ipsec/nss,ipsec_key_file_t, covered by the same rule), and restartsipsec.service; it puts the files together in its own directory,/var/lib/stronghold, because it may not make directories in/etc/ipsec.d. Tried enforcing, no denials. - OpenVPN (
openvpn_t, Fedora's policy): strongholdd writes the servers' configs, certificates and keys in/etc/openvpn/server(openvpn_etc_t:manage_files_pattern) and reads their status files (openvpn_var_run_t); the units are started withsystemctl. Tried enforcing, no denials. - UPnP (miniupnpd): no domain in Fedora's policy, so on the host it
runs unconfined (
unconfined_service_t), and in its container ascontainer_twithNET_ADMIN; strongholdd writes its config (etc_t), its unit's drop-in, and its nftables table (nft, as for NAT). - Kerberos single sign-on (the standalone UI): strongholdd reads the
box's keytab (
krb5_keytab_t) or Cockpit's (etc_t), and krb5.conf; SSSD's plugin maps a ticket's user (sssd_stream_connect); in AD,adcli update, as strongholdd, rewrites the box's keytab (kerberos_rw_keytab,kerberos_etc_filetrans_keytab). Its replay cache is in/var/lib/stronghold/krb5(KRB5RCACHEDIR), not/var/tmp. In FreeIPA,ipa, as strongholdd, reads the client's state in/var/lib/ipa-client(realmd_var_lib_t). - Installing a function's packages, and the updates:
pkcon, as strongholdd, talks to packagekitd (rpm_t) over D-Bus (rpm_dbus_chat); packagekitd installs.offline-get-preparedreads/var/lib/PackageKititself (rpm_read_db); the reboot into an offline update asks logind (systemd_dbus_chat_logind). Whether a package is there (rpm -q, before a join installs its programs) runs rpm as strongholdd (rpm_exec_t, the database read only). - BGP (FRR,
frr_t, from Fedora'sfrr-selinux): strongholdd writes/etc/frr(frr_conf_t);vtyshandfrr-reload.pyrun as FRR (frr_domtrans), which has its own sockets, logs and run files, and answer on strongholdd's pipes. vtysh keeps the history of what it is given in FRR's run directory (VTYSH_HISTFILE, strongholdd's unit): root's home isn'tfrr_t's to write. - Suricata has no domain in Fedora's policy (
unconfined_service_t): strongholdd reads its alerts in/var/log/suricataand sets the group of that directory and its files (var_log_t); suricata-update runs as strongholdd, keeping the labels of the backup it copies from its temporary directory (a relabel fromstronghold_tmp_t); Python's bytecode for its modules isn't written (libs_dontaudit_write_lib_dirs). The unit's drop-in may need its directory made first. - headscale (
unconfined_service_ttoo): its CLI, run by strongholdd for keys and nodes, talks to the server on its socket in/run/headscale(var_run_t). - lego (ACME) runs as strongholdd; its directory,
/var/lib/lego(no package's), is strongholdd's (stronghold.fc), or the request it writes gets the directory's defaultvar_lib_tand can't be replaced. Nothing more as far as the lab went: an account at Let's Encrypt's staging CA, refused there (the lab's names aren't real). Its HTTP-01 listener on port 80 isn't tried yet. - firewalld 2.4 ships
firewalld.confas a link to one of its variants (firewalld-standard.conf): strongholdd's settings replace the link with a file (firewalld_etc_rw_t:lnk_file unlink). - Kea and Knot have no domain in Fedora's policy: they run as
unconfined_service_t, their configs areetc_t, and Knot's zones in/var/lib/knot/stronghold*are strongholdd's own type (stronghold_var_lib_t,stronghold.fc), which Knot reads unconfined. The programs that parse DHCP packets and zone transfers are the ones left unconfined on the host: a reason to run them in their containers (container_t), or for policy of their own upstream. - A path a function shares with its container: podman relabels it for
the container (
:z,container_file_t), which the host's Unbound can't read; the host's types (named_cache_t) the container can't. The resolver's blocklist directory is the only such path, and strongholdd checks its type on every apply:restorecon -Fwith the program on the host, a restart of the container (podman relabels) in it (functions.md, "How it runs here"). - Learnt on the package-mode VM (
lab.md, "Package mode"), the same way: permissive, the features, the denials into interfaces, enforcing.
Not yet¶
- lego's HTTP-01 challenge on the host (port 80), which needs a name the CA can reach.