VPS・クラウドで WordPress や自社アプリを動かし、MySQL / MariaDB も同じサーバ(または隣のサーバ)で管理している Web 担当者・制作会社向けの記事です。
「DB はサーバの中にあるから外からは見えない」と思っていても、bind-address の変更・Docker のポート公開・phpMyAdmin の置きっぱなしで、データベースが直接インターネットに面していることがあります。
この記事では、外部に晒されていないかを確認するコマンドと、塞ぐための 7 つの設定をコピペで使える形でまとめます。
なぜ DB の露出が起きるのか
典型的な経路は次の 3 つです。
| 経路 | よくあるきっかけ |
|---|---|
| DB が全インタフェースで待ち受けている | 「別サーバから繋ぎたい」で bind-address を 0.0.0.0 に変えたまま |
| Docker のポート公開 |
ports: - "3306:3306" と書いた。ホストのファイアウォール(ufw)を素通りすることがある |
| 管理画面の公開 | phpMyAdmin / Adminer をドキュメントルートに置いたまま |
MySQL 公式リファレンスでは、bind_address の既定値は *(すべての IPv4 / IPv6 インタフェースで待ち受け)です。Ubuntu / Debian のパッケージは多くの場合 127.0.0.1 に絞った設定で配布されますが、公式バイナリや Docker イメージ、手作業の変更では全公開になっていることがあります。
DB が外から見えると、パスワード総当たりの的になるだけでなく、DB ソフトの脆弱性が見つかったときに直接攻撃を受けます。「強いパスワードがあるから大丈夫」ではなく、そもそも外から届かない状態にするのが基本です。
設定1:いま何番ポートで、どこに向けて待ち受けているか確認する
まずはサーバ上で現状を見ます。
# 3306(MySQL/MariaDB)と 33060(MySQL X プロトコル)の待ち受け先
sudo ss -tlnp | grep -E ':(3306|33060)\b'
結果の見方です。
| 表示 | 意味 |
|---|---|
127.0.0.1:3306 / [::1]:3306
|
サーバ内からのみ。安全 |
0.0.0.0:3306 / *:3306 / [::]:3306
|
全インタフェースで待ち受け。要対処 |
docker-proxy が 3306 を持っている |
Docker のポート公開(設定4へ) |
DB 側で効いている値も確認します。
sudo mysql -e "SHOW VARIABLES WHERE Variable_name IN ('bind_address','skip_networking','port');"
次に、サーバの外(自宅 PC や別のサーバ)から到達できるかを確かめます。サーバ内からの確認だけでは、ファイアウォールやクラウド側の設定が反映されないためです。
# 外部の端末から実行(203.0.113.20 は対象サーバの IP に置き換え)
nc -vz -w 3 203.0.113.20 3306
Connection refused やタイムアウトなら到達不可、succeeded / open と出たら外から届いています。
設定2:同じサーバからしか使わないなら 127.0.0.1 に絞る
WordPress と DB が同じサーバにある構成なら、DB はループバック(サーバ自身)だけで待ち受ければ十分です。
MySQL(Ubuntu の場合は /etc/mysql/mysql.conf.d/ 配下のファイル):
[mysqld]
bind-address = 127.0.0.1
mysqlx-bind-address = 127.0.0.1
MariaDB(Debian / Ubuntu の場合は /etc/mysql/mariadb.conf.d/ 配下のファイル):
[mysqld]
bind-address = 127.0.0.1
TCP 接続自体が不要で、アプリが UNIX ソケット(localhost 指定)で繋いでいるなら、ネットワーク待ち受けを止める skip-networking という選択肢もあります。ただしアプリの接続先が 127.0.0.1 になっていると繋がらなくなるので、接続設定を確認してから使います。
設定ファイルは複数に分かれていることが多く、別のファイルに古い値が残っていると意図しない値が勝ちます。どのファイルに何が書いてあるかを洗い出してから変更し、反映後は実際の値で確認します。
# 設定ファイル内の bind-address を一覧
sudo grep -rn -E '^\s*(bind-address|bind_address|mysqlx-bind-address|skip-networking)' /etc/mysql/
# 再起動(bind-address は再起動が必要)
sudo systemctl restart mysql # MariaDB の場合は mariadb
# 反映確認
sudo ss -tlnp | grep -E ':(3306|33060)\b'
sudo mysql -e "SHOW VARIABLES LIKE 'bind_address';"
手元の PC から GUI で触りたいとき
DB を外に開ける代わりに、SSH トンネルで繋ぐのが安全です。DB は 127.0.0.1 のまま使えます。
# 手元の PC で実行:手元の 13306 番 → サーバ内の 127.0.0.1:3306 へ転送
ssh -N -L 13306:127.0.0.1:3306 deploy@203.0.113.20
# 別ターミナルで手元から接続
mysql -h 127.0.0.1 -P 13306 -u wpuser -p
設定3:別サーバから繋ぐなら「送信元を絞る」を 3 重にする
Web サーバと DB サーバを分けている構成では、待ち受けを全面的に閉じることはできません。その場合は、送信元を次の 3 か所で絞ります。
① DB はプライベート IP だけで待ち受ける
[mysqld]
# DB サーバのプライベート IP(例)
bind-address = 10.0.0.10
② ファイアウォールで Web サーバからだけ許可する(クラウドならセキュリティグループも同じ条件に)
# Web サーバ(10.0.0.5)からの 3306 だけ許可
sudo ufw allow from 10.0.0.5 to any port 3306 proto tcp
sudo ufw status numbered
ufw allow 3306 のように送信元を付けない許可ルールが残っていれば削除します。
③ DB ユーザーの接続元ホストを限定し、TLS を必須にする
-- 接続元を Web サーバの IP に限定したユーザー
CREATE USER 'wpuser'@'10.0.0.5' IDENTIFIED BY '長くランダムなパスワード';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP ON wpdb.* TO 'wpuser'@'10.0.0.5';
-- このユーザーは TLS 接続のみ許可
ALTER USER 'wpuser'@'10.0.0.5' REQUIRE SSL;
'wpuser'@'%'(どこからでも接続可)のようなユーザーは作らないようにします。
設定4:Docker の DB はポートを公開しない(するなら 127.0.0.1 に)
Docker 公式ドキュメントは、ポート公開について「既定では安全ではない」と明記しています。ホスト IP を指定せずに公開すると、ホストのすべてのアドレス(0.0.0.0 と [::])に公開され、ufw と組み合わせた場合はファイアウォールのルールが適用される前にパケットが転送されるとされています。「ufw で 3306 を閉じているから大丈夫」は通用しません。
アプリと DB が同じ Compose 内にあるなら、DB のポートはそもそも公開不要です。コンテナ同士はサービス名で通信できます。
# compose.yaml
services:
db:
image: mysql:8.4
# ports は書かない(同じネットワーク内のコンテナからは db:3306 で届く)
volumes:
- db_data:/var/lib/mysql
wordpress:
image: wordpress:latest
environment:
WORDPRESS_DB_HOST: db:3306
volumes:
db_data:
(DB 名・ユーザー・パスワードの環境変数は省略しています。既存の設定をそのまま使ってください。)
ホストから直接触る必要がある場合だけ、ループバックに限定して公開します。
services:
db:
ports:
- "127.0.0.1:3306:3306"
なお Docker 公式ドキュメントでは、28.0.0 より前のバージョンでは 127.0.0.1 に公開したポートでも同じ L2 セグメント内のホストから届く場合がある、と注意書きがあります。古い Docker Engine を使っている場合は更新も検討してください。
現在の公開状況はこれで確認できます。
# 0.0.0.0 や :::(全公開)で 3306 が出ていないか
docker ps --format '{{.Names}}\t{{.Ports}}' | grep 3306
設定5:不要なアカウントと「どこからでも root」を消す
初期状態や過去の作業で、匿名ユーザー・リモート root・テスト用 DB が残っていることがあります。まず一覧で確認します。
sudo mysql -e "SELECT user, host, plugin FROM mysql.user ORDER BY user, host;"
特に次の 3 つを探します。
-- 匿名ユーザー(user が空)
SELECT user, host FROM mysql.user WHERE user = '';
-- どこからでも接続できるユーザー(host が % を含む)
SELECT user, host FROM mysql.user WHERE host LIKE '%\%%';
-- localhost 以外から接続できる root
SELECT user, host FROM mysql.user WHERE user = 'root' AND host NOT IN ('localhost', '127.0.0.1', '::1');
見つかったものは、使っていないことを確認したうえで削除します。初期設定のやり直しには付属ツールが使えます。
# MySQL
sudo mysql_secure_installation
# MariaDB
sudo mariadb-secure-installation
MariaDB の root@localhost は既定で OS ユーザーで認証する unix_socket 方式です(sudo mysql でパスワードなしに入れるのはこのため)。
MySQL 8.4 では旧方式の認証プラグイン mysql_native_password が既定で無効です(MySQL 9.0 で削除)。古いクライアントのために有効化している場合は、接続元を限定したうえで、移行計画を立てておくのが安全です。
設定6:アプリ用ユーザーは「自分の DB だけ」「全体権限なし」にする
WordPress を root ユーザーで動かしていたり、複数サイトで 1 つの DB ユーザーを共有していると、1 サイトが乗っ取られたときに同じサーバ上の全サイトの DBが読まれます。サイトごとにユーザーを分け、権限はその DB の中に限定します。
# サーバ全体に効く強い権限(SUPER / FILE / GRANT)を持つユーザーを確認
sudo mysql -e "SELECT user, host FROM mysql.user WHERE Super_priv='Y' OR File_priv='Y' OR Grant_priv='Y';"
# アプリ用ユーザーの権限を確認
sudo mysql -e "SHOW GRANTS FOR 'wpuser'@'localhost';"
GRANT ALL PRIVILEGES ON *.*(全 DB に全権限)や WITH GRANT OPTION が付いていたら、自分の DB だけに絞り直します。
-- 全体権限をいったん外す
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'wpuser'@'localhost';
-- 自分の DB(wpdb)の中だけに付け直す
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP ON wpdb.* TO 'wpuser'@'localhost';
WordPress の公式ハードニングガイドでは、通常運用に必要なのは読み書き権限だけとしつつ、プラグインやメジャー更新が DB 構造を変更するため、CREATE や ALTER などを外すことは推奨しない(戻せるバックアップ体制がある場合を除く)としています。上の例はこの方針に合わせ、「権限の範囲を自分の DB に閉じる」ことを優先しています。
あわせて、DB からサーバ上のファイルを読み書きする機能も確認しておきます。
sudo mysql -e "SHOW VARIABLES WHERE Variable_name IN ('local_infile','secure_file_priv');"
local_infile が ON で、使う予定がなければ OFF にします。
[mysqld]
local_infile = OFF
設定7:phpMyAdmin / Adminer は「置かない」か「IP + 認証で二重に閉じる」
DB 本体を閉じても、ブラウザから DB を操作できる管理ツールが公開されていれば同じことです。まず、使っていない管理ツールが残っていないか探します。
# ドキュメントルート配下の Adminer / phpMyAdmin を探す
sudo find /var/www -maxdepth 4 \( -iname 'adminer*.php' -o -iname 'phpmyadmin*' -o -iname 'pma*' \) 2>/dev/null
使っていないものは削除し、必要な場合は SSH トンネル(設定2)を優先します。どうしても Web で使うなら、IP 制限と Basic 認証の両方をかけます。
nginx の場合(PHP の処理を内側の location に入れるのがポイントです。外側の制限が内側にも引き継がれます):
location ^~ /phpmyadmin/ {
allow 203.0.113.10; # 管理者の固定 IP
deny all;
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd-pma;
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
}
Apache の場合(phpMyAdmin のディレクトリ設定の中に追加):
<Directory /usr/share/phpmyadmin>
AuthType Basic
AuthName "Restricted"
AuthUserFile /etc/apache2/.htpasswd-pma
<RequireAll>
Require ip 203.0.113.10
Require valid-user
</RequireAll>
</Directory>
# Basic 認証のパスワードファイル作成(bcrypt)
sudo htpasswd -B -c /etc/nginx/.htpasswd-pma pmaadmin # Apache は /etc/apache2/ に
phpMyAdmin 側でも root ログインを禁止します(AllowRoot の既定値は true)。
// config.inc.php
$cfg['Servers'][$i]['AllowRoot'] = false;
$cfg['Servers'][$i]['AllowNoPassword'] = false;
残す場合は、phpMyAdmin 自体もパッケージ更新の対象に入っているか確認してください。
まとめ
| # | 設定 | 防げること |
|---|---|---|
| 1 |
ss とサーバ外からの nc で現状確認 |
「閉じているつもり」の見落とし |
| 2 | 同一サーバなら 127.0.0.1 に絞る / SSH トンネル |
DB の全インタフェース待ち受け |
| 3 | 別サーバ構成は bind・FW・ユーザーの 3 重で送信元限定 | どこからでも総当たりされる |
| 4 | Docker は ports を書かない / 127.0.0.1: を付ける |
ufw を素通りするポート公開 |
| 5 | 匿名ユーザー・リモート root・% ホストの削除 |
推測しやすい入口が残る |
| 6 | アプリ用ユーザーを自分の DB に限定 | 1 サイトの侵害が全サイトへ波及 |
| 7 | 管理ツールは削除 or IP + 認証の二重制限 | ブラウザ経由で DB を操作される |
DB の守りは「パスワードを強くする」より前に、外から届かない構成にすることが一番効きます。まずは設定1の 2 つのコマンド(サーバ内の ss と、外部からの nc)を流して、3306 番が外に開いていないかを確認してみてください。
参考(公式ドキュメント):
- MySQL 8.4 Reference Manual「Server System Variables(bind_address)」 https://dev.mysql.com/doc/refman/8.4/en/server-system-variables.html
- MySQL 8.4 Reference Manual「Native Pluggable Authentication」 https://dev.mysql.com/doc/refman/8.4/en/native-pluggable-authentication.html
- MariaDB Knowledge Base「Configuring MariaDB for Remote Client Access」 https://mariadb.com/kb/en/configuring-mariadb-for-remote-client-access/
- Docker Docs「Port publishing and mapping」 https://docs.docker.com/engine/network/port-publishing/
- Docker Docs「Packet filtering and firewalls」 https://docs.docker.com/engine/network/packet-filtering-firewalls/
- WordPress Advanced Administration「Hardening WordPress」 https://developer.wordpress.org/advanced-administration/security/hardening/
- phpMyAdmin Documentation「Configuration」 https://docs.phpmyadmin.net/en/latest/config.html
関連記事
本記事のような設定の抜けは、Web サイトを 9 つの守りでまるごと守るサイトドックでまとめて対策できます → https://sitedock.jp