Secure phpMyAdmin Installation on Linux: Authentication, IP Restrictions, and Nginx Reverse Proxy

MySQL tutorial - IT technology blog
MySQL tutorial - IT technology blog

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.

Share: