You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

支持MFA的PAM模块开发:SSHD多凭证验证失败问题求助

问题:SSH PAM模块多凭证(密码+OTP)验证失败,无第二次输入提示

我正在开发一款对接HTTPS服务的PAM模块,目前仅编写测试版本验证流程,未接入实际业务逻辑。测试代码如下:

static const char *valid_username = "xrfang";
static const char *valid_password = "password";
static const char *valid_otpcode = "123456";

PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) {
    const char *username;
    char *password = NULL;
    char *otpcode = NULL;
    int rc = PAM_AUTH_ERR;
    openlog("pam_ums", LOG_PID | LOG_CONS, LOG_AUTH);

    if (pam_get_user(pamh, &username, NULL) != PAM_SUCCESS) {
        syslog(LOG_ALERT, "Failed to get username");
        goto done;
    }
    syslog(LOG_ALERT, "username:>%s<", username);

    if (pam_prompt(pamh, PAM_PROMPT_ECHO_OFF, &password, "Password: ") != PAM_SUCCESS) {
        syslog(LOG_ALERT, "Failed to get password");
        goto done;
    }
    syslog(LOG_ALERT, "password:>%s<", password);

    if (pam_prompt(pamh, PAM_PROMPT_ECHO_ON, &otpcode, "OTP Code: ") != PAM_SUCCESS) {
        syslog(LOG_ALERT, "Failed to get otp code");
        goto done;
    }
    syslog(LOG_ALERT, "otpcode:>%s<", otpcode);

    if (strcmp(username, valid_username) != 0) {
        syslog(LOG_ALERT, "invalid username: given=%s; wanted=%s;", username, valid_username);
        goto done;
    }
    if (strcmp(password, valid_password) != 0) {
        syslog(LOG_ALERT, "invalid password: given=%s; wanted=%s;", password, valid_password);
        goto done;
    }
    if (strcmp(otpcode, valid_otpcode) != 0) {
        syslog(LOG_ALERT, "invalid otpcode: given=%s; wanted=%s;", otpcode, valid_otpcode);
        goto done;
    }
    rc = PAM_SUCCESS;
    syslog(LOG_ALERT, "Authentication successful");
done:
    closelog();
    if (password) free(password);
    if (otpcode) free(otpcode);
    return rc;
}

将该模块配置到/etc/pam.d/sshd:

# PAM configuration for the Secure Shell service

auth sufficient pam_ums.so

问题现象

未添加OTP码提示时认证正常,添加第二次OTP提示后认证失败,SSH未弹出OTP输入框,日志如下:

Feb  5 17:49:15 office pam_ums[1145092]: username:>xrfang<
Feb  5 17:49:15 office pam_ums[1145092]: password:>password<
Feb  5 17:49:15 office pam_ums[1145092]: pam_ums(sshd:auth): conversation failed
Feb  5 17:49:15 office pam_ums[1145092]: Failed to get otp code
Feb  5 17:49:15 office sshd[1145092]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.93.200  user=xrfang
Feb  5 17:49:17 office sshd[1145092]: Failed password for xrfang from 192.168.93.200 port 47670 ssh2

需求:希望在单个PAM模块内实现多凭证验证,不想拆分用两个模块(比如类似Google Authenticator的方式)。


解决方案

1. 开启SSH的Challenge-Response支持

SSH默认可能限制单次PAM认证的交互次数,修改/etc/ssh/sshd_config确保以下配置:

ChallengeResponseAuthentication yes
UsePAM yes

修改后重启sshd:

systemctl restart sshd

2. 替换pam_prompt为底层pam_conv函数

pam_prompt是简化封装,在多交互场景下兼容性较差,改用底层对话接口更可靠。修改OTP获取部分的代码:

struct pam_message msg[1];
struct pam_response *resp = NULL;
const struct pam_message *pmsg[1];
int rc;

// 构建OTP提示消息
msg[0].msg_style = PAM_PROMPT_ECHO_ON;
msg[0].msg = "OTP Code: ";
pmsg[0] = &msg[0];

rc = pam_conv(pamh, 1, pmsg, &resp);
if (rc != PAM_SUCCESS) {
    syslog(LOG_ALERT, "Failed to get otp code via pam_conv");
    goto done;
}
otpcode = resp[0].resp;
// 注意:此处不要手动free(resp),PAM框架会负责释放;若需释放需调用pam_free

3. 调整PAM规则的优先级

确保你的模块是第一个被调用的认证模块,避免其他模块(如pam_unix)提前拦截或干扰交互。可以修改/etc/pam.d/sshd:

auth sufficient pam_ums.so
auth required pam_unix.so nullok_secure

如果使用sufficient,确保模块失败后不会直接跳过后续验证,或者根据需求调整为required(但会强制所有模块通过)。

4. 验证客户端支持

某些SSH客户端可能不支持多次Challenge-Response交互,使用ssh -v xrfang@your-server查看详细日志,确认客户端是否接收到第二次提示。如果是客户端问题,建议使用标准OpenSSH客户端测试。

5. 调试PAM交互流程

添加更详细的日志跟踪pam_conv的返回值,或者临时启用PAM调试日志:

echo "debug" >> /etc/pam.d/sshd

重启sshd后查看系统日志,确认PAM对话的每一步是否正常执行。


内容的提问来源于stack exchange,提问作者xrfang

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 15:42:32