Edited at
mrubyDay 21

libpam-mruby を試してみた

More than 3 years have passed since last update.

mrubyアドベントカレンダー、21日目です!!! 前回は、harasouさんによる超便利ツールでした

ぼくも難しい記事は無理なのですが、今日は、あんちぽさんが以前スパイクで作っていた libpam-mruby をいじってみて、OS認証のmruby化について実験してみることにします。


PAMについてのおさらい

PAMとは、 Pluggable Authentication Module の略で、各種の認証処理を実行するモジュール群と、それを利用するための標準的なAPIを備えたライブラリによる認証システムの呼称です。

代表的には、LDAP、SMBなどでのLinuxのユーザ認証に利用されているため、Linuxのユーザ認証のためのプラガブルな認証システムのことという文脈で使われることが多いようです。

Sambaユーザーのパスワード管理 PAMを利用して認証を行う より、図を引用させていただきます。

図です

libpam-mrubyは、そのような認証の中身のロジックをmrubyを用いて記述することができるPAMミドルウェアです。


まずはビルドを通す

環境はCentOS 7.1です。

git clone https://github.com/pepabo/libpam-mruby.git

cd libpam-mruby/
sudo yum groupinstall "Development Tools"
sudo yum install rake pam-devel
rake
sudo install build/mruby.so /usr/lib64/security/pam_mruby.so

ここまでで一応ビルドはできるのですが、実は記事執筆直前の段階 0bd52ea では、このままではロードしてくれませんでした。syslog(/var/log/secure)を見ると、以下のようなエラーが出ています。

PAM unable to dlopen(/usr/lib64/security/pam_mruby.so): /usr/lib64/security/pam_mruby.so: undefined symbol: mrb_str_new_cstr

PAM adding faulty module: /usr/lib64/security/pam_mruby.so

どうもmrubyのライブラリなどのリンクに失敗しているようです。


ビルドスクリプトを変更する

リンクを上手く行かせるために、以下のように最後の pam_mruby.so を生成するコマンドを変更します(成果物の名前もpamの規約に沿って変えておきます)。

--- a/Rakefile

+++ b/Rakefile
@@ -21,7 +21,7 @@ end

desc 'build'
task :build => [:build_mruby_c, :build_pam_c] do
- sh 'ld -x --shared -o build/mruby.so build/mruby.o build/pam.o'
+ sh 'gcc -shared -Wl,-soname=pam_mruby.so.1 -lm -lpam -o build/pam_mruby.so build/mruby.o build/pam.o mruby/build/host/lib/libmruby.a'
end

desc 'install'

また、 build_config.rb も以下のように変えます。

--- a/build_config.rb

+++ b/build_config.rb
@@ -1,4 +1,8 @@
MRuby::Build.new do |conf|
toolchain :gcc
conf.gembox 'default'
+
+ conf.cc do |cc|
+ cc.flags << '-fPIC'
+ end
end

この上でビルドをやり直すと、リンクがいい感じになり、mrubyも pam_mruby.so の中にちゃんと組み込まれるようになります。

[vagrant@log libpam-mruby]$ ldd build/pam_mruby.so 

linux-vdso.so.1 => (0x00007fffa8973000)
libm.so.6 => /lib64/libm.so.6 (0x00007fe657b65000)
libpam.so.0 => /lib64/libpam.so.0 (0x00007fe657956000)
libc.so.6 => /lib64/libc.so.6 (0x00007fe657594000)
libaudit.so.1 => /lib64/libaudit.so.1 (0x00007fe65736e000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007fe65716a000)
/lib64/ld-linux-x86-64.so.2 (0x00007fe65810b000)


パスワードをmrubyで認識できるようにする

ここまでで、mrubyで認証が可能になったのですが、実はこの段階ではmrubyのスクリプトではユーザ名しか認証に利用できないので、パスワードを認識できるようにコードを変更してみます。

以下のようにmruby周りの定義を変更し、 check メソッドの引数を1つから2つにします。

diff --git a/src/mruby.c b/src/mruby.c

index 9606465..60c1a52 100644
--- a/src/mruby.c
+++ b/src/mruby.c
@@ -1,12 +1,14 @@
#include "mruby.h"

-int pam_mruby_check(FILE *rbfile, const char *name)
+int pam_mruby_check(FILE *rbfile, const char *name, const char *passwd)
{
mrb_value ret;

mrb_state *mrb = mrb_open();
mrb_load_file(mrb, rbfile);
- ret = mrb_funcall(mrb, mrb_top_self(mrb), "check", 1, mrb_str_new_cstr(mrb, name));
+ ret = mrb_funcall(mrb, mrb_top_self(mrb), "check", 2,
+ mrb_str_new_cstr(mrb, name),
+ mrb_str_new_cstr(mrb, passwd));
mrb_close(mrb);

if (mrb_type(ret) == MRB_TT_TRUE)
diff --git a/src/mruby.h b/src/mruby.h
index 8075ed5..6a1828e 100644
--- a/src/mruby.h
+++ b/src/mruby.h
@@ -7,6 +7,6 @@

#include <stdio.h>

-int pam_mruby_check(FILE *rbfile, const char *name);
+int pam_mruby_check(FILE *rbfile, const char *name, const char *passwd);

#endif

そうしたら、PAM認証の肝である pam_sm_authenticate 内部で、パスワードを取得するコードを追加します。 pam_get_authtok という関数を利用できるようです。

--- a/src/pam.c

+++ b/src/pam.c
@@ -46,7 +46,12 @@ PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags,int argc, const
}
if (debug) pam_syslog(pamh, LOG_DEBUG, "username is %s", username);

- ret = pam_mruby_check(rbfile, username);
+ if (pam_get_authtok(pamh, PAM_AUTHTOK, &password, NULL) != PAM_SUCCESS) {
+ pam_syslog(pamh, LOG_ERR, "couldn't get password from PAM stack");
+ return PAM_AUTH_ERR;
+ }
+
+ ret = pam_mruby_check(rbfile, username, password);
fclose(rbfile);

if (ret > 0)


/etc/pam.d/system-auth-ac

auth required pam_env.so

# この行を追加
auth sufficient pam_mruby.so rbfile=/etc/pam-mruby.d/auth.rb debug try_first_pass
auth sufficient pam_unix.so nullok try_first_pass
auth requisite pam_succeed_if.so uid >= 500 quiet
auth required pam_deny.so

また、このままだと PAM unable to resolve symbol: pam_sm_setcred というログが(実害はないようですが)出続けてしまうので、何もしない pam_sm_setcred を定義する必要があります。

diff --git a/src/pam.c b/src/pam.c

index 5d2abe2..ff7a9f5 100644
--- a/src/pam.c
+++ b/src/pam.c
@@ -59,3 +59,8 @@ PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags,int argc, const
else
return PAM_AUTH_ERR;
}
+
+PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+ return PAM_SUCCESS;
+}

これで、一通りの変更が終わりましたので、ビルドとインストールをやり直しておきます。


mrubyをつかってユーザ認証をする

あとは、以下のエントリを /etc/pam.d/system-auth-ac に追加し、


/etc/pam.d/system-auth-ac

auth required pam_env.so

# ここを追加
auth sufficient pam_mruby.so rbfile=/etc/pam-mruby.d/auth.rb debug try_first_pass
auth sufficient pam_unix.so nullok try_first_pass
auth requisite pam_succeed_if.so uid >= 500 quiet
auth required pam_deny.so
#...

以下のような認証のためのmrubyスクリプトを用意して確認してみましょう。

(追記: 最新のバージョンでは authenticate というメソッドを定義する必要があります)


/etc/pam-mruby.d/auth.rb

def check(username, password)

if username == 'udzura'
password == 'p@ssw0rd'
else
true
end
end

$ sudo adduser udzura

## 雑にUNIXパスワードを潰す
$ echo "udzura:$(uuidgen)" | sudo chpasswd
$ su - udzura
パスワード: # <- p@ssw0rd を入力!
最終ログイン: 2015/12/22 (火) 15:23:48 JST日時 pts/0
[udzura@mruby ~]$

見事にmrubyでユーザログインができました!

認証手段にmrubyを利用できることで、例えば外部APIとの連携など、様々な応用が考えられると思います。

今回のコードの最終的なdiffは こちらのPR にあります。


PAMの拡張について

PAMの拡張の書き方は、The Linux-PAM Module Writers' Guide の3章によると、以下に大別されるそうです。


  • 3.2 Authentication management

  • 3.3 Account management

  • 3.4 Session management

  • 3.5 Password management

これは、 pam.d 配下のファイルの auth/account/session/password の項目に対応しています。LDAPでの認証のように、自動でLDAPのデータベースからアカウントを作成するような処理は Account management で行われるようですが、今回は Authentication management のみを行うので、既存のユーザの認証方法を操作できるということでした。

その他の項目についても実装すれば、より便利になるかもしれません。


なお、このサンプルは、あくまでもサンプルとして、セキュリティなどに関しては不十分な箇所もある(認証用のmrubyファイルが丸見え、とか...)ことをご留意ください。

明日(今日...)は、 yasuyuki さんです。