MySQLのユーザーと権限:GRANTで最小権限にする方法

silver ring on white paper SQL

アプリを root でデータベースにつないでいると、SQL インジェクションの穴が1つあっただけで、テーブルを丸ごと消されるおそれがあります。MySQL では CREATE USER でユーザーを作り、GRANT で必要な操作だけを許しておけば、もしものときの被害は許した範囲で止まります。この記事では、アプリ用と集計用のユーザーを作り、テーブルごと・列ごとの権限、ロール(権限のまとまり)、REVOKE での取り消しまでを順に試します。MySQL 8.0 で GRANT からユーザーを作れなくなった点にも触れます。

以下の実行結果は MySQL 8.0 で実際に実行したものです。

このシナリオで考える

通販サイトの shop データベースを例にします。顧客の customers と注文の orders の2つのテーブルがあり、customers の email 列は個人情報です。このデータベースを使うのは、Web アプリと、売上を集計する社員の2者です。

アカウント 使う人 許す操作
app_user@10.0.% Web アプリ(10.0.x.x のサーバー) shop の全テーブルの読み書き
report_user@% 売上を集計する社員 orders の読み取りと、customers の id・name・prefecture の読み取り
CREATE DATABASE shop;
USE shop;

CREATE TABLE customers (
  id INT PRIMARY KEY,
  name VARCHAR(50) NOT NULL,
  prefecture VARCHAR(10) NOT NULL,
  email VARCHAR(100) NOT NULL
);

CREATE TABLE orders (
  id INT PRIMARY KEY,
  customer_id INT NOT NULL,
  amount INT NOT NULL,
  ordered_at DATETIME NOT NULL
);

INSERT INTO customers VALUES
  (1, '佐藤', '東京都', 'sato@example.com'),
  (2, '鈴木', '大阪府', 'suzuki@example.com');

INSERT INTO orders VALUES
  (1, 1, 3200, '2026-11-01 10:00:00'),
  (2, 2, 1800, '2026-11-02 15:30:00');

CREATE USERでアカウントを作る

MySQL のアカウントは、ユーザー名と接続元ホストの組で1つと数えます。同じ app_user でも、どこからつなぐかを host の部分で決めます。

CREATE USER 'app_user'@'10.0.%' IDENTIFIED BY 'App#Pass_2026';
CREATE USER 'report_user'@'%' IDENTIFIED BY 'Report#Pass_2026';

SELECT user, host FROM mysql.user
WHERE user IN ('app_user', 'report_user')
ORDER BY user;
+-------------+--------+
| user        | host   |
+-------------+--------+
| app_user    | 10.0.% |
| report_user | %      |
+-------------+--------+

host の書き方は次のとおりです。

host の書き方 つなげる接続元
‘localhost’ MySQL と同じサーバーから
‘10.0.%’ 10.0. で始まる IP アドレスから
‘10.0.0.0/255.255.0.0’ ネットマスクで指定した範囲から(上と同じ範囲)
‘10.0.0.0/16’ CIDR 表記で指定した範囲から(MySQL 8.0.23 以降)
‘%’ どこからでも

‘%’ は手軽ですが、パスワードが漏れると外からも入れてしまいます。アプリのサーバーの IP が決まっているなら、その範囲に絞っておくと、ほかの場所からはログインできません。なお、ここでのパスワードは説明用です。実際には長いランダムな文字列にしてください。

GRANTで必要な権限だけを渡す

作ったばかりのアカウントは、ログインはできても何も読めません。GRANT で「どの操作を」「どこに対して」許すかを指定します。

範囲 ON の書き方 例
サーバー全体 *.* GRANT PROCESS ON *.* TO …
データベース shop.* GRANT SELECT ON shop.* TO …
テーブル shop.orders GRANT SELECT ON shop.orders TO …
列 権限のあとに列名 GRANT SELECT (id, name) ON shop.customers TO …

アプリ用には、shop の中のデータの読み書きだけを許します。テーブルを作る・消す(CREATE・DROP)は渡しません。

GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'app_user'@'10.0.%';
SHOW GRANTS FOR 'app_user'@'10.0.%';
+-------------------------------------------------------------------------+
| Grants for app_user@10.0.%                                              |
+-------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `app_user`@`10.0.%`                               |
| GRANT SELECT, INSERT, UPDATE, DELETE ON `shop`.* TO `app_user`@`10.0.%` |
+-------------------------------------------------------------------------+

1行目の USAGE は「権限なし」を表す行で、どのアカウントにも出ます。この app_user が DROP TABLE shop.orders を実行しても、次のエラーで止まります(@ のあとには、実際につないだ接続元が入ります)。

ERROR 1142 (42000): DROP command denied to user 'app_user'@'10.0.%' for table 'orders'

GRANT と REVOKE は、実行した時点でサーバーに反映されます。FLUSH PRIVILEGES を続けて打つ必要はありません。アプリ側の対策と組み合わせると、インジェクションの被害をさらに小さくできます。プレースホルダの使い方は「SQLインジェクションの仕組みと対策:プレースホルダで防ぐ方法」で説明しています。

列ごとの権限で個人情報を見せない

集計用の report_user には、orders は全部、customers は email を除いた3列だけを読ませます。列の権限は、権限名のあとにカッコで列名を並べます。

GRANT SELECT ON shop.orders TO 'report_user'@'%';
GRANT SELECT (id, name, prefecture) ON shop.customers TO 'report_user'@'%';
SHOW GRANTS FOR 'report_user'@'%';
+--------------------------------------------------------------------------------------+
| Grants for report_user@%                                                             |
+--------------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `report_user`@`%`                                              |
| GRANT SELECT (`id`, `name`, `prefecture`) ON `shop`.`customers` TO `report_user`@`%` |
| GRANT SELECT ON `shop`.`orders` TO `report_user`@`%`                                 |
+--------------------------------------------------------------------------------------+

report_user で email を含めて SELECT すると、列の名前を挙げたエラーになります。SELECT * も、許していない列が含まれるので通りません。

-- SELECT name, email FROM shop.customers;
ERROR 1143 (42000): SELECT command denied to user 'report_user'@'%' for column 'email' in table 'customers'

-- SELECT * FROM shop.customers;
ERROR 1142 (42000): SELECT command denied to user 'report_user'@'%' for table 'customers'

SELECT id, name, prefecture のように3列だけを書けば、ふつうに読めます。集計担当の人に列名を毎回気にさせたくないときは、3列だけのビューを作り、そのビューにだけ SELECT を許すやり方もあります。ビューの作り方は「MySQLのVIEW入門:基本構文・更新可能ビュー・性能注意点」で説明しています。

ロールで権限をまとめて渡す

MySQL 8.0 からは、権限のまとまりをロールとして作り、それを人に渡せます。集計担当が5人いても、ロールに1回 GRANT するだけで済みます。

CREATE ROLE 'shop_read', 'shop_write';
GRANT SELECT ON shop.* TO 'shop_read';
GRANT INSERT, UPDATE, DELETE ON shop.* TO 'shop_write';

CREATE USER 'kato'@'%' IDENTIFIED BY 'Kato#Pass_2026';
GRANT 'shop_read' TO 'kato'@'%';
SET DEFAULT ROLE 'shop_read' TO 'kato'@'%';

SHOW GRANTS FOR 'kato'@'%';
SHOW GRANTS FOR 'kato'@'%' USING 'shop_read';
+-------------------------------------+
| Grants for kato@%                   |
+-------------------------------------+
| GRANT USAGE ON *.* TO `kato`@`%`    |
| GRANT `shop_read`@`%` TO `kato`@`%` |
+-------------------------------------+

+----------------------------------------+
| Grants for kato@%                      |
+----------------------------------------+
| GRANT USAGE ON *.* TO `kato`@`%`       |
| GRANT SELECT ON `shop`.* TO `kato`@`%` |
| GRANT `shop_read`@`%` TO `kato`@`%`    |
+----------------------------------------+

ふつうの SHOW GRANTS では、ロールの名前しか出ません。USING でロールを指定すると、中身の権限まで展開して見られます。

SET DEFAULT ROLE の行は省けません。ロールは渡しただけでは有効にならず、ログインした直後は何も読めない状態です。SET DEFAULT ROLE で指定したロールが、ログインのたびに有効になります。サーバーの設定 activate_all_roles_on_login を ON にすると、渡したロールがすべてログイン時に有効になります。

REVOKEで取り消し、DROP USERで消す

渡した権限は REVOKE で取り消せます。書き方は GRANT とほぼ同じで、TO が FROM に変わります。

REVOKE DELETE ON shop.* FROM 'app_user'@'10.0.%';
SHOW GRANTS FOR 'app_user'@'10.0.%';

DROP USER 'kato'@'%';
+-----------------------------------------------------------------+
| Grants for app_user@10.0.%                                      |
+-----------------------------------------------------------------+
| GRANT USAGE ON *.* TO `app_user`@`10.0.%`                       |
| GRANT SELECT, INSERT, UPDATE ON `shop`.* TO `app_user`@`10.0.%` |
+-----------------------------------------------------------------+

すでにつながっている接続にいつ効くかは、権限の範囲で変わります。

変えた権限の範囲 いまの接続に効くとき
テーブル・列 次に SQL を送ったとき
データベース 次に USE を実行したとき
サーバー全体(PROCESS などの静的な権限) つなぎ直したとき

アプリはコネクションプールで接続を使い回すことが多いので、データベース単位の権限を外したあとは、アプリを再起動して接続を作り直すと確実です。

よくある落とし穴

古い手順書の「GRANT … TO 新しいユーザー」は、MySQL 8.0 ではユーザーを作ってくれません。まだないアカウントに GRANT すると、次のエラーになります。CREATE USER を先に実行してください。

GRANT SELECT ON shop.* TO 'batch_user'@'localhost';
ERROR 1410 (42000): You are not allowed to create a user with GRANT

名前が同じでも、host が違えば別のアカウントです。’report_user’@’localhost’ を作ると、’report_user’@’%’ とは別にパスワードと権限を持ちます。

CREATE USER 'report_user'@'localhost' IDENTIFIED BY 'Other#Pass_2026';
SELECT user, host FROM mysql.user WHERE user = 'report_user' ORDER BY host;
SHOW GRANTS FOR 'report_user'@'localhost';
+-------------+-----------+
| user        | host      |
+-------------+-----------+
| report_user | %         |
| report_user | localhost |
+-------------+-----------+

+-------------------------------------------------+
| Grants for report_user@localhost                |
+-------------------------------------------------+
| GRANT USAGE ON *.* TO `report_user`@`localhost` |
+-------------------------------------------------+

ログインするときは、接続元に合うアカウントのうち host がより細かいほうが選ばれます。「権限を付けたのに読めない」ときは、SELECT CURRENT_USER(); でどのアカウントとして入っているかを確かめてください。host を省いて CREATE USER ‘tmp_user’ と書くと、host は ‘%’ になります。

ほかにも次の点に気をつけてください。

  • アプリに GRANT ALL PRIVILEGES ON *.* を渡さない。ほかのデータベースやユーザー管理まで触れてしまいます
  • mysql.user などの表を UPDATE で直接書き換えない。FLUSH PRIVILEGES を打つまで反映されず、書き間違いにも気づきにくくなります
  • 使わなくなったアカウントは DROP USER で消す。退職者や古いバッチのアカウントが残ると、パスワードが漏れたときの入口になります

まとめ

MySQL のアカウントはユーザー名と接続元の組で、CREATE USER で作ってから GRANT で権限を渡します。アプリには対象のデータベースの読み書きだけ、集計担当には必要なテーブルと列の読み取りだけ、と役割ごとに分けておけば、パスワードが漏れても被害は許した範囲で止まります。人が増えるならロールにまとめ、SET DEFAULT ROLE まで忘れずに実行してください。権限を変えたら SHOW GRANTS で中身を確かめる習慣をつけておくと、付け忘れや付けすぎにすぐ気づけます。

もっと学ぶなら【PR】

  • MySQL徹底入門 第5版:MySQL の機能を広く解説した本。日付関数などの細かい動きを調べたいときに

※Amazonのアソシエイトとして、当サイトは適格販売により収入を得ています。

参考リンク

アイキャッチ画像: Photo by AngTheDesigner on Unsplash

タイトルとURLをコピーしました