Logo

Install and Secure phpMyAdmin behind Nginx on Debian 12

Install and Secure phpMyAdmin behind Nginx on Debian 12

Optimized for a lightweight production environment (Debian 12 Bookworm, Nginx, PHP‑FPM, MariaDB/MySQL). Also suitable for labs. Includes hardening, automation, and troubleshooting.

1. Prerequisites

  • Debian 12 (Bookworm) with sudo access.
  • Nginx installed (see below if not installed).
  • Ports 80 and 443 open.
  • Domain name pointing to the server (recommended for HTTPS).
  • Tip: if multiple PHP versions are installed, check the active one:
php -v
# outputs 8.x.y → note down 8.2 or 8.1, etc.

2. System Update

Run apt update + full upgrade:

sudo apt update && sudo apt -y full-upgrade
sudo reboot

3. Install Nginx, PHP‑FPM and Extensions

sudo apt install -y nginx php-fpm php-cli php-mbstring php-xml php-zip php-curl php-mysql php-gd php-intl php-bcmath php-json
sudo apt install -y php-imagick php-apcu

Check the PHP‑FPM socket (replace 8.2 with your version if needed):

ls -l /run/php/ | grep fpm
# Example: /run/php/php8.2-fpm.sock

4. Install MariaDB/MySQL and Secure

sudo apt install -y mariadb-server mariadb-client
sudo mysql_secure_installation

Recommended during the assistant:

  • Set a root SQL password (if not set).
  • Remove anonymous users.
  • Disable remote root login.
  • Remove the test database.
  • Reload privileges.

5. Create a Dedicated DB User for phpMyAdmin

phpMyAdmin does not need root. Create a limited admin account.

sudo mysql

Inside SQL shell:

CREATE USER 'pma_admin'@'localhost' IDENTIFIED BY 'StrongPasswordHere!';
GRANT ALL PRIVILEGES ON *.* TO 'pma_admin'@'localhost' WITH GRANT OPTION;
FLUSH PRIVILEGES;
EXIT;

6. Install phpMyAdmin (2 methods)

6.A: via apt (simple)

sudo apt install -y phpmyadmin

During installation:

  • Choose nginx: No (it won’t configure automatically) → configure Nginx manually.
  • Choose dbconfig-common: Yes and set a password for phpMyAdmin’s internal user pma (different from pma_admin).

Files are usually located in /usr/share/phpmyadmin.

6.B: via official archive (more up-to-date)

cd /var/www
sudo mkdir -p pma && cd pma
# Download the latest version from https://www.phpmyadmin.net/downloads/
# Example:
wget https://files.phpmyadmin.net/phpMyAdmin/5.2.1/phpMyAdmin-5.2.1-all-languages.tar.gz
sudo tar xzf phpMyAdmin-*-all-languages.tar.gz
sudo mv phpMyAdmin-*-all-languages phpmyadmin
sudo chown -R www-data:www-data /var/www/pma/phpmyadmin

Create the config file if needed:

sudo -u www-data cp /var/www/pma/phpmyadmin/config.sample.inc.php /var/www/pma/phpmyadmin/config.inc.php
sudo -u www-data nano /var/www/pma/phpmyadmin/config.inc.php

In config.inc.php, generate a blowfish_secret with 32 random characters:

<?php
$cfg['blowfish_secret'] = 'PutYourUniqueLongRandomSecretHere!!!!';

7. Configure Nginx: alias or dedicated subdomain

Two approaches: /phpmyadmin as an alias on an existing vhost, or pma.domain.tld as a dedicated vhost. The latter simplifies ACLs and rate limiting.

Option A — Alias /phpmyadmin on an existing vhost

In your server block (e.g. /etc/nginx/sites-available/my-site.conf):

location /phpmyadmin {
    alias /usr/share/phpmyadmin;  # or /var/www/pma/phpmyadmin if method 6.B
    index index.php;
    try_files $uri $uri/ /phpmyadmin/index.php?$args;
}

location ~ ^/phpmyadmin/(.+\.php)$ {
    alias /usr/share/phpmyadmin;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $request_filename;
    fastcgi_pass unix:/run/php/php8.2-fpm.sock; # adjust version
    fastcgi_read_timeout 120s;
}

location ~* ^/phpmyadmin/(.+\.(?:png|jpg|jpeg|gif|css|js|ico|html|svg))$ {
    alias /usr/share/phpmyadmin;
    access_log off;
    log_not_found off;
    expires 7d;
}

Option B — Dedicated vhost pma.domain.tld

Create /etc/nginx/sites-available/pma.conf:

server {
    listen 80;
    server_name pma.example.com;

    root /usr/share/phpmyadmin;   # or /var/www/pma/phpmyadmin
    index index.php index.html;

    return 301 https://$host$request_uri; # redirect to HTTPS after Certbot
}

server {
    listen 443 ssl http2;
    server_name pma.example.com;

    ssl_certificate /etc/letsencrypt/live/pma.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/pma.example.com/privkey.pem;

    root /usr/share/phpmyadmin;   # or /var/www/pma/phpmyadmin
    index index.php;

    add_header X-Frame-Options SAMEORIGIN always;
    add_header X-Content-Type-Options nosniff always;
    add_header Referrer-Policy no-referrer-when-downgrade always;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock; # adjust version
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
        fastcgi_read_timeout 120s;
    }

    location ~* \.(?:css|js|ico|gif|jpe?g|png|svg)$ {
        access_log off;
        expires 7d;
    }
}

Enable and test:

sudo ln -s /etc/nginx/sites-available/pma.conf /etc/nginx/sites-enabled/pma.conf
sudo nginx -t && sudo systemctl reload nginx

8. Enable HTTPS with Let’s Encrypt

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d pma.example.com
# If alias: certbot --nginx -d my-site.example.com

Check auto renewal:

systemctl status certbot.timer

9. phpMyAdmin Hardening

  • Disable access to /setup (deprecated). Alias example:
location ^~ /phpmyadmin/setup { deny all; }

Or in dedicated vhost:

location ^~ /setup { deny all; }
  • Use a unique, long blowfish secret (see above).
  • Disable AllowArbitraryServer to prevent SSRF. In config.inc.php:
<?php
$cfg['AllowArbitraryServer'] = false;
  • Limit DB connections to localhost.
  • Disable remote root login, prefer sudo mysql.

10. Adjust PHP (upload, execution time)

Edit /etc/php/8.2/fpm/php.ini (adjust version):

upload_max_filesize = 512M
post_max_size = 512M
max_execution_time = 300
memory_limit = 512M

Restart PHP‑FPM:

sudo systemctl restart php8.2-fpm

11. Optimizations (Opcache/APCu)

In /etc/php/8.2/fpm/php.ini:

opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=192
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=100000
opcache.validate_timestamps=1
opcache.revalidate_freq=2

APCu (if installed) in /etc/php/8.2/mods-available/apcu.ini:

apc.enabled=1
apc.shm_size=128M

12. Restrict Access: IP allowlist and HTTP Auth

IP Allowlist (example alias /phpmyadmin)

location /phpmyadmin {
    allow 203.0.113.10;   # your static IP
    allow 2001:db8::/48;  # your IPv6 prefix
    deny all;
    alias /usr/share/phpmyadmin;
    index index.php;
}

HTTP Auth (Basic)

sudo apt install -y apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd-pma admin

Add to vhost:

location /phpmyadmin {
    auth_basic "Restricted";
    auth_basic_user_file /etc/nginx/.htpasswd-pma;
    alias /usr/share/phpmyadmin;
    index index.php;
    try_files $uri $uri/ /phpmyadmin/index.php?$args;
}

Combine IP allowlist and HTTP Auth for better security.

13. Rate‑limit and Fail2ban

Rate‑limit Nginx

In http context (e.g. /etc/nginx/nginx.conf):

limit_req_zone $binary_remote_addr zone=pma_limit:10m rate=5r/s;

In server block:

location /phpmyadmin/ {
    limit_req zone=pma_limit burst=20 nodelay;
}

Fail2ban (Nginx filter)

sudo apt install -y fail2ban
sudo tee /etc/fail2ban/filter.d/nginx-phpmyadmin.conf >/dev/null <<'EOF'
[Definition]
failregex = ^<HOST> - .* "(GET|POST) /phpmyadmin.*" .* (401|403) .*$
ignoreregex =
EOF

sudo tee /etc/fail2ban/jail.d/nginx-phpmyadmin.local >/dev/null <<'EOF'
[nginx-phpmyadmin]
enabled = true
port    = http,https
filter  = nginx-phpmyadmin
logpath = /var/log/nginx/access.log
maxretry = 8
findtime = 10m
bantime  = 1h
EOF

sudo systemctl restart fail2ban
sudo fail2ban-client status nginx-phpmyadmin

14. Backups & Updates

  • Backup configs: /etc/nginx/, /etc/php/*/fpm/, /etc/phpmyadmin/ or /var/www/pma/phpmyadmin/.
  • Regular updates:
sudo apt update && sudo apt -y upgrade
  • If using archive method (6.B): re-download new archive and replace folder.
  • Take VM/container snapshots before major changes.

15. Testing & Troubleshooting

  • Test Nginx:
sudo nginx -t
sudo systemctl reload nginx
  • Logs: /var/log/nginx/access.log, /var/log/nginx/error.log, journalctl -u php8.2-fpm.
  • PHP‑FPM down: check socket /run/php/php8.2-fpm.sock or switch to fastcgi_pass 127.0.0.1:9000 if using TCP.
  • 403 error on alias: check alias/root, try_files, and www-data permissions.
  • Import timeout: increase client_max_body_size in server block and fastcgi_read_timeout.
  • Invalid CSRF token: check cookies, blowfish_secret, and system clock (timedatectl).

16. Useful Annexes

Nginx Security Headers Snippet

add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Minimal CSP (test your assets before enforcing in production)
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' data: blob: https:;" always;

PHP‑FPM Pool Tuning

File /etc/php/8.2/fpm/pool.d/www.conf:

pm = dynamic
pm.max_children = 32
pm.start_servers = 4
pm.min_spare_servers = 4
pm.max_spare_servers = 16
pm.max_requests = 500

Adjust according to RAM and workload.

UFW (simple firewall)

sudo apt install -y ufw
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status

Clean up Debian default alias

If installed via apt, Debian may create an Apache config. For Nginx, ignore it and maintain vhosts in /etc/nginx/sites-available/.


17. Final Checklist

  • apt update and upgrade done
  • Nginx + PHP‑FPM working (phpinfo test page)
  • MariaDB secured with mysql_secure_installation
  • phpMyAdmin installed (apt or archive) and accessible
  • HTTPS enabled (Let’s Encrypt)
  • ACL (IP and/or HTTP Auth) in place
  • Rate‑limit + Fail2ban active
  • Backups planned & logs monitored

End of tutorial. Do you need a version adapted to your exact domain, PHP version, and paths, or an automated Bash/Ansible script?

Join our Discord community server

For any questions, suggestions, or just to chat with the community, join us on Discord!

900+Members