Apacheで統合Windows認証を使う

前書き

統合 Windows 認証とは,ドメインの認証情報を使って HTTP サーバに認証してもらう方式です。Windows クライアントがドメインにログインしていれば,認証ダイアログが出現することなく自動的に認証されます。統合 Windows 認証には以下の2通りがあります。

今回はわけあって NTLM 認証を扱います。

Apache on Unix*1 で NTLM 認証をサポートするものには,有名なもので以下の物があります。

前者 2 つはほぼ同じもの(2 つめが改良版で Apache 2.2 にも対応している)ですが,後者の mod_auth_ntlm_winbind は次の利点があります。

  • Samba に付属の ntlm_auth ヘルパコマンド*2をバックエンドに利用 / おまかせしている
  • そのため所属グループ単位の認証等ができる
  • NTLMv2 に対応している(らしい);Vista のデフォルト設定でも OK
  • SPNEGO 認証に対応している(はず);オープン規格万歳 & よりシンプルなネゴシエーション
  • Basic 認証にも対応している*3

デメリットとしては,

  • Samba suite がインストールされている必要がある
  • winbindd がきちんと設定されている必要がある
  • smbd / nmbd も立ち上がっているほうがいいと思う
  • バックエンドとして ntlm_auth を子プロセスとして起動するのでちょっと重そう

一応,ntlm_auth コマンドは同一 connection だと使い回すのでそこまで重さについては気遣わなくてもいいはず。あと,どうやら smbd / nmbd だけではなく winbindd も立ち上がってなくてもよいみたいです。ただし smb.conf の winbind のための設定はしておく必要があります。その代わり NTLM だとどうかわかりませんが少なくとも SPNEGO だと,net join してある必要があります。

やっぱり winbindd が立ち上がってないとダメなケースに遭遇しました。あと,ntlm_auth というヘルパスクリプトhttpd のサーバ権限で起きるのですが,/var/cache/samba/winbindd_privileged/pipe にアクセスできるようにしておく必要があります。

今回は mod_auth_ntlm_winbind を扱います。単純に Unofficial mod_ntlm に気づかなかっただけです。

NTLM 認証の流れ

The NTLM Authentication Protocol and Security Support Provider】が参考になります。

  1. Client がコンテンツを要求する
  2. Server が 401 Unauthorized を返す
    • サポートしている認証様式として WWW-Authenticate: NTLM を返す
    • ここで一度 Connection を切る
  3. Client が Authorization: NTLM hogehoge というヘッダつきでコンテンツを要求する
    • hogehoge は nagotiate パケット
    • ここから keep-alive 接続である必要がある
  4. Server が 401 Unauthorized を返す
    • WWW-Authenticate: NTLM fugafuga ヘッダを返す
    • fugafuga は challenge パケット
  5. Client が Authrorization: NTLM gomogomo というヘッダつきでコンテンツを要求する
    • gomogomo は authenticate パケット
  6. Server は authenticate パケットを認証し,正しければ 200 OK を返す
  7. ここから同じコネクションで WWW-Authenticate / Authorizatation ヘッダなしでコンテンツをやりとりする

mod_auth_ntlm_winbind のビルド

どうやら Fedora のレポジトリに入るみたいです。ですがとりあえず今のところは手ビルド,ってことで。
配布元から必要なファイルをダウンロードします。configure.in, Makefile.in, mod_auth_ntlm_winbind.c が必須です。また .in しかないことからおわかりのとおり,autoconf も必要です。ほか RedHat 系なら apxs が必要なので httpd-devel もインストールしておいてください。

そのままビルドして一応動くのですが,いくらか気にくわないところがあったので,パッチをおいておきます。

--- mod_auth_ntlm_winbind.c.orig        2007-01-29 13:00:09.000000000 +0900
+++ mod_auth_ntlm_winbind.c 2007-07-11 12:22:13.000000000 +0900
@@ -80,4 +80,10 @@
 #endif
 
+#if defined(APACHE2) && AP_SERVER_MINORVERSION_NUMBER >= 2
+#define APACHE22 1
+#include "ap_provider.h"
+#include "mod_auth.h"
+#endif
+
 /* The name of the NTLM authentication scheme.  This appears in the
    'WWW-Authenticate' header in the initial HTTP request. */
@@ -490,5 +496,5 @@
 /* Call winbind to authenticate a (user, password)
    pair */
-static int winbind_authenticate_plaintext( request_rec *r, ntlm_config_rec * crec, char *user, char *pass)
+static int winbind_authenticate_plaintext( request_rec *r, ntlm_config_rec * crec, const char *user, const char *pass)
 {
     ntlm_connection_context_t *ctxt = get_connection_context( r->connection );
@@ -917,5 +923,5 @@
         } else
             sent_pw = "";
-        if ((s = strchr(sent_user, '\\')) != NULL
+        if (1 || (s = strchr(sent_user, '\\')) != NULL
             || (s = strchr(sent_user, '/')) != NULL) {
 
@@ -950,4 +956,10 @@
                                           : "Authorization");
     const char *auth_line2;
+#ifdef APACHE22
+    const char *current_auth = ap_auth_type(r);
+    if (!current_auth || (strcasecmp(current_auth, NTLM_AUTH_NAME) && strcasecmp(current_auth, NEGOTIATE_AUTH_NAME))) {
+        return DECLINED;
+    }
+#endif
 
     /* Trust the authentication on an existing connection */
@@ -1040,4 +1052,21 @@
 }
 
+#ifdef APACHE22
+static authn_status check_ntlm_winbind_plaintext(request_rec *r,
+                                                 const char *user,
+                                                 const char *password)
+{
+    ntlm_config_rec *conf = ap_get_module_config(r->per_dir_config,
+                                                 &auth_ntlm_winbind_module);
+    return
+        winbind_authenticate_plaintext(r, conf, user, password) == OK
+            ? AUTH_GRANTED : AUTH_DENIED;
+}
+
+static const authn_provider authn_ntlm_winbind_provider = {
+    &check_ntlm_winbind_plaintext,
+    NULL
+};
+#endif
 
 static void register_hooks(apr_pool_t *pool)
@@ -1045,4 +1074,7 @@
     ap_hook_pre_connection(ntlm_pre_conn,NULL,NULL,APR_HOOK_MIDDLE);
     ap_hook_check_user_id(check_user_id,NULL,NULL,APR_HOOK_MIDDLE);
+#ifdef APACHE22
+    ap_register_provider(pool,AUTHN_PROVIDER_GROUP,"ntlm_winbind","0",&authn_ntlm_winbind_provider);
+#endif
 };
 

変更したところは,

  • mod_auth_basic の authn バックエンドとして使えるようにした(あまりメリットはないです)
  • デフォルトの plaintext 認証ではなぜかドメイン名つきだと認証できなかったので(smb.conf の設定のせいかもしれません)ドメイン名なしのユーザ名でも winbind 認証に投げるようにした
  • AuthType 設定がなされているかチェックするようにした

の以上です。

winbindd の設定

winbindd の設定については他のリソースを参照するか手前味噌ながら【winbind で Linux の認証を ActiveDirectory にまかせる - daily dayflower】を参照してください。

Apache の設定(NTLM 認証編)

先ほど述べた NTLM 認証の流れをみていただければわかりますが,keep-alive 接続がサポートされていることが前提になっています。ですから KeepAlive は必ず On にしておいてください。RedHat 系の httpd.conf だとデフォルトで Off と書いてあるのではまりました。

モジュール自身の設定については README に書いてありますが,間違い*4もあるので書いておきます。

デフォルト設定は以下のようになっています。

NTLMAuth      off    # NTLM認証を有効にする
NegotiateAuth off    # SPNEGO認証を有効にする
NTLMBasicAuth off    # (モジュール内蔵)Basic認証を有効にする

NTLMBasicAuthoritateive on  # (実は使われていない)
NTLMBasicRealm "REALM"      # モジュール内蔵Basic認証の場合のレルム

NTLMAuthHelper "ntlm_auth --helper-protocol=squid-2.5-ntlmssp"
NegotiateAuthHelper "ntlm_auth --helper-protocol=gss-spnego"
PlaintextAuthHelper "ntlm_auth --helper-protocol=squid-2.5-basic"

ですから NTLM 認証のみを行う場合の典型的なコンフィグ(.htaccess とか <Location> 内とか)は以下のようになります。

Require valid-user

AuthType NTLM

NTLMAuth on
NTLMAuthHelper "/usr/bin/ntlm_auth --helper-protocol=squid-2.5-ntlmssp"

これだけです。実は一番最後の行も先に挙げたデフォルトの通りなので必要ありません。

では認証してみます。ドメインにログオンした WindowsIE からアクセスすると,ダイアログなしで認証されます。ログを抜粋すると,

mod_auth_ntlm_winbind.c: doing ntlm auth dance
mod_auth_ntlm_winbind.c: Launched ntlm_helper, pid 14277
mod_auth_ntlm_winbind.c: creating auth user
mod_auth_ntlm_winbind.c: parsing reply from helper to YR TlRM...

libsmb/ntlmssp.c:debug_ntlmssp_flags(63)
  Got NTLMSSP neg_flags=0xa208b207

mod_auth_ntlm_winbind.c: got response: TT TlRM...
mod_auth_ntlm_winbind.c: sending back TlRM....

mod_auth_ntlm_winbind.c: doing ntlm auth dance
mod_auth_ntlm_winbind.c: Using existing auth helper 14277
mod_auth_ntlm_winbind.c: parsing reply from helper to KK TlRM...

libsmb/ntlmssp.c:ntlmssp_server_auth(672)
  Got user=[dayflower] domain=[HOGE] workstation=[WINDOWS] len1=24 len2=24
libsmb/ntlmssp_sign.c:ntlmssp_sign_init(338)
  NTLMSSP Sign/Seal - Initialising with flags:
libsmb/ntlmssp.c:debug_ntlmssp_flags(63)
  Got NTLMSSP neg_flags=0xa2088205

mod_auth_ntlm_winbind.c: got response: AF dayflower
mod_auth_ntlm_winbind.c: authenticated dayflower
mod_auth_ntlm_winbind.c: retaining user dayflower
mod_auth_ntlm_winbind.c: keepalives: 2

のようになっていました(YR だの TT だの KK だの AF だのが気になる方は,【Squid NTLM authentication project】の【The Squid-NTLM helper protocol】参照)。また,$ENV['REMOTE_USER'] に dayflower, $ENV['AUTH_TYPE'] が NTLM となっていました。

実は Firefox 2 (for Linux) も NTLM 認証に対応しています。この場合認証ダイアログが出るので,ドメインのユーザ名とパスワードを入力すると認証されます。

応用編(fallback として Basic 認証も使う)

このままだと例えば Opera (for Linux) ではアクセスできなくなってしまうので,Basic 認証も付け加えてみましょう。この場合,mod_auth_ntlm_winbind 組み込みの Basic 認証メカニズムを使う必要があります*5

Require valid-user

AuthType NTLM

NTLMAuth on
NTLMBasicAuth on
NTLMBasicRealm "Input Windows ID / Password"

NTLMAuthHelper "/usr/bin/ntlm_auth --helper-protocol=squid-2.5-ntlmssp"
PlaintextAuthHelper "/usr/bin/ntlm_auth --helper-protocol=squid-2.5-basic"

これで,

  • WindowsIE からだと(「統合 Windows 認証」が有効な場合)認証ダイアログなしで認証
  • WindowsFirefox からだと条件付きで*6認証ダイアログなしで認証
  • LinuxFirefox からだと認証ダイアログが出現するが NTLM 認証で
  • Linux / WindowsOpera からだと Basic 認証で

認証できるようになりました。

応用編(任意のグループだけアクセス許可する)

man ntlm_auth の内容から,

NTLMAuthHelper "/usr/bin/ntlm_auth --helper-protocol=squid-2.5-ntlmssp
                --require-membership-of='Domain Admins'"

みたいにすればいけるのではないかなぁと思うのですが,ためしていません。

おまけ(ドメインアカウントで Basic 認証だけ使う)

AuthBasicProvider として登録するようにソースを変更したので,Basic 認証のみを使う場合,

Require valid-user

AuthType Basic
AuthName "Input ID / Password"
AuthBasicProvider file ntlm_winbind

AuthUserFile "/foo/bar/.htpasswd"
PlaintextAuthHelper "/usr/bin/ntlm_auth --helper-protocol=squid-2.5-basic"

のようにすることもできます。この例だと,

  • .htpasswd にユーザがいれば OK
  • いなければ NTLM にユーザがいれば OK

のようになります。

積み残し

SPNEGO 認証もいけるはず,なんですがどうしてもうまくいきません。Kerberos のチケットをうまく validate できてないみたいなので(私的環境の AD / )DNS まわりの問題かなぁと思ったりしていますが,検証めんどくさい……

keep-alived な connection では一度認証したら二度としないという点で NTLM 認証はあやういな,と思いました。サーバ側の実装を間違えると。

*1:win32 の場合,素直に IIS を利用するか mod_auth_sspi を利用してください

*2:もともとは Squid で利用するために開発されたものです

*3:通信路をドメインパスワードが解読可能な形で流れるのであんまりおすすめできない

*4:AuthType は複数指定しても無効で一番最後の指定が有効になる;のに複数指定した例がある

*5:事実上「認証ハンドラ」モジュールは単一であることしか考慮されていない気がします……Apacheのコードが

*6:network.automatic-ntlm-auth.trusted-uris に適切な URI(一部で可;たとえば http:// とかでも OK)を指定する