Approaches to Server Security: Stop Thinking Like It’s 2010

Server Security  /  March 2026

The patterns showing up in server logs over recent months suggest that the attack surface has shifted in some fairly predictable ways. A few straightforward measures appear to address the bulk of it.

The Pattern in the Logs: Digital Ocean

Anyone running a public-facing server and watching their /var/log/auth.log or fail2ban output will likely notice something consistent: a notable proportion of brute force and port scanning activity appears to originate from Digital Ocean IP ranges.

This is not particularly surprising. A low-cost VPS can be provisioned in seconds, carries a clean IP not yet on most blocklists, and can be destroyed without a trace once a campaign is complete. It would appear this has become a fairly common setup for automated credential testing.

This is not a criticism of Digital Ocean specifically. The same pattern appears across AWS, Vultr, Linode and others. It is simply where the activity seems most concentrated at present, based on log observation.

Once you can identify where the traffic is coming from, blocking it at the network level before it reaches your services is relatively straightforward.

Watching the Logs and Blocking at Range Level

Blocking individual IPs as they appear is largely ineffective since the same underlying infrastructure will simply rotate addresses. Watching for patterns across a few days and then blocking the entire subnet tends to be considerably more efficient.

Step 1: Extract the Top Attacking IPs

bash
# Top attacking IPs from auth log
grep "Failed password" /var/log/auth.log | awk '{print $11}' | sort | uniq -c | sort -rn | head -20

Run this over several days. The same /16 or /24 ranges will tend to reappear. That is the signal to act on.

Step 2: Find the Full CIDR Range

bash
whois 167.99.1.1 | grep -i "CIDR\|NetRange\|inetnum"

Step 3: Block the Entire Range

Rather than managing individual IPs, the script below blocks all known Digital Ocean IPv4 ranges in a single pass. Save it as block-digitalocean.sh and run as root. It skips ranges already blocked, detects your OS, and persists the rules across reboots on Debian, Ubuntu, AlmaLinux, and RHEL.

bash
sudo chmod +x block-digitalocean.sh
sudo ./block-digitalocean.sh

The Script: block-digitalocean.sh

bash: block-digitalocean.sh
#!/bin/bash
#
# Block Digital Ocean IP Ranges
# Usage: sudo ./block-digitalocean.sh
#

set -e

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

if [[ $EUID -ne 0 ]]; then
   echo -e "${RED}Error: This script must be run as root${NC}"
   exit 1
fi

DO_RANGES=(
    "5.101.0.0/16"    "24.144.0.0/16"   "24.199.0.0/16"
    "37.139.0.0/16"   "45.55.0.0/16"    "46.101.0.0/16"
    "64.23.0.0/16"    "64.225.0.0/16"   "64.226.0.0/16"
    "64.227.0.0/16"   "67.205.0.0/16"   "67.207.0.0/16"
    "68.183.0.0/16"   "69.55.0.0/16"    "80.240.0.0/16"
    "82.196.0.0/16"   "95.85.0.0/16"    "103.253.0.0/16"
    "104.131.0.0/16"  "104.236.0.0/16"  "104.248.0.0/16"
    "107.170.0.0/16"  "128.199.0.0/16"  "129.212.0.0/16"
    "134.122.0.0/16"  "134.199.0.0/16"  "134.209.0.0/16"
    "137.184.0.0/16"  "138.68.0.0/16"   "138.197.0.0/16"
    "139.59.0.0/16"   "141.0.0.0/16"    "142.93.0.0/16"
    "143.110.0.0/16"  "143.198.0.0/16"  "143.244.0.0/16"
    "144.126.0.0/16"  "146.185.0.0/16"  "146.190.0.0/16"
    "147.182.0.0/16"  "152.42.0.0/16"   "157.230.0.0/16"
    "157.245.0.0/16"  "159.65.0.0/16"   "159.89.0.0/16"
    "159.203.0.0/16"  "159.223.0.0/16"  "161.35.0.0/16"
    "162.243.0.0/16"  "163.47.0.0/16"   "164.90.0.0/16"
    "164.92.0.0/16"   "165.22.0.0/16"   "165.227.0.0/16"
    "165.232.0.0/16"  "165.245.0.0/16"  "167.71.0.0/16"
    "167.99.0.0/16"   "167.172.0.0/16"  "168.144.0.0/16"
    "170.64.0.0/16"   "174.138.0.0/16"  "178.62.0.0/16"
    "178.128.0.0/16"  "185.14.0.0/16"   "188.166.0.0/16"
    "188.226.0.0/16"  "192.34.0.0/16"   "192.81.0.0/16"
    "192.241.0.0/16"  "198.199.0.0/16"  "198.211.0.0/16"
    "204.48.0.0/16"   "206.81.0.0/16"   "206.189.0.0/16"
    "207.154.0.0/16"  "208.68.0.0/16"   "209.38.0.0/16"
    "209.97.0.0/16"
)

is_blocked() { iptables -L INPUT -n | grep -q "$1"; }

save_iptables() {
    if command -v netfilter-persistent &> /dev/null; then
        netfilter-persistent save
    elif [[ -f /etc/redhat-release ]]; then
        iptables-save > /etc/sysconfig/iptables
    else
        iptables-save > /etc/iptables.rules
        if [[ ! -f /etc/network/if-pre-up.d/iptables ]]; then
            echo '#!/bin/sh\n/sbin/iptables-restore < /etc/iptables.rules' \
              > /etc/network/if-pre-up.d/iptables
            chmod +x /etc/network/if-pre-up.d/iptables
        fi
    fi
}

added=0; skipped=0

for range in "${DO_RANGES[@]}"; do
    if is_blocked "$range"; then ((skipped++))
    else
        iptables -I INPUT -s "$range" -j DROP -m comment --comment "DigitalOcean Block"
        ((added++))
    fi
done

echo "Blocked: $added | Skipped: $skipped"
[[ $added -gt 0 ]] && save_iptables

echo "Done. Verify: iptables -L INPUT -n | grep 'DigitalOcean' | wc -l"

1Avoid Predictable Usernames

Every automated credential campaign works from roughly the same list: admin, administrator, root, user, test. If your system account appears on that list, a significant portion of the work has already been done before any real effort is made.

The less obvious improvement is to move away from English usernames entirely. Credential wordlists are almost exclusively English-centric. A username like gweinyddwr (Welsh), rendszergazda (Hungarian), or järjestelmänvalvoja (Finnish) simply will not appear in any standard dictionary attack.

bash
# Create a non-English admin user
adduser gweinyddwr
usermod -aG sudo gweinyddwr

# Disable root SSH login
echo "PermitRootLogin no" >> /etc/ssh/sshd_config
systemctl restart sshd

2A Practical Approach to Password Entropy

Take a memorable word, run it through an MD5 hash, and use a portion of the output as the password. The result is genuinely high-entropy, looks entirely random to anyone who does not know the source word, and can be regenerated at any time without ever being written down.

bash
echo -n "lighthouse" | md5sum
# Output:   6f6c60b5a8e5f6a4b2c3d1e9f7a8b0c2
# Password: 6f6c60b5a8e5 (first 12 characters)

No dictionary-based attack will arrive at 6f6c60b5 by working through common English words. Additional complexity can be introduced by using a phrase rather than a single word, selecting a different character range, or appending a symbol.

3Restrict SSH to Known IP Ranges

There is generally no good reason for SSH to be reachable from the open internet. Restricting access to your known IP ranges at the firewall level means the majority of automated scanners will receive no response and move on.

UFW

bash
ufw allow from 203.0.113.0/24 to any port 22
ufw deny 22
ufw enable

iptables

bash
iptables -A INPUT -p tcp --dport 22 -s 203.0.113.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j DROP

For environments with dynamic IPs, a VPN is the sensible approach. Establish the connection first and SSH from within that tunnel. The VPN endpoint becomes the single controlled entry point.

4Consider a Honeypot for Threat Intelligence

The previous approaches are all preventative. A honeypot serves a different purpose: rather than blocking activity, it allows it into a controlled environment in order to observe it. When an attacker reaches a honeypot, you gain visibility into which vectors were used, what they do once they believe they have access, and where the traffic originated.

This is useful for auditing real systems. If the honeypot shows repeated attempts against a particular service or configuration, that is worth examining in production.

bash: Cowrie SSH Honeypot
apt install python3-virtualenv libssl-dev libffi-dev build-essential
git clone https://github.com/cowrie/cowrie
cd cowrie
virtualenv cowrie-env
source cowrie-env/bin/activate
pip install -r requirements.txt
cp etc/cowrie.cfg.dist etc/cowrie.cfg
bin/cowrie start

Cowrie presents a convincing SSH environment. Everything an attacker types within it is logged in full. The session logs tend to be instructive.

5Maintain Reliable Backups

The layers above reduce the likelihood of a successful intrusion considerably. They do not eliminate it entirely. A zero-day, a misconfigured service, or a compromised credential can all create an opening regardless of how well everything else is configured.

A well-maintained backup changes the calculus significantly. If an attacker gains access, causes damage, and the system is restored within a few minutes from a clean snapshot, the effort has achieved nothing of lasting consequence. The time spent on the attack is simply wasted.

Daily rsync to a Remote Server

bash
# Sync web root and config to a remote backup server
rsync -avz --delete /var/www/ user@backup-server:/backups/www/
rsync -avz --delete /etc/ user@backup-server:/backups/etc/

Nightly Database Dumps via Cron

bash
# MySQL / MariaDB nightly backup
mysqldump -u root -p --all-databases | gzip > /backups/db-$(date +%F).sql.gz

# Cron entry: runs at 2am daily
0 2 * * * mysqldump -u root -p --all-databases | gzip > /backups/db-$(date +%F).sql.gz

A backup that has never been tested is not a backup in any meaningful sense. Run a restore drill on a test machine periodically so the steps are familiar when they are actually needed.


Summary

Layer Approach What It Addresses
Network Block known attack ranges Removes entire blocks of abusive infrastructure
Identity Non-English usernames Dictionary and credential stuffing campaigns
Auth MD5-derived passwords Brute force and pattern-based cracking
Access IP-restricted SSH Automated scanning and opportunistic access
Intel Honeypot deployment Visibility into attacker methods and tooling
Recovery Tested backups and snapshots Ensures a successful attack has no lasting impact

None of this requires significant budget or specialist tooling. Most of it is a matter of configuration discipline. The automated activity showing up in server logs at present does not appear especially sophisticated. Systems that present even modest resistance tend to be skipped in favour of easier targets.

Further Reading

How to Deploy OpenAKC (Authorized Key Chain)

The approaches above reduce the attack surface considerably. OpenAKC takes a different step altogether. It is an open-source authentication gateway that allows the authorized_keys mechanism to be disabled entirely across an estate, with SSH trust managed centrally. It also introduces the ability to strip specific Linux capabilities from root, meaning even a fully privileged user cannot touch files or directories you have designated as protected. If centralised access control, full session recording, and granular root capability management are relevant to your environment, the deployment guide is worth reading.

nicktailor.com ↗

Leave a Reply

Your email address will not be published. Required fields are marked *