A single misconfigured service, an unpatched library, or a stolen password can turn your server into an attacker's playground. I've seen data breaches start from the simplest oversights: an exposed backup directory, a default admin password, or a WordPress plugin that hadn't been updated in two years.
The good news? Most breaches follow predictable patterns. When you layer defenses across encryption, access control, monitoring, and response, you force attackers to overcome multiple barriers instead of one. Here are ten controls that actually stop intrusions at the server and application level.
1. Encrypt data at rest on disk
Files sitting on disk—databases, backups, uploaded documents—are vulnerable if an attacker gains shell access or physical access to your drive. Full-disk encryption (LUKS on Linux) protects the entire volume, but you still need to unlock it at boot. For more granular control, encrypt individual directories or database tablespaces.
On a typical VPS running MySQL, enable transparent encryption for InnoDB tables in your my.cnf:
[mysqld]
early-plugin-load=keyring_file.so
keyring_file_data=/var/lib/mysql-keyring/keyring
innodb_encrypt_tables=ON
innodb_encrypt_log=ON
Restart MySQL and new tables will be encrypted automatically. Existing tables need an ALTER TABLE statement to convert. The keyring file itself should live on a separate mount with restricted permissions.
For file-level encryption, eCryptfs or gocryptfs work well for user upload directories. The key is keeping encryption keys out of the same directory tree you're protecting.
2. Enforce TLS everywhere traffic moves
HTTP traffic is readable by anyone on the network path. This includes passwords, session cookies, and API tokens. Even internal traffic between your web server and database should be encrypted if those hosts sit on a shared network or cross availability zones.
For web traffic, redirect HTTP to HTTPS at the server level and enable HSTS to prevent downgrade attacks:
# Apache
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
# Nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
For MySQL connections from your application server, edit my.cnf on the database host:
[mysqld]
require_secure_transport=ON
Then configure your app's database connection string to verify the certificate. In PHP with PDO:
$options = [
PDO::MYSQL_ATTR_SSL_CA => '/etc/ssl/certs/ca-cert.pem',
PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => true,
];
API calls to third-party services, mail submission (port 587 with STARTTLS), and even Redis connections should use TLS when crossing untrusted networks.
3. Require multi-factor authentication for admin access
Passwords alone are not enough. Phishing, credential stuffing, and keyloggers bypass even strong passwords. MFA adds a second factor—usually a time-based code from an authenticator app—that attackers can't reuse.
For SSH, integrate Google Authenticator PAM module:
sudo apt install libpam-google-authenticator
Run google-authenticator as each user to generate a secret, then edit /etc/pam.d/sshd and add:
auth required pam_google_authenticator.so
In /etc/ssh/sshd_config, set:
ChallengeResponseAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
Restart sshd. Now SSH requires both your private key and a TOTP code. For WordPress admin panels, plugins like WP 2FA handle TOTP setup. For cPanel, WHM has built-in two-factor support under Security Center.
Don't skip service accounts. If your deploy script or monitoring agent logs in with a password, that password can be stolen too. Use key-based auth or short-lived tokens wherever possible.
4. Log all authentication and privilege escalation events
You can't investigate what you didn't record. At minimum, capture successful and failed logins, sudo usage, and any change to user accounts or file permissions.
Linux logs these to /var/log/auth.log (Debian/Ubuntu) or /var/log/secure (RHEL/CentOS) by default. Make sure rsyslog or systemd-journald is running and configure log rotation so you don't fill the disk.
For centralized collection, ship logs to a remote syslog server or a SIEM. A simple rsyslog forward rule:
*.* @@log-server.example.com:514
On the application side, log login attempts, password resets, and permission changes. In a typical web app:
error_log("Failed login attempt for user: $username from IP: $ip");
Store these in a separate log file and rotate them daily. If an attacker compromises the web server, the first thing they'll do is clear logs. Off-host storage makes that harder.
5. Scan for vulnerabilities on a schedule
Software ages badly. A library that was secure last year might have a published exploit today. Automated scanning catches known CVEs before attackers do.
For system packages, unattended-upgrades on Debian-based systems applies security patches automatically:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
For web applications, dependency checkers like npm audit, composer audit, or pip-audit report vulnerable packages in your project. Run them in CI pipelines and fail the build on high-severity findings.
Port scanning your own servers with nmap or masscan shows what an attacker sees:
nmap -sV -p- your-server.com
Close any port you didn't intend to expose. If MySQL is listening on 0.0.0.0:3306, bind it to 127.0.0.1 instead unless remote access is genuinely required.
6. Apply least-privilege access to files and services
Every process and user should have the minimum permissions needed to do their job. When your web server runs as root, a single exploit gives the attacker full system access. When it runs as www-data with read-only access to application code, the blast radius shrinks.
For file permissions, separate application code from writable upload directories. A typical setup:
chown -R root:www-data /var/www/html
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;
chown -R www-data:www-data /var/www/html/uploads
For database users, grant only the permissions each application needs. Your CMS doesn't need DROP TABLE in production:
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'webapp'@'localhost';
For Linux users, disable root SSH login and use sudo with a whitelist of allowed commands in /etc/sudoers.d/. Avoid wildcard ALL=(ALL:ALL) grants.
7. Harden SSH configuration
SSH is the front door to your server. Default settings allow password authentication and permit root login, both of which attackers abuse in brute-force campaigns.
Edit /etc/ssh/sshd_config and set:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
Port 2222 # optional: move off the default port
Restart sshd and test the connection before closing your current session. If you lock yourself out, you'll need console access from your hosting provider's control panel.
Fail2ban adds automatic IP blocking after repeated failed attempts:
sudo apt install fail2ban
sudo systemctl enable fail2ban
The default jail watches SSH and bans IPs for ten minutes after five failures. For servers under heavy attack, consider dropping all traffic from non-whitelisted countries using iptables with a GeoIP database.
8. Deploy a web application firewall
A WAF sits in front of your application and filters malicious requests—SQL injection attempts, XSS payloads, and scanner probes. Cloudflare's free tier includes basic WAF rules. For self-hosted setups, ModSecurity with the OWASP Core Rule Set integrates with Apache and Nginx.
Install ModSecurity on Nginx:
sudo apt install libnginx-mod-security
sudo mkdir /etc/nginx/modsec
sudo cp /usr/share/modsecurity-crs/crs-setup.conf.example /etc/nginx/modsec/crs-setup.conf
Then enable it in your site config:
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/main.conf;
Start with detection mode (SecRuleEngine DetectionOnly) and review logs for false positives before switching to blocking mode. WAFs are noisy and will block legitimate traffic if you skip tuning.
9. Back up data and test restoration
Backups are the last line of defense when prevention fails. Ransomware, accidental deletions, and hardware failures all require working backups to recover from.
Automate daily backups to off-site storage. For databases:
mysqldump --all-databases --single-transaction > /backup/mysql-$(date +%F).sql
For files, rsync to a remote server or use a cloud provider's object storage. Encrypt backups before uploading:
tar czf - /var/www | openssl enc -aes-256-cbc -out backup.tar.gz.enc -pass file:/etc/backup.key
Store the encryption key separately, never on the same server. Test restoration quarterly by spinning up a clean VM and verifying you can rebuild the application from backups alone. If you can't, your backup process is broken.
10. Write and drill an incident response plan
When you discover a breach at 2 a.m., you won't think clearly. A written plan tells you what to check first, who to notify, and how to contain the damage.
Your plan should cover:
- Immediate containment: disconnect compromised systems, revoke credentials, block attacker IPs
- Forensics: preserve logs, take disk snapshots, document the timeline
- Communication: internal team escalation, customer notification if PII was exposed, law enforcement if required
- Recovery: rebuild from clean backups, patch the vulnerability, rotate all secrets
Run a tabletop exercise twice a year. Present a scenario—"an attacker accessed the database server via a compromised WordPress plugin"—and walk through the response steps. You'll find gaps in access, missing runbooks, and assumptions that don't hold under pressure.
Why defense-in-depth matters
No single control stops every attack. Encryption protects data at rest but doesn't prevent an attacker from exfiltrating it through the application. MFA blocks stolen passwords but won't stop an unpatched RCE exploit. Logging records the breach but doesn't prevent it.
When you stack controls, you force attackers to chain multiple exploits. That takes more skill, more time, and more noise—all of which increase your chances of detecting and stopping them before they reach sensitive data. In support tickets I handled, breaches that succeeded were almost always missing two or more of these controls, not just one.
What to check first
Start with SSH hardening, TLS enforcement, and automated patching—three controls that close the most common entry points. Then add MFA, logging, and vulnerability scanning. The remaining controls refine your defense as your infrastructure matures. Security is not a one-time project. Treat it as operational discipline, like backups or monitoring, and review your posture every quarter.
