An Auditor’s Checklist for Securing WordPress Backends on DigitalOcean
SSH Hardening and Access Control
Securing the SSH daemon is paramount for any server, especially those hosting critical applications like WordPress. On DigitalOcean, this starts with disabling root login and enforcing key-based authentication. We’ll also configure the SSH daemon to listen on a non-standard port to reduce automated brute-force attempts.
First, ensure you have generated an SSH key pair and added your public key to DigitalOcean’s SSH keys or directly to the ~/.ssh/authorized_keys file on your server. Then, edit the SSH daemon configuration file.
Disabling Root Login and Password Authentication
Connect to your DigitalOcean droplet via SSH. It’s highly recommended to do this as a non-root user with sudo privileges. Then, edit the sshd_config file:
Use your preferred text editor (e.g., nano or vim).
sudo nano /etc/ssh/sshd_config
Locate and modify the following lines:
PermitRootLogin no PasswordAuthentication no PubkeyAuthentication yes
If you are not yet using key-based authentication, you will need to set up your SSH keys and test them thoroughly before disabling password authentication. A common mistake is to disable passwords without a working key, leading to lockout.
Changing the Default SSH Port
To further deter automated attacks, change the default SSH port (22) to a higher, less common port. Choose a port number between 1024 and 65535 that is not already in use by another service. For this example, we’ll use port 2222.
Port 2222
After making these changes, restart the SSH service to apply them:
sudo systemctl restart sshd
Important: When reconnecting, you must specify the new port:
ssh your_user@your_droplet_ip -p 2222
You will also need to update your firewall rules to allow traffic on the new SSH port. If you are using ufw:
sudo ufw allow 2222/tcp sudo ufw delete allow 22/tcp sudo ufw reload
Web Server Configuration (Nginx Example)
Securing the web server is critical for protecting your WordPress site from common web vulnerabilities. This section focuses on Nginx configuration best practices, including disabling unnecessary HTTP methods, enforcing TLS/SSL, and mitigating common attacks.
TLS/SSL Configuration
A properly configured TLS/SSL certificate (e.g., from Let’s Encrypt) is non-negotiable. Ensure you are redirecting all HTTP traffic to HTTPS.
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# Modern TLS settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets off;
# HSTS (HTTP Strict Transport Security)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s; # Google DNS, adjust as needed
resolver_timeout 5s;
# ... rest of your WordPress Nginx configuration ...
root /var/www/yourdomain.com/html;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; # Adjust PHP version
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Deny access to sensitive files
location ~* /(?:uploads|files)/.*\.php$ {
deny all;
}
location ~ /\.ht {
deny all;
}
}
The add_header Strict-Transport-Security directive forces browsers to always use HTTPS, and ssl_stapling improves performance and security by allowing the server to provide OCSP responses directly.
Disabling Unnecessary HTTP Methods
WordPress and its plugins might expose methods like PUT or DELETE that are not typically needed for standard web browsing. Restricting these can prevent certain types of attacks.
location / {
# ... existing location block ...
if ($request_method !~ ^(GET|HEAD|POST)$) {
return 405;
}
}
This configuration snippet will return a 405 Method Not Allowed error for any request that doesn’t use GET, HEAD, or POST.
Rate Limiting and Request Filtering
To protect against brute-force attacks on login pages or XML-RPC endpoints, Nginx’s limit_req module can be invaluable. Ensure the module is compiled into your Nginx binary (it usually is by default).
Add the following to your http block (outside of any server blocks):
http {
# ... other http settings ...
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=5r/min;
limit_req_zone $binary_remote_addr zone=xmlrpc_limit:10m rate=10r/min;
# ... rest of http block ...
}
Then, apply these zones to specific locations within your WordPress server block:
server {
# ... server block settings ...
# Apply rate limiting to the entire site, except for static assets
location / {
limit_req zone=mylimit burst=20 nodelay;
# ... existing location block ...
}
# Specific rate limiting for wp-login.php
location = /wp-login.php {
limit_req zone=mylimit burst=5 nodelay;
try_files $uri $uri/ /index.php?$args;
}
# Specific rate limiting for xmlrpc.php
location = /xmlrpc.php {
limit_req zone=xmlrpc_limit burst=10 nodelay;
try_files $uri $uri/ /index.php?$args;
}
# ... rest of server block ...
}
zone=mylimit:10m defines a shared memory zone named mylimit with a size of 10 megabytes. rate=5r/min sets the maximum request rate to 5 requests per minute. burst=20 allows for a burst of up to 20 requests before rate limiting kicks in. nodelay means requests exceeding the rate are immediately rejected, rather than being delayed.
Database Security (MySQL/MariaDB)
The WordPress database is a prime target. Securing it involves strong credentials, restricted access, and regular backups.
Dedicated Database User and Strong Passwords
Never use the MySQL root user for WordPress. Create a dedicated user with the minimum necessary privileges. Log in to your MySQL/MariaDB server:
sudo mysql -u root -p
Then, create a new user and grant privileges. Replace wp_user, 'strong_password', and your_database_name with your actual values.
CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'strong_password'; GRANT SELECT,INSERT,UPDATE,DELETE,CREATE,ALTER,DROP,INDEX,KEY,REFERENCES ON your_database_name.* TO 'wp_user'@'localhost'; FLUSH PRIVILEGES;
In your wp-config.php file, update the database credentials:
define( 'DB_USER', 'wp_user' ); define( 'DB_PASSWORD', 'strong_password' ); define( 'DB_HOST', 'localhost' ); define( 'DB_NAME', 'your_database_name' );
Restricting Database Access
If your web server and database server are on the same DigitalOcean droplet, the user should only need to connect from localhost. If they are on separate droplets, adjust the host part of the user creation (e.g., 'wp_user'@'your_webserver_ip') and ensure your firewall allows traffic on port 3306 between the droplets.
For enhanced security, consider disabling remote MySQL access entirely if not strictly needed. Edit my.cnf (or mariadb.conf.d/50-server.cnf on some systems):
[mysqld] bind-address = 127.0.0.1
Then restart the MySQL/MariaDB service:
sudo systemctl restart mysql
Regular Backups
Automated, off-site backups are a critical part of any disaster recovery and security plan. Use DigitalOcean’s snapshot feature or a dedicated backup solution. For database backups, a cron job can be set up:
# Example backup script (e.g., /usr/local/bin/backup_db.sh) #!/bin/bash DB_USER="wp_user" DB_PASS="strong_password" DB_NAME="your_database_name" BACKUP_DIR="/var/backups/mysql" DATE=$(date +"%Y-%m-%d_%H-%M-%S") mkdir -p $BACKUP_DIR mysqldump -u $DB_USER -p$DB_PASS $DB_NAME > $BACKUP_DIR/$DB_NAME-$DATE.sql gzip $BACKUP_DIR/$DB_NAME-$DATE.sql # Optional: Remove old backups (e.g., older than 7 days) find $BACKUP_DIR -type f -name "*.gz" -mtime +7 -delete # Optional: Upload to S3 or other cloud storage # aws s3 cp $BACKUP_DIR/$DB_NAME-$DATE.sql.gz s3://your-backup-bucket/mysql/
Make the script executable:
sudo chmod +x /usr/local/bin/backup_db.sh
And add it to cron:
sudo crontab -e
Add a line to run the script daily at 3 AM:
0 3 * * * /usr/local/bin/backup_db.sh >> /var/log/backup_db.log 2>&1
WordPress Core and Plugin Security
Beyond server-level security, the WordPress application itself requires diligent management.
Keep Everything Updated
This is the most fundamental security practice. Outdated software is a primary vector for exploits. Regularly update WordPress core, themes, and plugins. Consider enabling automatic updates for minor core releases.
For plugins and themes, manual updates are often safer to avoid compatibility issues. However, for critical security patches, prompt updates are essential.
Secure File Permissions
Incorrect file permissions can allow attackers to upload malicious files or modify existing ones. The general rule is to set directories to 755 and files to 644. The wp-config.php file should be even more restrictive.
# Navigate to your WordPress root directory
cd /var/www/yourdomain.com/html
# Set directory permissions
find . -type d -exec chmod 755 {} \;
# Set file permissions
find . -type f -exec chmod 644 {} \;
# Set wp-config.php permissions
chmod 640 wp-config.php # Or 600 if not using group access
Ensure the web server user (e.g., www-data for Nginx/Apache on Debian/Ubuntu) has write access only to specific directories like wp-content/uploads and wp-content/upgrade.
sudo chown -R www-data:www-data wp-content/uploads sudo chmod -R 755 wp-content/uploads
Disable File Editing
WordPress’s built-in theme and plugin editor can be a security risk if an attacker gains administrative access. Disable it by adding the following to your wp-config.php file:
define( 'DISALLOW_FILE_EDIT', true );
Security Plugins and WAF
While not a replacement for server-level security, reputable WordPress security plugins (e.g., Wordfence, Sucuri Security) can provide an additional layer of defense, including malware scanning, brute-force protection, and firewalling. A Web Application Firewall (WAF), either as a plugin or a cloud service (like Cloudflare or Sucuri’s WAF), is highly recommended.
Monitoring and Auditing
Continuous monitoring is key to detecting and responding to security incidents.
Log Analysis
Regularly review server logs (Nginx access/error logs, SSH logs, system logs) and WordPress logs (if using a security plugin that generates them). Look for suspicious patterns such as:
- Repeated failed login attempts.
- Requests to non-existent files or unusual URLs.
- Unusual traffic spikes or geographic origins.
- Errors indicating potential exploits.
Tools like fail2ban can automatically parse logs and block offending IP addresses. Configure it to monitor Nginx, SSH, and other relevant logs.
# Example fail2ban jail for Nginx (add to /etc/fail2ban/jail.local) [nginx-http-auth] enabled = true port = http,https filter = nginx-http-auth logpath = /var/log/nginx/access.log maxretry = 3 [nginx-badbots] enabled = true port = http,https filter = nginx-badbots logpath = /var/log/nginx/access.log maxretry = 2 [sshd] enabled = true port = 2222 # Your custom SSH port filter = sshd logpath = /var/log/auth.log maxretry = 3 bantime = 1h
Security Audits
Perform periodic security audits. This includes:
- Reviewing user accounts (both server and WordPress).
- Checking for unauthorized plugins or themes.
- Verifying firewall rules.
- Testing for common web vulnerabilities (e.g., using OWASP ZAP or Nessus).
- Ensuring all security configurations are still in place and effective.
DigitalOcean’s security best practices documentation and compliance checklists can serve as a valuable reference during these audits.