3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

みなさんはPostgreSQLのパスワードポリシーについて考えたことがあるでしょうか。

実はPostgreSQLにはデフォルトでパスワードの文字種や文字数に制限がありません(私の過去の投稿「PostgreSQLのユーザのパスワードに制限はあるのか?」も参照)。SQLが文字列として処理できるデータであれば何でも設定できてしまいます。つまり、CREATE ROLE alice WITH PASSWORD 'a'; とやれば1文字のパスワードが普通に作れてしまいます。

セキュリティポリシー的に「パスワードは英大文字・小文字・数字・記号を含む8文字以上」などの制限をかけたいとなったとき、PostgreSQLの標準機能では対応できません。あまり有名ではありませんがpasswordcheckという拡張機能が標準で用意されています。しかし、これはソースを直接修正してビルドしないとポリシーの変更ができないという結構使用するハードルが高いものです。

そんなわけで、postgresql.confにパラメータを設定するだけでパスワードポリシーを柔軟に変更できる拡張機能が欲しいなあと常々思っていました。

しかし自慢じゃありませんが私はC言語の素人です。PostgreSQLそのものがC言語で書かれており、その拡張機能も基本はC言語で書きます。しかもマニュアルには懇切丁寧な拡張機能の作り方があるわけではなく、他の拡張機能を参考に書くのが普通です。それで諦めていたのですが、現在我々には強い味方がいます。

そう、AIです。

Claude Code とは

今回私が使用したAIはClaude Codeです。今さら説明するまでもないですが、Claude CodeとはAnthropic社が提供するAIコーディングツールです。ターミナルからclaudeコマンドで起動し、コードの作成・編集・テスト実行などをAIと対話しながら進められます。

普通のChatGPTのようなチャットAIと違うのは、ファイルの読み書きやコマンドの実行をAI自身が行える点です。「このファイルを修正して」と頼めば実際にファイルを書き換えてくれますし、「ビルドして確認して」と言えばmakeを走らせてエラーがあれば自分で直してくれます。すごすぎ。

今回のように「C言語はよくわからないけど、PostgreSQL拡張機能を作りたい」という状況では心強い味方になります。

作る拡張機能の要件

今回作る拡張機能の要件はこんな感じです。

  • postgresql.confでパスワードポリシーを設定できる
  • CREATE ROLE / ALTER ROLE 時にパスワードを検証し、ポリシー違反はエラーにする
  • パスワードの最小・最大文字数を設定できる
  • 英大文字・小文字・数字・記号の必須化をそれぞれ設定できる
  • CREATE ROLE ... LOGIN でパスワードの指定を必須にできる

PostgreSQL拡張機能の仕組み

PostgreSQL拡張機能を作る上で重要な概念がフックです。

PostgreSQLはいくつかの処理ポイントにフック(関数ポインタ)を用意しており、そこに自前の関数を差し込むことができます。拡張機能をロードすると_PG_init()が呼ばれるので、そこでフックを登録します。

今回使うフックは2種類です。

check_password_hook

パスワードが設定・変更されるタイミングで呼ばれるフックです。CREATE ROLE ... PASSWORD 'xxx'ALTER ROLE ... PASSWORD 'xxx' が実行されると、パスワードの内容がここに渡ってきます。

static check_password_hook_type prev_check_password_hook = NULL;

void _PG_init(void)
{
    prev_check_password_hook = check_password_hook;
    check_password_hook = pg_passwd_policy_check;
}

既存のフックをprev_check_password_hookに退避してから自分のフックを登録します。こうすることで、他の拡張機能のフックと共存できるようです。

ProcessUtility_hook

CREATE ROLEDROP TABLEなどのDDL文を処理するタイミングで呼ばれるフックです。check_password_hookはパスワードが提供されたときにしか呼ばれないため、CREATE ROLE foo LOGIN; のようにパスワードを指定しなかった場合は素通りしてしまいます。パスワード未指定を検知するにはこちらを使う必要があります。

パスワード強度チェックの実装

check_password_hookに登録する関数を実装します。

static void
pg_passwd_policy_check(const char *username, const char *shadow_pass,
                       PasswordType password_type, Datum validuntil_time,
                       bool validuntil_null)
{
    int     len;
    bool    has_upper = false;
    bool    has_lower = false;
    bool    has_digit = false;
    bool    has_special = false;

    if (prev_check_password_hook)
        prev_check_password_hook(username, shadow_pass, password_type,
                                 validuntil_time, validuntil_null);

    /* MD5やSCRAMのハッシュはチェックできないのでスキップ */
    if (password_type != PASSWORD_TYPE_PLAINTEXT)
        return;

    len = strlen(shadow_pass);

    if (min_length > 0 && len < min_length)
        ereport(ERROR,
                (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                 errmsg("password is too short"),
                 errdetail("Password must be at least %d characters long.", min_length)));
    
    /* 以降、文字種チェックが続く */
}

ここでのポイントは、PASSWORD_TYPE_PLAINTEXT(平文パスワード)の場合のみチェックするということです。MD5やSCRAMでハッシュされたパスワードは、ハッシュ後の文字列しか渡ってこないのでポリシーチェックのしようがありません。実運用ではpassword_encryptionの設定も合わせて確認してください。

postgresql.confで設定するGUCパラメータは_PG_init()で定義します。

DefineCustomIntVariable("pg_passwd_policy.min_length",
                        "Minimum password length (0 = disabled).",
                        NULL,
                        &min_length,
                        0, 0, INT_MAX,
                        PGC_SIGHUP,
                        0,
                        NULL, NULL, NULL);

PGC_SIGHUPを指定することで、pg_reload_conf()を実行するだけでパラメータの変更が反映されます。PostgreSQLの再起動は不要です。

LOGINロールへのパスワード必須化

パスワード強度チェックだけでも十分便利ですが、「そもそもLOGINロールにパスワードが設定されていない」という状況も防ぎたくなりました。これはClaude Codeと相談しながら追加した機能です。

前述のとおり、check_password_hookはパスワードが提供されたときにしか呼ばれません。ProcessUtility_hookを使ってCREATE ROLE文のパース木を解析する必要があります。

パース木を見ると、CREATE ROLE ... LOGINCreateRoleStmtノードとして表現され、オプション一覧の中にcanloginpasswordが入っています。

static void
pg_passwd_policy_process_utility(PlannedStmt *pstmt, ...)
{
    if (require_password_for_login && IsA(pstmt->utilityStmt, CreateRoleStmt))
    {
        CreateRoleStmt *stmt = (CreateRoleStmt *) pstmt->utilityStmt;
        /* CREATE USERはLOGINが暗黙付与されるのでtrueで初期化 */
        bool    has_login = (stmt->stmt_type == ROLESTMT_USER);
        bool    has_password = false;
        ListCell *option;

        foreach(option, stmt->options)
        {
            DefElem *defel = (DefElem *) lfirst(option);

            if (strcmp(defel->defname, "canlogin") == 0 && defel->arg != NULL)
                has_login = (intVal(defel->arg) != 0);
            else if (strcmp(defel->defname, "password") == 0)
                has_password = true;  /* PASSWORD NULLも「明示指定あり」として許可 */
        }

        if (has_login && !has_password)
            ereport(ERROR,
                    (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                     errmsg("password is required for LOGIN role"),
                     errdetail("Roles with LOGIN privilege must have an explicit PASSWORD specified.")));
    }

    /* 前のフック or 標準処理を呼び出す */
    if (prev_process_utility_hook)
        prev_process_utility_hook(...);
    else
        standard_ProcessUtility(...);
}

CREATE USERCREATE ROLE ... LOGINの別名であり、LOGINが暗黙的に付与されます。これを見落とすとCREATE USERコマンドだけ抜け穴になってしまうので、stmt_type == ROLESTMT_USERのケースも考慮しています。

また、PASSWORD NULL(明示的なパスワード無効化)は許可しています。これはpeer認証やcert認証を使うロールでも使えるようにするためです。

CLIツールから使う場合は?

PostgreSQLにはcreateuserというCLIツールがあります。これはPostgreSQLサーバに接続してCREATE ROLEのSQLを発行するラッパーです。

createuser foo   # → サーバに「CREATE ROLE foo LOGIN;」を送信

ProcessUtility_hookはサーバ側で動くため、どのクライアントから来たSQLかには関係なく、すべてのCREATE ROLE文に適用されます。

# NG: パスワードなし(require_password_for_login = on のときはエラー)
createuser foo

# OK: -P オプションでインタラクティブにパスワードを入力
createuser -P foo

# OK: LOGINなし
createuser -L foo

設定と動作確認

postgresql.confに以下を追加して再起動します。

shared_preload_libraries = '$libdir/pg_passwd_policy'

pg_passwd_policy.min_length                = 8
pg_passwd_policy.max_length                = 128
pg_passwd_policy.require_uppercase         = on
pg_passwd_policy.require_lowercase         = on
pg_passwd_policy.require_digits            = on
pg_passwd_policy.require_special           = on
pg_passwd_policy.require_password_for_login = on

動作確認はこんな感じです。

-- NG: 7文字(短すぎる)
CREATE ROLE alice WITH PASSWORD 'Abc!123';
-- ERROR:  password is too short
-- DETAIL:  Password must be at least 8 characters long.

-- NG: 大文字なし
CREATE ROLE alice WITH PASSWORD 'abcd!1234';
-- ERROR:  password must contain at least one uppercase letter

-- NG: LOGINロールにパスワードなし
CREATE ROLE bob LOGIN;
-- ERROR:  password is required for LOGIN role
-- DETAIL:  Roles with LOGIN privilege must have an explicit PASSWORD specified.

-- OK: 全条件クリア
CREATE ROLE bob LOGIN PASSWORD 'Str0ng!Pass';

パスワードポリシーのパラメータはpg_reload_conf()で即時反映されます。require_password_for_loginはスーパーユーザがSETコマンドでセッション単位に変更することもできます。

Claude Code を使ってみた感想

今回初めてClaude Codeを使って拡張機能を作りましたが、C言語の素人でもここまで動くものが作れてドン引き驚きでした。

PostgreSQLのフックの仕組みや、ProcessUtility_hookを使ったDDLの横取り方法など、自分一人で調べていたら相当な時間がかかったと思います。特に「CREATE USERCREATE ROLE ... LOGINの別名なのでROLESTMT_USERの判定が必要」といったポイントも気づいてくれて助かりました。そもそもビルド環境整えるのが大変なんすよね……。

一方で、PostgreSQLの内部動作についての深い理解がないと、提案された実装が正しいかどうか判断できないこともあります。まあ、PostgreSQLはソースもマニュアルも公開されているので、それもAIに読ませてレビューすりゃいいっていう時代がもう来てますかね。

コードを書いてもらうだけでなく、「CREATE USERの場合はどうなる?」「PASSWORD NULLは許可すべき?」といった設計上の判断についても一緒に考えてもらえるのは、単なるコード補完ツールとは一線を画しているところだと思います。

まとめ

PostgreSQLのパスワードポリシー拡張機能pg_passwd_policyを、Claude Codeと一緒に作りました。

  • check_password_hookでパスワード強度をチェック
  • ProcessUtility_hookでLOGINロール作成時のパスワード必須化を実現
  • postgresql.confのパラメータで柔軟に設定可能(再起動不要)

ソースコードはGitHubで公開しています。

C言語の素人がAIの力を借りてPostgreSQL拡張機能を作るというのは、数年前では考えられなかったことで、AIツールの進化を改めて実感しました。

3
0
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
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?