DB-유

Security Checklist for a Public Linux Server

· linux, security, ssh

Put a fresh server on a public IP and check /var/log/auth.log ten minutes later: SSH brute-force attempts already, with web scanners close behind. None of it is sophisticated, and none of it works against a machine with a short list of basics done properly. This is the checklist I run on every new internet-facing box before it does any real work. It assumes Ubuntu 20.04 or CentOS 7/8 with systemd, and the commands note the differences.

The list is short on purpose. Fancy hardening frameworks are fine, but the compromises I've actually seen came from password SSH logins, unpatched packages, and services listening on all interfaces that never needed to.

1. A sudo user, and no more root

adduser deploy
usermod -aG sudo deploy          # Ubuntu; on CentOS: usermod -aG wheel deploy

Copy your SSH public key into /home/deploy/.ssh/authorized_keys with ssh-copy-id deploy@server, log in as deploy in a second terminal, and confirm sudo -i works before you lock anything down in the next step. I keep that second terminal open the whole time.

2. Harden SSH

Edit /etc/ssh/sshd_config:

PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
PubkeyAuthentication yes
X11Forwarding no
MaxAuthTries 3
LoginGraceTime 30
AllowUsers deploy
ClientAliveInterval 300
ClientAliveCountMax 2

Check the syntax and reload without dropping your session:

sudo sshd -t
sudo systemctl reload sshd

Moving SSH to a non-standard port reduces log noise but is not a security measure; key-only authentication is. On Ubuntu 20.04 also check /etc/ssh/sshd_config.d/ for drop-in files that could override your settings.

3. Firewall, default deny

Ubuntu with ufw:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

CentOS with firewalld:

sudo firewall-cmd --set-default-zone=public
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-all

Anything else (databases, admin panels, monitoring agents) should be bound to 127.0.0.1 or reached through a VPN or SSH tunnel, not opened here. Restrict SSH to your office or VPN range if it is static: sudo ufw allow from 203.0.113.0/24 to any port 22 proto tcp.

4. Security updates on autopilot

Ubuntu:

sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

Confirm /etc/apt/apt.conf.d/50unattended-upgrades has the ${distro_id}:${distro_codename}-security origin uncommented, and set Unattended-Upgrade::Automatic-Reboot "false"; unless you are happy with unscheduled reboots. On CentOS 8 use dnf-automatic:

sudo dnf install dnf-automatic
sudo sed -i 's/^upgrade_type = .*/upgrade_type = security/; s/^apply_updates = .*/apply_updates = yes/' /etc/dnf/automatic.conf
sudo systemctl enable --now dnf-automatic.timer

On CentOS 7 the equivalent is yum-cron with update_cmd = security.

5. fail2ban

sudo apt install fail2ban        # or: sudo dnf install epel-release fail2ban

Create /etc/fail2ban/jail.local rather than editing jail.conf:

[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
banaction = ufw
ignoreip = 127.0.0.1/8 203.0.113.0/24

[sshd]
enabled = true

On CentOS set banaction = firewallcmd-ipset. Then sudo systemctl enable --now fail2ban and check with sudo fail2ban-client status sshd. If you run nginx, the nginx-http-auth and nginx-botsearch jails are worth enabling too.

6. What's actually listening?

sudo ss -tulpn
systemctl list-units --type=service --state=running

Every line in the ss output with 0.0.0.0 or [::] in the local address is reachable from the internet unless the firewall blocks it. Typical offenders are MySQL on 3306, Redis on 6379 and Docker-published ports, which bypass ufw entirely by inserting their own iptables rules. Bind them to localhost in their own configuration (bind-address = 127.0.0.1 in MySQL, bind 127.0.0.1 in Redis, -p 127.0.0.1:8080:8080 for Docker) and systemctl disable --now anything you do not recognise.

7. Logs and time

Keep auditd or at least persistent journald logs, and know where to look:

sudo mkdir -p /var/log/journal && sudo systemctl restart systemd-journald
sudo journalctl -u sshd --since "24 hours ago" | grep -i "accepted"
sudo last -a | head

Set a timezone and NTP so that logs correlate across servers: sudo timedatectl set-timezone UTC and confirm timedatectl reports System clock synchronized: yes.

Test it from outside

From another machine, scan your own server:

nmap -sS -Pn -p- your.server.ip
ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no deploy@your.server.ip

The scan should show only the ports you opened. The second command must fail with Permission denied (publickey). Then attempt six bad logins from another host and confirm fail2ban-client status sshd lists the IP as banned.

Ways I've locked myself out

Locked out after enabling ufw. You enabled the firewall before allowing SSH. Use the provider's console or rescue mode and run ufw allow 22/tcp. Always add the SSH rule before ufw enable.

"Permission denied (publickey)" for your own key. Permissions are too open: ~/.ssh must be 700 and authorized_keys 600, owned by the user. On CentOS also check sudo restorecon -Rv /home/deploy/.ssh for SELinux labels.

fail2ban is running but never bans. The backend cannot read the log. On systems that log only to journald, set backend = systemd in jail.local.

Docker containers reachable despite the firewall. Docker manipulates iptables directly. Either publish to 127.0.0.1 as above or put your rules in the DOCKER-USER chain.

All of it takes about twenty minutes. Do it before the server is in DNS, and rerun the ss -tulpn check every time you install something new.