RED HAT ENTERPRISE LINUX

Firewall & SELinux

Controlling Network Access to Services

College-Level Course Module | RHEL System Administration

Learning Objectives

1
Understand firewalld concepts

Zones, services, ports, and runtime vs permanent configuration

2
Configure firewall rules with firewall-cmd

Allow services, open ports, manage zones

3
Manage SELinux port bindings

Allow services to bind to non-standard ports

4
Troubleshoot network access issues

Diagnose firewall blocks and SELinux denials

Defense in Depth

Network security uses multiple layers. The firewall controls external access; SELinux controls internal service behavior. Both must allow traffic for connections to succeed.

Layer 1: Firewall (firewalld)
Controls which external connections reach the system
↓ If allowed ↓
Layer 2: SELinux Port Binding
Controls which services can listen on which ports
↓ If allowed ↓
Layer 3: Service Configuration
The actual application listening and responding
Troubleshooting order: When connections fail, check: (1) Is the service running? (2) Is the firewall allowing traffic? (3) Is SELinux allowing the port binding?

Introduction to firewalld

firewalld is RHEL's dynamic firewall manager. It provides a frontend to the kernel's netfilter framework, allowing runtime changes without disrupting existing connections.

# Check firewalld status
[root@server ~]# systemctl status firewalld
● firewalld.service - firewalld - dynamic firewall daemon
     Loaded: loaded (/usr/lib/systemd/system/firewalld.service; enabled)
     Active: active (running) since Mon 2024-01-20 10:00:00 EST

# firewalld should be enabled and running by default
[root@server ~]# systemctl enable --now firewalld

# Check if firewalld is running
[root@server ~]# firewall-cmd --state
running
Default behavior: firewalld is enabled by default in RHEL. It blocks all incoming connections except those explicitly allowed, while permitting all outgoing connections.

Firewall Zones

Zones define trust levels for network connections. Each zone has a set of rules determining what traffic is allowed. Interfaces and connections are assigned to zones.

ZoneTrust LevelDefault Behavior
dropNoneDrop all incoming, no reply sent
blockNoneReject all incoming with ICMP message
publicLowDefault zone - ssh and dhcpv6-client only
externalLowFor routers - masquerading enabled
dmzLowFor DMZ servers - limited access
workMediumFor work networks - ssh, dhcpv6-client
homeMediumFor home networks - more services allowed
internalHighFor internal networks - similar to home
trustedFullAll traffic accepted - use with caution!
Default zone: The public zone is the default. New interfaces are assigned here unless configured otherwise.

Managing Zones

# List all available zones
[root@server ~]# firewall-cmd --get-zones
block dmz drop external home internal nm-shared public trusted work

# Show the default zone
[root@server ~]# firewall-cmd --get-default-zone
public

# Show active zones and their interfaces
[root@server ~]# firewall-cmd --get-active-zones
public
  interfaces: eth0 eth1

# Change the default zone
[root@server ~]# firewall-cmd --set-default-zone=internal

# Assign an interface to a different zone
[root@server ~]# firewall-cmd --zone=internal --change-interface=eth1 --permanent
[root@server ~]# firewall-cmd --reload

# Show all settings for a zone
[root@server ~]# firewall-cmd --zone=public --list-all
public (active)
  target: default
  interfaces: eth0
  services: cockpit dhcpv6-client ssh
  ports: 
  ...

Runtime vs Permanent

Critical concept! firewalld maintains two configurations: runtime (active now, lost on reload/reboot) and permanent (saved, applied on reload/reboot).

# Runtime change - immediate but temporary
[root@server ~]# firewall-cmd --add-service=http
success
# This is active NOW but lost after reload or reboot

# Permanent change - saved but not active until reload
[root@server ~]# firewall-cmd --add-service=http --permanent
success
# This is saved but NOT active yet

# Apply permanent changes to runtime
[root@server ~]# firewall-cmd --reload

# Best practice: Do both at once
[root@server ~]# firewall-cmd --add-service=http
[root@server ~]# firewall-cmd --add-service=http --permanent

# Or use --runtime-to-permanent after testing
[root@server ~]# firewall-cmd --add-service=http
# Test that it works...
[root@server ~]# firewall-cmd --runtime-to-permanent

Adding Services

Services are predefined port and protocol combinations. Using services is preferred over raw port numbers - they are self-documenting and can include multiple ports.

# List all available predefined services
[root@server ~]# firewall-cmd --get-services
RH-Satellite-6 amanda-client amanda-k5-client ... http https ... ssh

# Show what a service includes
[root@server ~]# firewall-cmd --info-service=http
http
  ports: 80/tcp
  protocols:
  ...

# Add a service to the default zone
[root@server ~]# firewall-cmd --add-service=http --permanent
[root@server ~]# firewall-cmd --add-service=https --permanent
[root@server ~]# firewall-cmd --reload

# Add service to a specific zone
[root@server ~]# firewall-cmd --zone=internal --add-service=nfs --permanent

# Remove a service
[root@server ~]# firewall-cmd --remove-service=http --permanent

# List services in the current zone
[root@server ~]# firewall-cmd --list-services
cockpit dhcpv6-client http https ssh

Opening Ports

When no predefined service exists, open ports directly. Specify the port number and protocol (tcp or udp).

# Open a specific port
[root@server ~]# firewall-cmd --add-port=8080/tcp --permanent
[root@server ~]# firewall-cmd --reload

# Open a range of ports
[root@server ~]# firewall-cmd --add-port=5000-5100/tcp --permanent

# Open UDP port
[root@server ~]# firewall-cmd --add-port=53/udp --permanent

# Open port in specific zone
[root@server ~]# firewall-cmd --zone=internal --add-port=3306/tcp --permanent

# List open ports
[root@server ~]# firewall-cmd --list-ports
8080/tcp 5000-5100/tcp

# Remove a port
[root@server ~]# firewall-cmd --remove-port=8080/tcp --permanent

# Verify configuration
[root@server ~]# firewall-cmd --list-all
Best practice: Use services when available. Only use raw port numbers for custom applications without predefined service definitions.

Rich Rules

Rich rules provide fine-grained control - allowing traffic from specific sources, logging, rate limiting, and more complex conditions.

# Allow HTTP only from specific network
[root@server ~]# firewall-cmd --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" service name="http" accept' --permanent

# Allow SSH from single IP
[root@server ~]# firewall-cmd --add-rich-rule='rule family="ipv4" source address="10.0.0.50" service name="ssh" accept' --permanent

# Block specific IP
[root@server ~]# firewall-cmd --add-rich-rule='rule family="ipv4" source address="192.168.1.100" drop' --permanent

# Allow port with logging
[root@server ~]# firewall-cmd --add-rich-rule='rule family="ipv4" port port="8080" protocol="tcp" log prefix="Custom8080: " accept' --permanent

# List rich rules
[root@server ~]# firewall-cmd --list-rich-rules

# Remove a rich rule (must match exactly)
[root@server ~]# firewall-cmd --remove-rich-rule='rule family="ipv4" source address="192.168.1.100" drop' --permanent

Common Tasks

# Web server setup (HTTP and HTTPS)
[root@server ~]# firewall-cmd --add-service={http,https} --permanent
[root@server ~]# firewall-cmd --reload

# Database server (MySQL) - internal zone only
[root@server ~]# firewall-cmd --zone=internal --add-service=mysql --permanent

# Custom application on port 8443
[root@server ~]# firewall-cmd --add-port=8443/tcp --permanent
[root@server ~]# firewall-cmd --reload

# Check what is allowed
[root@server ~]# firewall-cmd --list-all
public (active)
  target: default
  interfaces: eth0
  services: cockpit dhcpv6-client http https ssh
  ports: 8443/tcp
  ...

# Temporarily test a change (auto-reverts after timeout)
[root@server ~]# firewall-cmd --add-service=ftp --timeout=300
# FTP allowed for 5 minutes, then automatically removed
Pro tip: Use --timeout=SECONDS to test firewall changes. If you lock yourself out, the rule auto-reverts!

SELinux Port Bindings

SELinux port bindings control which SELinux types (services) can bind to which network ports. Even if the firewall allows traffic, SELinux must permit the service to use that port.

Firewall allows port 8080
SELinux: Can httpd bind to 8080?
Service starts (or fails)
# List all SELinux port bindings
[root@server ~]# semanage port -l
SELinux Port Type              Proto    Port Number
http_port_t                    tcp      80, 81, 443, 488, 8008, 8009, 8443, 9000
ssh_port_t                     tcp      22
...

# Filter for specific service type
[root@server ~]# semanage port -l | grep http
http_port_t                    tcp      80, 81, 443, 488, 8008, 8009, 8443, 9000
...

Adding Port Bindings

# Check if a port is already assigned
[root@server ~]# semanage port -l | grep 8080
http_port_t                    tcp      80, 81, 443, 488, 8008, 8009, 8443, 9000
# 8080 is included in http_port_t - httpd can use it

# Add a new port to an existing type
[root@server ~]# semanage port -a -t http_port_t -p tcp 8888
# Now httpd can bind to port 8888

# Verify the addition
[root@server ~]# semanage port -l | grep 8888
http_port_t                    tcp      8888

# Add port for SSH on non-standard port
[root@server ~]# semanage port -a -t ssh_port_t -p tcp 2222

# Remove a port assignment
[root@server ~]# semanage port -d -t http_port_t -p tcp 8888

# Modify existing assignment (if port already assigned to different type)
[root@server ~]# semanage port -m -t http_port_t -p tcp 8888
Note: semanage requires the policycoreutils-python-utils package. Install if missing: dnf install policycoreutils-python-utils

Common Port Types

Port TypeServicesDefault Ports
http_port_tApache, Nginx, web servers80, 443, 8008, 8009, 8443
ssh_port_tOpenSSH server22
mysqld_port_tMySQL, MariaDB3306
postgresql_port_tPostgreSQL5432
dns_port_tBIND, DNS servers53
smtp_port_tPostfix, mail servers25, 465, 587
ftp_port_tvsftpd, FTP servers21
nfs_port_tNFS server2049
# Find the port type for a specific service
[root@server ~]# semanage port -l | grep -i mysql
mysqld_port_t                  tcp      1186, 3306, 63132-63164

# Find what type a specific port uses
[root@server ~]# semanage port -l | grep " 80"
http_port_t                    tcp      80, 81, 443, 488, 8008...

SELinux Troubleshooting

# Scenario: Apache configured for port 8888 but fails to start
[root@server ~]# systemctl start httpd
Job for httpd.service failed...

# Check SELinux denials in audit log
[root@server ~]# ausearch -m avc -ts recent
type=AVC msg=... denied { name_bind } for ... 
src=8888 ... scontext=system_u:system_r:httpd_t:s0 
tcontext=system_u:object_r:unreserved_port_t:s0 ...

# Use audit2why for explanation
[root@server ~]# ausearch -m avc -ts recent | audit2why
Was caused by:
The boolean nis_enabled was set incorrectly.
...OR...
Missing type enforcement (TE) allow rule.
    You can use audit2allow to generate a loadable module...

# Get specific fix suggestion
[root@server ~]# ausearch -m avc -ts recent | audit2allow
#============= httpd_t ==============
allow httpd_t unreserved_port_t:tcp_socket name_bind;

# Better solution: add port to http_port_t
[root@server ~]# semanage port -a -t http_port_t -p tcp 8888
[root@server ~]# systemctl start httpd   # Now works!

Combining Firewall + SELinux

When running a service on a non-standard port, you typically need to configure both the firewall AND SELinux. Missing either will cause connection failures.

# Example: Run Apache on port 8888

# Step 1: Configure Apache to listen on 8888
[root@server ~]# vi /etc/httpd/conf/httpd.conf
# Change: Listen 8888

# Step 2: Add SELinux port binding
[root@server ~]# semanage port -a -t http_port_t -p tcp 8888

# Step 3: Open firewall port
[root@server ~]# firewall-cmd --add-port=8888/tcp --permanent
[root@server ~]# firewall-cmd --reload

# Step 4: Start/restart the service
[root@server ~]# systemctl restart httpd

# Step 5: Verify
[root@server ~]# ss -tlnp | grep 8888
LISTEN  0  128  *:8888  *:*  users:(("httpd",pid=1234,fd=4))

[root@server ~]# curl http://localhost:8888
<html>...</html>

Troubleshooting Flow

1. Is the service running?
systemctl status SERVICE | ss -tlnp | grep PORT
↓ Yes ↓
2. Is SELinux allowing the port binding?
semanage port -l | grep PORT | ausearch -m avc
↓ Yes ↓
3. Is the firewall allowing traffic?
firewall-cmd --list-all | firewall-cmd --list-ports
↓ Yes ↓
4. Check network/routing/client issues
ping | traceroute | client firewall

Diagnostic Commands

# Check if service is listening
[root@server ~]# ss -tlnp | grep httpd
LISTEN 0 128 *:80 *:* users:(("httpd",pid=1234...))

# Check firewall status
[root@server ~]# firewall-cmd --list-all

# Check SELinux mode
[root@server ~]# getenforce
Enforcing

# Check SELinux port assignments
[root@server ~]# semanage port -l | grep http_port_t

# Check for SELinux denials
[root@server ~]# ausearch -m avc -ts recent

# Test connectivity from local
[root@server ~]# curl http://localhost:80

# Test connectivity from remote (on client)
[user@client ~]$ curl http://server:80
[user@client ~]$ nc -zv server 80
Connection to server 80 port [tcp/http] succeeded!

# Check iptables rules (low-level)
[root@server ~]# iptables -L -n | head -20

Common Scenarios

ScenarioFirewallSELinux
Web server on port 80/443 --add-service={http,https} Default ports - no change needed
Web server on port 8080 --add-port=8080/tcp Default in http_port_t - no change needed
Web server on port 9999 --add-port=9999/tcp semanage port -a -t http_port_t -p tcp 9999
SSH on port 2222 --add-port=2222/tcp semanage port -a -t ssh_port_t -p tcp 2222
MySQL internal only --zone=internal --add-service=mysql Default port - no change needed
MySQL on port 13306 --add-port=13306/tcp semanage port -a -t mysqld_port_t -p tcp 13306
Pattern: Standard ports usually need only firewall changes. Non-standard ports need both firewall AND SELinux configuration.

Best Practices

Do

  • Use services instead of raw ports when possible
  • Always use --permanent with firewall-cmd
  • Test changes with --timeout before committing
  • Use zones to segment network trust levels
  • Keep SELinux in enforcing mode
  • Document all port and service changes
  • Use rich rules for source-based restrictions
  • Verify with --list-all after changes

Do Not

  • Disable firewalld on production systems
  • Set SELinux to permissive/disabled permanently
  • Open ports without understanding what uses them
  • Use trusted zone for internet-facing interfaces
  • Forget --permanent (changes lost on reboot)
  • Ignore SELinux denials - fix them properly
  • Skip testing after firewall changes
  • Make firewall changes via SSH without --timeout
Critical: When making firewall changes via SSH, always use --timeout. If you accidentally block SSH, the rule auto-reverts and you regain access.

Key Takeaways

1

Firewall (firewalld): Controls external access. Use firewall-cmd to add services/ports. Always use --permanent.

2

Zones: Group interfaces by trust level. Default is public. Use appropriate zones for different networks.

3

SELinux Ports: Controls which services can bind to which ports. Use semanage port -a for non-standard ports.

4

Both Required: Non-standard ports need both firewall AND SELinux configuration. Check both when troubleshooting.

LAB EXERCISES

  • Configure Apache on port 8888 (firewall + SELinux)
  • Move SSH to port 2222 (both configurations required)
  • Create rich rule allowing HTTP only from 192.168.1.0/24
  • Troubleshoot a service that fails to start (SELinux denial)
  • Configure different zones for different interfaces
  • Use --timeout to safely test firewall changes

Next: Managing Containers with Podman