コンテンツにスキップ

Python auth_request LDAP認証の設定

オフライン環境で、支給媒体から libnginx-mod-http-auth-pam を導入できない場合の最終手段として、nginx の auth_request とローカル Python helper で LDAP Bind 認証を行う手順。

nginx は /ldap-auth へ内部サブリクエストを送り、Python helper が ldapwhoami で LDAP Bind できるかを確認する。インターネット接続や追加パッケージの制約がない環境では、LDAP 認証用の nginx モジュールを使う構成も選択肢になる。


項目 例
OS Debian 13.x
公開URL https://hq-sv.skills.local/auth/
ドキュメントルート /var/www/hq-sv.skills.local
LDAP URI ldap://127.0.0.1
LDAPユーザーの親DN ou=People,dc=skills,dc=local
Python helper 127.0.0.1:8888
systemd unit ldap-auth-backend.service
名前解決 hq-sv.skills.local がnginxサーバーのIPアドレスへ解決できる

FQDN には hq-sv.skills.local、Base DN には dc=skills,dc=local、ユーザーの親 DN には ou=People を使用する。実際の要件に合わせて FQDN、Base DN、ユーザーの親 DN、証明書の SAN を一貫して置き換える。

基本構成では、LDAP サーバーが同じホスト上の 127.0.0.1 で動作し、LDAP 側の接続に TLS を使用しない。リモート LDAP や TLS が必要な構成では、StartTLSを使う場合の設定を使用する。

nginxの証明書は HTTP/HTTPSとリダイレクトの設定 と同じく、fullchain.pem とroot所有・600の privkey.pem を使用する。


browser
-> HTTPS + Basic Authentication
-> nginx location /auth/
-> auth_request /ldap-auth
-> proxy_pass http://127.0.0.1:8888/verify
-> Python helper
-> ldapwhoami -D uid=<user>,ou=People,dc=skills,dc=local
-> LDAP Bind

auth_request のサブリクエストが 2xx を返せば、nginx は元のリクエスト処理を続ける。401 を返せば、nginx は Basic 認証を要求する。


1. auth_requestが使えるか確認する

Section titled “1. auth_requestが使えるか確認する”

nginx に ngx_http_auth_request_module が入っているか確認する。

Terminal window
nginx -V 2>&1 | grep -- --with-http_auth_request_module

何も表示されない場合、この nginx では auth_request を使えない。別の nginx パッケージを使うか、LDAP 認証モジュール、PAM 認証モジュールなど別方式に切り替える。


ldapwhoami を使うため、LDAP クライアントコマンドを入れておく。

Terminal window
sudo apt install ldap-utils python3

helper 用の専用ユーザーを作る。

Terminal window
sudo useradd --system --no-create-home --shell /usr/sbin/nologin ldapauth

/usr/local/sbin/ldap-auth-backend.py を作成する。

/usr/local/sbin/ldap-auth-backend.py
#!/usr/bin/env python3
import base64
import os
import re
import subprocess
import tempfile
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
LDAP_URI = os.environ.get("LDAP_URI", "ldap://127.0.0.1")
BASE_DN = os.environ.get("LDAP_BASE_DN", "ou=People,dc=skills,dc=local")
RUNTIME_DIR = os.environ.get("LDAP_AUTH_RUNTIME_DIR", "/run/ldap-auth-backend")
LISTEN_HOST = os.environ.get("LDAP_AUTH_LISTEN_HOST", "127.0.0.1")
LISTEN_PORT = int(os.environ.get("LDAP_AUTH_LISTEN_PORT", "8888"))
LDAP_STARTTLS = os.environ.get("LDAP_STARTTLS", "0") == "1"
USER_RE = re.compile(r"^[A-Za-z0-9._-]{1,64}$")
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.handle_auth()
def do_HEAD(self):
self.handle_auth()
def handle_auth(self):
if self.path != "/verify":
self.send_response(404)
self.end_headers()
return
auth = self.headers.get("Authorization", "")
if not auth.startswith("Basic "):
self.send_response(401)
self.end_headers()
return
try:
raw = base64.b64decode(auth[6:], validate=True).decode("utf-8")
user, password = raw.split(":", 1)
except Exception:
self.send_response(401)
self.end_headers()
return
if not password:
self.send_response(401)
self.end_headers()
return
if not USER_RE.fullmatch(user):
self.send_response(401)
self.end_headers()
return
user_dn = f"uid={user},{BASE_DN}"
pwfile = None
try:
with tempfile.NamedTemporaryFile(
mode="w",
encoding="utf-8",
delete=False,
dir=RUNTIME_DIR,
prefix="ldapauth-",
) as f:
os.chmod(f.name, 0o600)
f.write(password)
pwfile = f.name
cmd = ["/usr/bin/ldapwhoami", "-x", "-H", LDAP_URI]
if LDAP_STARTTLS:
cmd.append("-ZZ")
cmd += ["-D", user_dn, "-y", pwfile]
result = subprocess.run(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=3,
)
self.send_response(200 if result.returncode == 0 else 401)
self.end_headers()
except subprocess.TimeoutExpired:
self.send_response(503)
self.end_headers()
finally:
if pwfile:
try:
os.unlink(pwfile)
except FileNotFoundError:
pass
def log_message(self, fmt, *args):
pass
if __name__ == "__main__":
ThreadingHTTPServer((LISTEN_HOST, LISTEN_PORT), Handler).serve_forever()

実行権限を付ける。

Terminal window
sudo chown root:root /usr/local/sbin/ldap-auth-backend.py
sudo chmod 755 /usr/local/sbin/ldap-auth-backend.py

この helper は、ユーザー名を uid=<user>,<BASE_DN> に変換して LDAP Bind を試す。グループ所属による認可は行わない。空パスワードは unauthenticated Bind として成功する可能性があるため、ldapwhoami を実行する前に拒否する。

また、ldapwhoami の終了コードが 0 以外なら、タイムアウトを除いてすべて 401 として返す。クライアントからは、パスワード誤り、LDAP 停止、TLS 検証失敗、接続失敗を区別できない。常に 401 になる場合は、helper と同じオプションで ldapwhoami を直接実行して切り分ける。


/etc/systemd/system/ldap-auth-backend.service を作成する。

/etc/systemd/system/ldap-auth-backend.service
[Unit]
Description=LDAP authentication backend for nginx auth_request
Requires=slapd.service
After=network.target slapd.service
[Service]
Type=simple
User=ldapauth
Group=ldapauth
RuntimeDirectory=ldap-auth-backend
RuntimeDirectoryMode=0700
Environment=LDAP_URI=ldap://127.0.0.1
Environment=LDAP_BASE_DN=ou=People,dc=skills,dc=local
Environment=LDAP_AUTH_RUNTIME_DIR=/run/ldap-auth-backend
Environment=LDAP_STARTTLS=0
ExecStart=/usr/bin/python3 /usr/local/sbin/ldap-auth-backend.py
Restart=on-failure
RestartSec=1s
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=full
[Install]
WantedBy=multi-user.target

After= は起動順序を指定する。Requires= は slapd.service への依存を指定する。ローカル LDAP ではなく別サーバーの LDAP を使う場合は、Requires=slapd.service と slapd.service への After= は不要である。

LDAP 接続を TLS で保護する場合は、証明書の SAN と一致する名前で LDAP へ接続し、Root CA を DebianのTrust Storeへ登録 する。ldap://127.0.0.1 のままでは、証明書に IP アドレスの SAN がない限り名前検証に失敗する。

systemd unitの環境変数を次のように変更する。

Environment=LDAP_URI=ldap://hq-sv.skills.local
Environment=LDAP_BASE_DN=ou=People,dc=skills,dc=local
Environment=LDAP_AUTH_RUNTIME_DIR=/run/ldap-auth-backend
Environment=LDAP_STARTTLS=1

OpenLDAPクライアントが検証するCAを /etc/ldap/ldap.conf で指定する。

/etc/ldap/ldap.conf
TLS_CACERT /etc/ssl/certs/ca-certificates.crt
TLS_REQCERT demand

RuntimeDirectory=ldap-auth-backend により、systemd が /run/ldap-auth-backend を作る。Python helper はそこに一時パスワードファイルを作るため、/run 直下へ直接書き込まない。

サービスを有効化して起動する。

Terminal window
sudo systemctl daemon-reload
sudo systemctl enable ldap-auth-backend
sudo systemctl start ldap-auth-backend

確認する。

Terminal window
systemctl status ldap-auth-backend
ss -ltnp | grep 8888
journalctl -u ldap-auth-backend

helper を直接確認する。

Terminal window
curl -i http://127.0.0.1:8888/verify
curl -i -u ldapuser http://127.0.0.1:8888/verify

認証情報なしでは401、正しいLDAPユーザーとパスワードでは200になることを確認する。-u ldapuser とだけ指定するとcurlがパスワードを対話入力するため、実際のパスワードをシェル履歴へ残さずに確認できる。


静的サイトの /auth/ 配下を LDAP 認証で保護する。

/etc/nginx/sites-available/hq-sv.skills.local.conf
server {
listen 80;
server_name hq-sv.skills.local;
return 301 https://hq-sv.skills.local$request_uri;
}
server {
listen 443 ssl;
server_name hq-sv.skills.local;
ssl_certificate /etc/nginx/tls/hq-sv.skills.local/fullchain.pem;
ssl_certificate_key /etc/nginx/tls/hq-sv.skills.local/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
root /var/www/hq-sv.skills.local;
index index.html;
location = / {
default_type text/plain;
return 200 "hq-sv.skills.local\n";
}
location /auth/ {
auth_request /ldap-auth;
error_page 401 = @need_auth;
try_files $uri $uri/ =404;
}
location = /ldap-auth {
internal;
proxy_method GET;
proxy_pass http://127.0.0.1:8888/verify;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Authorization $http_authorization;
proxy_set_header X-Original-URI $request_uri;
}
location @need_auth {
add_header WWW-Authenticate 'Basic realm="LDAP Users"' always;
return 401;
}
}

location /auth/ の中で return 200 を使わない。return は早い段階でレスポンスを返すため、認証チェックを通らずにコンテンツが返る原因になる。認証後に返す内容は try_files、静的ファイル、または別の通常処理に任せる。

テスト用のファイルを作る。

Terminal window
sudo mkdir -p /var/www/hq-sv.skills.local/auth
printf '%s\n' 'for LDAP users' | sudo tee /var/www/hq-sv.skills.local/auth/index.html

site を有効化する。

Terminal window
sudo ln -s /etc/nginx/sites-available/hq-sv.skills.local.conf \
/etc/nginx/sites-enabled/hq-sv.skills.local.conf
sudo nginx -t
sudo systemctl reload nginx

認証なしでアクセスする。

Terminal window
curl -I https://hq-sv.skills.local/auth/

期待する応答は 401 と WWW-Authenticate ヘッダーである。

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="LDAP Users"

LDAP ユーザーでアクセスする。

Terminal window
curl -u ldapuser https://hq-sv.skills.local/auth/

成功の目安は、LDAP ユーザーのパスワードで認証が通り、for LDAP users が返ることである。

Root CA を検証端末へ登録し、証明書エラーが発生しない状態で確認する。curl -k は証明書検証を無効化するため、証明書問題の一時的な切り分けにだけ使用し、最終確認には使わない。


症状 主な確認箇所
認証なしでコンテンツが返る location /auth/ に return 200 を置いていないか
Basic認証のポップアップが出ない 401 応答に WWW-Authenticate が付いているか
常に 401 になる helper の BASE_DN、ユーザーDN、LDAPパスワード、LDAP停止、StartTLSとCA検証、ldapwhoami の直接実行結果
helper が起動しない journalctl -u ldap-auth-backend、Python構文、ldapauth ユーザー、RuntimeDirectory
/run に書けない Python 側の一時ファイル作成先が /run/ldap-auth-backend になっているか
auth_request が unknown directive になる nginx に ngx_http_auth_request_module が入っているか

  • この方式は PAM の auth / account 判定を使わない。
  • LDAP Bind に成功したユーザーを許可するだけで、グループによる認可は行わない。
  • helper はリクエストごとに ldapwhoami を実行するため、高負荷用途には向かない。
  • リモート LDAP を使う場合は、ldaps:// または StartTLS を使う。
  • nginx から helper への接続先は 127.0.0.1 に限定し、外部公開しない。
  • インターネット公開や本番運用では、レート制限、冗長化、監視、secret 管理を備えた認証 backend を使用する。