Skip to content

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, because getcert (run as strongholdd) checks it may write there first (as cert_t it 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, podman or tcpdump need.
  • No policy changes at run time: strongholdd used to turn on keepalived_connect_any itself, 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/selinux has local changes (a boolean set when it joined IdM, say) keeps its own booleans.local across image updates, and the image's booleans never reach it; the modules do. chcon became the module's file context and restorecon.
  • The host's SSH keys: public and private keys share sshd_key_t, so strongholdd doesn't read them; it asks ssh-keygen -y, in ssh_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's stronghold_stream_connect interface 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-control on 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, for CONNECT (not squid_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' (not haproxy_connect_any). strongholdd makes /etc/haproxy/certs (directories in etc_t); haproxy -c keeping its startup messages in /dev/shm is denied and not audited.
  • IPsec (libreswan, ipsec_t): Fedora's policy confines it. strongholdd writes /etc/ipsec.d (ipsec_key_file_t, so ipsec_manage_key_file) and runs ipsec (ipsec_mgmt_t, entered from unconfined_service_t and allowed the pipes strongholdd gives it). With certificates it also runs openssl pkcs12, pk12util, certutil and crlutil on libreswan's NSS database (/var/lib/ipsec/nss, ipsec_key_file_t, covered by the same rule), and restarts ipsec.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 with systemctl. 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 as container_t with NET_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-prepared reads /var/lib/PackageKit itself (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's frr-selinux): strongholdd writes /etc/frr (frr_conf_t); vtysh and frr-reload.py run 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't frr_t's to write.
  • Suricata has no domain in Fedora's policy (unconfined_service_t): strongholdd reads its alerts in /var/log/suricata and 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 from stronghold_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_t too): 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 default var_lib_t and 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.conf as 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 are etc_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 -F with 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.