約20分で読めます
DBRE
PostgreSQLのCREATEDBはGRANTでは付けられない ―― MySQLとの権限モデルの違いと、SET ROLE利用時の落とし穴
こんにちは。KINTO テクノロジーズの DBRE チーム所属の makoto.sato です。2026年3月に中途入社し、普段は Aurora(MySQL / PostgreSQL)の運用・信頼性向上や、社内向け DB 作業基盤の開発をしています。前職では長く DBA としてオンプレの MySQL / PostgreSQL / Oracle を運用していました。
はじめに
KINTO テクノロジーズの DBRE チームでは、DB 作業の申請〜承認〜一時的な作業環境の自動構築までを Slack から完結させる社内ツール「PowerPole」を開発・運用しています。全体像は Amazon Web Services ブログの クルマのサブスク「KINTO」のアジリティとガバナンスを両立した DBRE の取り組み で紹介しました。
仕組みをかいつまんで言うと、Slack から申請 → 承認 → EC2 と一時的な DB 認証情報が自動発行 → 作業完了後に自動削除、という流れで、踏み台構築から利用終了までのガバナンスと作業スピードを両立させるものです。
PowerPole はこれまで Aurora MySQL のみ対応でしたが、社内での Aurora PostgreSQL 利用の広がりに合わせて PostgreSQL 対応を進めることになりました。本記事では、その中で遭遇した「権限を付けたはずなのに効かない」問題と、その根底にあった MySQL と PostgreSQL の権限モデルの違いについて書きます。
なお、扱う挙動はすべてコミュニティ版 MySQL / PostgreSQL 共通の仕様で、Aurora 固有のものではありません。検証環境は Aurora PostgreSQL 17 と MySQL 8.0 です。MySQL のロール機能は 8.0 で追加されたものなので、5.7 では本記事の MySQL 側の記述は成立しません。CREATEROLE は PostgreSQL 16 で意味が大きく変わっているため、15 以前の環境では本記事の構成をそのまま適用しないでください。
症状:\du には権限が見えているのに、権限エラーになる
あるロールに CREATEDB を付与し、\du(psql でロールの一覧と属性を表示するコマンド)でも確かに付いていることを確認しました。
List of roles
Role name | Attributes
-----------+-------------------------
dbuser | Create role, Create DB
ところが、その dbuser で接続してデータベースを作ろうとすると失敗します。
CREATE DATABASE test_db;
-- ERROR: permission denied to create database
属性は付いている。しかし効いていない。一見矛盾した状況です。
MySQL 版ではどう書いていたか
PowerPole には、申請時に選べる権限レベルの 1 つとして「DDLOperator」があります。テーブルやデータベースの作成・変更を一時的に許可する権限レベルです。
MySQL 版では、pp_ddl_operator という共有ロールに必要な権限をすべて集約し、発行する一時ユーザーにこのロールを付与しています(一部抜粋)。
CREATE ROLE 'pp_ddl_operator';
GRANT CREATE, ALTER, DROP, SELECT, INSERT, UPDATE, DELETE, INDEX,
CREATE VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE TEMPORARY TABLES
ON *.* TO 'pp_ddl_operator';
GRANT CREATE USER ON *.* TO 'pp_ddl_operator'; -- ユーザーとロールの作成・削除
GRANT ROLE_ADMIN ON *.* TO 'pp_ddl_operator'; -- ロールの付与・剥奪
-- 一時ユーザーへロールを付与し、次回ログイン時に自動で有効になるよう既定ロールにする
-- (MySQL は activate_all_roles_on_login が既定 OFF なので、付与しただけでは有効にならない)
GRANT 'pp_ddl_operator' TO 'dbuser';
ALTER USER 'dbuser' DEFAULT ROLE 'pp_ddl_operator';
ここで注目してほしいのは、権限の種類が違っても書き方が変わらない点です。テーブル操作のようなオブジェクト権限も、DB 作成やユーザー作成のようなサーバー全体に関わる能力も、すべて同じ GRANT ... ON *.* で表現できています。
PostgreSQL 版でも同じ設計を踏襲し、共有ロールに権限を集約して一時ユーザー dbuser にロールを付与する構成にしました。テーブルやスキーマへのオブジェクト権限は GRANT で集約でき、ここまでは順調でした。
つまずき①:GRANT CREATEDB という書き方はできない
問題は「データベース作成」「ロール作成」でした。MySQL の感覚でこう書きたくなりますが、エラーになります。
GRANT CREATEDB TO pp_ddl_operator;
-- ERROR: role "createdb" does not exist
PostgreSQL の GRANT <名前> TO <ロール> はロールメンバーシップの付与として解釈されます。PostgreSQL は「createdb という名前のロール」を探し、存在しないためエラーになります。
さらに紛らわしいのが次の書き方です。構文としては通りますが、意味はまったく違います。
GRANT CREATE ON DATABASE mydb TO pp_ddl_operator; -- 通るが「DB作成権限」ではない
これは「mydb の中にスキーマなどを作る権限」であって、データベースを作る権限ではありません。
データベース作成・ロール作成は、GRANT とは別の「ロール属性」という仕組みで表現されていて、ALTER ROLE(または CREATE ROLE 時のオプション)でしか付与できません。
ALTER ROLE <ロール名> CREATEDB CREATEROLE;
つまずき②:属性を付けたのに効かない
ALTER ROLE で付ければよいと分かったので、次はどのロールに付けるかです。最初は一時ユーザーの dbuser に付与しました。
ALTER ROLE dbuser CREATEDB CREATEROLE;
dbuser は一定期間を過ぎると削除される使い捨てのユーザーなので、属性もユーザーごと消えます。常設の共有ロールに永続的な属性を持たせるより影響範囲が狭く、妥当な選択に思えました。
その結果が、冒頭に書いた「\du には見えているのに権限エラー」でした。
原因調査
PowerPole が生成しているユーザー作成処理では、このように権限を付与しています。
GRANT pp_ddl_operator TO dbuser;
ALTER ROLE dbuser SET role = 'pp_ddl_operator'; -- role パラメータの値として渡すのでクォートしている(識別子でも書ける)
2 行目は role パラメータの既定値をロール単位で設定するもので、dbuser でログインするとセッション開始時に自動的に SET ROLE pp_ddl_operator された状態になります。利用者が接続後に毎回 SET ROLE を打たなくても、付与されたロールの権限ですぐ作業を始められるように入れているものです。
この SET ROLE が怪しいので、実際に接続して確認してみます。
SELECT current_user, session_user;
-- current_user | session_user
-- ------------------+--------------
-- pp_ddl_operator | dbuser
session_user(ログインしたロール)は dbuser のままですが、current_user(権限チェックの対象になるロール)は pp_ddl_operator に切り替わっていました。
PostgreSQL のロール属性チェックは、current_user そのものの属性だけを見ます。(CREATEDB / CREATEROLE / SUPERUSER のように SQL の実行時に評価される属性の話です。LOGIN は接続を確立する時点でログインロールに対して評価されるので、SET ROLE の影響を受けません。)CREATE DATABASE 実行時に評価されるのは pp_ddl_operator の属性であって、dbuser の属性ではありません。dbuser にいくら CREATEDB を付与しても、既定の current_user では評価対象になっていなかったのです。
メンバーシップで継承されるのはオブジェクト権限だけ
「dbuser は pp_ddl_operator のメンバーなのだから、属性もどこかで繋がるのでは?」と思うかもしれません。しかし、継承されるのはオブジェクト権限(テーブルの SELECT / INSERT など)に限られ、CREATEDB / CREATEROLE / LOGIN / SUPERUSER といったロール属性は継承対象外です。これはドキュメントにも明記されています。
The role attributes
LOGIN,SUPERUSER,CREATEDB, andCREATEROLEcan be thought of as special privileges, but they are never inherited as ordinary privileges on database objects are. You must actuallySET ROLEto a specific role having one of these attributes in order to make use of the attribute.
つまり、同じ「権限」と呼ばれるものの中に、評価のされ方が違う 2 種類が同居しています。
| 評価の対象 | 継承 | |
|---|---|---|
ロール属性(CREATEDB など) |
current_user そのものの属性だけ |
されない |
| オブジェクト権限(テーブルの SELECT など) | current_user と、それが継承しているロール群 |
される |
図にすると次のようになります。

\du で属性が見えていたのに効かなかった理由は、この 2 つの仕様の組み合わせでした。
dbuserの属性は、SET ROLE後のcurrent_user(pp_ddl_operator)の評価では参照されない- メンバーシップがあっても、属性は継承されない
この非対称性は、Aurora / RDS を使っていれば身近なところに現れています。マスターユーザーは rds_superuser のメンバーですが、その定義は AWS のドキュメントで次のように示されています。
CREATE ROLE postgres WITH LOGIN NOSUPERUSER INHERIT CREATEDB CREATEROLE NOREPLICATION VALID UNTIL 'infinity'
CREATEDB と CREATEROLE がマスターユーザー自身に直接付与されています。rds_superuser のメンバーであることで属性が得られるなら、この直接付与は要りませんでした。
対処
原因は「既定の current_user が共有ロールになっていること」なので、属性を付ける先を、実際に current_user として振る舞うロールに変えます。
ALTER ROLE pp_ddl_operator CREATEDB CREATEROLE;
これで期待どおりデータベース・ロールの作成ができるようになりました。結果として、MySQL 版と同じ「権限はすべて共有ロールに集約し、ユーザーにはロールを渡すだけ」という形に属性の置き場所も揃いました。
共有ロールに属性が残り続けることが気になるかもしれませんが、pp_ddl_operator は NOLOGIN で直接ログインできず、属性を使えるのはメンバーシップを持つロールに限られます。付与と剥奪は申請のライフサイクルで管理されています。逆に言えば実際の境界は NOLOGIN ではなくメンバーシップなので、恒久ユーザーにこのロールを付与しないことが前提になります。
なぜ GRANT に乗らないのか ―― オブジェクト空間の違い
ここまでが実際にハマった話です。最後に、そもそもなぜ CREATEDB が GRANT で書けないのかを整理します。
MySQL では Database と Schema は同義で、サーバーの中はフラットな構造です。
Server(MySQL サーバー全体)← GRANT ... ON *.* がここ全体を覆う
└── Database(= Schema。同義)
└── Table / View / Procedure / ...
DB を作ることも、その中にテーブルを作ることも同じ空間の中の出来事なので、この記事で扱う権限セットはすべて GRANT ... ON *.* の 1 つの語彙で書けます。
一方 PostgreSQL は 3 層構造です。
Cluster(PostgreSQL サーバー全体)← Database作成・Role作成はここ(ロール属性で管理)
└── Database(完全に分離された名前空間)
└── Schema(Database 内の名前空間)
└── Table / View / Function / ...
GRANT で付与するオブジェクト権限は、Database や Schema といった入れ物ごとに指定する形になります。MySQL の ON *.* に相当する全 Database 一括指定が無いのは、テーブルなどのカタログが Database ごとに分離していて、接続中の Database の外にあるオブジェクトを指定できないためです。
では Database や Role はどうかというと、これらはクラスタ全体で共有されるカタログに属します。ただし層が違うから GRANT に乗らない、という単純な話ではありません。GRANT ... ON DATABASE や GRANT ... ON TABLESPACE は、クラスタ全体に関わる対象を GRANT で扱えています。
違いは権限を書き留める先があるかどうかです。GRANT は権限を対象オブジェクトのアクセス制御リスト(ACL)に書き込みますが、CREATE DATABASE と CREATE ROLE が相手にするクラスタ自身は、ACL を持つオブジェクトとして存在しません。そのため PostgreSQL はこれらをロール自身の属性として保持し、ALTER ROLE で操作します。ただし定義済みロールという抜け道はあり、PostgreSQL 16 の pg_create_subscription は「まだ存在しないものを作る能力」をメンバーシップで配っています。CREATEDB / CREATEROLE が属性側にあるのは原理的な制約ではなく、仕組みが古いという経緯です。
いずれにしても、MySQL で 1 つの GRANT に乗っていた権限セットは、PostgreSQL では「オブジェクトの ACL に書く権限」と「ロール自身に持たせる属性」の 2 系統に分かれます。
同じ権限セットを書き比べると
DDLOperator 相当の権限セット(DDL/DML + DB 作成 + ロール作成)を共有ロールに集約すると、こうなります。
MySQL はすべて GRANT で完結します。
CREATE ROLE 'pp_ddl_operator';
-- オブジェクト権限もサーバーレベルの能力も、すべて同じ GRANT
GRANT CREATE, ALTER, DROP, SELECT, INSERT, UPDATE, DELETE, INDEX,
CREATE VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE TEMPORARY TABLES
ON *.* TO 'pp_ddl_operator';
GRANT CREATE USER ON *.* TO 'pp_ddl_operator'; -- ユーザーとロールの作成も GRANT
GRANT ROLE_ADMIN ON *.* TO 'pp_ddl_operator'; -- ロールの付与・剥奪も GRANT
-- ユーザーへはロールを付与し、次回ログイン時に有効になるよう既定ロールにする
GRANT 'pp_ddl_operator' TO 'dbuser';
ALTER USER 'dbuser' DEFAULT ROLE 'pp_ddl_operator';
PostgreSQL は 2 系統に分かれます。
CREATE ROLE pp_ddl_operator NOLOGIN;
-- オブジェクト権限は GRANT。ただし *.* のような全体指定は無く、
-- データベース・スキーマごとに付与する(ALL TABLES は実行時点の既存テーブルのみ)
GRANT ALL PRIVILEGES ON SCHEMA myschema TO pp_ddl_operator;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA myschema TO pp_ddl_operator;
GRANT TEMPORARY ON DATABASE mydb TO pp_ddl_operator;
-- データベース作成・ロール作成は GRANT では書けない。ALTER ROLE で属性として付与する
ALTER ROLE pp_ddl_operator CREATEDB CREATEROLE;
-- ユーザーへはメンバーシップを付与
GRANT pp_ddl_operator TO dbuser;
先頭の CREATE ROLE を除くと、MySQL 側は最後の 1 行以外すべて GRANT で、PostgreSQL 側は ALTER ROLE の 1 行だけが別系統に落ちます。この 1 行が今回のハマりどころでした。
やりたいことを軸に並べると、対応はこうなります。
| やりたいこと | MySQL | PostgreSQL |
|---|---|---|
| テーブルの DML | GRANT ... ON *.* |
GRANT ... ON ALL TABLES IN SCHEMA x(既存テーブルのみ。以後の分は ALTER DEFAULT PRIVILEGES) |
テーブルの ALTER / DROP |
GRANT ALTER, DROP ON *.* |
GRANT では不可。所有ロールのメンバーになる |
| 一時テーブル | GRANT CREATE TEMPORARY TABLES ON *.* |
GRANT TEMPORARY ON DATABASE x |
| データベース作成 | GRANT CREATE ON *.* |
ALTER ROLE x CREATEDB |
| ロール・ユーザー作成 | GRANT CREATE USER ON *.* |
ALTER ROLE x CREATEROLE |
| ロールの付与 | GRANT 'role' TO 'user' |
GRANT role TO user |
| ロールの有効化 | ALTER USER 'user' DEFAULT ROLE 'role' |
ALTER ROLE user SET role = 'role'(任意) |
MySQL 側は GRANT 主体で埋まり、PostgreSQL 側はデータベース作成とロール作成の 2 行だけ ALTER ROLE に落ちます。
もう 1 つ、GRANT に乗らないものは CREATEDB / CREATEROLE だけではありません。テーブルの ALTER / DROP は PostgreSQL では所有権に固有の権利で、GRANT では切り出せません。MySQL の ALTER / DROP 権限に相当するものが無いので、DDL を打たせたい場合は対象の所有ロールに参加させる形になります。所有者かどうかの判定は所有ロールのメンバーであれば満たされるので、ここは属性と違って共有ロール経由で渡せます。
MySQL 側も実は 2 系統ある
MySQL は「すべて GRANT で書ける」ように見えますが、これは構文の見せ方の話です。
MySQL でも CREATE USER や ROLE_ADMIN のようなグローバル権限は、オブジェクトの ACL ではなくアカウントに紐づいて格納されています(静的権限は mysql.user の列、ROLE_ADMIN のような動的権限は mysql.global_grants の行として)。この点は PostgreSQL のロール属性と本質的に同じです。さらに次のものは GRANT では書けず、CREATE USER / ALTER USER で指定します。
ACCOUNT LOCK/UNLOCKPASSWORD EXPIREなどのパスワードポリシーMAX_QUERIES_PER_HOURなどのリソース制限REQUIRE SSL、認証プラグインの指定DEFAULT ROLE(SET DEFAULT ROLEでも設定できる)
なお SSL 要件・リソース制限・認証方式は 5.7 までは GRANT でも書けましたが、5.7 で非推奨になり 8.0.11 で削除されました。
つまり 2 系統に分かれているのは PostgreSQL だけではありません。違いは系統の数ではなく、「データベースを作る」「ロールを作る」という能力がどちら側に置かれているかです。
SET ROLE の意味論も違う
もう 1 つ、MySQL にも SET ROLE はありますが意味論が違います。
| MySQL | PostgreSQL | |
|---|---|---|
| ロール有効化後の現在のユーザー | 変わらない(有効ロールは CURRENT_ROLE() で確認) |
切り替わる(current_user が置き換わる) |
| 権限の合成 | アカウント自身の権限と有効ロールの権限の和集合 | current_user 基準。ロール属性は継承されない |
MySQL は加算的なので、「ユーザー側に付けた権限がロール有効化で見えなくなる」という事故は起きません。「SET ROLE があるかどうか」ではなく「SET ROLE が current_user を置き換えるかどうか」 が、今回の落とし穴の分かれ目でした。
CREATEROLE はバージョンで危険度が違う
PostgreSQL 15 以前では、CREATEROLE を持つロールが他の非スーパーユーザーのパスワードを変更でき、定義済みロールを自分に付与できました。つまり CREATEROLE はスーパーユーザーに準じる強さを持っていました(既存のスーパーユーザーには手を出せません)。
PostgreSQL 16 でこの挙動は変更され、権限の範囲が自分が管理するロールに限定されています。本記事の構成は 16 以降を前提としているので、15 以前の環境で共有ロールに CREATEROLE を付与するのは避けてください。
まとめ
- PostgreSQL の
CREATEDB/CREATEROLEはGRANTでは書けず、ALTER ROLEで付与する「ロール属性」 - ロール属性は
current_userそのものだけを見て、継承されない。一方オブジェクト権限はcurrent_userと継承ロール群を見る。この非対称性が落とし穴の正体 - したがって
SET ROLEを使う構成では、属性の付与先はcurrent_userを基準に決める
移植のチェックポイントとして一般化するなら、MySQL で ON *.* と書いていた GRANT 文を洗い出し、どれがオブジェクト権限でどれがロール属性かを最初に仕分けることです。そして権限が効かないときに最初に打つべきは SELECT current_user, session_user; です。この 1 行で切り分けられる問題は、思っているより多いはずです。
参考
- CREATE ROLE privilege cannot be inherited?! – select * from depesz; ―― 2009 年の記事。今回と同じ落とし穴が当時から記録されています
- PostgreSQL: Documentation: Role Attributes ―― ロール属性の一覧と、それぞれが何を許可するか
- PostgreSQL 16でのロールに関する変更点 ――
CREATEROLEの変更点と、GRANTのSET/INHERITオプション - 「MySQL」と「PostgreSQL」のセキュリティ機能を比較する ―― 権限管理を含む、より広い範囲での MySQL / PostgreSQL 比較
関連記事 | Related Posts
We are hiring!
Follow Us
KINTOテクノロジーズの最新情報をSNSで発信中!イベント・テックブログ更新情報もお届けします。
