关于 kanidm-expert
kanidm-expert 是用于配置和运维 Kanidm 身份管理的 Skill,涵盖身份生命周期、认证、授权和集成,并提供部署安全与运维指导。
主要功能
- 01用户、组和服务账户管理
- 02WebAuthn、TOTP 与密码策略
- 03OAuth2/OIDC 客户端及范围配置
- 04LDAP、RADIUS、SSH 和 PAM 集成
- 05审计、备份与复制指南
适用场景
- 01为应用配置 OAuth2/OIDC 登录
- 02通过 LDAP 连接旧有服务
- 03使用 RADIUS 管理网络认证
- 04规划身份数据备份与恢复
安装
复制下面的命令,在终端中运行。
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 驱动、命令行优先的设计
- 可靠:支持复制、具备备份策略和灾难恢复能力
- 集成:兼容 LDAP、支持 RADIUS、支持 SSH 密钥分发
- 易于维护:策略清晰、流程有文档记录、可用于自动化
风险级别:🔴 严重 — 身份与访问管理是安全的基础。错误配置可能导致未经授权的访问、权限提升、凭证泄露以及整个系统被接管。
3. 核心原则
-
测试驱动开发优先 — 在实施 Kanidm 配置前编写测试。部署前使用自动化测试验证身份验证流程、群组成员身份和访问策略。
-
关注性能 — 优化连接复用、高效 LDAP 查询和令牌缓存,并尽量降低身份验证延迟。身份系统必须快速且响应及时。
-
安全优先 — 特权账户使用 WebAuthn,全面启用 TLS,采用严格的凭证策略,并审计所有操作。绝不在安全性上妥协。
-
现代身份 — 原生支持 OAuth2/OIDC、由 API 驱动、命令行优先的设计。使用现代标准构建集成。
-
卓越运维 — 自动化备份、监控、灾难恢复流程和定期访问审查。
-
最小权限 — 仅授予所需的最低权限,分离读写访问权限,并为应用程序使用服务账户。
-
审计所有操作 — 记录所有身份验证尝试、特权操作和 API 令牌使用情况。维护完整的审计跟踪。
2. 核心职责
1. 用户与群组管理
- 使用适当的属性(displayname、mail、POSIX uid/gid)创建用户
- 管理群组成员身份以控制访问
- 设置 POSIX 属性,以集成 Unix/Linux
- 为应用程序管理服务账户
- 实施账户生命周期管理(创建、停用、删除)
- 账户删除后绝不重复使用 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 密钥轮换策略
- 与 PAM 集成,用于 Unix 身份验证
- 管理 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. 七大实施模式
模式 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:编写代码前
-
了解需求
- 审查身份管理需求
- 确定所需的身份验证方式(WebAuthn、TOTP、密码)
- 记录集成点(OAuth2、LDAP、RADIUS、SSH)
- 定义用户/组结构和访问策略
-
安全规划
- 确定各用户等级的凭据策略要求
- 规划 TLS 证书策略(生产环境使用 CA 签名证书)
- 制定 RADIUS 共享密钥轮换计划
- 记录 OAuth2 客户端要求和作用域
-
先编写测试(TDD)
- 为服务层创建单元测试
- 为 LDAP/OAuth2/RADIUS 创建集成测试
- 为 TLS、锁定和重定向验证创建安全测试
- 确认在实施前测试会失败
阶段 2:实施期间
-
核心配置
- 配置启用 TLS 的 Kanidm 服务
- 设置备份流程
- 创建用户和组,并设置适当的 POSIX 属性
- 配置凭据策略
-
身份验证设置
- 为特权账户启用 WebAuthn
- 将 TOTP 配置为备用方式
- 设置强密码策略
- 配置账户锁定阈值
-
集成配置
- 注册 OAuth2 客户端,并准确设置重定向 URI
- 为公共客户端启用 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
- 单元测试:
-
高可用性与备份
- 配置每日自动备份
- 成功测试备份恢复
- 配置异地备份存储
- 安排数据库完整性验证
- 配置复制(如需高可用性)
- 编写灾难恢复计划文档
-
集成验证
- 使用旧版应用测试 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 驱动、命令行优先
- 旧系统兼容性——为现有系统集成 LDAP、RADIUS、PAM
- 卓越运维——备份/恢复、监控、灾难恢复
- 访问控制——最小权限、基于群组的授权、定期审查
核心原则:特权账户使用 WebAuthn,所有连接使用 TLS,重定向 URI 必须精确匹配,使用强 RADIUS 密钥,每日备份,全面审计,绝不重复使用 UID,锁定账户而非删除账户,测试恢复流程,遵循最小权限原则。
Kanidm 是一个兼顾安全性与易用性的现代身份平台。构建安全、可靠且易于维护的身份基础设施。
请牢记:身份管理至关重要。配置错误可能危及整个基础设施。务必先在非生产环境中测试,变更前先备份,并审计特权操作。