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 Bindauth_request のサブリクエストが 2xx を返せば、nginx は元のリクエスト処理を続ける。401 を返せば、nginx は Basic 認証を要求する。
1. auth_requestが使えるか確認する
Section titled “1. auth_requestが使えるか確認する”nginx に ngx_http_auth_request_module が入っているか確認する。
nginx -V 2>&1 | grep -- --with-http_auth_request_module何も表示されない場合、この nginx では auth_request を使えない。別の nginx パッケージを使うか、LDAP 認証モジュール、PAM 認証モジュールなど別方式に切り替える。
2. Python helperを配置する
Section titled “2. Python helperを配置する”ldapwhoami を使うため、LDAP クライアントコマンドを入れておく。
sudo apt install ldap-utils python3helper 用の専用ユーザーを作る。
sudo useradd --system --no-create-home --shell /usr/sbin/nologin ldapauth/usr/local/sbin/ldap-auth-backend.py を作成する。
#!/usr/bin/env python3import base64import osimport reimport subprocessimport tempfilefrom 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()実行権限を付ける。
sudo chown root:root /usr/local/sbin/ldap-auth-backend.pysudo 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 を直接実行して切り分ける。
3. systemdサービスを作る
Section titled “3. systemdサービスを作る”/etc/systemd/system/ldap-auth-backend.service を作成する。
[Unit]Description=LDAP authentication backend for nginx auth_requestRequires=slapd.serviceAfter=network.target slapd.service
[Service]Type=simpleUser=ldapauthGroup=ldapauthRuntimeDirectory=ldap-auth-backendRuntimeDirectoryMode=0700Environment=LDAP_URI=ldap://127.0.0.1Environment=LDAP_BASE_DN=ou=People,dc=skills,dc=localEnvironment=LDAP_AUTH_RUNTIME_DIR=/run/ldap-auth-backendEnvironment=LDAP_STARTTLS=0ExecStart=/usr/bin/python3 /usr/local/sbin/ldap-auth-backend.pyRestart=on-failureRestartSec=1sNoNewPrivileges=truePrivateTmp=trueProtectHome=trueProtectSystem=full
[Install]WantedBy=multi-user.targetAfter= は起動順序を指定する。Requires= は slapd.service への依存を指定する。ローカル LDAP ではなく別サーバーの LDAP を使う場合は、Requires=slapd.service と slapd.service への After= は不要である。
StartTLSを使う場合
Section titled “StartTLSを使う場合”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.localEnvironment=LDAP_BASE_DN=ou=People,dc=skills,dc=localEnvironment=LDAP_AUTH_RUNTIME_DIR=/run/ldap-auth-backendEnvironment=LDAP_STARTTLS=1OpenLDAPクライアントが検証するCAを /etc/ldap/ldap.conf で指定する。
TLS_CACERT /etc/ssl/certs/ca-certificates.crtTLS_REQCERT demandRuntimeDirectory=ldap-auth-backend により、systemd が /run/ldap-auth-backend を作る。Python helper はそこに一時パスワードファイルを作るため、/run 直下へ直接書き込まない。
サービスを有効化して起動する。
sudo systemctl daemon-reloadsudo systemctl enable ldap-auth-backendsudo systemctl start ldap-auth-backend確認する。
systemctl status ldap-auth-backendss -ltnp | grep 8888journalctl -u ldap-auth-backendhelper を直接確認する。
curl -i http://127.0.0.1:8888/verifycurl -i -u ldapuser http://127.0.0.1:8888/verify認証情報なしでは401、正しいLDAPユーザーとパスワードでは200になることを確認する。-u ldapuser とだけ指定するとcurlがパスワードを対話入力するため、実際のパスワードをシェル履歴へ残さずに確認できる。
4. nginxでauth_requestを設定する
Section titled “4. nginxでauth_requestを設定する”静的サイトの /auth/ 配下を LDAP 認証で保護する。
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、静的ファイル、または別の通常処理に任せる。
テスト用のファイルを作る。
sudo mkdir -p /var/www/hq-sv.skills.local/authprintf '%s\n' 'for LDAP users' | sudo tee /var/www/hq-sv.skills.local/auth/index.htmlsite を有効化する。
sudo ln -s /etc/nginx/sites-available/hq-sv.skills.local.conf \ /etc/nginx/sites-enabled/hq-sv.skills.local.conf
sudo nginx -tsudo systemctl reload nginx5. 動作確認する
Section titled “5. 動作確認する”認証なしでアクセスする。
curl -I https://hq-sv.skills.local/auth/期待する応答は 401 と WWW-Authenticate ヘッダーである。
HTTP/1.1 401 UnauthorizedWWW-Authenticate: Basic realm="LDAP Users"LDAP ユーザーでアクセスする。
curl -u ldapuser https://hq-sv.skills.local/auth/成功の目安は、LDAP ユーザーのパスワードで認証が通り、for LDAP users が返ることである。
Root CA を検証端末へ登録し、証明書エラーが発生しない状態で確認する。curl -k は証明書検証を無効化するため、証明書問題の一時的な切り分けにだけ使用し、最終確認には使わない。
よくある失敗
Section titled “よくある失敗”| 症状 | 主な確認箇所 |
|---|---|
| 認証なしでコンテンツが返る | 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 を使用する。