Cài đặt phpMyAdmin an toàn trên Linux: Xác thực, Giới hạn IP và Nginx Reverse Proxy

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

2 giờ sáng, server alert và phpMyAdmin mở toang cho cả thế giới

Tháng trước mình nhận alert từ fail2ban lúc 2 giờ sáng: gần 400 request/phút vào /phpmyadmin của một server production. Bot scanner đang brute-force — thứ mà đồng nghiệp cài tạm 3 tháng trước rồi để nguyên đó.

Đáng sợ hơn: phpMyAdmin đang chạy ở port 80, không có xác thực thêm, không giới hạn IP, username root vẫn để mặc định. Mình phải vừa block tạm bằng iptables, vừa hỏi người setup — câu trả lời là “tưởng chỉ dùng nội bộ thôi.”

Mình từng gặp sự cố database corruption lúc 3 giờ sáng và phải restore từ backup — từ đó mình luôn kiểm tra backup hàng ngày. Nhưng backup không giải quyết được vấn đề nếu attacker đã dump toàn bộ data trước khi bạn kịp phát hiện.

Phân tích: phpMyAdmin nguy hiểm ở điểm nào?

phpMyAdmin là công cụ quản lý MySQL qua web — tiện, nhưng cũng là lý do nó là mục tiêu hàng đầu của bot scanner. Một số điểm yếu phổ biến:

  • URL mặc định dễ đoán: /phpmyadmin, /pma, /mysql — bot scan tất cả liên tục 24/7.
  • Không có lớp xác thực bổ sung: Chỉ dựa vào username/password MySQL. Nếu mật khẩu yếu là lộ toàn bộ database.
  • Mở cho mọi IP: Không có lý do gì phpMyAdmin cần public ra internet nếu chỉ dùng nội bộ.
  • Phiên bản cũ có CVE: phpMyAdmin có lịch sử bị lỗ hổng RCE nghiêm trọng. Version cũ mà expose ra ngoài là rủi ro cao.

Các cách bảo mật phpMyAdmin

Cách 1: Đổi URL mặc định (tối thiểu, chưa đủ)

Đổi path từ /phpmyadmin sang cái gì đó khó đoán hơn. Với Nginx, chỉ cần đổi location block. Cách này giảm noise từ bot scanner nhưng không ngăn được targeted attack — attacker vẫn có thể tìm thấy qua fingerprinting.

Cách 2: Giới hạn IP bằng iptables/ufw

Nếu chỉ quản trị từ một số IP cố định (office, VPN), block tất cả trừ những IP đó:

# Cho phép IP văn phòng và VPN
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

Hiệu quả nhưng bất tiện khi IP động hoặc làm việc remote — phải nhớ cập nhật whitelist thủ công.

Cách 3: HTTP Basic Auth (tốt hơn, kết hợp tốt)

Thêm một lớp xác thực trước khi đến login form phpMyAdmin:

# Tạo file password
sudo apt install apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd admin_pma
# Nhập password mạnh khi được hỏi

Lớp này buộc bot phải vượt qua HTTP Basic Auth trước khi thấy login form phpMyAdmin — hiệu quả hơn nhiều so với chỉ đổi URL.

Cách tốt nhất: Nginx Reverse Proxy + IP Whitelist + Basic Auth

Kết hợp cả ba: phpMyAdmin chạy local (không expose trực tiếp), Nginx làm reverse proxy với IP whitelist và Basic Auth. Setup này mình đang dùng trên production và chưa thấy vấn đề gì sau hơn một năm.

Bước 1: Cài phpMyAdmin

sudo apt update
sudo apt install phpmyadmin php-mbstring php-zip php-gd php-json php-curl

# Khi hỏi web server: chọn "none" nếu dùng Nginx + PHP-FPM
# Configure database for phpmyadmin: Yes
# Nhập MySQL root password để tạo database nội bộ cho phpMyAdmin

Bước 2: Cấu hình phpMyAdmin — tắt root login

Chỉnh /etc/phpmyadmin/config.inc.php để tắt login bằng root và giới hạn kết nối local:

<?php
// Kết nối qua socket local thay vì TCP
$cfg['Servers'][$i]['host'] = '127.0.0.1';

// Tắt login root — bắt buộc dùng user riêng
$cfg['Servers'][$i]['AllowRoot'] = false;

// Tắt tính năng import file lớn nếu không cần
$cfg['UploadDir'] = '';
$cfg['SaveDir']   = '';

Bước 3: Tạo MySQL user riêng cho phpMyAdmin

Không bao giờ dùng root để login phpMyAdmin. Tạo user với quyền giới hạn:

-- Tạo user chỉ cho phép kết nối local
CREATE USER 'pma_admin'@'127.0.0.1' IDENTIFIED BY 'StrongPass_2024!';

-- Cấp quyền vừa đủ — không SUPER, không GRANT OPTION
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER
  ON *.* TO 'pma_admin'@'127.0.0.1';

-- Hoặc giới hạn chỉ một database cụ thể
GRANT ALL PRIVILEGES ON myapp_db.* TO 'pma_admin'@'127.0.0.1';

FLUSH PRIVILEGES;

Bước 4: Cấu hình Nginx Reverse Proxy

Tạo file config Nginx. URL mình đổi thành cái gì đó không ai đoán được:

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;

    # Giới hạn IP — chỉ văn phòng và VPN
    allow 203.0.113.10;
    allow 10.0.0.0/8;
    deny  all;

    # Block URL mặc định — trả về 404
    location ~ ^/(phpmyadmin|pma|mysql|dbadmin) {
        return 404;
    }

    location /dbmanager_x7k2 {
        # Lớp xác thực thứ hai: 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 cú pháp và reload
sudo nginx -t
sudo systemctl reload nginx

Bước 5: Giới hạn rate và cấu hình fail2ban

# Trong nginx.conf, http block:
limit_req_zone $binary_remote_addr zone=pma:10m rate=5r/m;

# Trong location block phpMyAdmin:
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

Kiểm tra lại toàn bộ

Sau khi setup, mình chạy quick check để đảm bảo mọi thứ đúng:

# URL mặc định phải trả về 404
curl -k -o /dev/null -w "%{http_code}" https://db.example.com/phpmyadmin
# Expected: 404

# Không có Basic Auth credentials phải trả về 401
curl -k -o /dev/null -w "%{http_code}" https://db.example.com/dbmanager_x7k2/
# Expected: 401

# Thử từ IP không trong whitelist (dùng mobile data hoặc VPS khác)
# Expected: 403 Forbidden

# Verify root không thể login
mysql -u root -p -e "SHOW GRANTS FOR 'pma_admin'@'127.0.0.1';"

Một số lưu ý cuối

  • Luôn dùng HTTPS: Basic Auth qua HTTP là vô nghĩa vì credentials đi dạng plain text. Let’s Encrypt miễn phí, không có lý do bỏ qua.
  • Cập nhật phpMyAdmin định kỳ: Check GitHub releases mỗi tháng, không để chạy version cũ hơn 6 tháng.
  • Tắt khi không dùng: Nếu chỉ cần phpMyAdmin thỉnh thoảng, comment location block trong Nginx giữa các lần dùng — an toàn nhất.
  • Giữ log đủ lâu: Nginx access log nên giữ tối thiểu 30 ngày để phân tích khi có incident.

Setup này nghe nhiều bước nhưng thực ra chỉ mất khoảng 30 phút. Sau cái đêm ngồi xử lý brute-force attack lúc 2 giờ sáng, mình thấy 30 phút đó xứng đáng hơn rất nhiều so với vài tiếng restore database giữa đêm — chưa kể giải trình với khách hàng tại sao data của họ có nguy cơ bị lộ.

Share: