背景/きっかけ
pacman -Syuを実行しようとしたところ、正しいはずのパスワードでsudoが通らなくなった。
[sudo] user のパスワード:
残念、また試してください。
[sudo] user のパスワード:
残念、また試してください。
[sudo] user のパスワード:
sudo: 3 回パスワード試行を間違えました
以前(sudoのfaillockロックアウト記事)と似た症状だったので、まずIME(日本語入力)やpam_faillockのロックアウトを疑った。でも今回は全然違う原因だった。
環境はCachyOS(Arch系)、KDE Plasma 6.7.4のWaylandセッション。
調査/原因
1. faillockとIMEは無関係だった
まずfaillock --user userでロック状況を確認したが記録は空。IMEも英数モードだったので、これらは原因ではなかった。念のため/etc/pam.d/system-authを見ると、以前の記事の対策でpam_faillock関連の行はすでにコメントアウト済みだった。振り出しに戻る。
2. suは通るのにsudoだけ通らない
同じパスワードでsu -(rootのパスワード)を試すと成功する。「同じユーザー・同じパスワードなのにsudoだけ失敗するのか」と思ったが、これが勘違いだった。su -は引数なしだとrootのパスワードを聞いていて、userユーザー自身のパスワードの検証にはなっていなかった。 これに気づくまでかなり時間を溶かした。
3. PAM認証だけを直接テストするツールを自作
sudoやsuのバイナリを経由すると余計な要因が絡んでくるので、PAMの認証チェーンだけを直接叩けるツールが欲しくなった。libpamを使う小さなCプログラムを書く。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <security/pam_appl.h>
static int conv_func(int num_msg, const struct pam_message **msg,
struct pam_response **resp, void *appdata_ptr) {
struct pam_response *reply = calloc(num_msg, sizeof(struct pam_response));
for (int i = 0; i < num_msg; i++) {
if (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF || msg[i]->msg_style == PAM_PROMPT_ECHO_ON) {
char buf[256];
printf("%s", msg[i]->msg);
fflush(stdout);
if (!fgets(buf, sizeof(buf), stdin)) buf[0] = 0;
buf[strcspn(buf, "\n")] = 0;
reply[i].resp = strdup(buf);
reply[i].resp_retcode = 0;
} else {
printf("%s\n", msg[i]->msg);
}
}
*resp = reply;
return PAM_SUCCESS;
}
int main(int argc, char **argv) {
if (argc < 3) {
fprintf(stderr, "usage: %s <service> <user>\n", argv[0]);
return 1;
}
struct pam_conv conv = { conv_func, NULL };
pam_handle_t *pamh = NULL;
int rc = pam_start(argv[1], argv[2], &conv, &pamh);
if (rc != PAM_SUCCESS) {
fprintf(stderr, "pam_start failed: %d\n", rc);
return 1;
}
rc = pam_authenticate(pamh, 0);
printf("pam_authenticate: %s (%d)\n", pam_strerror(pamh, rc), rc);
if (rc == PAM_SUCCESS) {
int rc2 = pam_acct_mgmt(pamh, 0);
printf("pam_acct_mgmt: %s (%d)\n", pam_strerror(pamh, rc2), rc2);
}
pam_end(pamh, rc);
return 0;
}
gcc -o pamtest pamtest.c -lpam
これを使うとpam_start("sudo", "user", ...)のように、PAMサービス名とユーザー名を指定して認証チェーンだけを直接叩ける。sudo/suバイナリ固有の挙動(ttyの扱いやsecure_path等)を排除して検証できるのがポイント。
正しいパスワードで試したら、はっきり差が出た。
❯❯❯ ./pamtest sudo user
Password: (正しいパスワード)
pam_authenticate: Permission denied (6)
❯❯❯ ./pamtest su user
Password: (同じ正しいパスワード)
pam_authenticate: Success (0)
suサービスはSuccess、sudoサービスはPermission denied。同じパスワード・同じユーザーなのにPAMレベルで結果が割れている。エラーコードも単純な「パスワード不一致」(Authentication failure, code 7)ではなくPermission denied(code 6)という別物だったのも手がかりになった。
4. /etc/pam.d/にテスト用サービスファイルを作って二分探索
/etc/pam.d/sudoはsystem-authをincludeしているだけ。なのでsystem-authのauthスタックを1行ずつ削った独自のテスト用PAMサービスファイル(/etc/pam.d/sudo-testN)を作って、pamtest sudo-testN userで切り分けていった。
system-authの該当部分:
-auth [success=2 default=ignore] pam_systemd_home.so
auth [success=1 default=bad] pam_unix.so try_first_pass nullok
auth optional pam_permit.so
auth required pam_env.so
pam_unix.so単体、required指定 → 成功pam_unix.so単体、try_first_pass nullok付き、required指定 → 成功pam_unix.soを[success=1 default=bad]というsystem-authと同じ制御構文にしただけ(それ以外は変更なし) →Permission deniedで失敗
つまり原因はtry_first_passやnullokといった引数ではなく、[success=1 default=bad]という角括弧の制御構文そのものだった。パスワードはpam_unix.so内部の照合をちゃんと通っているのに、この構文を経由するとPermission deniedとして握りつぶされてしまう。
5. pamパッケージのバージョンと既知の不具合
❯❯❯ pacman -Q pam
pam 1.7.2-2.1
Web検索したら、Arch LinuxフォーラムにPermission denied on login: a possible issue with pamという関連スレッドを発見。こちらはpam_access.soで角括弧付きグループ指定(+(wheel):LOCAL)が正しく評価されず拒否ルールに落ちる、という別の症状だが、pam 1.7.0で角括弧を使った制御構文のパースにリグレッションが入っていたという点で今回の症状と一致する。修正パッチはlinux-pam本家リポジトリに上がっていて、次のArch側の更新で取り込まれる見込みとのこと。
今回踏んだ[success=1 default=bad]はaccess.confの括弧グループ指定とは別の構文だけど、同じバージョンの同じ「角括弧の制御構文パーサー」由来の不具合と考えれば筋が通る。
解決策の実装
system-authの該当行を、バグのある角括弧構文からrequiredに書き換えた。
# 修正前
auth [success=1 default=bad] pam_unix.so try_first_pass nullok
# 修正後
auth required pam_unix.so try_first_pass nullok
[success=1 default=bad]は「成功したら次の1行(pam_permit.so)をスキップする」という意味だが、pam_permit.soは常に成功するoptionalモジュールなので、スキップしてもしなくても結果は同じ。requiredに変えれば機能は落とさずバグのある構文だけ避けられる。
最終的にsystem-authの該当行は以下の設定に落ち着いた。
auth required pam_unix.so try_first_pass nullok
su -
sed -i "7s/.*/auth required pam_unix.so try_first_pass nullok/" /etc/pam.d/system-auth
sed -n 7p /etc/pam.d/system-auth
exit
修正後、sudo -vが無事パスワードを受け付けるようになった。良かった。
ハマった点
su -がrootのパスワードを要求することを忘れていた
一番時間を溶かしたのがこれ。「suは通るのにsudoは通らない」という前提で調査を進めていたけど、実際にはsu -(引数なし)はrootのパスワードを聞いていて、userユーザー自身のパスワードの検証には全くなっていなかった。その間、passwd userで何度もパスワードを変更しては「新しいパスワードでもsuが通らない」と混乱していたが、それはそもそもsuがuserのパスワードを見ていなかったから。結局ユーザー本人の指摘(「su -ってrootのパスワードじゃない?」)で気づいた。恥ずかしい。
user自身のパスワードを直接検証したいならsu - user(rootから)か、今回作ったPAM直接テストツールを使う必要がある。
エラーコードの違いに気づくまで「パスワードが違う」だと思い込んでいた
sudoが失敗したときのメッセージは「残念、また試してください。」という汎用のパスワード不一致メッセージで、su成功時との違いが見た目上わかりにくい。PAMの戻り値を直接見るツールを作ったおかげで、Authentication failure(単純な不一致, code 7)ではなくPermission denied(code 6)という別のコードが返っていることに気づけた。これが「パスワード自体は合っているのに、スタックのどこかで握りつぶされている」という方向に調査を進める決め手になった。
sudoは/etc/pam.d/sudo→system-authという長いPAMチェーンを通るが、suはpam_unix.soだけのシンプルなチェーンで、同じパスワードでも通る/通らないが分かれることがあるsu -(引数なし)はrootのパスワードを要求する。ユーザー自身のパスワードを検証したい場合はsu - <user>か別の手段を使うこと- 「パスワードが違う」系のエラーは、必ずしも本当にパスワードが違うとは限らない。PAMの戻り値(
Authentication failurevsPermission deniedなど)を直接確認できると原因の切り分けが格段に速くなる。libpamを直接叩く小さなCプログラムは数十行で書け、sudo/suバイナリの挙動を排除して検証するのに有効 - pam 1.7.0系には角括弧の制御構文(
[success=N default=...])のパースに関するリグレッションが複数箇所で報告されている(Archフォーラムのpam_access.soグループ指定の例)。今回踏んだpam_unix.soの[success=1 default=bad]も同種の不具合とみられる - 回避策としては、角括弧の制御構文を実質的に同じ意味を持つ
required/sufficientなどの単純なキーワードに書き換えると良い(将来pamパッケージが更新され本家で修正されたら、必要に応じて元の構文に戻すことも検討する)