Unix network security: services, firewalls, SSH, NFS and DNS
A Unix server is only as exposed as the services it runs. Find out what listens, remove what you do not need, and filter the rest with a firewall.
Find what is listening
Start with an inventory. ss lists every listening socket and the program that owns it:
sudo ss -tulpn
Netid State Local Address:Port Process
udp UNCONN 127.0.0.1:323 users:(("chronyd",pid=612))
tcp LISTEN 0.0.0.0:22 users:(("sshd",pid=801))
tcp LISTEN 127.0.0.1:3306 users:(("mariadbd",pid=950))
tcp LISTEN 0.0.0.0:443 users:(("nginx",pid=1022))
Read the Local Address column carefully. 127.0.0.1 or ::1 means only local programs can connect. 0.0.0.0, * or [::] means every network interface. In this example the database correctly listens only on localhost.
On older systems without ss, use netstat -tulpn (Linux), sockstat -l (FreeBSD) or netstat -an | grep LISTEN (AIX, Solaris), and lsof -i -P to see the process.
Remove what you do not need
Every service you switch off is one less thing to patch and one less way in. On a typical server you can usually remove printing (CUPS), mDNS (Avahi), Bluetooth, the RPC portmapper unless you use NFS, and any desktop software.
systemctl list-units --type=service --state=running
sudo systemctl disable --now avahi-daemon.service avahi-daemon.socket cups.service
sudo apt purge telnetd rsh-server tftpd-hpa vsftpd # if any of them are installed
Services that should never run on a modern system
| Service | Why | Use instead |
|---|---|---|
| telnet, rlogin, rsh, rexec | Passwords and sessions travel in clear text; rsh trusts host names | SSH |
| FTP | Clear-text passwords | SFTP or HTTPS |
| TFTP | No authentication at all | Only on an isolated install network |
| finger, rusers, rwho | Tell an attacker your user names | Nothing |
| NIS (YP) | Password hashes readable over the network | LDAP or FreeIPA with Kerberos |
| inetd services (echo, chargen, daytime) | Useful only for attacks | Nothing |
On AIX and HP-UX, check /etc/inetd.conf, comment out everything you do not use, then run refresh -s inetd (AIX) or inetd -c (HP-UX). On Solaris 10 and 11, inetd services are managed by SMF: list them with inetadm and switch them off with inetadm -d or svcadm disable.
Bind services to the right address
A database, cache or admin panel that only local programs use should listen on 127.0.0.1, not on every interface. Examples:
# MariaDB / MySQL: /etc/mysql/mariadb.conf.d/50-server.cnf
bind-address = 127.0.0.1
# Redis: /etc/redis/redis.conf
bind 127.0.0.1 -::1
protected-mode yes
# PostgreSQL: /etc/postgresql/*/main/postgresql.conf
listen_addresses = 'localhost'
Host firewall
A firewall on the server itself is your last line of defence if something starts listening by mistake. Allow only what you mean to offer, and deny everything else coming in. Modern Linux uses nftables in the kernel; ufw (Ubuntu) and firewalld (RHEL, Fedora, SUSE) are friendlier front ends to it. Use one of them, not several.
Ubuntu and Debian with ufw
sudo apt install ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow from 10.0.0.0/24 to any port 5432 proto tcp # PostgreSQL from the office network only
sudo ufw allow 80,443/tcp
sudo ufw limit OpenSSH # rate-limit new SSH connections
sudo ufw enable
sudo ufw status verbose
RHEL, Fedora and SUSE with firewalld
sudo firewall-cmd --get-active-zones
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --remove-service=cockpit
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.0.0/24" port port="5432" protocol="tcp" accept'
sudo firewall-cmd --reload
sudo firewall-cmd --list-all
Plain nftables
A complete server rule set in /etc/nftables.conf. Load it with sudo nft -f /etc/nftables.conf and enable it at boot with sudo systemctl enable --now nftables (on RHEL the service reads /etc/sysconfig/nftables.conf):
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
ct state invalid drop
iif "lo" accept
meta l4proto { icmp, ipv6-icmp } accept
tcp dport 22 ct state new limit rate 10/minute accept
tcp dport { 80, 443 } accept
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
When you change firewall rules on a remote server, schedule an automatic rollback first, for example echo "nft flush ruleset" | sudo at now + 5 minutes (install the at package first), and cancel it with sudo atrm once you have confirmed you can still log in.
On the BSDs the firewall is pf (configured in /etc/pf.conf, loaded with pfctl -f). AIX has IP filtering in the genfilt and mkfilt commands, and Solaris 11.4 uses the PF packet filter as well.
SSH
SSH is usually the only service open to the internet for administration. Turn off password logins, block root, limit who may log in and slow down password guessing, as shown in the security guide. If only a few places need to reach SSH, allow just their addresses in the firewall, or put SSH behind a VPN such as WireGuard.
NFS
NFS trusts the client to tell the truth about who the user is, so treat NFS exports carefully.
- Export only to named hosts or a small subnet, never to
*. - Keep
root_squash(the default), so root on a client becomes an unprivileged user on the server. - Export read-only (
ro) wherever clients do not need to write. - Use NFSv4 with Kerberos (
sec=krb5p) when the data is sensitive or the network is shared.
# /etc/exports
/srv/share 10.0.0.21(rw,sync,root_squash,no_subtree_check) 10.0.0.22(ro,sync,root_squash)
sudo exportfs -ra # apply changes
sudo exportfs -v # show what is exported, with options
Time and name services
- Time: use
chronyorsystemd-timesyncdas a client only. A time server should not answer queries from the internet unless that is its job. - DNS: a server that is not a DNS server should not run one. If you run BIND or Unbound, disable recursion for outside clients, so you are not used for amplification attacks.
- Mail: local mail daemons (Postfix, Exim) should listen on localhost only unless the machine is a mail server:
inet_interfaces = loopback-onlyin Postfix.
Kernel network settings
Linux defaults are reasonable on current distributions. On a server that is not a router, check these values in /etc/sysctl.d/90-network.conf:
net.ipv4.ip_forward = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.log_martians = 1
Apply them with sudo sysctl --system. Do not set ip_forward = 0 on hosts that run containers or virtual machines with routed networking, because that breaks their network.
Watch the network
sudo ss -tnp state established # who is connected right now
sudo tcpdump -ni eth0 port 22 # watch SSH traffic on one interface
sudo journalctl -k | grep -i 'nft\|ufw' # packets the firewall logged
Check your server from the outside as well: from another machine, scan it with nmap to see what an attacker would see. Unix security testing explains how.