2 AM, a server alert, and phpMyAdmin wide open to the world
Last month I got a fail2ban alert at 2 AM: nearly 400 requests per minute hitting /phpmyadmin on a production server. A bot scanner was brute-forcing — something a colleague had installed temporarily three months earlier and never touched again.
What made it worse: phpMyAdmin was running on port 80, no additional auth, no IP restrictions, root username still set to default. I had to block it temporarily with iptables while tracking down whoever set it up — the answer was “I thought it was only for internal use.”
I’ve dealt with a database corruption incident at 3 AM that required a full restore from backup — since then I check backups daily. But backups won’t help if an attacker has already dumped all your data before you even notice.
Why phpMyAdmin Is a High-Value Target
phpMyAdmin is a web-based MySQL management tool — convenient, and for exactly that reason it’s one of the top targets for bot scanners. Common weak points:
- Predictable default URLs:
/phpmyadmin,/pma,/mysql— bots scan all of them continuously, 24/7. - No additional authentication layer: Relying solely on MySQL username/password. A weak password means full database exposure.
- Open to all IPs: There’s no reason phpMyAdmin needs to be publicly accessible if it’s only used internally.
- Outdated versions with CVEs: phpMyAdmin has a history of serious RCE vulnerabilities. Running an old version exposed to the internet is a significant risk.
Ways to Secure phpMyAdmin
Option 1: Change the Default URL (minimal, not enough)
Change the path from /phpmyadmin to something harder to guess. With Nginx, just update the location block. This reduces noise from bot scanners but won’t stop a targeted attack — an attacker can still find it via fingerprinting.
Option 2: Restrict by IP with iptables/ufw
If you only administer from a fixed set of IPs (office, VPN), block everything else:
# Allow office and VPN IPs
sudo ufw allow from 203.0.113.10 to any port 443 proto tcp comment "pma office"
sudo ufw allow from 10.0.0.0/8 to any port 443 proto tcp comment "pma vpn"
sudo ufw deny 443
Effective, but inconvenient when your IP is dynamic or you’re working remotely — you have to manually update the whitelist each time.
Option 3: HTTP Basic Auth (better, works well in combination)
Add an authentication layer before the phpMyAdmin login form:
# Create the password file
sudo apt install apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd admin_pma
# Enter a strong password when prompted
This forces bots to get past HTTP Basic Auth before they even see the phpMyAdmin login form — far more effective than just changing the URL.
The Best Approach: Nginx Reverse Proxy + IP Whitelist + Basic Auth
Combine all three: phpMyAdmin runs locally (not directly exposed), with Nginx as a reverse proxy enforcing IP whitelisting and Basic Auth. This is the setup I use in production, with no issues after more than a year.
Step 1: Install phpMyAdmin
sudo apt update
sudo apt install phpmyadmin php-mbstring php-zip php-gd php-json php-curl
# When prompted for web server: select "none" if using Nginx + PHP-FPM
# Configure database for phpmyadmin: Yes
# Enter MySQL root password to create the internal phpMyAdmin database
Step 2: Configure phpMyAdmin — disable root login
Edit /etc/phpmyadmin/config.inc.php to disable root login and restrict to local connections:
<?php
// Connect via local socket instead of TCP
$cfg['Servers'][$i]['host'] = '127.0.0.1';
// Disable root login — force use of a dedicated user
$cfg['Servers'][$i]['AllowRoot'] = false;
// Disable large file import if not needed
$cfg['UploadDir'] = '';
$cfg['SaveDir'] = '';
Step 3: Create a dedicated MySQL user for phpMyAdmin
Never use root to log into phpMyAdmin. Create a user with limited privileges:
-- Create user restricted to local connections only
CREATE USER 'pma_admin'@'127.0.0.1' IDENTIFIED BY 'StrongPass_2024!';
-- Grant minimal privileges — no SUPER, no GRANT OPTION
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER
ON *.* TO 'pma_admin'@'127.0.0.1';
-- Or restrict to a specific database only
GRANT ALL PRIVILEGES ON myapp_db.* TO 'pma_admin'@'127.0.0.1';
FLUSH PRIVILEGES;
Step 4: Configure Nginx Reverse Proxy
Create the Nginx config file. I changed the URL to something no one would guess:
server {
listen 443 ssl;
server_name db.example.com;
ssl_certificate /etc/letsencrypt/live/db.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/db.example.com/privkey.pem;
# Restrict by IP — office and VPN only
allow 203.0.113.10;
allow 10.0.0.0/8;
deny all;
# Block default URLs — return 404
location ~ ^/(phpmyadmin|pma|mysql|dbadmin) {
return 404;
}
location /dbmanager_x7k2 {
# Second authentication layer: HTTP Basic Auth
auth_basic "Restricted Access";
auth_basic_user_file /etc/nginx/.htpasswd;
alias /usr/share/phpmyadmin;
index index.php;
try_files $uri $uri/ =404;
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $request_filename;
include fastcgi_params;
}
location ~* \.(js|css|png|jpg|ico)$ {
expires max;
log_not_found off;
}
}
}
# Test syntax and reload
sudo nginx -t
sudo systemctl reload nginx
Step 5: Rate limiting and fail2ban configuration
# In nginx.conf, http block:
limit_req_zone $binary_remote_addr zone=pma:10m rate=5r/m;
# In the phpMyAdmin location block:
limit_req zone=pma burst=10 nodelay;
# /etc/fail2ban/filter.d/phpmyadmin-auth.conf
[Definition]
failregex = ^<HOST> .* "POST .*(phpmyadmin|dbmanager).* HTTP.*" (200|401)
ignoreregex =
# /etc/fail2ban/jail.local
[phpmyadmin-auth]
enabled = true
port = http,https
filter = phpmyadmin-auth
logpath = /var/log/nginx/access.log
maxretry = 5
bantime = 3600
sudo systemctl restart fail2ban
sudo fail2ban-client status phpmyadmin-auth
Full verification check
After setting up, I run a quick check to make sure everything is correct:
# Default URL must return 404
curl -k -o /dev/null -w "%{http_code}" https://db.example.com/phpmyadmin
# Expected: 404
# Without Basic Auth credentials must return 401
curl -k -o /dev/null -w "%{http_code}" https://db.example.com/dbmanager_x7k2/
# Expected: 401
# Try from an IP not in the whitelist (use mobile data or another VPS)
# Expected: 403 Forbidden
# Verify root cannot log in
mysql -u root -p -e "SHOW GRANTS FOR 'pma_admin'@'127.0.0.1';"
A few final notes
- Always use HTTPS: Basic Auth over plain HTTP is pointless since credentials are transmitted as plain text. Let’s Encrypt is free — there’s no excuse to skip it.
- Update phpMyAdmin regularly: Check GitHub releases monthly, and never run a version older than 6 months.
- Disable when not in use: If you only need phpMyAdmin occasionally, comment out the location block in Nginx between uses — the safest option.
- Keep logs long enough: Nginx access logs should be retained for at least 30 days for incident analysis.
This setup sounds like a lot of steps, but it actually takes about 30 minutes. After spending a night dealing with a brute-force attack at 2 AM, I can say those 30 minutes are absolutely worth it compared to spending hours restoring a database at midnight — not to mention explaining to clients why their data may have been compromised.

