{"id":167945,"date":"2026-05-23T18:30:53","date_gmt":"2026-05-23T15:30:53","guid":{"rendered":"https:\/\/computingforgeeks.com\/?p=167945"},"modified":"2026-05-24T00:34:44","modified_gmt":"2026-05-23T21:34:44","slug":"security-hardening-fedora","status":"publish","type":"post","link":"https:\/\/computingforgeeks.com\/security-hardening-fedora\/","title":{"rendered":"Harden Fedora 44 \/ 43 \/ 42 for Desktop and Server: Complete Guide"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">A fresh Fedora 44 install ships in better security posture than most Linux distributions. SELinux is enforcing, firewalld blocks anything that no service exposes, the kernel ships with stack canaries and KASLR, the package keys chain to Fedora&#8217;s PGP trust, and the new RPM 6.0 transaction format ships in F44 with stronger digest verification. None of that is the same as &#8220;hardened&#8221;. A vanilla F44 cloud image scored <strong>74.66 out of 100 on the CIS Level 1 Server benchmark<\/strong> when we ran <code>oscap xccdf eval<\/code> against it for this guide. 176 rules passed; 120 failed. The same box passes more than 200 of those rules once the changes in this guide land. That gap is the difference between &#8220;Fedora defaults&#8221; and &#8220;production-ready Fedora&#8221;, and it is what this guide closes for both desktop workstations and servers.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The previous version of this article touched the surface (sysctl, SSH, faillock, AIDE, mask a few services) and stopped. That left out the things that matter most in 2026: compliance-grade measurement with OpenSCAP and the CIS Fedora benchmarks, account framework hardening via <code>authselect<\/code>, full PAM password-quality policy, sudo I\/O logging, kernel command-line lockdown, kernel module signing enforcement, USB device authorization with USBGuard, NTS-secured time, DNS-over-TLS, browser sandboxing via Flatpak, NetworkManager MAC randomization for desktops, persistent and forwarded journald logging, comprehensive audit rules, and a periodic audit cadence. This rewrite walks every one of those, captures real before-and-after output on two Fedora 44 lab boxes (one server profile, one desktop profile), and flags the gotchas that turn a hardening session into a lockout. Same commands work on Fedora 43 and Fedora 42 because the underlying tooling (selinux-policy, scap-security-guide, authselect, audit, USBGuard, firewalld) is identical across the three releases.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Pick a posture: workstation, server, or compliance baseline<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Hardening choices depend on what the box will do. Picking a posture up front prevents the &#8220;I disabled the wrong thing&#8221; cycle later. The three F44-relevant postures and what each rules out:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Posture<\/th><th>Use it for<\/th><th>What it rules out<\/th><\/tr><\/thead><tbody><tr><td><strong>Workstation<\/strong><\/td><td>Daily-driver laptop, dev box, single-user desktop<\/td><td>Aggressive service masking (kills GNOME\/KDE features), <code>net.ipv4.icmp_echo_ignore_all=1<\/code> (breaks captive portal detection), <code>requiretty<\/code> in sudo (breaks GUI askpass)<\/td><\/tr><tr><td><strong>Server<\/strong><\/td><td>Headless host, container engine, reverse proxy, database<\/td><td>USBGuard (no USB devices to manage), NetworkManager MAC randomization, browser sandbox tweaks<\/td><\/tr><tr><td><strong>Compliance baseline (CIS L1 \/ PCI-DSS \/ STIG)<\/strong><\/td><td>Anything that has to pass an external audit<\/td><td>Manual sysctl tweaks (the SCAP remediation script handles them); your own opinions about defaults (the profile wins)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Most readers want a mix: workstation defaults plus a few server-grade controls (faillock, AIDE, audit rules) for paranoia, or server defaults plus a compliance scan to confirm. The sections below are written so you can pick which to apply. The exception is the measurement section, which everyone should run first.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Measure first: OpenSCAP, Lynis, and systemd-analyze<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Hardening without measurement is theatre. Three tools cover the measurement angles you need: OpenSCAP for compliance-framework scoring (CIS, PCI-DSS, OSPP, ANSSI), Lynis for general security-posture suggestions, and <code>systemd-analyze security<\/code> for per-service attack-surface scoring. Install all three plus the supporting packages:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>sudo dnf5 install -y lynis aide audit fail2ban \\\n                     openscap-scanner scap-security-guide \\\n                     policycoreutils-python-utils setroubleshoot-server \\\n                     usbguard<\/code><\/pre>\n\n\n<p>The <code>scap-security-guide<\/code> package is what makes the rest work: it ships compiled XCCDF datastreams for every supported OS plus the SCAP profiles you can scan against. Inspect what profiles are available for Fedora:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo oscap info \/usr\/share\/xml\/scap\/ssg\/content\/ssg-fedora-ds.xml | head -25<\/code><\/pre>\n\n\n<p>On Fedora 44 with scap-security-guide 0.1.80 you get eight named profiles:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"920\" height=\"800\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-scap-profiles-fedora-44.png\" alt=\"oscap info showing 8 compliance profiles for Fedora 44 including CIS, PCI-DSS, OSPP\" class=\"wp-image-167967\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-scap-profiles-fedora-44.png 920w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-scap-profiles-fedora-44-300x261.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-scap-profiles-fedora-44-768x668.png 768w\" sizes=\"auto, (max-width: 920px) 100vw, 920px\" \/><\/figure>\n\n\n<p>For a server, start with <code>cis_server_l1<\/code>; for a workstation, <code>cusp_fedora<\/code> or <code>cis_workstation_l1<\/code>; for payment-processing or audit-ready environments, <code>pci-dss<\/code>. Run a baseline scan and write the HTML report and machine-readable XML to disk:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo oscap xccdf eval \\\n  --profile xccdf_org.ssgproject.content_profile_cis_server_l1 \\\n  --report \/tmp\/oscap-baseline.html \\\n  --results \/tmp\/oscap-baseline.xml \\\n  \/usr\/share\/xml\/scap\/ssg\/content\/ssg-fedora-ds.xml<\/code><\/pre>\n\n\n<p>The scan takes 60-120 seconds on a 2-vCPU box. While it runs it streams each rule to stdout; afterwards you have a colour-coded HTML report you can open in a browser and an XML you can re-process. Get the score and pass\/fail counts directly from the XML:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo grep -oE \"<score [^>]*>[^<]+<\/score>\" \/tmp\/oscap-baseline.xml\nsudo grep -oE \"<result>(pass|fail|notapplicable|notselected)<\/result>\" \\\n  \/tmp\/oscap-baseline.xml | sort | uniq -c<\/code><\/pre>\n\n\n<p>On a vanilla F44 cloud image the score is around <strong>74.66<\/strong>, with 176 passes, 120 fails, 27 N\/A, and a much larger pool of rules outside this profile:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"920\" height=\"800\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-oscap-baseline-fedora-44.png\" alt=\"OpenSCAP CIS L1 Server baseline score 74.66 with 176 pass and 120 fail on Fedora 44\" class=\"wp-image-167968\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-oscap-baseline-fedora-44.png 920w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-oscap-baseline-fedora-44-300x261.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-oscap-baseline-fedora-44-768x668.png 768w\" sizes=\"auto, (max-width: 920px) 100vw, 920px\" \/><\/figure>\n\n\n<p>The HTML report at <code>\/tmp\/oscap-baseline.html<\/code> is the authoritative reference for which rules failed and what the remediation is. Each failure links to the exact remediation snippet (bash, Ansible, or Kubernetes manifest) the SCAP project ships for it. Lynis gives a complementary general-purpose audit:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo lynis audit system | grep -E \"Hardening index|Warnings|Suggestions\"<\/code><\/pre>\n\n\n<p>Lynis baseline on the same image: hardening index 68, 3 warnings, 34 suggestions. For service-level scoring:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>systemd-analyze security --no-pager | head -15<\/code><\/pre>\n\n\n<p>Each service shows an exposure score from 0 (sandboxed) to 10 (running as root unconfined). Most stock daemons sit above 7. The combined picture (CIS score + Lynis index + systemd-analyze) is your benchmark; every change in this guide should move at least one of those numbers in the right direction.<\/p>\n<h2>Authentication and account safety<\/h2>\n<p>Fedora has used <code>authselect<\/code> to manage the PAM stack since F28; the current profile on a fresh F44 install is <code>local<\/code> with default features. Switch to the <code>sssd<\/code> profile with the hardening features turned on. <code>with-faillock<\/code> wires <code>pam_faillock<\/code> into the auth stack, <code>with-mkhomedir<\/code> auto-creates home directories for AD\/LDAP users on first login:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo authselect select sssd with-faillock with-mkhomedir --force\nsudo authselect current\nsudo authselect check<\/code><\/pre>\n\n\n<p>The output confirms the profile and lists the enabled features:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"920\" height=\"800\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-authselect-faillock-fedora-44.png\" alt=\"authselect select sssd with-faillock with-mkhomedir and faillock user query on Fedora 44\" class=\"wp-image-167971\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-authselect-faillock-fedora-44.png 920w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-authselect-faillock-fedora-44-300x261.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-authselect-faillock-fedora-44-768x668.png 768w\" sizes=\"auto, (max-width: 920px) 100vw, 920px\" \/><\/figure>\n\n\n<p><code>authselect check<\/code> returns &#8220;Current configuration is valid&#8221; on success; any drift in <code>\/etc\/pam.d\/*<\/code> from manual edits is reported here. List the full feature catalog with <code>sudo authselect list-features sssd<\/code>; relevant extras include <code>with-fingerprint<\/code> for laptop fingerprint readers, <code>with-pamaccess<\/code> for per-user host restrictions via <code>\/etc\/security\/access.conf<\/code>, and <code>without-nullok<\/code> to reject empty passwords (the CIS profile flags this).<\/p>\n<p>Tune the faillock policy in <code>\/etc\/security\/faillock.conf<\/code>. The defaults are too loose for production (15 failures, 600s lockout). Five attempts within 15 minutes locks for 15 minutes, with audit logging of the failure events:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo tee \/etc\/security\/faillock.conf &gt; \/dev\/null &lt;&lt;'EOF'\ndeny = 5\nunlock_time = 900\nfail_interval = 900\nsilent\naudit\nEOF<\/code><\/pre>\n\n\n<p>The <code>silent<\/code> directive prevents leaking which accounts exist; <code>audit<\/code> writes lock events to <code>\/var\/log\/audit\/audit.log<\/code>. Test by intentionally failing twice from another shell, then inspect:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo faillock<\/code><\/pre>\n\n\n<p>Output lists every user with a failure count, the timestamp, and the source. To clear a locked user: <code>sudo faillock --user username --reset<\/code>. Pair faillock with strong password quality. <code>pwquality<\/code> is already loaded by the <code>sssd<\/code> authselect profile; drop a config file with sane thresholds:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo mkdir -p \/etc\/security\/pwquality.conf.d\nsudo tee \/etc\/security\/pwquality.conf.d\/cfg-hardening.conf &gt; \/dev\/null &lt;&lt;'EOF'\nminlen = 14\nminclass = 4\nmaxrepeat = 3\nmaxclassrepeat = 4\nucredit = -1\nlcredit = -1\ndcredit = -1\nocredit = -1\ndifok = 8\nenforcing = 1\nenforce_for_root\nEOF<\/code><\/pre>\n\n\n<p>14 minimum length, four character classes (upper, lower, digit, symbol), no more than three identical characters in a row, root included. PAM picks up the change at the next password change; existing passwords are not retroactively forced to comply. The <code>enforce_for_root<\/code> directive is the most-missed line; without it, root can still set a weak password.<\/p>\n<h3>Sudo I\/O logging and timeout<\/h3>\n<p>Sudo&#8217;s default config logs the command but not stdin\/stdout. For incident investigation, full I\/O capture is invaluable. Drop a sudoers file with full logging plus a shorter cred timeout. <strong>Critical gotcha:<\/strong> do NOT include <code>requiretty<\/code> on a server you administer over SSH. We tested this and it cleanly locked out every <code>ssh user@host 'sudo ...'<\/code> command pattern; <code>requiretty<\/code> is the kind of &#8220;looks more secure on paper&#8221; setting that ships breakage and was officially deprecated by the sudo project. <code>use_pty<\/code> alone gives you the audit trail without the SSH breakage:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo tee \/etc\/sudoers.d\/50-cfg-logging &gt; \/dev\/null &lt;&lt;'EOF'\nDefaults    log_input, log_output\nDefaults    iolog_dir=\"\/var\/log\/sudo-io\/%{user}\"\nDefaults    log_subcmds, log_exit_status\nDefaults    use_pty\nDefaults    timestamp_timeout=5\nEOF\nsudo visudo -c -f \/etc\/sudoers.d\/50-cfg-logging<\/code><\/pre>\n\n\n<p>The <code>visudo -c<\/code> dry-run is mandatory; a syntax error in sudoers can disable all sudo until console rescue. After the change, <code>sudo cat \/etc\/sudoers<\/code> records both the command and its output under <code>\/var\/log\/sudo-io\/<\/code>. Replay a session with <code>sudo sudoreplay TS_ID<\/code>.<\/p>\n<h3>Lock down su, root SSH, and the wheel group<\/h3>\n<p>Three settings that take seconds and matter for years. Restrict <code>su<\/code> to wheel members only (Fedora ships this commented out by default):<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo sed -i 's\/^#\\(auth.*pam_wheel.so use_uid\\)\/\\1\/' \/etc\/pam.d\/su\ngrep pam_wheel \/etc\/pam.d\/su<\/code><\/pre>\n\n\n<p>Lock the root account from direct console password login if every admin has a sudoer account (the safer pattern):<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo passwd -l root\nsudo passwd -S root<\/code><\/pre>\n\n\n<p>The status line should read <code>root LK<\/code>. To unlock for emergency console access: <code>sudo passwd -u root<\/code>. The <code>PermitRootLogin no<\/code> in SSH (below) covers the network angle; locking the password covers physical and emergency console.<\/p>\n<h2>Kernel and sysctl hardening<\/h2>\n<p>A tuned <code>sysctl<\/code> drop-in is the single biggest one-shot improvement on a stock install. The set below combines the Red Hat security hardening guide for RHEL 10, the Fedora workstation CUSP profile, and the privacyguides.org reference, with the desktop-breaking exceptions called out. Write to <code>\/etc\/sysctl.d\/99-cfg-hardening.conf<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo vi \/etc\/sysctl.d\/99-cfg-hardening.conf<\/code><\/pre>\n\n\n<p>Paste:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code># === Network ===\nnet.ipv4.conf.all.rp_filter = 1\nnet.ipv4.conf.default.rp_filter = 1\nnet.ipv4.conf.all.accept_source_route = 0\nnet.ipv4.conf.default.accept_source_route = 0\nnet.ipv4.conf.all.accept_redirects = 0\nnet.ipv4.conf.default.accept_redirects = 0\nnet.ipv4.conf.all.secure_redirects = 0\nnet.ipv4.conf.default.secure_redirects = 0\nnet.ipv4.conf.all.send_redirects = 0\nnet.ipv4.conf.default.send_redirects = 0\nnet.ipv4.conf.all.log_martians = 1\nnet.ipv4.icmp_echo_ignore_broadcasts = 1\nnet.ipv4.icmp_ignore_bogus_error_responses = 1\nnet.ipv4.tcp_syncookies = 1\nnet.ipv4.tcp_rfc1337 = 1\nnet.ipv6.conf.all.accept_redirects = 0\nnet.ipv6.conf.default.accept_redirects = 0\nnet.ipv6.conf.all.accept_source_route = 0\nnet.ipv6.conf.default.accept_source_route = 0\nnet.ipv6.conf.all.use_tempaddr = 2\nnet.ipv6.conf.default.use_tempaddr = 2\n\n# === Kernel ===\nkernel.kptr_restrict = 2\nkernel.dmesg_restrict = 1\nkernel.printk = 3 3 3 3\nkernel.unprivileged_bpf_disabled = 1\nnet.core.bpf_jit_harden = 2\nkernel.kexec_load_disabled = 1\nkernel.yama.ptrace_scope = 1\nkernel.sysrq = 4\nkernel.perf_event_paranoid = 3\nkernel.core_pattern = |\/bin\/false\nvm.unprivileged_userfaultfd = 0\n\n# === Filesystem ===\nfs.protected_fifos = 2\nfs.protected_regular = 2\nfs.protected_symlinks = 1\nfs.protected_hardlinks = 1\nfs.suid_dumpable = 0\nEOF<\/code><\/pre>\n\n\n<p>Apply and verify with explicit reads of the most consequential keys:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo sysctl -p \/etc\/sysctl.d\/99-cfg-hardening.conf\nsudo sysctl kernel.kptr_restrict kernel.yama.ptrace_scope \\\n            kernel.unprivileged_bpf_disabled fs.protected_fifos<\/code><\/pre>\n\n\n<p>On the F44 lab box every key reports its new value (<code>kernel.kptr_restrict = 2<\/code>, <code>kernel.yama.ptrace_scope = 1<\/code>, etc.). Important side effects to know before you ship this to a workstation:<\/p>\n<ul>\n<li><code>kernel.unprivileged_bpf_disabled = 1<\/code> blocks bpftrace, bpftop, and other BPF tools for non-root users. Pre-loaded BPF service tools (Cilium, Falco) keep working because they run as root.<\/li>\n<li><code>kernel.yama.ptrace_scope = 1<\/code> means <code>gdb -p PID<\/code> can only attach to your own children. For debugging an arbitrary process, prefix with <code>sudo<\/code>. <code>= 2<\/code> requires CAP_SYS_PTRACE; <code>= 3<\/code> disables ptrace entirely.<\/li>\n<li><code>kernel.kexec_load_disabled = 1<\/code> kills <code>kexec<\/code> until reboot. If you actually use kexec for fast OS swaps, leave it off; otherwise it removes a privileged-attack path.<\/li>\n<li><code>net.ipv4.icmp_echo_ignore_broadcasts = 1<\/code> is safe everywhere. <code>net.ipv4.icmp_echo_ignore_all = 1<\/code> (not in our set) breaks captive-portal detection on laptops and is not worth shipping on workstations.<\/li>\n<\/ul>\n<h3>Kernel command-line lockdown<\/h3>\n<p>A few kernel-cmdline parameters extend the protection further. <code>lockdown=integrity<\/code> blocks runtime kernel modifications even by root (kexec into untrusted kernels, \/dev\/mem writes, module loading without a valid signature). <code>module.sig_enforce=1<\/code> requires every loadable module to carry a valid signature. <code>slab_nomerge<\/code> hardens slab allocations. <code>init_on_alloc=1 init_on_free=1<\/code> zeroes memory on alloc and free, costing ~1% performance for a real heap-spray mitigation:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo grubby --update-kernel=ALL --args=\"lockdown=integrity module.sig_enforce=1 \\\n  slab_nomerge init_on_alloc=1 init_on_free=1 randomize_kstack_offset=on \\\n  vsyscall=none debugfs=off oops=panic\"\nsudo grub2-mkconfig -o \/boot\/grub2\/grub.cfg\nsudo grubby --info=DEFAULT | grep args<\/code><\/pre>\n\n\n<p>Reboot to apply. After boot, confirm: <code>cat \/proc\/cmdline<\/code> should include the new args, <code>cat \/sys\/kernel\/security\/lockdown<\/code> should report <code>[integrity]<\/code>, and <code>cat \/sys\/module\/module\/parameters\/sig_enforce<\/code> should be <code>Y<\/code>.<\/p>\n<h3>GRUB password protection<\/h3>\n<p>Without a GRUB password, anyone with physical access can press <code>e<\/code> at boot, append <code>rd.break<\/code> or <code>init=\/bin\/bash<\/code>, and reset root. For any box that leaves your control (laptop, colo server, branch office), set a GRUB password:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo grub2-setpassword\nsudo grub2-mkconfig -o \/boot\/grub2\/grub.cfg<\/code><\/pre>\n\n\n<p><code>grub2-setpassword<\/code> stores a PBKDF2 hash in <code>\/boot\/grub2\/user.cfg<\/code>. The first boot after this requires the password to edit any menu entry; default booting an entry still happens with no prompt, which is the right trade-off for unattended reboots.<\/p>\n<h2>SSH: hardened, key-only, restricted<\/h2>\n<p>The CIS L1 Server scan flagged seven SSH-related findings as failing on the default install: missing warning banner, no MaxAuthTries cap, no LoginGraceTime, no MaxSessions\/MaxStartups limit, no verbose LogLevel, no user restriction. Address all of them in one override file. Drop the file in <code>\/etc\/ssh\/sshd_config.d\/<\/code> (which the main config already includes) so package upgrades do not blow it away:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo vi \/etc\/ssh\/sshd_config.d\/99-cfg-hardening.conf<\/code><\/pre>\n\n\n<p>Paste. The key, MAC, and cipher lines force modern algorithms only (curve25519, ChaCha20-Poly1305, AES-GCM, ETM-style MACs); the <code>AllowGroups<\/code> directive restricts SSH login to members of a named group, which lets you control SSH access by adding\/removing one membership:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>PermitRootLogin no\nPasswordAuthentication no\nPermitEmptyPasswords no\nKbdInteractiveAuthentication no\nUsePAM yes\n\nX11Forwarding no\nAllowAgentForwarding no\nAllowTcpForwarding no\n\nMaxAuthTries 3\nMaxSessions 4\nMaxStartups 10:30:60\nLoginGraceTime 30\nClientAliveInterval 300\nClientAliveCountMax 2\n\nLogLevel VERBOSE\nBanner \/etc\/issue.net\nAuthorizedKeysFile .ssh\/authorized_keys\nProtocol 2\n\nKexAlgorithms curve25519-sha256@libssh.org,curve25519-sha256,sntrup761x25519-sha512@openssh.com,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr\nMACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com\n\nAllowGroups ssh-users<\/code><\/pre>\n\n\n<p>Create the <code>ssh-users<\/code> group and add the accounts that should keep SSH access. Write a banner file. Then validate the config and reload:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo groupadd -f ssh-users\nsudo usermod -aG ssh-users $USER\nsudo tee \/etc\/issue.net &gt; \/dev\/null &lt;&lt;'EOF'\nWARNING: Authorized users only. All activity is logged.\nDisconnect now if you are not an authorized user.\nEOF\nsudo sshd -t &amp;&amp; sudo systemctl reload sshd<\/code><\/pre>\n\n\n<p><strong>Crucial:<\/strong> keep your existing SSH session open and verify you can log in fresh from a new terminal before closing the first one. The <code>AllowGroups<\/code> line is the most common lockout cause; if you forget to add yourself to <code>ssh-users<\/code>, the only path back is console access. Bonus: if you administer one host from a fixed network range, add a <code>Match Address 10.0.0.0\/8<\/code> stanza that relaxes a few restrictions only for trusted sources.<\/p>\n<h2>SELinux: keep it on, learn the booleans<\/h2>\n<p>SELinux is the highest-impact security mechanism on Fedora and the one most readers reach for <code>setenforce 0<\/code> to silence. Don&#8217;t. The <a href=\"https:\/\/computingforgeeks.com\/selinux-survival-fedora\/\">SELinux survival guide<\/a> walks the full troubleshooting workflow (<code>sealert<\/code>, <code>semanage port<\/code>, <code>setsebool<\/code>, <code>semanage fcontext<\/code> plus <code>restorecon<\/code>, <code>audit2allow<\/code>). The minimum verification step on every box:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>getenforce\nsestatus | head -8<\/code><\/pre>\n\n\n<p>Output should read <code>Enforcing<\/code>; <code>Current mode<\/code> and <code>Mode from config file<\/code> must both say <code>enforcing<\/code>. The booleans worth knowing without grepping every time on a hardened server:<\/p>\n<table>\n<thead>\n<tr>\n<th>Boolean<\/th>\n<th>What it does<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>secure_mode_insmod<\/code><\/td>\n<td>Disable runtime module loading regardless of cmdline lockdown<\/td>\n<\/tr>\n<tr>\n<td><code>nis_enabled<\/code> = off<\/td>\n<td>Keep NIS-style outbound name lookups blocked (default off on F44)<\/td>\n<\/tr>\n<tr>\n<td><code>deny_ptrace<\/code> = on<\/td>\n<td>Block ptrace for all confined domains; complements sysctl ptrace_scope<\/td>\n<\/tr>\n<tr>\n<td><code>ssh_chroot_rw_homedirs<\/code> = off<\/td>\n<td>Block SFTP chroot writes to user home dirs by default<\/td>\n<\/tr>\n<tr>\n<td><code>httpd_can_network_connect_db<\/code><\/td>\n<td>On for web apps that connect to a separate DB host (otherwise denied)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Toggle persistently with <code>sudo setsebool -P boolean_name on<\/code>. The <code>-P<\/code> writes the change to the policy; without it the change reverts on reboot.<\/p>\n<h2>Firewalld: drop by default, allow by exception<\/h2>\n<p>Fedora&#8217;s default firewalld zone is <code>public<\/code>, which allows SSH and DHCP client. Switch to <code>drop<\/code> (no inbound except what you explicitly allow), re-add SSH, and add per-source allowances for services that should only answer the LAN:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo systemctl enable --now firewalld\nsudo firewall-cmd --set-default-zone=drop\nsudo firewall-cmd --permanent --zone=drop --add-service=ssh\nsudo firewall-cmd --reload\nsudo firewall-cmd --list-all<\/code><\/pre>\n\n\n<p>Output confirms the new zone state with target DROP and the SSH service allowed:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"920\" height=\"800\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-firewalld-drop-fedora-44.png\" alt=\"firewall-cmd set default zone to drop with SSH service allowed on Fedora 44\" class=\"wp-image-167970\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-firewalld-drop-fedora-44.png 920w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-firewalld-drop-fedora-44-300x261.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-firewalld-drop-fedora-44-768x668.png 768w\" sizes=\"auto, (max-width: 920px) 100vw, 920px\" \/><\/figure>\n\n\n<p>For services that should only answer specific source networks (e.g. a Postgres instance reachable only from the app subnet), use the <code>trusted<\/code> zone with a source range:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo firewall-cmd --permanent --zone=trusted --add-source=10.0.5.0\/24\nsudo firewall-cmd --permanent --zone=trusted --add-port=5432\/tcp\nsudo firewall-cmd --reload\nsudo firewall-cmd --get-active-zones<\/code><\/pre>\n\n\n<p>Rate-limit brute force at the firewall layer with a rich rule:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule service name=\"ssh\" \\\n  accept limit value=\"5\/m\"'\nsudo firewall-cmd --reload<\/code><\/pre>\n\n\n<p>5 connections per minute is enough for a human and far below what a brute-force script needs. Pair with fail2ban (next section) for a second layer.<\/p>\n<h3>fail2ban for SSH<\/h3>\n<p>fail2ban watches log files and pushes offending IPs into firewall bans. Drop in a minimal SSH jail and start the service:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo tee \/etc\/fail2ban\/jail.d\/sshd.local &gt; \/dev\/null &lt;&lt;'EOF'\n&#91;sshd]\nenabled = true\nbackend = systemd\nmaxretry = 3\nfindtime = 10m\nbantime = 1h\nbantime.increment = true\nbantime.factor = 4\nEOF\nsudo systemctl enable --now fail2ban\nsudo fail2ban-client status sshd<\/code><\/pre>\n\n\n<p>The <code>bantime.increment<\/code> + <code>bantime.factor = 4<\/code> means the first ban is 1h, the second 4h, the third 16h, and so on; repeat offenders graduate to effectively permanent bans. Status command shows currently banned IPs and total failures.<\/p>\n<h2>Custom audit rules<\/h2>\n<p>The <code>auditd<\/code> daemon ships disabled-by-policy on a fresh install but logs nothing useful by default. Drop a custom ruleset that watches the configuration files, user-management binaries, time-change syscalls, and module load events; these are the events you need for incident investigation. Write to <code>\/etc\/audit\/rules.d\/cfg-hardening.rules<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo vi \/etc\/audit\/rules.d\/cfg-hardening.rules<\/code><\/pre>\n\n\n<p>Paste:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code># Watch critical config files\n-w \/etc\/passwd -p wa -k identity\n-w \/etc\/shadow -p wa -k identity\n-w \/etc\/group -p wa -k identity\n-w \/etc\/sudoers -p wa -k actions\n-w \/etc\/sudoers.d\/ -p wa -k actions\n-w \/etc\/ssh\/sshd_config -p wa -k sshd_config\n-w \/etc\/ssh\/sshd_config.d\/ -p wa -k sshd_config\n-w \/etc\/selinux\/ -p wa -k selinux_config\n-w \/etc\/audit\/ -p wa -k auditd\n\n# User\/group modifications\n-w \/usr\/sbin\/useradd -p x -k user_mgmt\n-w \/usr\/sbin\/userdel -p x -k user_mgmt\n-w \/usr\/sbin\/usermod -p x -k user_mgmt\n-w \/usr\/sbin\/passwd -p x -k user_mgmt\n\n# Time changes\n-a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time_change\n\n# Mount events\n-a always,exit -F arch=b64 -S mount,umount2 -F auid&gt;=1000 -F auid!=unset -k mount\n\n# Unauthorized access attempts\n-a always,exit -F arch=b64 -S openat,truncate,ftruncate -F exit=-EACCES \\\n   -F auid&gt;=1000 -F auid!=unset -k unauthed_access\n-a always,exit -F arch=b64 -S openat,truncate,ftruncate -F exit=-EPERM \\\n   -F auid&gt;=1000 -F auid!=unset -k unauthed_access\n\n# Module load\/unload\n-w \/sbin\/insmod -p x -k module_load\n-w \/sbin\/rmmod -p x -k module_load\n-w \/sbin\/modprobe -p x -k module_load\n-a always,exit -F arch=b64 -S init_module,delete_module -k module_load<\/code><\/pre>\n\n\n<p>Load them and verify:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo augenrules --load\nsudo auditctl -l | head -10\nsudo auditctl -l | wc -l<\/code><\/pre>\n\n\n<p>The count should match the rule lines above (around 22 once syscall rules are expanded). Query later with <code>sudo ausearch -k identity<\/code> or <code>sudo aureport --summary<\/code>.<\/p>\n<h2>File integrity with AIDE + a systemd timer<\/h2>\n<p>AIDE builds a cryptographic baseline of every file in directories listed in <code>\/etc\/aide.conf<\/code>, then alerts when any of those files change. Initialize once, then schedule a daily check via systemd:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo aide --init\nsudo mv \/var\/lib\/aide\/aide.db.new.gz \/var\/lib\/aide\/aide.db.gz\nsudo aide --check<\/code><\/pre>\n\n\n<p>The initial check should print zero differences (you just built the baseline). For daily automation, create a one-shot service and matching timer:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo tee \/etc\/systemd\/system\/aide-check.service &gt; \/dev\/null &lt;&lt;'EOF'\n&#91;Unit]\nDescription=AIDE file integrity check\nAfter=local-fs.target\n\n&#91;Service]\nType=oneshot\nExecStart=\/usr\/sbin\/aide --check\nNice=15\nIOSchedulingClass=idle\nEOF\n\nsudo tee \/etc\/systemd\/system\/aide-check.timer &gt; \/dev\/null &lt;&lt;'EOF'\n&#91;Unit]\nDescription=Run AIDE file integrity check daily\n\n&#91;Timer]\nOnCalendar=daily\nPersistent=true\nRandomizedDelaySec=30m\n\n&#91;Install]\nWantedBy=timers.target\nEOF\n\nsudo systemctl daemon-reload\nsudo systemctl enable --now aide-check.timer\nsudo systemctl list-timers aide-check.timer --no-pager<\/code><\/pre>\n\n\n<p>Output of <code>journalctl -u aide-check<\/code> after the first scheduled run shows the AIDE report. After legitimate changes (a <code>dnf upgrade<\/code>, an admin edit), rebuild the baseline so the next check is quiet: <code>sudo aide --update &amp;&amp; sudo mv \/var\/lib\/aide\/aide.db.new.gz \/var\/lib\/aide\/aide.db.gz<\/code>. The discipline is that every alert gets reviewed, never ignored; the day one unexpected change shows up among legitimate ones is the day AIDE pays for itself.<\/p>\n<h2>Filesystem mount-option hardening<\/h2>\n<p>The kernel honours mount options that disable execution from a partition (<code>noexec<\/code>), block setuid bits (<code>nosuid<\/code>), and forbid device nodes (<code>nodev<\/code>). For partitions that should never carry executables or setuid binaries, these flags eliminate whole classes of attacks. Apply to <code>\/tmp<\/code>, <code>\/var\/tmp<\/code>, <code>\/home<\/code>, and <code>\/dev\/shm<\/code>; <code>\/boot<\/code> also benefits from <code>nosuid,nodev,noexec<\/code> on systems where it is a separate partition:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo vi \/etc\/fstab<\/code><\/pre>\n\n\n<p>For each entry, add the flags to the options column. Example for ext4 partitions on a server:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>UUID=...   \/home  ext4  defaults,nodev,nosuid                   1 2\nUUID=...   \/tmp   ext4  defaults,nodev,nosuid,noexec             1 2\nUUID=...   \/var\/tmp  ext4  defaults,nodev,nosuid,noexec          1 2\nUUID=...   \/boot  ext4  defaults,nodev,nosuid,noexec             1 2\ntmpfs      \/dev\/shm  tmpfs defaults,nodev,nosuid,noexec          0 0<\/code><\/pre>\n\n\n<p><strong>Workstation exception:<\/strong> Flatpak and Snap need <code>exec<\/code> on <code>\/home<\/code>; do not add <code>noexec<\/code> there if you use sandboxed apps. After editing, remount without rebooting:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo mount -o remount \/tmp \/home \/var\/tmp \/dev\/shm\nfindmnt -o TARGET,OPTIONS | grep -E '\/tmp|\/home|\/var\/tmp|\/dev\/shm'<\/code><\/pre>\n\n\n<p>The output confirms the new flags. <code>hidepid=2<\/code> on <code>\/proc<\/code> hides processes that do not belong to the user, which is useful on multi-user boxes (the <code>privacyguides.org<\/code> reference covers the supplementary-group dance you need to keep systemd-logind working with it).<\/p>\n<h2>Mask the services you do not use<\/h2>\n<p>Every active socket is attack surface. The default F44 cloud image enables a handful of daemons that are useful on a workstation and noise on a server. Mask them, which is stronger than disabling because the unit symlink becomes <code>\/dev\/null<\/code> and any attempt to start the service (even as a dependency) fails:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo systemctl mask avahi-daemon.service avahi-daemon.socket\nsudo systemctl mask cups.service cups.socket\nsudo systemctl mask abrt-journal-core.service abrt-oops.service abrt-vmcore.service\n# Server only. Keep these on a workstation:\nsudo systemctl mask ModemManager.service\nsudo systemctl mask switcheroo-control.service<\/code><\/pre>\n\n\n<p>To see what is currently listening on any interface:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo ss -tulpn | grep LISTEN<\/code><\/pre>\n\n\n<p>Anything not recognized deserves investigation. Once you have only the services you need, tighten the ones that remain with per-unit sandboxing. <code>systemctl edit nginx.service<\/code> and add:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>&#91;Service]\nNoNewPrivileges=true\nProtectSystem=strict\nProtectHome=true\nReadWritePaths=\/var\/log\/nginx \/var\/lib\/nginx \/run\nPrivateTmp=true\nPrivateDevices=true\nProtectKernelTunables=true\nProtectKernelModules=true\nProtectControlGroups=true\nRestrictNamespaces=true\nLockPersonality=true\nMemoryDenyWriteExecute=true\nRestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX<\/code><\/pre>\n\n\n<p>Reload and restart. Re-run <code>systemd-analyze security nginx.service<\/code>; the exposure score drops from 9.2 (UNSAFE) to about 4 (OK). Repeat the pattern for every service you depend on.<\/p>\n<h2>Package and update hygiene<\/h2>\n<p>GPG check is on by default in <code>\/etc\/dnf\/dnf.conf<\/code>; confirm it stays that way and lock the config:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>grep -E \"^gpgcheck|^localpkg_gpgcheck|^repo_gpgcheck\" \/etc\/dnf\/dnf.conf\nsudo chattr +i \/etc\/dnf\/dnf.conf  # only if your config is final\nlsattr \/etc\/dnf\/dnf.conf<\/code><\/pre>\n\n\n<p>The <code>chattr +i<\/code> sets the immutable bit; nothing including root can modify until you <code>chattr -i<\/code>. Audit every enabled repo and the corresponding GPG key fingerprint:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo dnf5 repo list --enabled\nsudo dnf5 repolist --enabled --quiet --json | python3 -c \\\n  \"import json,sys; &#91;print(r&#91;'id'],r.get('baseurl','')) for r in json.load(sys.stdin)]\"\nrpm -q --qf '%{nvra} %{summary}\\n' gpg-pubkey-*<\/code><\/pre>\n\n\n<p>Any repo you do not recognize should be disabled (<code>sudo dnf5 config-manager disable repo_id<\/code>) and any orphan GPG key removed (<code>sudo rpm -e gpg-pubkey-NNN<\/code>). For automatic security updates, install <code>dnf5-plugin-automatic<\/code> and configure it for security-only:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo dnf5 install -y dnf5-plugin-automatic\nsudo sed -i 's\/^upgrade_type.*\/upgrade_type = security\/' \/etc\/dnf\/automatic.conf\nsudo sed -i 's\/^download_updates.*\/download_updates = yes\/' \/etc\/dnf\/automatic.conf\nsudo sed -i 's\/^apply_updates.*\/apply_updates = yes\/' \/etc\/dnf\/automatic.conf\nsudo systemctl enable --now dnf-automatic.timer\nsudo systemctl list-timers dnf-automatic --no-pager<\/code><\/pre>\n\n\n<p>For <strong>desktop installs<\/strong>, Flatpak adds its own dimension. Audit the permissions of every installed Flatpak app and revoke anything excessive:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>flatpak list --app --columns=application,permissions\nflatpak install -y flathub com.github.tchx84.Flatseal\n# Open Flatseal, revoke filesystem=host, filesystem=home, dri, network from\n# apps that do not need them.<\/code><\/pre>\n\n\n<p>Flatseal is the GUI for editing per-app overrides. The defaults vendors ship are often broader than the app actually needs; tighten via Flatseal and you keep the app working without exposing the rest of <code>\/home<\/code>.<\/p>\n<h2>Time, DNS, and network hygiene<\/h2>\n<p>The <code>chronyd<\/code> daemon ships using the Fedora NTP pool, unauthenticated. NTS (Network Time Security) wraps NTP exchanges in TLS-style authentication so a man-in-the-middle cannot drift your clock and break TLS validity. Cloudflare, Netnod, and NIST all run public NTS servers. Edit <code>\/etc\/chrony.conf<\/code>, replace the default pool with the NTS-enabled servers, restart, and confirm:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo sed -i 's|^pool .*|# &amp;|' \/etc\/chrony.conf\nsudo tee -a \/etc\/chrony.conf &gt; \/dev\/null &lt;&lt;'EOF'\n\n# NTS-secured time sources\nserver time.cloudflare.com iburst nts\nserver nts.netnod.se iburst nts\nserver time.nist.gov iburst nts\nEOF\nsudo systemctl restart chronyd\nchronyc -N authdata<\/code><\/pre>\n\n\n<p>The <code>authdata<\/code> output should show <code>NTS<\/code> in the Mode column for each server once the NTS handshake completes. For DNS, switch the system resolver to DNS-over-TLS so queries are encrypted to the upstream:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo mkdir -p \/etc\/systemd\/resolved.conf.d\nsudo tee \/etc\/systemd\/resolved.conf.d\/cfg-dot.conf &gt; \/dev\/null &lt;&lt;'EOF'\n&#91;Resolve]\nDNS=1.1.1.1#one.one.one.one 1.0.0.1#one.one.one.one 9.9.9.9#dns.quad9.net\nDNSOverTLS=yes\nDNSSEC=allow-downgrade\nFallbackDNS=\nCache=yes\nDNSStubListener=yes\nEOF\nsudo systemctl restart systemd-resolved\nresolvectl status | grep -E \"DNS Server|DNSOverTLS|DNSSEC\"<\/code><\/pre>\n\n\n<p>Output should report your DoT-capable resolvers and <code>+DNSOverTLS<\/code>. Test a query: <code>resolvectl query computingforgeeks.com<\/code>.<\/p>\n<h3>NetworkManager MAC randomization (desktop)<\/h3>\n<p>Laptops connecting to public Wi-Fi leak a persistent MAC address that lets the network track you across visits. Tell NetworkManager to randomize per-network for Wi-Fi and per-connection for wired:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo tee \/etc\/NetworkManager\/conf.d\/00-cfg-mac-rand.conf &gt; \/dev\/null &lt;&lt;'EOF'\n&#91;connection-mac-randomization]\nethernet.cloned-mac-address=stable\nwifi.cloned-mac-address=random\nEOF\nsudo systemctl restart NetworkManager\nnmcli -f GENERAL.HWADDR,GENERAL.CLONED-HWADDR dev show wlan0 2&gt;\/dev\/null || \\\nnmcli connection show --active<\/code><\/pre>\n\n\n<p><code>wifi.cloned-mac-address=random<\/code> generates a fresh MAC for every Wi-Fi association. <code>ethernet.cloned-mac-address=stable<\/code> uses a stable but distinct-from-hardware MAC for wired networks, which works with DHCP reservations. To verify the active MAC on a Wi-Fi connection: <code>ip link show wlan0<\/code>.<\/p>\n<h2>USB device authorization with USBGuard<\/h2>\n<p>USBGuard is the answer to BadUSB-style attacks. It runs as a daemon that intercepts every USB device attach and consults a policy file; only devices whose hash matches an allow rule get authorized. Generate an initial policy from the currently-connected devices, install it with the right SELinux label, and start the daemon:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo usbguard generate-policy -P &gt; \/tmp\/rules.conf\nsudo mv \/tmp\/rules.conf \/etc\/usbguard\/rules.conf\nsudo chown root:root \/etc\/usbguard\/rules.conf\nsudo chmod 0600 \/etc\/usbguard\/rules.conf\nsudo restorecon -v \/etc\/usbguard\/rules.conf\nsudo systemctl enable --now usbguard\nsudo systemctl status usbguard --no-pager | head -5<\/code><\/pre>\n\n\n<p><strong>Critical gotcha:<\/strong> we hit a real failure here. After <code>mv \/tmp\/rules.conf \/etc\/usbguard\/rules.conf<\/code> the file inherits the <code>user_tmp_t<\/code> SELinux label from <code>\/tmp<\/code>, and the USBGuard daemon (running as <code>usbguard_t<\/code>) is not allowed to read that label. The daemon fails to start with &#8220;Permission denied&#8221;:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"920\" height=\"800\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-usbguard-selinux-fedora-44.png\" alt=\"USBGuard failing with Permission denied due to SELinux user_tmp_t label, fixed by restorecon on Fedora 44\" class=\"wp-image-167969\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-usbguard-selinux-fedora-44.png 920w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-usbguard-selinux-fedora-44-300x261.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-usbguard-selinux-fedora-44-768x668.png 768w\" sizes=\"auto, (max-width: 920px) 100vw, 920px\" \/><\/figure>\n\n\n<p>The <code>restorecon<\/code> step relabels to <code>usbguard_rules_t<\/code>; the daemon starts cleanly after. This is exactly the kind of denial the <a href=\"https:\/\/computingforgeeks.com\/selinux-survival-fedora\/\">SELinux survival guide<\/a> covers, and forgetting to relabel is the most common reason USBGuard fails on its first start.<\/p>\n<p>Add a new USB device after the daemon is up. First plug the device, then list pending devices, then promote the temporary allow to a permanent rule:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo usbguard list-devices\nsudo usbguard allow-device &lt;device-id&gt;\nsudo usbguard append-rule \"allow id 1234:5678 serial \\\"...\\\" \\\n  name \\\"YubiKey FIDO+CCID\\\" hash \\\"...\\\"\"<\/code><\/pre>\n\n\n<p>For interactive prompts on desktop, the <code>usbguard-applet-qt<\/code> package adds a tray icon that pops up &#8220;Allow \/ Block \/ Always allow&#8221; when a new device appears. On servers, <code>usbguard append-rule<\/code> from a script is the deployment path.<\/p>\n<h2>Persistent journald + remote logging<\/h2>\n<p>Journald defaults to volatile storage on cloud images, which means a reboot loses everything. Persistent storage for forensic investigation:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo mkdir -p \/etc\/systemd\/journald.conf.d\nsudo tee \/etc\/systemd\/journald.conf.d\/cfg-persistent.conf &gt; \/dev\/null &lt;&lt;'EOF'\n&#91;Journal]\nStorage=persistent\nForwardToSyslog=no\nMaxRetentionSec=1month\nSystemMaxUse=1G\nCompress=yes\nSeal=yes\nEOF\nsudo systemctl restart systemd-journald\nls -ld \/var\/log\/journal\/*\/<\/code><\/pre>\n\n\n<p>The <code>Seal=yes<\/code> directive computes a Forward Secure Sealing key (FSS), which makes after-the-fact log tampering detectable. For multi-host environments, forward to a central log server using <code>systemd-journal-remote<\/code> over HTTPS, or use rsyslog\/Vector if you already run one. The journald-upload pattern:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo dnf5 install -y systemd-journal-remote\nsudo tee \/etc\/systemd\/journal-upload.conf &gt; \/dev\/null &lt;&lt;'EOF'\n&#91;Upload]\nURL=https:\/\/logs.example.com:19532\nServerKeyFile=\/etc\/ssl\/private\/upload-key.pem\nServerCertificateFile=\/etc\/ssl\/certs\/upload-cert.pem\nTrustedCertificateFile=\/etc\/ssl\/certs\/log-ca.pem\nEOF\nsudo systemctl enable --now systemd-journal-upload<\/code><\/pre>\n\n\n<p>Provision the certs separately (Let&#8217;s Encrypt + a private CA both work). The receiver runs <code>systemd-journal-remote<\/code> on the same TCP\/HTTPS port. The full pipeline gives you tamper-evident logs locally plus a remote copy the attacker can not edit.<\/p>\n<h2>Browser sandboxing (desktop)<\/h2>\n<p>The single biggest desktop attack surface is the browser. The Fedora-shipped Firefox is excellent, but the Flatpak version adds a real sandbox boundary (Bubblewrap + portals). Switch:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo dnf5 remove -y firefox\nflatpak install -y flathub org.mozilla.firefox\nflatpak install -y flathub com.github.tchx84.Flatseal\nflatpak override --user --nofilesystem=home org.mozilla.firefox\nflatpak override --user --filesystem=~\/Downloads org.mozilla.firefox<\/code><\/pre>\n\n\n<p>The two <code>override<\/code> commands strip the broad <code>filesystem=home<\/code> default and grant only <code>~\/Downloads<\/code>. Verify with Flatseal. Chrome and Chromium follow the same pattern (<code>com.google.Chrome<\/code> or <code>org.chromium.Chromium<\/code>). For paranoid setups, run the browser inside Firejail with a sample profile, or in a Distrobox-confined container; either layer reduces the blast radius of a browser-zero-day exploit.<\/p>\n<h2>Disk encryption (verify LUKS)<\/h2>\n<p>Fedora&#8217;s installer offers full-disk encryption by default. Confirm it actually landed:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo blkid | grep crypto_LUKS\nsudo cryptsetup status $(findmnt -no SOURCE \/ | xargs -I{} lsblk -no PKNAME {})\nsudo cryptsetup luksDump \/dev\/sda3 | head -20<\/code><\/pre>\n\n\n<p>The <code>luksDump<\/code> output shows the cipher (should be <code>aes-xts-plain64<\/code>), key size (<code>512 bits<\/code>, which is AES-256 in XTS), and PBKDF (should be <code>argon2id<\/code> on LUKS2). If the install used LUKS1 or weaker parameters, <code>cryptsetup reencrypt<\/code> can upgrade in place without losing data, but plan a long maintenance window. On laptops, also configure clevis + Tang or TPM2 unlock so the system can boot without a password on the trusted network or against the trusted TPM, while still requiring the password off-network.<\/p>\n<h2>Re-measure: prove the work landed<\/h2>\n<p>After applying the changes above, run the same OpenSCAP scan and the same Lynis audit; the deltas are the proof of work. The CIS L1 Server profile picks up the SSH hardening rules, the audit rules, the faillock and pwquality changes, and the masked services. Lynis picks up the sysctl, AIDE, and faillock changes. systemd-analyze picks up the per-service sandboxing overrides:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo oscap xccdf eval \\\n  --profile xccdf_org.ssgproject.content_profile_cis_server_l1 \\\n  --results \/tmp\/oscap-after.xml \\\n  \/usr\/share\/xml\/scap\/ssg\/content\/ssg-fedora-ds.xml\n\nsudo grep -oE \"&lt;result&gt;(pass|fail)&lt;\/result&gt;\" \/tmp\/oscap-after.xml | sort | uniq -c\nsudo grep -oE \"&lt;score &#91;^&gt;]*&gt;&#91;^&lt;]+&lt;\/score&gt;\" \/tmp\/oscap-after.xml<\/code><\/pre>\n\n\n<p>For a Fedora 44 server box with every section of this guide applied, the CIS L1 Server score moves from the baseline 74.66 into the high 80s; the remaining failures are mostly the rules that require a real authentication backend (sssd configured against AD or IPA), a real log forwarder (the journal-upload section requires certs), and mount-option changes that need a reboot to apply. Reaching 95+ requires either the auto-remediation script (<code>oscap xccdf eval --remediate<\/code>) or addressing each remaining rule manually using the HTML report.<\/p>\n<h3>Apply auto-remediation (with care)<\/h3>\n<p>The remediation flag generates and runs the bash equivalent of every failing rule&#8217;s remediation snippet. It is fast and useful but it does not understand your environment, so review carefully on a non-production box first:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code># Preview first (writes a remediation script without running it)\nsudo oscap xccdf generate fix \\\n  --profile xccdf_org.ssgproject.content_profile_cis_server_l1 \\\n  --output \/tmp\/remediation.sh \\\n  \/usr\/share\/xml\/scap\/ssg\/content\/ssg-fedora-ds.xml\nless \/tmp\/remediation.sh\n\n# Then run with caution\nsudo oscap xccdf eval --remediate \\\n  --profile xccdf_org.ssgproject.content_profile_cis_server_l1 \\\n  \/usr\/share\/xml\/scap\/ssg\/content\/ssg-fedora-ds.xml<\/code><\/pre>\n\n\n<p>Skim the generated script for anything that would break your stack (changes to default umask, mount option changes that need a reboot, removal of services you depend on). The preview pattern is the safe one; auto-remediation directly is fine on a hardening-only test box.<\/p>\n<h2>Periodic audit cadence<\/h2>\n<p>Hardening is not a one-shot. The cadence we run on the lab boxes for this guide:<\/p>\n<table>\n<thead>\n<tr>\n<th>Frequency<\/th>\n<th>What to run<\/th>\n<th>What you are watching for<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Daily<\/td>\n<td><code>aide --check<\/code> (automated via the timer above)<\/td>\n<td>Unexpected file changes; alert on any non-empty report<\/td>\n<\/tr>\n<tr>\n<td>Daily<\/td>\n<td><code>fail2ban-client status sshd<\/code><\/td>\n<td>New banned IPs; spikes indicate active brute-force<\/td>\n<\/tr>\n<tr>\n<td>Weekly<\/td>\n<td><code>sudo lynis audit system<\/code><\/td>\n<td>New warnings; pin the hardening index trend<\/td>\n<\/tr>\n<tr>\n<td>Weekly<\/td>\n<td><code>sudo dnf5 advisory list --available<\/code><\/td>\n<td>Security advisories you have not applied yet<\/td>\n<\/tr>\n<tr>\n<td>Monthly<\/td>\n<td><code>sudo oscap xccdf eval --profile cis_server_l1 ...<\/code><\/td>\n<td>Compliance drift after policy or app changes<\/td>\n<\/tr>\n<tr>\n<td>Monthly<\/td>\n<td>Audit <code>\/etc\/sudoers.d\/<\/code> and <code>\/etc\/ssh\/sshd_config.d\/<\/code><\/td>\n<td>Drop-in files added by automation you forgot about<\/td>\n<\/tr>\n<tr>\n<td>Quarterly<\/td>\n<td>Review enabled repos and Flatpak permissions<\/td>\n<td>Third-party repos that snuck in via dev tooling<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Wrap these into a runbook and assign each to an owner; security hardening that nobody owns rots within a year.<\/p>\n<h2>Common hardening gotchas (real ones from this lab)<\/h2>\n<h3>SSH + sudo broken after sudoers hardening<\/h3>\n<p><code>Defaults requiretty<\/code> in sudoers cleanly locks out every <code>ssh user@host 'sudo ...'<\/code> invocation. We hit this in the lab and had to recover via console. Use <code>Defaults use_pty<\/code> instead; same audit trail without the breakage. <code>requiretty<\/code> was officially deprecated by the sudo project for exactly this reason.<\/p>\n<h3>USBGuard daemon fails to start with &#8220;Permission denied&#8221;<\/h3>\n<p>SELinux label on <code>\/etc\/usbguard\/rules.conf<\/code> is wrong after <code>mv<\/code> from <code>\/tmp<\/code>. Fix with <code>sudo restorecon -v \/etc\/usbguard\/rules.conf<\/code>; daemon starts cleanly after. The Permission denied wording is misleading because the DAC permissions are correct; only the MAC label is wrong.<\/p>\n<h3>Locked out via SSH after PasswordAuthentication no<\/h3>\n<p>You set the override before installing your SSH key on the box. Recovery: Proxmox console, AWS EC2 Instance Connect, KVM\/IPMI serial, or whatever out-of-band access the host offers. Re-enable password auth, install the key, switch back. Always keep one open SSH session through the change and verify a fresh login works before closing it.<\/p>\n<h3>Workstation captive-portal detection breaks<\/h3>\n<p>You set <code>net.ipv4.icmp_echo_ignore_all = 1<\/code> (not in our recommended set, but common in older hardening guides). NetworkManager&#8217;s captive-portal check pings <code>fedoraproject.org\/static\/hotspot.txt<\/code> and falls over. Remove that line and keep the safer <code>icmp_echo_ignore_broadcasts<\/code>.<\/p>\n<h3>Flatpak apps stop launching after noexec on \/home<\/h3>\n<p>Flatpak runtimes are stored under <code>~\/.var\/app\/<\/code> and need <code>exec<\/code>. Either drop <code>noexec<\/code> from the <code>\/home<\/code> mount line or relocate Flatpak&#8217;s app dir to a partition that allows exec. The lesson is that mount-option hardening needs to know what apps live on the partition.<\/p>\n<h3>BPF tools stopped working<\/h3>\n<p><code>kernel.unprivileged_bpf_disabled = 1<\/code> blocks BPF for non-root users. For ad-hoc <code>bpftrace<\/code>, prefix with <code>sudo<\/code>; for systemd-managed tools (Cilium, Falco), they run as root and are unaffected.<\/p>\n<h3>AIDE reports thousands of &#8220;added&#8221; files after dnf upgrade<\/h3>\n<p>Expected; every <code>dnf upgrade<\/code> replaces files AIDE watches. Workflow: review the diff, confirm changes are from your upgrade, then rebuild the baseline. Never ignore AIDE alerts; the day a single unexpected change shows up among legitimate ones is the day AIDE earns its keep.<\/p>\n<h3>OpenSCAP score regresses after a routine package update<\/h3>\n<p>The SCAP profile is versioned (currently 0.1.80). Updates to scap-security-guide can add new rules or tighten existing ones. Pin the profile version if you care about score stability, or accept that the bar gets higher over time and re-baseline.<\/p>\n<h2>What to leave on the table (and why)<\/h2>\n<p>Three things are tempting but cause more pain than they prevent on a modern Fedora workstation:<\/p>\n<ul>\n<li><strong>FIPS mode<\/strong> (<code>fips=1<\/code> kernel argument plus <code>fips-mode-setup --enable<\/code>) is mandatory for some compliance regimes and a footgun otherwise. It cuts the available cipher set and breaks SSH against any older counterparty. Enable only if your auditor demands it.<\/li>\n<li><strong>Disabling user namespaces<\/strong> (<code>kernel.unprivileged_userns_clone = 0<\/code>) breaks rootless Podman, Docker rootless, LXC unprivileged, Flatpak, and Snap. Keep enabled unless you genuinely run no containers.<\/li>\n<li><strong>Aggressive module blacklisting<\/strong> (cramfs, freevxfs, hfs, hfsplus, jffs2, udf, etc.) is a CIS rule and the listed module families are obscure, but blacklisting <code>nfs<\/code>, <code>cifs<\/code>, or USB drivers breaks workstation mounting of network shares and external drives. Apply selectively from the privacyguides reference based on what your box actually does.<\/li>\n<\/ul>\n<p>With this guide applied, the CIS L1 Server score moves from a baseline ~75 into the high 80s, Lynis from ~68 to ~80, and systemd-analyze drops every confined service two or three exposure tiers. Pair this with the <a href=\"https:\/\/computingforgeeks.com\/selinux-survival-fedora\/\">SELinux survival guide<\/a> for the MAC layer that already does half the work, the <a href=\"https:\/\/computingforgeeks.com\/configure-firewalld-fedora\/\">firewalld walkthrough<\/a> for the inbound network policy, the <a href=\"https:\/\/computingforgeeks.com\/dnf5-cheatsheet-fedora\/\">DNF5 cheatsheet<\/a> for the package update commands the automatic-updates pattern assumes, the <a href=\"https:\/\/computingforgeeks.com\/btrfs-snapper-grub-btrfs-fedora\/\">Btrfs snapshots guide<\/a> for filesystem rollback below the policy layer, and the <a href=\"https:\/\/computingforgeeks.com\/post-install-fedora-44-workstation\/\">post-install setup guide<\/a> for the baseline configuration this all sits on top of.<\/p>","protected":false},"excerpt":{"rendered":"<p>A fresh Fedora 44 install ships in better security posture than most Linux distributions. SELinux is enforcing, firewalld blocks anything that no service exposes, the kernel ships with stack canaries and KASLR, the package keys chain to Fedora&#8217;s PGP trust, and the new RPM 6.0 transaction format ships in F44 with stronger digest verification. None &#8230; <a title=\"Harden Fedora 44 \/ 43 \/ 42 for Desktop and Server: Complete Guide\" class=\"read-more\" href=\"https:\/\/computingforgeeks.com\/security-hardening-fedora\/\" aria-label=\"Read more about Harden Fedora 44 \/ 43 \/ 42 for Desktop and Server: Complete Guide\">Read more<\/a><\/p>\n","protected":false},"author":24,"featured_media":167941,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[29,299,47,50],"tags":[681,282,205],"cfg_series":[39847],"class_list":["post-167945","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-fedora","category-how-to","category-linux","category-linux-tutorials","tag-fedora","tag-linux","tag-security","cfg_series-fedora-44-workstation"],"_links":{"self":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/167945","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/users\/24"}],"replies":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/comments?post=167945"}],"version-history":[{"count":3,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/167945\/revisions"}],"predecessor-version":[{"id":167973,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/167945\/revisions\/167973"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media\/167941"}],"wp:attachment":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media?parent=167945"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/categories?post=167945"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/tags?post=167945"},{"taxonomy":"cfg_series","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/cfg_series?post=167945"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}