You just deployed Fedora Server and the internet is knocking
You just finished installing Fedora Server on a VPS or a bare-metal box. The default installation is clean, minimal, and ready to run. It is also completely open. Every port is listening, root can log in over SSH, and SELinux is running in permissive mode. If you push this machine into production without changes, you are handing attackers a map of your system. Hardening is not about paranoia. It is about removing the default attack surface before you start building services.
What baseline hardening actually means
Think of a fresh Fedora Server like a house with the doors unlocked, the alarm system in test mode, and the windows wide open. The operating system prioritizes usability and compatibility out of the box. Production environments prioritize containment and least privilege. Hardening shifts the default posture from allow everything until told otherwise to deny everything until explicitly permitted. You will configure the firewall to drop unsolicited traffic, switch SELinux from permissive to enforcing, lock down SSH to key-only authentication, and automate security patches. Each step reduces the number of ways an external process can interact with your kernel or user space.
Apply these changes in a test environment first. A misconfigured firewall or SSH daemon can lock you out of a remote server instantly. Keep a console session or out-of-band management access open until you verify every step.
Lock down the network perimeter
Fedora ships with firewalld running by default, but the public zone allows a broad set of services. You need to strip that down to exactly what your application requires. The firewall operates on two configurations: runtime and permanent. Runtime changes apply immediately but vanish on reboot. Permanent changes survive reboots but require a reload to take effect. Always set permanent rules first, then reload. Skipping the reload causes the runtime and persistent configurations to diverge, which leads to confusing behavior after a restart.
Here is how to clear the default public zone and allow only SSH traffic.
# Remove all default services from the permanent public zone
firewall-cmd --permanent --zone=public --remove-service=mdns
firewall-cmd --permanent --zone=public --remove-service=samba-client
firewall-cmd --permanent --zone=public --remove-service=dhcpv6-client
# Add only the services your server actually needs
firewall-cmd --permanent --zone=public --add-service=ssh
firewall-cmd --permanent --zone=public --add-service=https
# Apply the permanent configuration to the running kernel
firewall-cmd --reload
The --permanent flag writes to /etc/firewalld/zones/public.xml. The --reload flag tells the firewalld daemon to read that file and update the netfilter rules in the kernel. You can verify the active rules by running firewall-cmd --list-all --zone=public. The output should show only the services you explicitly added.
Firewall rules are stateful by default. Return traffic for established connections is allowed automatically. You do not need to open outbound ports for your server to download packages or resolve DNS. Trust the stateful inspection. Only open inbound ports that external clients must reach.
Enforce mandatory access control
SELinux is often the most misunderstood security layer. It does not replace traditional Unix permissions. It adds a mandatory access control layer that restricts what processes can do, regardless of file ownership. A fresh Fedora Server install sets SELINUX=permissive in /etc/selinux/config. Permissive mode logs violations but does not block them. Enforcing mode blocks them. Switching to enforcing is safe if you rely on standard Fedora packages. Custom scripts or non-standard service paths will trigger denials.
Here is how to check the current mode and switch to enforcing.
# Display the current SELinux mode without changing it
getenforce
# Edit the persistent configuration file
# Config files in /etc/ are user-modified. Never edit files in /usr/lib/.
sudo nano /etc/selinux/config
# Change the line to read exactly this:
# SELINUX=enforcing
# Apply the change immediately without rebooting
sudo setenforce 1
The setenforce 1 command switches the kernel to enforcing mode for the current session. The /etc/selinux/config edit ensures the mode survives a reboot. If a service fails to start after this change, check the audit logs before disabling SELinux. SELinux denials show up in journalctl -t setroubleshoot with a one-line summary and a fix suggestion. Read those before touching setsebool or chcon. Manual context overrides drift across package updates. Policy modules stay consistent.
Restrict remote access
Remote access is the most common attack vector. The default sshd_config allows root login and password authentication. Both need to change. You will disable root login, disable password auth, and ensure key-based authentication is the only path. Restarting the service is required, but you must keep your current session open until you verify the new configuration works. A misconfigured SSH daemon can lock you out instantly.
Here is how to secure the SSH daemon configuration.
# Open the main SSH daemon configuration file
sudo nano /etc/ssh/sshd_config
# Locate and modify these directives. Remove the leading # if present.
# PermitRootLogin no
# PasswordAuthentication no
# PubkeyAuthentication yes
# Validate the configuration syntax before restarting
sudo sshd -t
# Restart the daemon to apply changes
sudo systemctl restart sshd
The sshd -t command parses the configuration file and reports syntax errors without restarting the service. If it returns nothing, the syntax is valid. If it prints an error like Missing privilege separation directory: /run/sshd, create the directory and try again. Never restart sshd without validating the config first. Keep your active terminal session open. Open a new terminal window and test the connection with your SSH key. Only close the original session after the new connection succeeds.
Automate security patches
Security patches arrive weekly. Manual updates drift. dnf-automatic handles this. It downloads and installs security updates on a timer. You need to install the package, enable the timer, and verify it runs. This is different from dnf upgrade --refresh, which is the normal weekly maintenance command you run manually. dnf-automatic runs unattended and only applies security updates by default, reducing the risk of breaking custom configurations.
Here is how to install and enable the automatic update timer.
# Install the automatic update package
sudo dnf install -y dnf-automatic
# Enable and start the systemd timer
sudo systemctl enable --now dnf-automatic-install.timer
# Verify the timer is active and scheduled
systemctl list-timers --all | grep dnf-automatic
The timer triggers the dnf-automatic-install.service daily. The service checks for updates, downloads security patches, and applies them. Reboots are not automatic. You must monitor the timer output and schedule reboots during maintenance windows. Check the timer logs with journalctl -xeu dnf-automatic-install.timer to see what was installed and whether a reboot is pending. Trust the package manager. Manual file edits drift, snapshots stay.
Verify the posture
Run these commands to confirm every layer is active and configured correctly.
# Confirm SELinux is enforcing
getenforce
# List active firewall rules for the public zone
firewall-cmd --list-all --zone=public
# Check SSH daemon status and recent log lines
systemctl status sshd
# Verify the automatic update timer is loaded and active
systemctl is-active dnf-automatic-install.timer
Each command should return a clean state. getenforce must print Enforcing. The firewall list should only show ssh and https (or whatever services you explicitly added). systemctl status sshd should show active (running) with no recent failed authentication attempts. The timer check should print active. If any command fails, address it before moving to the next layer. Security is a chain. One weak link breaks the whole posture.
Common pitfalls and what the error looks like
SSH lockout happens when you disable password authentication without uploading your public key first. The daemon will reject the connection and print Permission denied (publickey). Fix it by switching back to permissive mode or using a rescue console to re-enable password auth temporarily.
SELinux denials appear when you place web server files in a non-standard directory. The audit log will show type=AVC msg=audit(...): avc: denied { read } for pid=... comm="httpd" name="index.html". The fix is to restore the correct context with restorecon -Rv /path/to/files instead of disabling the policy.
Firewall rules disappear after a reboot if you forget the --permanent flag. The runtime configuration lives in memory. The persistent configuration lives in XML files under /etc/firewalld/. Always apply permanent rules, then reload.
Automatic updates fail when a third-party repository conflicts with a base package. The timer log will show Error: Transaction test error: package foo conflicts with bar. The conflict is intentional. Read the repository documentation before forcing. Remove the conflicting repo or pin the package version. Do not bypass dependency checks.
When to apply this baseline
Use this baseline hardening when you are running a standard Fedora Server installation that hosts long-lived services. Use a full CIS benchmark when you need compliance certification and can dedicate time to audit every daemon and kernel parameter. Use an immutable variant like Silverblue when you want a known-good base image you can always roll back to. Use containerized workloads when you need to isolate applications and avoid host-level configuration drift. Stay on the upstream Workstation if you only deviate from the defaults occasionally.