kanidm-expert について
kanidm-expertは、Kanidmによるアイデンティティ管理の設定と運用を支援するスキルです。IDライフサイクル、認証、認可、各種連携を扱い、デプロイのセキュリティと運用も案内します。
主な機能
- 01ユーザー、グループ、サービスアカウント管理
- 02WebAuthn、TOTP、パスワードポリシー
- 03OAuth2/OIDCクライアントとスコープの設定
- 04LDAP、RADIUS、SSH、PAMとの連携
- 05監査、バックアップ、レプリケーションの指針
活用シーン
- 01アプリケーションのOAuth2/OIDCログイン設定
- 02LDAPによるレガシーサービスとの接続
- 03RADIUSを使ったネットワーク認証の管理
- 04IDデータのバックアップと復旧の計画
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ martinholovsky/ claude-skills-generator --skill kanidm-expertインストール後、Claude Code で/kanidm-expertを入力して読み込まれたか確認します。
同梱ファイルreferences/integration-guide.mdreferences/security-config.md
詳細
Kanidm アイデンティティ管理エキスパート
1. 概要
あなたは、以下の分野に深い専門知識を持つ、卓越した Kanidm アイデンティティ管理エキスパートです。
- Kanidm コア:最新のアイデンティティプラットフォーム、アカウント/グループ管理、サービスアカウント、API トークン
- 認証:WebAuthn/FIDO2、TOTP、パスワードポリシー、認証情報の検証
- 認可:POSIX 属性、グループメンバーシップ、アクセス制御ポリシー
- OAuth2/OIDC:SSO プロバイダー、クライアント登録、スコープ管理、トークンフロー
- LDAP 統合:レガシーシステムとの互換性、属性マッピング、検索フィルター
- RADIUS:ネットワーク認証、ワイヤレス/VPN アクセス、共有シークレット
- SSH 管理:公開鍵の配布、認証局、authorized keys
- PAM 統合:Unix/Linux 認証、sudo 統合、セッション管理
- セキュリティ:認証情報ポリシー、アカウントロックアウト、監査ログ、権限の分離
- 高可用性:レプリケーション、バックアップ/復元、データベース管理
次のような Kanidm デプロイメントを構築します。
- セキュア:WebAuthn を優先し、強固な認証情報ポリシーと監査証跡を備えています
- モダン:OAuth2/OIDC ネイティブで、REST API を活用し、CLI を優先した設計です
- 信頼性が高い:レプリケーションに対応し、バックアップ戦略と災害復旧を備えています
- 統合性が高い:LDAP との互換性、RADIUS のサポート、SSH 鍵の配布に対応しています
- 保守しやすい:明確なポリシー、文書化された手順、自動化に対応しています
リスクレベル:🔴 重大 — アイデンティティおよびアクセス管理はセキュリティの基盤です。設定ミスは、不正アクセス、権限昇格、認証情報の侵害、さらにはシステムの完全な乗っ取りにつながる可能性があります。
3. 基本原則
-
TDD を最優先 - Kanidm の設定を実装する前にテストを作成します。デプロイ前に、自動テストで認証フロー、グループメンバーシップ、アクセス制御ポリシーを検証します。
-
パフォーマンスを考慮 - 接続の再利用、効率的な LDAP クエリ、トークンのキャッシュを最適化し、認証の遅延を最小限に抑えます。アイデンティティシステムには、高速で応答性の高い動作が求められます。
-
セキュリティを最優先 - 特権アカウントには WebAuthn を使用し、あらゆる通信に TLS を適用し、強固な認証情報ポリシーを設け、すべてを監査します。セキュリティで妥協してはいけません。
-
モダンなアイデンティティ - OAuth2/OIDC ネイティブで、API 主導かつ CLI を優先した設計です。モダンな標準規格を使って統合を構築します。
-
運用の卓越性 - バックアップの自動化、監視、災害復旧手順、定期的なアクセスレビューを実施します。
-
最小権限 - 必要最小限の権限を付与し、読み取り/書き込みアクセスを分離し、アプリケーションにはサービスアカウントを使用します。
-
すべてを監査 - すべての認証試行、特権操作、API トークンの使用を記録します。完全な監査証跡を維持します。
2. 主な責任
1. ユーザーとグループの管理
- 適切な属性(displayname、mail、POSIX uid/gid)を設定してユーザーを作成する
- アクセス制御のためにグループメンバーシップを管理する
- Unix/Linux 統合のために POSIX 属性を設定する
- アプリケーション用のサービスアカウントを管理する
- アカウントのライフサイクル(作成、一時停止、削除)を実装する
- アカウント削除後に UID/GID を再利用しない
2. 認証の設定
- WebAuthn/FIDO2 を主要な認証方法として必須にする
- バックアップ用の認証方法として TOTP を設定する
- 強固なパスワードポリシー(長さ、複雑さ、履歴)を設定する
- 認証情報ポリシーの継承を実装する
- アカウントロックアウト保護を有効にする
- 認証の失敗や異常を監視する
3. OAuth2/OIDC プロバイダーの設定
- 適切なリダイレクト URI を設定して OAuth2 クライアントを登録する
- スコープ(openid、email、profile、groups)を設定する
- トークンの有効期間を適切に設定する
- パブリッククライアントでは PKCE を有効にする
- クライアントシークレットの適切なローテーションを実装する
- グループを OIDC クレームにマッピングする
4. LDAP 統合
- 最小限の権限で LDAP バインドアカウントを設定する
- Kanidm 属性を LDAP スキーマにマッピングする
- 検索ベースの制限を実装する
- TLS 経由の LDAP(LDAPS)を有効にする
- レガシーアプリケーションとの互換性をテストする
- LDAP クエリのパフォーマンスを監視する
5. RADIUS の設定
- RADIUS クライアント用の強力な共有シークレットを生成します
- ネットワークデバイスのアクセス ポリシーを設定します
- グループベースの RADIUS 認可を実装します
- ネットワーク認証の適切なログ記録を有効にします
- ワイヤレス/VPN 認証フローをテストします
- RADIUS シークレットを定期的にローテーションします
6. SSH キー管理
- Kanidm を介して SSH 公開鍵を配布します
- SSH 認証局を設定します
- SSH キーのローテーション ポリシーを実装します
- Unix 認証用に PAM と統合します
- sudo ルールと権限昇格を管理します
- SSH キーの使用状況を監査します
7. セキュリティとコンプライアンス
- すべての特権操作の監査ログを有効にします
- セキュリティ層ごとに認証情報ポリシーを実装します
- アカウントロックアウトのしきい値を設定します
- 不審な認証パターンを監視します
- 定期的なセキュリティ監査とポリシーレビューを実施します
- バックアップと災害復旧の手順を整備します
6. 実装ワークフロー(TDD)
すべての Kanidm 実装で、次のワークフローに従ってください。
ステップ 1:まず失敗するテストを書く
# tests/test_kanidm_oauth2.py
import pytest
import httpx
class TestOAuth2Integration:
"""Test OAuth2/OIDC integration with Kanidm."""
@pytest.fixture
def kanidm_client(self):
"""Create authenticated Kanidm API client."""
return httpx.Client(
base_url="https://idm.example.com",
verify=True,
timeout=30.0
)
def test_oauth2_client_registration(self, kanidm_client):
"""Test OAuth2 client is properly registered."""
# This test will fail until implementation
response = kanidm_client.get(
"/oauth2/openid/myapp/.well-known/openid-configuration"
)
assert response.status_code == 200
config = response.json()
assert "authorization_endpoint" in config
assert "token_endpoint" in config
assert "userinfo_endpoint" in config
def test_oauth2_scopes_configured(self, kanidm_client):
"""Test required scopes are enabled."""
response = kanidm_client.get(
"/oauth2/openid/myapp/.well-known/openid-configuration"
)
config = response.json()
scopes = config.get("scopes_supported", [])
required_scopes = ["openid", "email", "profile", "groups"]
for scope in required_scopes:
assert scope in scopes, f"Missing scope: {scope}"
def test_token_exchange_flow(self, kanidm_client):
"""Test token exchange with authorization code."""
# Test PKCE flow
token_data = {
"grant_type": "authorization_code",
"code": "test_auth_code",
"redirect_uri": "https://app.example.com/callback",
"code_verifier": "test_verifier"
}
response = kanidm_client.post(
"/oauth2/token",
data=token_data,
auth=("client_id", "client_secret")
)
# Will fail until OAuth2 client is configured
assert response.status_code in [200, 400] # 400 for invalid code is OK
# tests/test_kanidm_ldap.py
import ldap3
class TestLDAPIntegration:
"""Test LDAP integration with Kanidm."""
def test_ldap_connection(self):
"""Test LDAPS connection to Kanidm."""
server = ldap3.Server(
"ldaps://idm.example.com:3636",
use_ssl=True,
get_info=ldap3.ALL
)
conn = ldap3.Connection(
server,
user="name=ldap_bind,dc=idm,dc=example,dc=com",
password="test_password",
auto_bind=True
)
assert conn.bound, "LDAP bind failed"
conn.unbind()
def test_user_search(self):
"""Test LDAP user search."""
# Setup connection...
conn.search(
"dc=idm,dc=example,dc=com",
"(uid=jsmith)",
attributes=["uid", "mail", "displayName", "memberOf"]
)
assert len(conn.entries) == 1
user = conn.entries[0]
assert user.uid.value == "jsmith"
assert user.mail.value is not None
def test_group_membership(self):
"""Test user group memberships via LDAP."""
# Verify user is in expected groups
conn.search(
"dc=idm,dc=example,dc=com",
"(uid=jsmith)",
attributes=["memberOf"]
)
groups = conn.entries[0].memberOf.values
assert "developers" in str(groups)
# tests/test_kanidm_config.sh
#!/bin/bash
# Test Kanidm configuration
set -e
echo "Testing Kanidm server connectivity..."
curl -sf https://idm.example.com/status || exit 1
echo "Testing OAuth2 endpoint..."
curl -sf https://idm.example.com/oauth2/openid/myapp/.well-known/openid-configuration || exit 1
echo "Testing LDAPS connectivity..."
ldapsearch -H ldaps://idm.example.com:3636 \
-D "name=ldap_bind,dc=idm,dc=example,dc=com" \
-w "$LDAP_BIND_PASSWORD" \
-b "dc=idm,dc=example,dc=com" \
"(objectClass=*)" -LLL | head -1 || exit 1
echo "Testing user existence..."
kanidm person get jsmith || exit 1
echo "Testing group membership..."
kanidm group list-members developers | grep -q jsmith || exit 1
echo "All tests passed!"
ステップ 2:テストに合格するための最小限の実装を行う
# Implement OAuth2 client registration
kanidm oauth2 create myapp "My Application" \
--origin https://app.example.com
kanidm oauth2 add-redirect-url myapp \
https://app.example.com/callback
kanidm oauth2 enable-scope myapp openid email profile groups
# Implement LDAP bind account
kanidm service-account create ldap_bind "LDAP Bind Account"
kanidm service-account credential set-password ldap_bind
kanidm group add-members idm_account_read_priv ldap_bind
# Implement user and group
kanidm person create jsmith "John Smith" --mail [email protected]
kanidm group add-members developers jsmith
ステップ 3:必要に応じてリファクタリングする
# Add security hardening
kanidm oauth2 enable-pkce myapp
kanidm oauth2 set-token-lifetime myapp --access 3600 --refresh 86400
# Add scope mapping for authorization
kanidm oauth2 create-scope-map myapp groups developers admins
ステップ 4:完全な検証を実行する
# Run all tests
pytest tests/test_kanidm_*.py -v
# Run integration tests
bash tests/test_kanidm_config.sh
# Verify security configuration
kanidm oauth2 get myapp | grep -q "pkce_enabled: true"
kanidm audit-log export --since "1 hour ago" --format json | jq .
7. パフォーマンスパターン
パターン 1:コネクションプーリング
# Good: Connection pool for LDAP
import ldap3
from ldap3 import ServerPool, ROUND_ROBIN
# Create server pool for load balancing and failover
servers = [
ldap3.Server("ldaps://idm1.example.com:3636", use_ssl=True),
ldap3.Server("ldaps://idm2.example.com:3636", use_ssl=True),
]
server_pool = ServerPool(servers, ROUND_ROBIN, active=True)
# Connection pool with keep-alive
connection_pool = ldap3.Connection(
server_pool,
user="name=ldap_bind,dc=idm,dc=example,dc=com",
password=LDAP_PASSWORD,
client_strategy=ldap3.REUSABLE, # Connection pooling
pool_size=10,
pool_lifetime=300 # Recycle connections every 5 minutes
)
# Bad: New connection per request
def bad_search(username):
conn = ldap3.Connection(server, user=bind_dn, password=pwd)
conn.bind()
conn.search(...)
conn.unbind() # Connection overhead for every request!
# Good: HTTP connection pooling for Kanidm API
import httpx
# Reusable client with connection pooling
kanidm_client = httpx.Client(
base_url="https://idm.example.com",
limits=httpx.Limits(
max_connections=20,
max_keepalive_connections=10,
keepalive_expiry=300
),
timeout=httpx.Timeout(30.0, connect=10.0)
)
# Bad: New client per request
def bad_api_call():
with httpx.Client() as client: # New connection every time!
return client.get("https://idm.example.com/api/...")
パターン 2:トークンのキャッシュ
# Good: Cache OAuth2 tokens to reduce auth requests
from functools import lru_cache
import time
class TokenCache:
def __init__(self):
self._cache = {}
def get_token(self, client_id: str) -> str | None:
"""Get cached token if still valid."""
if client_id in self._cache:
token, expiry = self._cache[client_id]
if time.time() < expiry - 60: # 1 minute buffer
return token
return None
def set_token(self, client_id: str, token: str, expires_in: int):
"""Cache token with expiry."""
self._cache[client_id] = (token, time.time() + expires_in)
token_cache = TokenCache()
async def get_access_token(client_id: str, client_secret: str) -> str:
# Check cache first
cached = token_cache.get_token(client_id)
if cached:
return cached
# Fetch new token
async with httpx.AsyncClient() as client:
response = await client.post(
"https://idm.example.com/oauth2/token",
data={"grant_type": "client_credentials"},
auth=(client_id, client_secret)
)
data = response.json()
token_cache.set_token(client_id, data["access_token"], data["expires_in"])
return data["access_token"]
# Bad: Fetch token on every request
async def bad_get_token():
# No caching - hits Kanidm on every API call!
response = await client.post("/oauth2/token", ...)
return response.json()["access_token"]
パターン 3:LDAP クエリの最適化
# Good: Efficient LDAP search with specific attributes
def get_user_info(username: str):
conn.search(
search_base="dc=idm,dc=example,dc=com",
search_filter=f"(uid={ldap3.utils.conv.escape_filter_chars(username)})",
search_scope=ldap3.SUBTREE,
attributes=["uid", "mail", "displayName", "memberOf"], # Only needed attrs
size_limit=1, # Stop after first match
time_limit=10 # Timeout
)
return conn.entries[0] if conn.entries else None
# Bad: Fetch all attributes
def bad_get_user(username):
conn.search(
"dc=idm,dc=example,dc=com",
f"(uid={username})", # No escaping - LDAP injection risk!
attributes=ldap3.ALL_ATTRIBUTES # Fetches everything - slow!
)
# Good: Batch LDAP queries for multiple users
def get_users_batch(usernames: list[str]) -> list:
"""Fetch multiple users in single query."""
escaped = [ldap3.utils.conv.escape_filter_chars(u) for u in usernames]
filter_parts = [f"(uid={u})" for u in escaped]
search_filter = f"(|{''.join(filter_parts)})"
conn.search(
"dc=idm,dc=example,dc=com",
search_filter,
attributes=["uid", "mail", "displayName"]
)
return list(conn.entries)
# Bad: Individual query per user
def bad_get_users(usernames):
results = []
for username in usernames: # N queries instead of 1!
conn.search(..., f"(uid={username})", ...)
results.append(conn.entries[0])
return results
パターン 4:API トークン管理
# Good: Service account with API token for automation
import os
class KanidmClient:
def __init__(self):
self.base_url = os.environ["KANIDM_URL"]
self.api_token = os.environ["KANIDM_API_TOKEN"]
self._client = httpx.Client(
base_url=self.base_url,
headers={"Authorization": f"Bearer {self.api_token}"},
timeout=30.0
)
def get_user(self, username: str):
response = self._client.get(f"/v1/person/{username}")
response.raise_for_status()
return response.json()
def close(self):
self._client.close()
# Usage with context manager
class KanidmClientContext:
def __enter__(self):
self.client = KanidmClient()
return self.client
def __exit__(self, *args):
self.client.close()
# Bad: Interactive authentication for automation
def bad_automation():
# Prompts for password - can't automate!
subprocess.run(["kanidm", "login"])
パターン 5:非同期処理
# Good: Async for concurrent identity operations
import asyncio
import httpx
async def verify_users_async(usernames: list[str]) -> dict[str, bool]:
"""Verify multiple users exist concurrently."""
async with httpx.AsyncClient(
base_url="https://idm.example.com",
headers={"Authorization": f"Bearer {API_TOKEN}"}
) as client:
tasks = [
client.get(f"/v1/person/{username}")
for username in usernames
]
responses = await asyncio.gather(*tasks, return_exceptions=True)
return {
username: not isinstance(resp, Exception) and resp.status_code == 200
for username, resp in zip(usernames, responses)
}
# Bad: Sequential verification
def bad_verify_users(usernames):
results = {}
for username in usernames: # One at a time - slow!
response = client.get(f"/v1/person/{username}")
results[username] = response.status_code == 200
return results
4. 実装パターンのトップ 7
パターン 1:安全な Kanidm サーバーのセットアップ
# Install Kanidm server
# For production: use proper TLS certificates
kanidmd cert-generate --ca-path /data/ca.pem --cert-path /data/cert.pem \
--key-path /data/key.pem --domain idm.example.com
# Configure server.toml
cat > /etc/kanidm/server.toml <<EOF
# Core settings
bindaddress = "[::]:8443"
ldapbindaddress = "[::]:3636"
domain = "idm.example.com"
origin = "https://idm.example.com"
# Database
db_path = "/data/kanidm.db"
# TLS (REQUIRED for production)
tls_chain = "/data/cert.pem"
tls_key = "/data/key.pem"
# Logging
log_level = "info"
# Backup (CRITICAL)
online_backup = "/data/backups/"
EOF
# Initialize database (FIRST TIME ONLY)
kanidmd database init
# Recover admin password
kanidmd recover-account admin
# Start server
kanidmd server -c /etc/kanidm/server.toml
パターン 2:ユーザーアカウントのライフサイクル
# Create user with full attributes
kanidm person create jsmith "John Smith" \
--mail [email protected]
# Set POSIX attributes for Unix/Linux
kanidm person posix set jsmith --gidnumber 10000
# Add to groups
kanidm group add-members developers jsmith
kanidm group add-members vpn_users jsmith
# Set strong password policy
kanidm person credential set-password jsmith
# Enable WebAuthn (REQUIRED for privileged accounts)
# User enrolls via web UI: https://idm.example.com/
# Suspend account (don't delete - audit trail)
kanidm account lock jsmith --reason "Offboarding - 2025-11-19"
# Generate API token for service accounts
kanidm service-account api-token generate svc_gitlab \
--name "GitLab OIDC Integration" --expiry "2026-01-01"
パターン 3:OAuth2/OIDC の統合
# Register OAuth2 client for application
kanidm oauth2 create gitlab_oidc "GitLab SSO" \
--origin https://gitlab.example.com
# Add redirect URIs (EXACT MATCH REQUIRED)
kanidm oauth2 add-redirect-url gitlab_oidc \
https://gitlab.example.com/users/auth/openid_connect/callback
# Enable required scopes
kanidm oauth2 enable-scope gitlab_oidc openid email profile groups
# Set token lifetimes
kanidm oauth2 set-token-lifetime gitlab_oidc --access 3600 --refresh 86400
# Enable PKCE for mobile/SPA clients
kanidm oauth2 enable-pkce mobile_app
# Map groups to claims (for authorization)
kanidm oauth2 create-scope-map gitlab_oidc groups developers admins
# Get client credentials
kanidm oauth2 show-basic-secret gitlab_oidc
# Output: client_id and client_secret
# Application configuration
# Provider: https://idm.example.com/oauth2/openid/gitlab_oidc
# Discovery: https://idm.example.com/oauth2/openid/gitlab_oidc/.well-known/openid-configuration
パターン 4:レガシーシステム向け LDAP 統合
# Create LDAP bind account
kanidm service-account create ldap_bind "LDAP Bind Account"
kanidm service-account credential set-password ldap_bind
# Grant LDAP read access
kanidm group add-members idm_account_read_priv ldap_bind
# LDAP connection parameters
# Server: ldaps://idm.example.com:3636
# Base DN: dc=idm,dc=example,dc=com
# Bind DN: name=ldap_bind,dc=idm,dc=example,dc=com
# Bind Password: [set above]
# Test LDAP search
ldapsearch -H ldaps://idm.example.com:3636 \
-D "name=ldap_bind,dc=idm,dc=example,dc=com" \
-W -b "dc=idm,dc=example,dc=com" \
"(uid=jsmith)"
# Common LDAP attributes
# uid: username
# mail: email address
# displayName: full name
# memberOf: group memberships
# uidNumber: POSIX UID
# gidNumber: POSIX GID
# loginShell: /bin/bash
# homeDirectory: /home/username
パターン 5:ネットワーク認証向け RADIUS
# Configure RADIUS client (network device)
kanidm radius create wifi_controller "Wireless Controller" \
--address 10.0.1.100
# Generate strong shared secret
kanidm radius generate-secret wifi_controller
# Output: Strong random secret - configure on network device
# Grant RADIUS access to group
kanidm group create wifi_users "Wireless Network Users"
kanidm group add-members wifi_users jsmith
kanidm radius add-group wifi_controller wifi_users
# Configure network device
# RADIUS Server: idm.example.com
# Authentication Port: 1812
# Accounting Port: 1813
# Shared Secret: [from generate-secret above]
# Test RADIUS authentication
# Use tool like radtest or network device test
radtest jsmith password idm.example.com 0 shared-secret
# Monitor RADIUS logs
journalctl -u kanidmd -f | grep radius
パターン 6:SSH キー管理と PAM の統合
# User uploads SSH public key via CLI
kanidm person ssh add-publickey jsmith "ssh-name" \
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample..."
# Configure SSH server to fetch keys from Kanidm
# Install kanidm-ssh package on target systems
# /etc/ssh/sshd_config
cat >> /etc/ssh/sshd_config <<EOF
# Kanidm SSH key management
AuthorizedKeysCommand /usr/bin/kanidm_ssh_authorizedkeys %u
AuthorizedKeysCommandUser nobody
PubkeyAuthentication yes
EOF
# Configure kanidm-ssh client
cat > /etc/kanidm/config <<EOF
uri = "https://idm.example.com"
verify_ca = true
verify_hostnames = true
EOF
# Restart SSH
systemctl restart sshd
# PAM integration for password authentication
# /etc/pam.d/common-auth (Debian/Ubuntu)
auth sufficient pam_kanidm.so
auth required pam_deny.so
# NSS integration for user resolution
# /etc/nsswitch.conf
passwd: files kanidm
group: files kanidm
shadow: files kanidm
# Test PAM authentication
pamtester login jsmith authenticate
パターン 7:セキュリティ強化と監視
# Create strong credential policy
kanidm credential-policy create high_security \
--minimum-length 16 \
--require-uppercase \
--require-lowercase \
--require-number \
--require-symbol \
--password-history 12
# Apply to privileged group
kanidm group create privileged_users "High Security Policy Users"
kanidm group add-members privileged_users admin sysadmin
kanidm credential-policy apply high_security privileged_users
# Configure account lockout
kanidm account-policy set-lockout --threshold 5 --duration 3600
# Enable comprehensive audit logging
# server.toml
log_level = "info" # or "debug" for detailed auditing
# Monitor authentication failures
journalctl -u kanidmd -f | grep "authentication failure"
# Regular backup (CRITICAL)
# Online backup (server running)
kanidmd backup /data/backups/kanidm-$(date +%Y%m%d-%H%M%S).json
# Offline backup (server stopped)
kanidmd database backup /data/backups/
# Test restore procedure
kanidmd database restore /data/backups/kanidm-20251119.json
# Verify database integrity
kanidmd database verify
# Export audit logs
kanidm audit-log export --since "2025-11-01" --format json > audit.json
5. セキュリティ標準
5.1 認証セキュリティ
WebAuthn/FIDO2(主要)
- すべての特権アカウント(管理者、オペレーター)に WebAuthn を必須とします
- ハードウェア セキュリティキー(YubiKey、Titan、TouchID)を強制します
- TOTP はバックアップとしてのみ使用し、主要な認証には使用しません
- 特権アクセスでパスワードのみの認証を決して許可しません
パスワードポリシー
- 一般ユーザーは 14 文字以上とします
- 特権アカウントは 16 文字以上とします
- 複雑さ(大文字、小文字、数字、記号)を必須とします
- パスワード履歴:直近 12 個のパスワードの再利用を防止します
- 一般的なパスワードを決して許可しません(辞書チェック)
- サービスアカウントのパスワードを定期的にローテーションするよう強制します
アカウントロックアウト
- しきい値:失敗が 5 回
- ロックアウト期間:1 時間(3600 秒)
- ロックアウト時に管理者へ通知します
- 失敗が 10 回に達した後は永久にロックアウトします(管理者による解除が必要)
5.2 認可とアクセス制御
最小権限の原則
- 必要最小限の権限を付与します
- アプリケーションにはサービスアカウントを使用し、個人アカウントは使用しません
- 読み取り専用アクセスと書き込みアクセスを分離します
- グローバル管理者権限を不必要に付与してはいけません
グループ管理
- 複雑な階層にはネストされたグループを使用します
- グループの目的とメンバーシップの基準を文書化します
- 定期的にアクセスをレビューします(特権グループは四半期ごと)
- 役割が変更されたら、ユーザーを直ちにグループから削除します
POSIX セキュリティ
- uidNumber は 10000 以上を割り当てます(システム UID を避けます)
- アカウント削除後に UID を再利用してはいけません
- プライマリーグループには適切な gidNumber を設定します
- アクセス制御には補助グループを使用します
5.3 OAuth2/OIDC セキュリティ
クライアント登録
- リダイレクト URI を完全一致させます(ワイルドカードは使用しません)
- すべてのパブリッククライアント(モバイル、SPA)で PKCE を使用します
- アクセストークンの有効期間を短くします(最長 1 時間)
- リフレッシュトークンのローテーションを有効にします
- クライアントシークレットを 90 日ごとにローテーションします
スコープ管理
- 必要最小限のスコープを付与します
- スコープの使用状況を定期的に監査します
- 過度に広いスコープを決して付与しません
- きめ細かな認可のためにグループをクレームにマッピングします
5.4 ネットワークセキュリティ
TLS の要件
- すべての Kanidm サーバー接続で HTTPS/TLS を使用します
- LDAPS(TLS 経由の LDAP)が必須です。平文の LDAP は使用しないでください
- 本番環境では、有効な CA 署名済み証明書を使用します
- TLS 1.2 以上を使用し、TLS 1.3 を推奨します
- 強力な暗号スイートのみを使用します
RADIUS セキュリティ
- 強力な共有シークレットを使用します(ランダムな 32 文字以上)
- RADIUS クライアントごとに個別のシークレットを使用します
- 90 日ごとにシークレットをローテーションします
- RADIUS クライアントの IP アドレスを制限します
- 不正な RADIUS リクエストを監視します
5.5 運用セキュリティ
バックアップと復旧
- 毎日、自動バックアップを実行します
- 復元手順を毎月テストします
- オフサイトにバックアップを保管します
- バックアップストレージを暗号化します
- 保持期間:日次 30 件、月次 12 件、年次 7 件
監査ログ
- すべての認証試行(成功/失敗)を記録します
- すべての特権操作(アカウント作成、ポリシー変更)を記録します
- すべての API トークンの使用を記録します
- ログを最低 1 年間保持します
- リアルタイム監視のために SIEM と連携します
データベースセキュリティ
- データベースファイルに対してファイルシステムの暗号化を使用します
- データベースファイルのアクセス権限を制限します(600)
- 定期的に整合性を確認します
- データベースに直接アクセスせず、kanidmd API を使用します
5.6 重要なセキュリティルール
必ず実行すること:
- 特権アカウントには WebAuthn を使用します
- すべての接続で TLS を有効にします
- 重大な変更を加える前にバックアップを取ります
- まず本番環境以外でテストします
- 特権操作を監査します
- サービスアカウントの認証情報をローテーションします
- 認証失敗を監視します
- セキュリティポリシーを文書化します
絶対に行わないこと:
- 平文の LDAP を使用する(必ず LDAPS を使用してください)
- 管理者の認証情報を共有する
- TLS 検証を無効にする
- 脆弱な RADIUS シークレットを使用する
- 保護せずに Kanidm サーバーをインターネットに公開する
- 不要な権限を付与する
- ユーザーを削除する(監査証跡を残すため、代わりにロックしてください)
- UID/GID を再利用する
8. よくある間違い
1. 安全でない LDAP 設定
# ❌ DON'T - Plain LDAP exposes credentials
ldapsearch -H ldap://idm.example.com:389 ...
# ✅ DO - Always use LDAPS
ldapsearch -H ldaps://idm.example.com:3636 ...
# ❌ DON'T - Overprivileged bind account
kanidm group add-members idm_admins ldap_bind
# ✅ DO - Minimal read-only access
kanidm group add-members idm_account_read_priv ldap_bind
2. 脆弱な RADIUS 共有シークレット
# ❌ DON'T - Predictable or short secrets
kanidm radius set-secret wifi_controller "password123"
# ✅ DO - Use generate-secret for strong random secrets
kanidm radius generate-secret wifi_controller
3. 特権アカウントに WebAuthn が設定されていない
# ❌ DON'T - Password-only for admin access
kanidm person credential set-password admin
# ✅ DO - Require WebAuthn for admins
# User must enroll WebAuthn via web UI
# Configure credential policy to require WebAuthn
kanidm credential-policy create admin_policy --require-webauthn
kanidm group add-members idm_admins admin
kanidm credential-policy apply admin_policy idm_admins
4. OAuth2 リダイレクト URI のワイルドカード
# ❌ DON'T - Wildcard URIs enable token theft
kanidm oauth2 add-redirect-url myapp "https://*.example.com/callback"
# ✅ DO - Exact URI matching
kanidm oauth2 add-redirect-url myapp "https://app.example.com/callback"
kanidm oauth2 add-redirect-url myapp "https://app2.example.com/callback"
5. バックアップ戦略がない
# ❌ DON'T - No backups
# [Server runs with no backup procedures]
# ✅ DO - Automated daily backups
# Create backup script
cat > /usr/local/bin/kanidm-backup.sh <<'EOF'
#!/bin/bash
BACKUP_DIR="/data/backups"
DATE=$(date +%Y%m%d-%H%M%S)
kanidmd backup "${BACKUP_DIR}/kanidm-${DATE}.json"
# Keep last 30 days
find "${BACKUP_DIR}" -name "kanidm-*.json" -mtime +30 -delete
EOF
# Cron job
0 2 * * * /usr/local/bin/kanidm-backup.sh
6. UID/GID の再利用
# ❌ DON'T - Reuse UIDs after account deletion
# User jsmith (uid=10001) deleted
kanidm person create newuser "New User" --gidnumber 10001 # DANGEROUS!
# ✅ DO - Increment UIDs, never reuse
kanidm person create newuser "New User" --gidnumber 10015 # Next available
7. 保護せずにサーバーを公開する
# ❌ DON'T - Direct internet exposure
bindaddress = "0.0.0.0:8443" # No firewall, no reverse proxy
# ✅ DO - Behind reverse proxy with rate limiting
# nginx reverse proxy with rate limiting
location / {
proxy_pass https://localhost:8443;
limit_req zone=auth burst=5;
}
# Or firewall restriction
ufw allow from 10.0.0.0/8 to any port 8443
8. 監査証跡がない
# ❌ DON'T - Delete accounts (loses audit trail)
kanidm person delete jsmith
# ✅ DO - Lock accounts to preserve history
kanidm account lock jsmith --reason "Offboarding - 2025-11-19"
# Review locked accounts
kanidm person get jsmith
9. テスト
Kanidm 連携の単体テスト
# tests/test_kanidm_service.py
import pytest
from unittest.mock import Mock, patch, MagicMock
import httpx
class TestKanidmService:
"""Unit tests for Kanidm service layer."""
@pytest.fixture
def mock_client(self):
"""Create mock httpx client."""
return Mock(spec=httpx.Client)
def test_get_user_success(self, mock_client):
"""Test successful user retrieval."""
mock_client.get.return_value = Mock(
status_code=200,
json=lambda: {
"attrs": {
"uuid": ["abc-123"],
"name": ["jsmith"],
"displayname": ["John Smith"],
"mail": ["[email protected]"]
}
}
)
from myapp.kanidm import KanidmService
service = KanidmService(client=mock_client)
user = service.get_user("jsmith")
assert user["name"] == "jsmith"
assert user["mail"] == "[email protected]"
mock_client.get.assert_called_once_with("/v1/person/jsmith")
def test_get_user_not_found(self, mock_client):
"""Test user not found handling."""
mock_client.get.return_value = Mock(status_code=404)
from myapp.kanidm import KanidmService
service = KanidmService(client=mock_client)
with pytest.raises(UserNotFoundError):
service.get_user("nonexistent")
def test_oauth2_token_validation(self, mock_client):
"""Test OAuth2 token introspection."""
mock_client.post.return_value = Mock(
status_code=200,
json=lambda: {
"active": True,
"sub": "jsmith",
"scope": "openid email profile",
"exp": 1732123456
}
)
from myapp.kanidm import validate_token
result = validate_token(mock_client, "test_token")
assert result["active"] is True
assert result["sub"] == "jsmith"
def test_group_membership_check(self, mock_client):
"""Test group membership verification."""
mock_client.get.return_value = Mock(
status_code=200,
json=lambda: {
"attrs": {
"memberof": ["developers", "vpn_users"]
}
}
)
from myapp.kanidm import is_member_of
assert is_member_of(mock_client, "jsmith", "developers") is True
assert is_member_of(mock_client, "jsmith", "admins") is False
統合テスト
# tests/integration/test_kanidm_integration.py
import pytest
import os
import httpx
import ldap3
@pytest.fixture(scope="session")
def kanidm_url():
"""Get Kanidm server URL from environment."""
return os.environ.get("KANIDM_TEST_URL", "https://idm.test.example.com")
@pytest.fixture(scope="session")
def api_token():
"""Get API token for testing."""
return os.environ["KANIDM_TEST_TOKEN"]
@pytest.fixture
def kanidm_client(kanidm_url, api_token):
"""Create authenticated Kanidm client."""
client = httpx.Client(
base_url=kanidm_url,
headers={"Authorization": f"Bearer {api_token}"},
timeout=30.0
)
yield client
client.close()
class TestOAuth2Integration:
"""Integration tests for OAuth2/OIDC."""
def test_openid_discovery(self, kanidm_client):
"""Test OpenID Connect discovery endpoint."""
response = kanidm_client.get(
"/oauth2/openid/testapp/.well-known/openid-configuration"
)
assert response.status_code == 200
config = response.json()
assert "issuer" in config
assert "authorization_endpoint" in config
assert "token_endpoint" in config
assert "jwks_uri" in config
def test_token_endpoint(self, kanidm_client):
"""Test token endpoint responds correctly."""
response = kanidm_client.post(
"/oauth2/token",
data={
"grant_type": "client_credentials",
"scope": "openid"
},
auth=("test_client", os.environ["TEST_CLIENT_SECRET"])
)
assert response.status_code == 200
tokens = response.json()
assert "access_token" in tokens
assert "token_type" in tokens
assert tokens["token_type"] == "Bearer"
class TestLDAPIntegration:
"""Integration tests for LDAP."""
@pytest.fixture
def ldap_connection(self):
"""Create LDAP connection."""
server = ldap3.Server(
os.environ.get("KANIDM_LDAP_URL", "ldaps://idm.test.example.com:3636"),
use_ssl=True,
get_info=ldap3.ALL
)
conn = ldap3.Connection(
server,
user=os.environ["LDAP_BIND_DN"],
password=os.environ["LDAP_BIND_PASSWORD"],
auto_bind=True
)
yield conn
conn.unbind()
def test_ldap_bind(self, ldap_connection):
"""Test LDAP bind succeeds."""
assert ldap_connection.bound
def test_user_search(self, ldap_connection):
"""Test LDAP user search."""
ldap_connection.search(
search_base=os.environ.get("LDAP_BASE_DN", "dc=idm,dc=example,dc=com"),
search_filter="(uid=testuser)",
attributes=["uid", "mail", "displayName"]
)
assert len(ldap_connection.entries) >= 0 # May or may not exist
def test_group_search(self, ldap_connection):
"""Test LDAP group search."""
ldap_connection.search(
search_base=os.environ.get("LDAP_BASE_DN", "dc=idm,dc=example,dc=com"),
search_filter="(objectClass=group)",
attributes=["cn", "member"]
)
assert ldap_connection.result["result"] == 0
class TestRADIUSIntegration:
"""Integration tests for RADIUS (requires radtest)."""
@pytest.mark.skip(reason="Requires RADIUS client tools")
def test_radius_authentication(self):
"""Test RADIUS authentication flow."""
import subprocess
result = subprocess.run(
[
"radtest",
"testuser",
os.environ["TEST_USER_PASSWORD"],
os.environ.get("RADIUS_SERVER", "idm.test.example.com"),
"0",
os.environ["RADIUS_SECRET"]
],
capture_output=True,
text=True
)
assert "Access-Accept" in result.stdout
エンドツーエンドテスト
# tests/e2e/test_auth_flows.py
import pytest
from playwright.sync_api import Page, expect
class TestWebAuthnFlow:
"""E2E tests for WebAuthn authentication."""
@pytest.fixture
def kanidm_url(self):
return "https://idm.test.example.com"
def test_login_page_loads(self, page: Page, kanidm_url):
"""Test login page is accessible."""
page.goto(kanidm_url)
expect(page.locator("input[name='username']")).to_be_visible()
expect(page.locator("button[type='submit']")).to_be_visible()
def test_oauth2_authorization_flow(self, page: Page, kanidm_url):
"""Test OAuth2 authorization code flow."""
# Start authorization
page.goto(
f"{kanidm_url}/oauth2/authorize?"
"client_id=testapp&"
"redirect_uri=https://app.test.example.com/callback&"
"response_type=code&"
"scope=openid%20email%20profile"
)
# Should redirect to login
expect(page.locator("input[name='username']")).to_be_visible()
# Login
page.fill("input[name='username']", "testuser")
page.fill("input[name='password']", "testpassword")
page.click("button[type='submit']")
# Should redirect to callback with code
page.wait_for_url("**/callback?code=*")
assert "code=" in page.url
セキュリティテスト
# tests/security/test_kanidm_security.py
import pytest
import httpx
class TestSecurityConfiguration:
"""Security configuration tests."""
@pytest.fixture
def client(self):
return httpx.Client(timeout=10.0, verify=True)
def test_tls_required(self, client):
"""Test that HTTP is rejected, only HTTPS works."""
# HTTP should fail or redirect
with pytest.raises(httpx.ConnectError):
client.get("http://idm.example.com:8080")
# HTTPS should work
response = client.get("https://idm.example.com/status")
assert response.status_code == 200
def test_no_plain_ldap(self):
"""Test that plain LDAP is disabled."""
import ldap3
import socket
# Plain LDAP (port 389) should be closed
server = ldap3.Server("idm.example.com", port=389, use_ssl=False)
conn = ldap3.Connection(server)
# Should fail to connect
with pytest.raises((ldap3.core.exceptions.LDAPSocketOpenError, socket.error)):
conn.bind()
def test_oauth2_redirect_uri_validation(self, client):
"""Test that only exact redirect URIs are allowed."""
# Valid redirect
response = client.get(
"https://idm.example.com/oauth2/authorize",
params={
"client_id": "testapp",
"redirect_uri": "https://app.example.com/callback",
"response_type": "code"
},
follow_redirects=False
)
assert response.status_code in [302, 200]
# Invalid redirect should be rejected
response = client.get(
"https://idm.example.com/oauth2/authorize",
params={
"client_id": "testapp",
"redirect_uri": "https://evil.com/callback",
"response_type": "code"
},
follow_redirects=False
)
assert response.status_code in [400, 403]
def test_account_lockout(self, client):
"""Test account lockout after failed attempts."""
# Attempt multiple failed logins
for _ in range(6):
response = client.post(
"https://idm.example.com/v1/auth",
json={"username": "testuser", "password": "wrongpassword"}
)
# Account should be locked
response = client.post(
"https://idm.example.com/v1/auth",
json={"username": "testuser", "password": "correctpassword"}
)
assert response.status_code == 403
assert "locked" in response.text.lower()
テストの実行
# Run all unit tests
pytest tests/test_*.py -v
# Run integration tests (requires test environment)
export KANIDM_TEST_URL="https://idm.test.example.com"
export KANIDM_TEST_TOKEN="your-test-token"
pytest tests/integration/ -v
# Run security tests
pytest tests/security/ -v --tb=short
# Run with coverage
pytest tests/ --cov=myapp --cov-report=html
# Run E2E tests
playwright install chromium
pytest tests/e2e/ -v
# Continuous integration
pytest tests/ -v --junitxml=results.xml
13. 重要な注意事項
実装前チェックリスト
フェーズ 1:コードを書く前に
-
要件を理解する
- ID 管理の要件を確認します
- 必要な認証方式を特定します(WebAuthn、TOTP、パスワード)
- 連携ポイントを文書化します(OAuth2、LDAP、RADIUS、SSH)
- ユーザー/グループ構成とアクセス ポリシーを定義します
-
セキュリティを計画する
- ユーザー層ごとの認証情報ポリシーの要件を特定します
- TLS 証明書の方針を計画します(本番環境では CA 署名済み)
- RADIUS 共有シークレットのローテーションスケジュールを定義します
- OAuth2 クライアントの要件とスコープを文書化します
-
まずテストを書く(TDD)
- サービス層の単体テストを作成します
- LDAP/OAuth2/RADIUS の統合テストを作成します
- TLS、ロックアウト、リダイレクト検証のセキュリティテストを作成します
- 実装前にテストが失敗することを確認します
フェーズ 2:実装中
-
コア設定
- Kanidm サーバーを TLS で設定する
- バックアップ手順を設定する
- 適切な POSIX 属性を持つユーザーとグループを作成する
- 認証情報ポリシーを設定する
-
認証の設定
- 特権アカウントで WebAuthn を有効にする
- バックアップとして TOTP を設定する
- 強力なパスワードポリシーを設定する
- アカウントのロックアウトしきい値を設定する
-
インテグレーションの設定
- 正確なリダイレクト URI を指定して OAuth2 クライアントを登録する
- パブリッククライアントで PKCE を有効にする
- 最小限の権限で LDAP バインドアカウントを設定する
- 強力な共有シークレットを使用して RADIUS クライアントを設定する
- SSH 鍵の配布を設定する
-
テストを継続的に実行する
- 各コンポーネントの実装後に単体テストを実行する
- 設定変更後にインテグレーションテストを実行する
- セキュリティテストが合格することを確認する
フェーズ 3:コミット/デプロイ前
-
セキュリティの検証
- 信頼された CA の TLS 証明書を使用する(本番環境では自己署名証明書を使用しない)
- すべての管理者アカウントで WebAuthn を必須にする
- 強力な認証情報ポリシーを設定する
- アカウントロックアウトポリシーを有効にする
- 監査ログを設定する
- LDAPS のみを使用する(平文 LDAP は無効にする)
- 強力な RADIUS 共有シークレットを使用する(手動ではなく生成する)
- OAuth2 リダイレクト URI を完全一致にする(ワイルドカードは使用しない)
- デフォルトのパスワードを使用しない
-
すべてのテストに合格する
- 単体テスト:
pytest tests/test_*.py -v - インテグレーションテスト:
pytest tests/integration/ -v - セキュリティテスト:
pytest tests/security/ -v - E2E テスト:
pytest tests/e2e/ -v
- 単体テスト:
-
高可用性とバックアップ
- 毎日の自動バックアップを設定する
- バックアップからの復元が正常に行われることをテストする
- オフサイトのバックアップストレージを設定する
- データベースの整合性検証をスケジュールする
- レプリケーションを設定する(HA が必要な場合)
- 災害復旧計画を文書化する
-
インテグレーションの検証
- レガシーアプリで LDAP インテグレーションをテストする
- すべてのクライアントで OAuth2/OIDC をテストする
- ネットワークデバイスで RADIUS をテストする
- SSH 鍵の配布をテストする
- PAM 認証をテストする
- グループメンバーシップの反映を検証する
-
運用準備
- 監視とアラートを設定する
- ログ集約を設定する
- 管理手順を文書化する
- インシデント対応計画を準備する
- 管理者アカウントで WebAuthn を登録する
- サービスアカウントの認証情報をローテーションする
- アクセスレビューのスケジュールを策定する
-
ネットワークセキュリティ
- ファイアウォールルールを設定する
- レート制限を有効にする
- リバースプロキシを設定する(該当する場合)
- TLS 1.2 以上を必須にする
- 保護なしでインターネットに直接公開しない
主な設定ファイル
サーバー設定:/etc/kanidm/server.toml
- ドメインとオリジンの設定を確認する
- TLS 証明書のパスを確認する
- バインドアドレスを確認する
- バックアップパスを検証する
クライアント設定:/etc/kanidm/config
- サーバー URI が正しいこと
- TLS 検証が有効であること
- 有効な CA 証明書があること
SSH インテグレーション:/etc/ssh/sshd_config
- AuthorizedKeysCommand が設定されていること
- PubkeyAuthentication が有効であること
PAM インテグレーション:/etc/pam.d/
- pam_kanidm.so が設定されていること
- 認証モジュールの順序が正しいこと
参考ドキュメント
包括的なインテグレーション例については、以下を参照してください。
references/integration-guide.md- LDAP、OAuth2/OIDC、RADIUS、PAM、SSH のインテグレーション例
詳細なセキュリティ設定については、以下を参照してください。
references/security-config.md- MFA の設定、WebAuthn、パスワードポリシー、認証情報ポリシー
14. まとめ
Kanidm のアイデンティティ管理に精通した専門家として、次の点に重点を置いてください。
- セキュリティを最優先 - WebAuthn、強固なポリシー、監査証跡、あらゆる通信での TLS
- 最新のアイデンティティ管理 - OAuth2/OIDC ネイティブ、API 駆動、CLI ファースト
- レガシーシステムとの互換性 - 既存システムとの LDAP、RADIUS、PAM 統合
- 優れた運用 - バックアップと復元、監視、災害復旧
- アクセス制御 - 最小権限、グループベースの認可、定期的なレビュー
重要な原則:特権アカウントには WebAuthn を使用し、すべての接続で TLS を使用し、リダイレクト URI を正確に指定し、強固な RADIUS シークレットを設定し、毎日バックアップを取り、すべてを監査し、UID を再利用せず、アカウントは削除せずにロックし、復元手順をテストし、最小権限の原則に従ってください。
Kanidm は、セキュリティと使いやすさのバランスが取れた最新のアイデンティティプラットフォームです。安全で信頼性が高く、保守しやすいアイデンティティインフラストラクチャを構築してください。
忘れないでください:アイデンティティ管理は極めて重要です。設定ミスは、インフラストラクチャ全体を危険にさらすおそれがあります。必ず本番環境以外でテストし、変更前にバックアップを取り、特権操作を監査してください。