1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MySQL/MariaDBを外部に晒さない7つの設定(Docker・phpMyAdmin対応・コピペOK)

1
Posted at

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 番が外に開いていないかを確認してみてください。

参考(公式ドキュメント):

関連記事


本記事のような設定の抜けは、Web サイトを 9 つの守りでまるごと守るサイトドックでまとめて対策できます → https://sitedock.jp

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?