A server that suddenly becomes slow, drops connections or shows a traffic spike may be under attack, but it may just as well be a busy backup job or a legitimate traffic peak. This tutorial shows how to tell the difference on Ubuntu 24.04 using built-in tools: ss for connections, tcpdump for packets, journalctl for SSH, and your web server logs. You will also check for signs that an attacker already got in, and learn how to block a source quickly.
Prerequisites
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
- A non-root user with
sudoprivileges and a working SSH or console session. - UFW as the host firewall (the examples use it to block addresses).
- The name of your public network interface. Find it with
ip -br addr; the examples useeth0.
Step 1 - Getting a quick overview
Start with the symptoms. Check the load, memory and who is logged in:
uptime
free -h
w
Then look at the socket summary. A healthy web server has mostly estab and some timewait connections:
ss -s
Total: 1843
TCP: 2210 (estab 1702, closed 360, orphaned 3, timewait 355)
Compare these numbers with what is normal for your server. If you have no baseline, run the same commands at a quiet time later and keep the output. Everything in the next steps is about finding what is unusual, and "unusual" only means something relative to normal.
Step 2 - Looking for connection floods
List the remote addresses with the most established connections. With a state filter, ss omits the state column, so the peer address is the fourth field:
ss -Htn state established | awk '{print $4}' | sed -E 's/:[0-9]+$//; s/[][]//g' | sort | uniq -c | sort -rn | head
412 203.0.113.50
38 198.51.100.7
12 192.0.2.14
One address holding hundreds of connections while others have a handful is a strong sign of abuse or a misbehaving client. Look up the owner of the address with whois 203.0.113.50 (install whois if needed) before blocking it: it could be your own monitoring or a partner's proxy.
A SYN flood shows up as many half-open connections in SYN-RECV:
ss -Htn state syn-recv | wc -l
A few dozen is normal on a busy server. Thousands, with a rising count between runs, indicate a SYN flood. Ubuntu enables SYN cookies by default, which keeps the server responsive under moderate floods. Confirm it:
sysctl net.ipv4.tcp_syncookies
net.ipv4.tcp_syncookies = 1
Step 3 - Measuring traffic volume
Volumetric attacks saturate the network link rather than the CPU. Read the interface counters twice, a few seconds apart, and compare:
ip -s link show dev eth0
For a live per-connection view, install iftop:
sudo apt install iftop
sudo iftop -i eth0 -nNP
-n and -N skip DNS and port name lookups (important under attack, when lookups add load), and -P shows ports. Press q to exit. If a single source or destination port dominates the bandwidth, note it for the next step.
Noteif the attack fills your uplink, nothing on the server itself can fix it, because the traffic is already consuming the link before your firewall sees it. That kind of attack has to be filtered upstream. Contact CubePath support with the target IP, the time it started and what you observed.
Step 4 - Sampling packets with tcpdump
tcpdump shows what the traffic actually is. Capture a limited sample so the command stops on its own, and count packets per source address (IPv4):
sudo tcpdump -nni eth0 -c 2000 -q 'ip and not port 22' 2>/dev/null | awk '{print $3}' | cut -d. -f1-4 | sort | uniq -c | sort -rn | head
Excluding port 22 keeps your own SSH session out of the count. To see only new TCP connection attempts (SYN without ACK), which is what a SYN flood consists of:
sudo tcpdump -nni eth0 -c 50 'tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn'
UDP floods often target or come from ports of common amplification services such as DNS (53), NTP (123) or memcached (11211):
sudo tcpdump -nni eth0 -c 50 udp
Save a short capture if you need to share it with support or analyze it later in Wireshark:
sudo tcpdump -nni eth0 -c 10000 -w /tmp/attack.pcap
Step 5 - Detecting SSH brute force
Every server with SSH exposed receives login attempts all day. What matters is the volume and whether any of them succeeded. Count failed attempts in the last hour:
sudo journalctl -u ssh --since "1 hour ago" | grep -cE 'Failed password|Invalid user'
List the addresses sending the most attempts today:
sudo journalctl -u ssh --since today | grep -oP '(Failed password for (invalid user )?\S+|Invalid user \S+) from \K[0-9a-fA-F.:]+' | sort | uniq -c | sort -rn | head
More important is whether anyone logged in successfully. Review every accepted login and make sure you recognize each user and address:
sudo journalctl -u ssh --since "7 days ago" | grep 'Accepted'
Sep 24 09:12:44 web01 sshd[21873]: Accepted publickey for deploy from 192.0.2.14 port 51022 ssh2: ED25519 SHA256:...
You can also review the login history kept in wtmp:
last -a | head -20
If password authentication is enabled and you see thousands of failures, disabling passwords in favor of SSH keys and installing fail2ban removes almost all of this risk.
Step 6 - Detecting attacks on web applications
Application-level attacks look like legitimate traffic at the network level, so the web server log is the place to find them. For Nginx, count requests per client address:
sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
See which paths are being hit the most. Scanners stand out by requesting files you do not have:
sudo awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
8812 /wp-login.php
2310 /xmlrpc.php
940 /.env
301 /
Look at requests from one suspicious address in detail:
sudo grep '^203.0.113.50 ' /var/log/nginx/access.log | tail -20
For Apache, the same commands work on /var/log/apache2/access.log. A single IP hammering wp-login.php or xmlrpc.php is a brute force against WordPress; requests for /.env, /.git/config or phpmyadmin are automated vulnerability scanners.
Step 7 - Checking for signs of compromise
Attacks that succeed tend to leave traces. Check for these even if the previous steps looked normal.
Processes using unexpected CPU are a classic sign of a cryptocurrency miner:
ps -eo pid,user,%cpu,etime,cmd --sort=-%cpu | head -15
List every listening port with the owning process, and question anything you did not install:
sudo ss -tulpn
Look for user accounts with a login shell that you did not create, and for unexpected keys in authorized_keys:
awk -F: '$7 !~ /(nologin|false)$/ {print $1, $3, $7}' /etc/passwd
sudo find / -xdev -name authorized_keys -exec ls -l {} \;
Check scheduled jobs, a common persistence method:
sudo ls -la /etc/cron.d /var/spool/cron/crontabs
systemctl list-timers --all
Finally, verify installed package files against their checksums to detect replaced binaries:
sudo apt install debsums
sudo debsums -s
No output means every file matches. Configuration files you edited yourself are not reported by -s, but modified binaries are.
Warningif you find evidence of a compromise, such as an unknown user, a replaced binary or an unknown process running as root, do not try to clean the server in place. Take a snapshot for analysis, rebuild from a clean image, restore data from a backup made before the intrusion, and rotate every password and key the server had access to.
Step 8 - Blocking an attacking address
For a single abusive address, insert a deny rule at the top of the UFW rule list so it is evaluated before your allow rules:
sudo ufw insert 1 deny from 203.0.113.50
For a whole range:
sudo ufw insert 1 deny from 203.0.113.0/24
Confirm the rule is in place and in first position:
sudo ufw status numbered
Manual blocks do not scale against distributed attacks. For recurring SSH or web brute force, fail2ban blocks offenders automatically based on the same log patterns you checked above. For HTTP floods, rate limit in the web server (for example, limit_req in Nginx). For attacks that exceed your link capacity, filtering must happen upstream, as noted in Step 3.
Conclusion
You checked the server at each layer: connection counts and SYN floods with ss, bandwidth with iftop, packet content with tcpdump, SSH and web logs for brute force and scans, and the system itself for signs of compromise. Keeping a baseline of normal values makes each of these checks much faster to interpret. As next steps, set up fail2ban for SSH and your web server, enable key-only SSH authentication, and configure monitoring with alerts so you notice the next spike before your users do.
