How to Secure phpMyAdmin: IP Restriction, Basic Auth, and Fail2ban

A database cylinder enclosed in a protective shield. Text: SECURE PHPMYADMIN

We may earn a referral fee at no additional cost to you when you shop through the links on this page.

Bots probe /phpmyadmin around the clock, because a default install puts a database admin panel one password away from anyone who finds it. This DevAnswers guide layers three real defenses in front of an existing Apache or nginx install: an IP allowlist, a second password prompt, and automatic brute-force banning.

Quick Answer: The Three Moves That Matter 

If you only do three things, do these. Restrict access to your own IP, put HTTP basic authentication in front of the login page, and let Fail2ban ban repeat offenders. On Apache: an IP allowlist looks like this (one caveat: the packaged phpmyadmin.conf ships a Require all granted that must also be removed, covered in the first section below):

Apache
<Directory /usr/share/phpmyadmin>
    Require ip 203.0.113.10
</Directory>

On nginx: the equivalent goes inside the phpMyAdmin location block:

Nginx
location ^~ /phpmyadmin {
    allow 203.0.113.10;
    deny all;

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}

The rest of this guide explains each layer, when to reach for which, and how to get back in if you lock yourself out. All of it assumes phpMyAdmin is already installed and served over HTTPS; if it is not yet, start with our guides on installing phpMyAdmin and forcing HTTPS. The paths below are the Debian and Ubuntu layout, where phpMyAdmin ships as a distro package.

Why phpMyAdmin Is a Target 

phpMyAdmin is the single most probed admin path on the web after /wp-admin. It lives at a well-known URL, it talks directly to your database, and a successful login is game over for your data. Automated scanners request /phpmyadmin, /pma, and a dozen variants on every server they touch, hoping for a default install with a weak MySQL password. Two things are non-negotiable before any of the layers below: phpMyAdmin must be served over HTTPS so credentials are not sent in the clear, and it must be kept updated through your package manager so known vulnerabilities are patched.

Restrict Access by IP Address 

If you administer your database from a static IP, the strongest and simplest control is to refuse every other address. On Apache: add a Require ip directive to the phpMyAdmin config and reload:

Terminal
sudo nano /etc/apache2/conf-available/phpmyadmin.conf
/etc/apache2/conf-available/phpmyadmin.conf
<Directory /usr/share/phpmyadmin>
    Require ip 203.0.113.10
</Directory>

Save and exit (press CTRL + X, press Y, then press ENTER)

The packaged phpmyadmin.conf already contains a <Directory /usr/share/phpmyadmin> block, often with Require all granted. Edit that block rather than adding a second one, and remove any existing Require all granted line. Apache 2.4 combines multiple Require directives as an implicit <RequireAny>, so leaving Require all granted in place quietly lets everyone through and undoes your restriction.
Terminal
sudo apachectl configtest && sudo systemctl reload apache2
Output
Syntax OK

On nginx: use allow and deny in the phpMyAdmin location block. The ^~ modifier is essential: without it, a later location ~ \.php$ block takes priority for /phpmyadmin/index.php and your allowlist never runs. Because ^~ makes this block win for every phpMyAdmin request, it must also carry the PHP-FPM handler. Match the socket to your PHP-FPM version (run ls /run/php/*-fpm.sock to find it, or copy the fastcgi_pass line from your existing PHP location block); the php8.3 socket shown here is only an example. Open the file with sudo nano /etc/nginx/sites-available/default, adjusting the filename to your site, add the block, then validate and reload:

/etc/nginx/sites-available/default
location ^~ /phpmyadmin {
    allow 203.0.113.10;
    deny all;

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}

Save and exit (press CTRL + X, press Y, then press ENTER)

Terminal
sudo nginx -t && sudo systemctl reload nginx
Output
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
If your connection uses a dynamic IP (most home broadband) or you travel, an IP allowlist will lock you out the moment your address changes. Use basic auth plus Fail2ban instead, which do not depend on a fixed address.

Add a Second Password with HTTP Basic Auth 

Basic auth puts a web-server password prompt in front of phpMyAdmin’s own login. An attacker now has to get past two independent credentials, which stops the credential-stuffing bots cold: they never even reach the phpMyAdmin page. The htpasswd tool comes with Apache; on an nginx-only server, install it from apache2-utils first. Then create the password file:

Terminal
sudo apt install apache2-utils && sudo mkdir -p /etc/phpmyadmin
sudo htpasswd -c /etc/phpmyadmin/.htpasswd billy

You will be prompted to set a password. Make it a strong one with the password generator. The -c flag creates the file, so use it only for this first user; to add more accounts later, run htpasswd without -c or you overwrite the file and lose the existing entries. On Apache: point the phpMyAdmin directory at that file with Require valid-user:

Terminal
sudo nano /etc/apache2/conf-available/phpmyadmin.conf
/etc/apache2/conf-available/phpmyadmin.conf
<Directory /usr/share/phpmyadmin>
    AuthType Basic
    AuthName "Restricted Access"
    AuthUserFile /etc/phpmyadmin/.htpasswd
    Require valid-user
</Directory>

Save and exit (press CTRL + X, press Y, then press ENTER)

Validate and reload to apply it (a clean Syntax OK, as before):

Terminal
sudo apachectl configtest && sudo systemctl reload apache2

On nginx: put auth_basic and auth_basic_user_file in the ^~ /phpmyadmin block. Keep the ^~ modifier and the nested PHP handler so the prompt covers phpMyAdmin’s PHP, not just its static files:

Terminal
sudo nano /etc/nginx/sites-available/default
/etc/nginx/sites-available/default
location ^~ /phpmyadmin {
    auth_basic "Restricted Access";
    auth_basic_user_file /etc/phpmyadmin/.htpasswd;

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}

Save and exit (press CTRL + X, press Y, then press ENTER)

Then test and reload:

Terminal
sudo nginx -t && sudo systemctl reload nginx

To confirm basic auth is live, open /phpmyadmin in a private browser window: you should get a browser username and password prompt before phpMyAdmin’s own login page appears. Two prompts means both layers are working.

Layer both together. If you have a static IP and want the allowlist and the password, combine them so an attacker must clear both. On Apache, wrap the two requirements in a <RequireAll> block; without it, Apache 2.4 treats two bare Require lines as an implicit <RequireAny>, where satisfying either one is enough:

/etc/apache2/conf-available/phpmyadmin.conf
<Directory /usr/share/phpmyadmin>
    AuthType Basic
    AuthName "Restricted Access"
    AuthUserFile /etc/phpmyadmin/.htpasswd
    <RequireAll>
        Require ip 203.0.113.10
        Require valid-user
    </RequireAll>
</Directory>

Save and exit (press CTRL + X, press Y, then press ENTER)

On nginx the two controls simply coexist in the one block: a request must satisfy allow/deny and pass auth_basic, so listing them together already means both apply.

/etc/nginx/sites-available/default
location ^~ /phpmyadmin {
    allow 203.0.113.10;
    deny all;
    auth_basic "Restricted Access";
    auth_basic_user_file /etc/phpmyadmin/.htpasswd;

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}

Save and exit (press CTRL + X, press Y, then press ENTER)

Ban Brute-Force Attempts with Fail2ban 

Basic auth blocks the guesses; Fail2ban punishes them. It watches the web server logs and bans any IP that piles up failed authentications. Install it and enable a jail that watches your web server’s auth failures (our SSH hardening guide walks through the jail.local mechanics in full):

Terminal
sudo apt update && sudo apt install fail2ban

Create the jail with sudo nano /etc/fail2ban/jail.local. The built-in filters read the web server’s error log, where failed basic-auth attempts are recorded: apache-auth follows Apache’s error log and nginx-http-auth follows nginx’s (their log paths ship preconfigured). Keep only the section that matches your server:

/etc/fail2ban/jail.local
[apache-auth]
enabled  = true
maxretry = 5

[nginx-http-auth]
enabled  = true
maxretry = 5

Save and exit (press CTRL + X, press Y, then press ENTER)

Then restart and confirm the jail is active:

Terminal
sudo systemctl restart fail2ban && sudo fail2ban-client status
Output
Status
|- Number of jail:	2
`- Jail list:	apache-auth, sshd

The sshd jail ships enabled on Debian and Ubuntu, which is why it appears alongside yours. Once the jail is live, repeated failed logins from one address earn a temporary ban, so an attacker gets a handful of attempts rather than an unlimited stream.

Serve phpMyAdmin from a Different Path 

Moving phpMyAdmin off its default URL is worth doing, but be clear about what it buys you: it cuts the automated scanner noise that targets /phpmyadmin specifically. It is not access control, because anyone who can see your traffic or guess the new path still reaches the login. Treat it as noise reduction on top of the real controls above, not a replacement for them. On Ubuntu and Debian, change the Alias line in the phpMyAdmin Apache config:

Terminal
sudo nano /etc/apache2/conf-available/phpmyadmin.conf
/etc/apache2/conf-available/phpmyadmin.conf
Alias /dbadmin /usr/share/phpmyadmin

Save and exit (press CTRL + X, press Y, then press ENTER)

Reload Apache and phpMyAdmin now answers at /dbadmin instead. The scanners hammering /phpmyadmin get a 404, and your logs get quieter.

On nginx, rename the location prefix from /phpmyadmin to /dbadmin in the block you built above, keeping the access controls and the PHP handler inside it:

/etc/nginx/sites-available/default
location ^~ /dbadmin {
    alias /usr/share/phpmyadmin/;
    allow 203.0.113.10;
    deny all;
    auth_basic "Restricted Access";
    auth_basic_user_file /etc/phpmyadmin/.htpasswd;

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $request_filename;
    }
}

Save and exit (press CTRL + X, press Y, then press ENTER)

When you serve phpMyAdmin from an alias like this, the extra fastcgi_param SCRIPT_FILENAME $request_filename; line is required so PHP-FPM resolves the aliased path instead of the default document root; without it the new path returns File not found. Reload nginx and phpMyAdmin answers at /dbadmin.

Harden config.inc.php 

phpMyAdmin’s own config file adds two useful controls. Block the MySQL root account from logging in through phpMyAdmin at all, so even a leaked root password cannot be used here, and stop users connecting to arbitrary database servers:

Terminal
sudo nano /etc/phpmyadmin/config.inc.php
/etc/phpmyadmin/config.inc.php
$cfg['Servers'][$i]['AllowRoot'] = false;
$cfg['AllowArbitraryServer'] = false;

Save and exit (press CTRL + X, press Y, then press ENTER)

With AllowRoot false, database administration happens through a dedicated, least-privilege MySQL user instead of root, which is good practice regardless of the other layers. To balance these security layers with usability, you can also increase the phpMyAdmin session timeout to avoid frequent logouts.

Troubleshooting: Locked Yourself Out? 

Adding access controls is exactly where people lock themselves out. Match the symptom to the fix:

  • 403 Forbidden after an IP rule. Your real public IP is not the one you allowed. Home connections, mobile networks, and CGNAT hand out addresses that change; check your current address and update the Require ip / allow line. From the server itself you can always edit the config back.
  • Endless 401 with basic auth. The AuthUserFile path is wrong or the .htpasswd file is unreadable by the web server. Confirm the path matches exactly and re-create the file if needed.
  • Service will not reload. A config typo. Run sudo apachectl configtest on Apache or sudo nginx -t on nginx; both name the offending file and line before you reload.
  • Root login errors after hardening. If you blocked root and now see access-denied errors, that is expected. Our guide to the phpMyAdmin access-denied error covers using a non-root account. If HTTPS or auth changes broke the login page, see the session-cookie error.

Locking yourself out is recoverable; a breached database is not, which is the whole case for layering. An IP allowlist where your address is fixed, HTTP basic auth in front of the login for everyone else, Fail2ban to punish the persistent, and config.inc.php hardening to keep root out are each cheap, and together they turn the most-probed admin panel on your server into a hard target. Test every control from your own browser before you close the terminal, so a typo never becomes a lockout.

Support DevAnswers
Written, tested and maintained by one developer. Your support keeps these guides accurate and ad-light.
Buy me a coffee

Comments

Leave a reply