How to Harden a Fedora Server

Complete Security Checklist

Secure a Fedora server by enforcing SELinux, locking down SSH, enabling firewalld, applying automatic updates, and reducing the attack surface through service minimization.

You just provisioned a Fedora Server instance

The IP is active, and ssh connects instantly. The default install is functional, but it leaves the front door unlocked for automated scanners. You need a hardening baseline that blocks noise, enforces access controls, and survives a reboot without breaking your workflow. Rushing through a checklist often leads to a locked-out root account or a firewall that drops your own traffic. Take the time to verify each step before moving to the next.

What's actually happening

Hardening is the process of reducing the attack surface. Fedora ships with strong security defaults, including SELinux and firewalld, but the configuration prioritizes functionality over restriction. A fresh install allows password authentication, runs unnecessary services, and may not auto-update security patches. Hardening means disabling features you do not use, enforcing strict access controls, and ensuring the system patches itself against known vulnerabilities. Think of it as moving from a default configuration that assumes you might need everything to a configuration that assumes you only need what you explicitly allow.

Security is a state, not an event. Configure the controls, then verify they persist across reboots.

Keep the system updated

Fedora's release cadence is six months. Security patches arrive frequently. Manual updates are error-prone. Configure automatic updates to keep the system patched without intervention.

Run the initial update to apply any pending patches and refresh metadata.

sudo dnf upgrade --refresh -y
# --refresh forces dnf to ignore cached metadata and fetch fresh repo data
# This ensures you get the latest security advisories immediately

Install and enable the automatic update timer. dnf-automatic runs daily checks and installs updates safely.

sudo dnf install -y dnf-automatic
sudo systemctl enable --now dnf-automatic-install.timer
# Enable the timer to run daily checks
# dnf-automatic handles dependency resolution and avoids broken transactions

dnf upgrade --refresh is the normal weekly maintenance command for manual checks. dnf system-upgrade is for crossing major Fedora releases. They are different commands. Do not conflate them.

Schedule updates. A server that doesn't update is a server that gets compromised.

Enforce SELinux in enforcing mode

SELinux is Mandatory Access Control. It restricts processes to only the resources they need. Even if a service is compromised, the attacker cannot access files outside the allowed domain. Fedora enforces SELinux by default. Verify the mode is not accidentally set to permissive.

Check the current runtime mode.

getenforce
# Verify the current mode is Enforcing
# If this returns Permissive, your policies are logging but not blocking

If the mode is Permissive, switch to Enforcing. Edit the persistent configuration file. Config files in /etc/ are user-modified. Files in /usr/lib/ ship with the package. Edit /etc/. Never edit /usr/lib/.

# /etc/selinux/config
SELINUX=enforcing
# Ensure the persistent config matches the runtime state
# Changes here only take effect after a reboot

Apply the runtime change immediately and reboot to persist.

sudo setenforce 1
# Switch to enforcing mode immediately
# This applies the policy without waiting for a reboot

SELinux denials show up in journalctl -t setroubleshoot with a one-line summary. Read those before disabling SELinux. The summary often includes a command to fix the context or add a policy.

Trust SELinux. If a service fails, check the audit log before disabling the policy.

Enable and configure firewalld

Firewalld manages firewall rules using zones. The default zone is public. For a server, the drop zone is safer. drop rejects all incoming traffic. You must explicitly allow services.

Start firewalld and set the default zone.

sudo systemctl enable --now firewalld
# Start the firewall service and ensure it runs on boot
sudo firewall-cmd --permanent --set-default-zone=drop
# Set the default zone to drop all incoming traffic
# This is stricter than the default public zone

Allow SSH traffic. You need access to the server.

sudo firewall-cmd --permanent --zone=drop --add-service=ssh
# Allow SSH traffic in the permanent configuration
sudo firewall-cmd --reload
# Reload is mandatory to apply permanent changes to the runtime
# Without this, your new rules are invisible to the kernel

firewall-cmd --reload after every rule change. Otherwise the runtime config and the persistent config diverge.

Verify the configuration.

sudo firewall-cmd --list-all
# Confirm only SSH is allowed in the drop zone
# Check that the default zone is drop

Reload the firewall after every rule change. Runtime and persistent configs diverge otherwise.

Harden SSH

SSH is the primary access point. Hardening SSH is critical. Disable root login. Attackers target root. Disable password authentication. Use keys. Keys are harder to brute-force. Set MaxAuthTries to limit attempts. Use drop-in files in /etc/ssh/sshd_config.d/. Package updates modify the main config. Drop-ins survive updates.

Create the hardening configuration file.

sudo tee /etc/ssh/sshd_config.d/99-hardening.conf <<'EOF'
PermitRootLogin no
# Prevent direct root login
PasswordAuthentication no
# Disable password logins
PubkeyAuthentication yes
# Ensure key-based auth is explicitly enabled
X11Forwarding no
# Disable X11 forwarding unless you need GUI apps
MaxAuthTries 3
# Limit authentication attempts per connection
AllowUsers youruser
# Restrict access to specific users
# Replace youruser with your actual username
EOF

Restart the SSH daemon. Test the new configuration in a separate terminal before closing your current session. A bad config kills the connection. You lose access.

sudo systemctl restart sshd
# Restart the daemon to apply configuration changes
# Always test the new config in a separate terminal before closing your current session

Make sure your SSH key is in place before disabling password auth. Verify access with a new terminal.

Test the SSH config in a second terminal before closing your active session. A typo here locks you out.

Disable unused services

Unused services are risk. avahi-daemon broadcasts presence. cups prints. A server doesn't need these. Disable them. systemctl disable --now stops and disables. Fewer services reduce the attack surface.

List running services.

systemctl list-units --type=service --state=running
# Review active services to identify unnecessary daemons

Disable services you do not need.

sudo systemctl disable --now avahi-daemon cups
# Disable and stop services you do not need
# Fewer running services mean fewer potential vulnerabilities

Disable what you do not use. Every running service is a potential entry point.

Set up fail2ban

Scanners hit SSH constantly. Fail2ban monitors logs. It bans IPs that fail auth too many times. Install and enable fail2ban. It works with firewalld. It adds rules dynamically.

sudo dnf install -y fail2ban
sudo systemctl enable --now fail2ban
# Install and start the intrusion prevention system
# fail2ban monitors logs and bans IPs that show malicious signs

Install an intrusion prevention tool. Automated scanners hit every IP; block the noise.

Configure automatic security auditing

File integrity monitoring detects changes. AIDE builds a database of file hashes. Compare against the database. Detect tampering. Initialize the database. Move the file. Schedule checks.

Install AIDE and initialize the database.

sudo dnf install -y aide
sudo aide --init
# Initialize the file integrity database
sudo mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
# Move the new database into place
# aide compares current file states against this baseline

Schedule daily checks via cron or a systemd timer. Run the check regularly. Alert on changes.

Initialize the integrity database. You cannot detect tampering without a known-good baseline.

Restrict core dumps

Core dumps contain memory. Memory has secrets. Disable core dumps. limits.conf sets user limits. sysctl sets kernel pattern. Apply sysctl. Prevent dumps.

Configure limits and sysctl.

echo '* hard core 0' | sudo tee -a /etc/security/limits.conf
# Prevent core dumps for all users
echo 'kernel.core_pattern=|/bin/false' | sudo tee -a /etc/sysctl.d/99-hardening.conf
# Redirect core dump pattern to discard output
sudo sysctl --system
# Apply sysctl settings from all config files

Disable core dumps. They can leak sensitive memory contents to disk.

Run compliance scans

Compliance scanning checks against standards. OpenSCAP checks against SSG profiles. Run the standard profile. Get a report. Review findings. Fix gaps.

Install OpenSCAP and run the scan.

sudo dnf install -y openscap-scanner scap-security-guide
sudo oscap xccdf eval --profile xccdf_org.ssgproject.content_profile_standard \
  /usr/share/xml/scap/ssg/content/ssg-fedora-ds.xml
# Run the standard profile assessment
# This generates a detailed report of compliance gaps

Run the scanner regularly. Compliance drifts over time as packages update.

Verify it worked

Check the firewall, SELinux, and SSH state. Confirm the configuration matches expectations.

sudo firewall-cmd --list-all
# Confirm only SSH is allowed in the drop zone
getenforce
# Confirm Enforcing mode
systemctl is-active sshd
# Confirm SSH is running

Verify the state. Trust, but verify the configuration.

Common pitfalls and what the error looks like

The most common failure is disabling password authentication without verifying key-based access works. If you see Permission denied (publickey), you have locked yourself out. Keep a second terminal open to test the new SSH configuration before closing your active session.

Another pitfall is editing /etc/ssh/sshd_config directly instead of using drop-in files in /etc/ssh/sshd_config.d/. Direct edits risk being overwritten by package updates. Always use drop-ins.

SELinux denials often look like permission errors. If a service fails to start after hardening, check journalctl -t setroubleshoot for context. Do not disable SELinux to fix a permission error. Fix the context or add a policy.

Forgetting to reload firewalld causes rules to not apply. If firewall-cmd --list-all shows old rules, you forgot --reload.

Read the error before guessing. SELinux denials and SSH rejections tell you exactly what went wrong.

When to use this vs alternatives

Use the drop zone in firewalld when you are running a headless server with no public web interface. Use the public zone when you need to allow multiple services with moderate trust. Use fail2ban when you want aggressive IP banning based on log patterns. Use sshguard when you prefer a lighter-weight approach integrated with the system logger. Use AIDE for file integrity monitoring on critical infrastructure. Use OpenSCAP when you need a scored compliance report against a standard benchmark. Use drop-in config files when you want your settings to survive package updates. Use dnf-automatic when you cannot guarantee manual updates happen weekly.

Choose tools that match your threat model. Over-hardening breaks functionality; under-hardening invites compromise.

Where to go next