Ubuntu下如何实现基于外部身份提供商的非本地用户Linux PAM认证以搭建SFTP服务器
你遇到的这个问题我之前帮朋友踩过坑,核心原因其实是SSH服务在触发PAM认证之前,就已经通过系统的getpwnam()函数检查用户是否存在了——也就是先查本地的/etc/passwd或者NSS(名称服务切换)配置的用户源。你之前在PAM脚本里动态创建用户的逻辑,是在认证阶段才执行的,这时候SSH已经完成了用户存在性校验,所以第一次登录必然失败,第二次因为用户已经创建在本地了,getpwnam()能查到,就正常通过了。
要实现完全依赖外部身份提供商(IDP)的用户认证,不用预创建本地用户,得同时搞定两个核心环节:一是让PAM通过IDP的REST API验证密码,二是让系统能从IDP获取用户的元数据(比如UID、GID、家目录这些),不然系统不知道怎么给用户创建会话。下面是具体的实现步骤:
一、调整SSH配置,让它完全依赖PAM和NSS做用户校验
默认情况下SSH会自己先做用户存在性检查,我们得把这个逻辑交给PAM和NSS来处理。编辑/etc/ssh/sshd_config,确保以下配置:
UsePAM yes PasswordAuthentication yes ChallengeResponseAuthentication yes # 如果之前有AllowUsers限制,改成允许所有或按需调整 AllowUsers *
修改完后重启SSH服务生效:
sudo systemctl restart sshd
二、用NSS模块动态提供用户元数据
系统要给用户创建会话,必须知道用户的UID、GID、家目录这些属性,所以得让getpwnam()这类函数能从你的IDP获取这些信息。这里推荐用nss-python来快速实现(如果Ubuntu仓库没有,可能需要源码编译安装):
- 编写NSS脚本,比如
/usr/lib/nss_python.py,用来调用IDP API获取用户信息:
import requests def getpwnam(name): # 调用你的IDP API获取用户详情 response = requests.get(f"https://你的IDP地址/api/users/{name}", headers={"X-API-Key": "你的API密钥"}) if response.status_code != 200: return None user_data = response.json() # 返回符合NSS要求的用户元数据格式:(用户名, 占位密码, UID, GID, 用户描述, 家目录, 登录shell) return ( user_data['username'], 'x', # 密码字段用x,因为PAM会单独处理密码验证 user_data['uid'], # 可以从IDP获取,也可以统一分配(比如从10000开始递增) 1001, # 这里用你之前创建的sftp组的GID user_data['full_name'], f"/home/{user_data['username']}", "/usr/sbin/nologin" # SFTP用户推荐用nologin,禁止登录shell ) # 按需实现其他NSS函数,比如getpwuid(通过UID查用户),如果不需要可以返回None def getpwuid(uid): return None
- 修改
/etc/nsswitch.conf,让系统在查询用户时优先(或同时)使用我们的Python模块:
passwd: compat python group: compat shadow: compat
三、优化PAM认证脚本,去掉本地用户创建逻辑
现在NSS已经能提供用户元数据了,就不用再在PAM里创建本地用户了,直接专注于密码验证即可。修改你的PAM脚本:
import requests def pam_sm_authenticate(pamh, flags, argv): try: user = pamh.get_user(None) # 获取用户输入的密码 password = pamh.conversation(pamh.Message(pamh.PAM_PROMPT_ECHO_OFF, "Password:")).resp except pamh.exception as e: return e.pam_result # 调用IDP的认证API验证密码 auth_url = "https://你的IDP地址/api/auth" payload = {"username": user, "password": password} headers = {"X-API-Key": "你的API密钥"} response = requests.post(auth_url, json=payload, headers=headers) if response.status_code == 200 and response.json().get('success'): # 验证成功后,确保用户家目录存在并设置正确权限(SFTP要求) home_dir = f"/home/{user}" if not os.path.exists(home_dir): import os os.makedirs(home_dir) # 从NSS获取用户的UID和GID,或者直接从IDP API返回的结果里拿 uid = response.json().get('uid', 10000) gid = 1001 # sftp组的GID os.chown(home_dir, uid, gid) os.chmod(home_dir, 0700) return pamh.PAM_SUCCESS else: return pamh.PAM_AUTH_ERR # 以下是PAM要求必须实现的空函数,直接返回成功即可 def pam_sm_setcred(pamh, flags, argv): return pamh.PAM_SUCCESS def pam_sm_acct_mgmt(pamh, flags, argv): return pamh.PAM_SUCCESS def pam_sm_open_session(pamh, flags, argv): return pamh.PAM_SUCCESS def pam_sm_close_session(pamh, flags, argv): return pamh.PAM_SUCCESS def pam_sm_chauthtok(pamh, flags, argv): return pamh.PAM_SUCCESS
然后修改/etc/pam.d/sshd,替换原有的认证模块为我们的Python脚本:
# 注释掉原来的common-auth引用,或者把我们的模块放在最前面 # @include common-auth auth required pam_python.so /path/to/你的PAM脚本.py account required pam_python.so /path/to/你的PAM脚本.py session required pam_python.so /path/to/你的PAM脚本.py
四、(可选但推荐)配置SFTP Chroot环境
如果要限制用户只能访问自己的家目录,防止越权访问,可以在sshd_config里添加以下配置:
Subsystem sftp internal-sftp Match Group sftp ChrootDirectory /home/%u ForceCommand internal-sftp X11Forwarding no AllowTcpForwarding no PermitTunnel no AllowAgentForwarding no
确保sftp组已经创建:
sudo groupadd sftp
关键注意事项
- 权限与安全:PAM和NSS脚本需要有调用外部API的权限,但不要给过高权限,避免安全风险;调用API一定要用HTTPS,同时给API加身份验证(比如API密钥),防止恶意调用。
- API可靠性:如果IDP API不稳定,用户可能无法登录,可以考虑添加本地缓存机制,比如把用户信息临时存在本地文件里,减少API调用频率。
- 测试验证:先在测试环境反复验证,不要直接在生产环境修改,避免不小心锁自己出系统。
备注:内容来源于stack exchange,提问作者Jonny Marotte

