Updated
Was my Linux server hacked? Read-only checks for suspicious logins and processes
Your cloud provider flags an "unusual login" or "possible crypto-mining", the CPU is pinned for no obvious reason, or your login records are full of IP addresses you've never seen. The question on your mind is whether someone already has control of the machine. This guide gives developers and site owners who run their own servers a set of read-only checks to tell "someone is trying" apart from "someone may have got in", so you can decide whether to escalate.
The limits come first:
Neither this guide nor OpsMate can tell you whether a server has been compromised, and nothing here can prove a server is clean. OpsMate is not a security product and does not do intrusion detection.
If an attacker does control the machine, they may have tampered with tools like
psandlsor edited the logs. No output from that machine can be fully trusted.Until you know more, don't delete files, kill processes or reboot. All three destroy evidence and can make you believe the problem is gone when it isn't.
The commands below work on common Linux distributions. Log paths and service names vary, so adjust them to your system.
0. Before you start: preserve the evidence
Note when you noticed the problem (with the time zone) and keep the original alert text.
If your provider already reports mining, malware or outbound attacks, take a snapshot of the system disk from the cloud console before you go further. A snapshot doesn't modify the running system but it does cost storage; the call is yours.
Copy the output of every step below to somewhere off this server, such as local notes.
1. Someone trying, or someone in?
Start with successful logins. Ubuntu/Debian usually log to /var/log/auth.log; RHEL, CentOS, Rocky and similar use /var/log/secure. If neither exists (for example on journald-only systems), query the journal:
sudo grep -E 'Accepted (password|publickey)' /var/log/auth.log | tail -n 50
sudo journalctl -u ssh -u sshd --since "7 days ago" --no-pager | grep -E 'Accepted|Failed password|Invalid user' | tail -n 50
Summarize successful logins by method, user and source IP (use the log path that exists on your system):
sudo grep -hoE 'Accepted [a-z-]+ for [^ ]+ from [^ ]+' /var/log/auth.log | sort | uniq -c | sort -rn
Then look at failed attempts and who is logged in right now:
sudo grep -hoE 'Failed password for (invalid user )?[^ ]+ from [^ ]+' /var/log/auth.log | awk '{print $NF}' | sort | uniq -c | sort -rn | head -n 20
who
last -a -n 30
sudo lastb -a -n 30
Any server with port 22 open to the internet collects a steady stream of
Failed passwordandInvalid userlines. Failed attempts on their own do not mean you were breached.Acceptedis what matters. A user you don't recognize, a source IP you can't explain, or a login at a time when nobody on your side was working is a strong signal to escalate.lastandlastbread the wtmp/btmp records. Some newer distributions (Debian 13, for example) no longer ship them; uselslogins --failedor the journal query above instead.
What this does not tell you: logs rotate and only cover a limited window; logs can be edited; and finding no suspicious Accepted line doesn't mean nobody got in. Attackers don't have to come through SSH. A web application bug or a leaked key works just as well.
While you're here, check the SSH authentication settings actually in effect. You'll need this for hardening decisions later (read-only, requires root):
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|pubkeyauthentication) '
passwordauthentication yes means password logins are allowed, which gives brute-force attempts a chance to succeed. permitrootlogin without-password (the same as prohibit-password) means root can only log in with a key.
2. Unusual processes and outbound connections
ps -eo pid,ppid,user,lstart,cmd --sort=-%cpu | head -n 15
sudo ss -tnp state established
sudo ss -ltnup
For the top CPU consumers, ask: do I recognize the name, which user runs it, and when did it start (
lstart)? Random-looking names, names imitating system processes, and heavy processes running as an unexpected user are all worth writing down.ss -tnp state establishedshows live connections. Look for an unfamiliar process holding a long-lived connection to an unknown external address.ss -ltnupshows listening ports; look for any you never opened.
For a suspicious PID, check its executable and working directory (replace PID with the number):
sudo ls -l /proc/PID/exe /proc/PID/cwd
An executable in /tmp, /var/tmp or /dev/shm, or one marked (deleted) (the file was removed after the program started), deserves serious attention.
What this does not tell you: high CPU can be legitimate workload, and well-hidden malware may not appear in ps or ss output at all.
3. Signs of a way back in
sudo crontab -l
crontab -l
sudo ls -la /var/spool/cron/ /var/spool/cron/crontabs/ 2>/dev/null
sudo ls -la /etc/cron.d/ /etc/cron.hourly/ /etc/cron.daily/
cat /etc/crontab
systemctl list-timers --all --no-pager
sudo find /etc/systemd/system -type f -newermt '2026-09-01' -ls
Scheduled jobs you don't recognize, especially ones that pipe
curlorwgetinto a shell or decode base64 and run it, are a common persistence trick.find -newermtlists systemd unit files changed after a given date. Set the date to just before the point you think trouble started.
Then accounts and keys:
awk -F: '$3 == 0 {print $1}' /etc/passwd
awk -F: '$3 >= 1000 && $3 < 65534 {print $1, $6, $7}' /etc/passwd
sudo find /root /home -name authorized_keys -exec ls -l --time-style=long-iso {} \;
sudo ls -la /etc/sudoers.d/
ls -l /etc/ld.so.preload
Normally
rootis the only account with UID 0.Make sure every regular user belongs to someone you know, then check each
authorized_keysfile's modification time and contents for keys you never added./etc/ld.so.preloaddoesn't exist on most systems. If it's there and you don't know why, take it seriously.
What this does not tell you: modification times can be faked, and finding nothing doesn't mean nothing is there.
4. What to do with what you found
Only failed attempts, no suspicious successful logins or processes: that's normal background noise for a public server. Hardening is worth considering, but every item is a change you decide on: key-only authentication, no root password logins, restricting port 22 to your own IP in the security group, a tool such as fail2ban. When you edit the SSH config, keep one logged-in session open, run sudo sshd -t to check the syntax, and test a fresh login from a new window. If something breaks, use the open session to revert. Your cloud console's VNC or serial access is the last-resort way back in. Afterwards, consider adding "spike in failed logins" and "new listening port" to your routine read-only checks; see the small-team VPS monitoring checklist.
Treat it as a likely compromise if you see any of these: a successful login you can't explain, an unknown UID 0 account or authorized_keys entry, an executable running from a temp directory or already deleted, or a malicious-activity alert your cloud provider has confirmed. Then:
Snapshot the disk if you didn't in step 0, to preserve evidence.
If you need to contain damage, restrict inbound and outbound traffic with security group rules in the cloud console rather than deleting files or killing processes on the machine. That's a change too: record the existing rules so you can restore them, and make sure you still have console access.
Contact your cloud provider's security support or a security professional.
Rebuilding from a clean image and restoring data from a known-good backup taken before the compromise is usually the safer route. Deleting the miner and carrying on leaves you unable to confirm every backdoor is gone.
From a clean device, rotate every credential the machine touched: SSH keys, database passwords, API keys in your app config. If you ever logged into this machine with a password, treat that password as exposed too.
Running these checks in OpsMate
OpsMate puts the SSH terminal and AI on the same page. You can type every command above yourself, or ask the AI in plain language to help. It proposes troubleshooting commands, and beyond the auth logs it can use ps, ss and journalctl for checks. After a command has run, click Analyze to get a summary of sources, users and a timeline. The commands and output stay in the terminal for you to check (see AI and SSH commands in one workspace).
Think about redaction first. When you click
Analyze, the command output is sent to cloud AI for analysis; what the desktop app keeps on your own machine by default is your SSH credentials. If the logs contain customer IPs, real usernames or anything else you'd rather not send, don't clickAnalyzeon them. Run the commands yourself, replace IPs and usernames with placeholders such asIP_AandUSER_A, and then ask the AI about the redacted excerpt.An AI summary is not a verdict. It can help you build a timeline and point at lines worth reading. It cannot decide whether you were compromised, and it certainly cannot prove the machine is clean.
The AI is mainly for troubleshooting. Dangerous commands are blocked. Snapshots, isolation, rebuilds and credential rotation are for you and whoever handles security to decide. For what OpsMate does and doesn't do on its own, see the FAQ.
The desktop app keeps SSH credentials on your own machine by default. Even so, once you suspect a compromise, rotate any password you used on that server and the keys associated with it.
Illustrative example
Illustrative example (not a real customer case): the cloud provider sends a "possible crypto-mining" alert. ps shows a process with a random-looking name using all the CPU, and /proc/PID/exe points at a file in /tmp marked (deleted). ss shows it holding a long-lived connection to an unfamiliar external address. The auth log has a 3 a.m. password login for user deploy from 203.0.113.45, sshd -T shows password authentication enabled, and that user's crontab downloads and runs a script every 10 minutes. Together this strongly suggests the machine is under someone else's control, though it's still not a formal verdict. The sensible path is to snapshot, isolate at the security-group level, contact the provider's security team, rebuild from a clean image and rotate all credentials. Killing the process, deleting the crontab line and carrying on would likely leave behind a backdoor nobody has found. (The IP address is from a range reserved for documentation.)
AI diagnostic prompt
I'm worried about suspicious logins on this server. Without changing anything, check the sources, users and timing of successful and failed SSH logins over the last 7 days, the top CPU processes and their executable paths, established external connections, cron jobs and systemd timers, accounts with UID 0, and the modification times of authorized_keys files. List only what you observe and what deserves attention. Do not conclude that the server is "compromised" or "safe", and tell me what information is missing. Do not delete files, kill processes, change configuration or reboot. If you recommend any change, explain its risks and rollback first and wait for my approval.
Try OpsMate
500 free AI calls per month and unlimited servers. The desktop app keeps your SSH credentials on your own machine by default.
Need help interpreting the evidence?
OpsMate helps developers and operators investigate with AI. Review the evidence. After you click Analyze, the command output is sent to cloud AI for analysis; redact sensitive information first.