Pam-Python实现SSH的短信双因素认证
字数 1297 2025-08-18 11:37:15

使用Pam-Python实现SSH短信双因素认证教学文档

一、双因素认证概述

双因素认证(2FA)是一种安全机制,要求用户提供两种不同类型的认证因素来验证身份。本文介绍如何使用Python的pam_python模块为SSH登录实现短信验证码形式的双因素认证。

常见的双因素认证方案:

  • 短信验证码
  • RSA动态令牌
  • Google Authenticator
  • Duo

二、PAM模块基础

PAM(Pluggable Authentication Module)是Linux中的可插拔认证模块机制,特点:

  • 模块化设计和插件功能
  • 无需修改应用程序即可插入新认证模块
  • 详细参考:http://www.infoq.com/cn/articles/linux-pam-one

三、pam_python模块安装

1. 安装依赖

yum install pam pam-devel -y

2. 下载并编译pam_python

wget -O pam-python_1.0.6.tar.gz https://sourceforge.net/projects/pam-python/files/latest/download?source=files --no-check-certificate
tar xvf pam-python_1.0.6.tar.gz
cd pam-python_1.0.6
make lib
cp src/build/lib.linux-x86_64-2.7/pam_python.so /lib64/security/

四、短信双因素认证实现

1. 核心Python脚本

完整代码保存为/lib64/security/Multiauth.py

# -*- coding: utf-8 -*-

import random, string, hashlib, requests
import urllib, urllib2
import pwd, syslog

class SMSOperation:
    """短信发送接口"""
    def __init__(self, pin, phone_num):
        self.pin = pin
        self.phone_num = phone_num
        self.url = "短信服务地址"
        self.params = {
            "account": "短信服务用户名",
            "pswd": "短信服务密码",
            "msg": "One Time Pin:" + str(pin),
            "mobile": str(phone_num),
            "needstatus": "false",
            "extno": ""
        }
    
    def parse_number(self):
        """设置用户手机号参数"""
        try:
            self.params['mobile'] = self.phone_num
            return 1
        except:
            auth_log("Invalid phone number %s. Please check." % (user))
    
    def send_text(self, pamh):
        """发送请求"""
        try:
            self.parse_number()
        except:
            auth_log("Invalid phone number %s. Please check." % (user))
            msg = pamh.Message(pamh.PAM_ERROR_MSG, "The params are : (%s)" % (self.params))
            pamh.conversation(msg)
        
        resp = requests.post(self.url, data=self.params)
        temp = resp.content.split(',')[1]
        if(temp != 0):
            auth_log("Message cannot be sent to (%s), please check." % (self.phone_num))

def auth_log(msg):
    """保存日志到/var/log/messages"""
    syslog.openlog(facility=syslog.LOG_AUTH)
    syslog.syslog("MultiFactors Authentication: " + msg)
    syslog.closelog()

def get_hash(plain_text):
    """获取短信验证码的sha512字符串"""
    key_hash = hashlib.sha512()
    key_hash.update(plain_text)
    return key_hash.digest()

def get_user_number(user):
    """获取用户手机号码"""
    try:
        comments = pwd.getpwnam(user).pw_gecos
    except:
        auth_log("No local user (%s) found." % user)
        return -1
    
    try:
        return comments.split(',')[2]  # 返回用户手机号
    except:
        auth_log("Invalid comment block for user %s. Phone number must be listed as Office Phone" % (user))
        return -1

def gen_key(pamh, user, user_number, length):
    """生成短信验证码并发送到用户手机"""
    pin = ''.join(random.choice(string.digits) for i in range(length))
    msg = pamh.Message(pamh.PAM_ERROR_MSG, "The pin is: (%s)" % (pin))  # 登陆界面输出验证码,测试目的,实际使用中注释掉即可
    pamh.conversation(msg)
    
    sms = SMSOperation(pin, user_number)
    try:
        sms.send_text(pamh)
    except:
        if not user_number:
            auth_log("No phone number listed for user (%s)." % (user))
        else:
            auth_log("Error sending PIN to the given SMS number. (%s)" % (user_number))
        return -1
    
    return get_hash(pin)

def pam_sm_authenticate(pamh, flags, argv):
    PIN_LENGTH = 6  # 短信验证码长度
    try:
        user = pamh.get_user()
        user_number = get_user_number(user)
    except pamh.exception, e:
        return e.pam_result
    
    if user is None or user_number == -1:
        msg = pamh.Message(pamh.PAM_ERROR_MSG, "[1]Unable to get user's phone number.\nPlease check.")
        pamh.conversation(msg)
        return pamh.PAM_ABORT
    
    pin = gen_key(pamh, user, user_number, PIN_LENGTH)
    if pin == -1:
        msg = pamh.Message(pamh.PAM_ERROR_MSG, "[2]One time PIN could not be generated.\nPlease check (%s)" % (user_number))
        pamh.conversation(msg)
        return pamh.PAM_ABORT
    
    for attempt in range(0, 3):  # 仅允许三次错误尝试
        msg = pamh.Message(pamh.PAM_PROMPT_ECHO_OFF, "Enter one time PIN:")
        resp = pamh.conversation(msg)
        
        if get_hash(resp.resp) == pin:  # 用户输入与生成的验证码进行校验
            return pamh.PAM_SUCCESS
        else:
            continue
    
    return pamh.PAM_AUTH_ERR

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

2. 关键功能说明

  1. 短信发送类(SMSOperation)

    • 初始化短信服务参数
    • 处理手机号码
    • 发送短信验证码
  2. 辅助函数

    • auth_log(): 记录认证日志到/var/log/messages
    • get_hash(): 使用SHA512哈希算法处理验证码
    • get_user_number(): 从/etc/passwd获取用户手机号
    • gen_key(): 生成随机验证码并发送
  3. PAM接口函数

    • pam_sm_authenticate(): 主认证函数,处理整个认证流程
    • 其他PAM标准接口函数保持默认成功返回

3. 配置SSH使用PAM模块

  1. 修改PAM配置:
vi /etc/pam.d/sshd

添加:

auth requisite pam_python.so Multiauth.py
  1. 启用ChallengeResponseAuthentication:
vi /etc/ssh/sshd_config

确保有:

ChallengeResponseAuthentication yes
  1. 重启SSH服务:
systemctl restart sshd.service

五、用户手机号配置

将用户手机号存储在/etc/passwd文件的GECOS字段中,格式为:

用户名:x:UID:GID:全名,办公室,手机号,其他信息

例如:

gary:x:1001:1001:Gary Smith,,13800138000,:/home/gary:/bin/bash

六、日志查看

  • 错误日志:/var/log/secure
  • 运行日志:/var/log/messages

七、安全增强建议

  1. 短信接口防护

    • 实现发送频率限制
    • 添加IP白名单
    • 使用HTTPS协议
  2. 验证码增强

    • 增加验证码有效期检查
    • 实现验证码使用后立即失效
  3. 用户管理

    • 提供备用认证方式
    • 实现手机号变更流程
  4. 审计日志

    • 记录完整的认证过程
    • 实现异常登录告警

八、测试与验证

  1. 使用SSH客户端连接服务器
  2. 系统应提示输入验证码
  3. 检查手机是否收到短信
  4. 输入正确验证码应能成功登录
  5. 错误输入应限制为3次尝试

九、故障排除

  1. PAM模块加载失败

    • 检查pam_python.so路径
    • 验证文件权限
  2. 短信发送失败

    • 检查短信接口配置
    • 验证账户余额和权限
  3. 用户手机号获取失败

    • 检查/etc/passwd格式
    • 确认GECOS字段包含手机号
  4. SSH连接无响应

    • 确认ChallengeResponseAuthentication已启用
    • 检查PAM配置顺序

通过以上步骤,您可以为SSH登录实现一个经济高效的短信双因素认证系统。

使用Pam-Python实现SSH短信双因素认证教学文档 一、双因素认证概述 双因素认证(2FA)是一种安全机制,要求用户提供两种不同类型的认证因素来验证身份。本文介绍如何使用Python的pam_ python模块为SSH登录实现短信验证码形式的双因素认证。 常见的双因素认证方案: 短信验证码 RSA动态令牌 Google Authenticator Duo 二、PAM模块基础 PAM(Pluggable Authentication Module)是Linux中的可插拔认证模块机制,特点: 模块化设计和插件功能 无需修改应用程序即可插入新认证模块 详细参考:http://www.infoq.com/cn/articles/linux-pam-one 三、pam_ python模块安装 1. 安装依赖 2. 下载并编译pam_ python 四、短信双因素认证实现 1. 核心Python脚本 完整代码保存为 /lib64/security/Multiauth.py : 2. 关键功能说明 短信发送类(SMSOperation) : 初始化短信服务参数 处理手机号码 发送短信验证码 辅助函数 : auth_log() : 记录认证日志到/var/log/messages get_hash() : 使用SHA512哈希算法处理验证码 get_user_number() : 从/etc/passwd获取用户手机号 gen_key() : 生成随机验证码并发送 PAM接口函数 : pam_sm_authenticate() : 主认证函数,处理整个认证流程 其他PAM标准接口函数保持默认成功返回 3. 配置SSH使用PAM模块 修改PAM配置: 添加: 启用ChallengeResponseAuthentication: 确保有: 重启SSH服务: 五、用户手机号配置 将用户手机号存储在/etc/passwd文件的GECOS字段中,格式为: 例如: 六、日志查看 错误日志:/var/log/secure 运行日志:/var/log/messages 七、安全增强建议 短信接口防护 : 实现发送频率限制 添加IP白名单 使用HTTPS协议 验证码增强 : 增加验证码有效期检查 实现验证码使用后立即失效 用户管理 : 提供备用认证方式 实现手机号变更流程 审计日志 : 记录完整的认证过程 实现异常登录告警 八、测试与验证 使用SSH客户端连接服务器 系统应提示输入验证码 检查手机是否收到短信 输入正确验证码应能成功登录 错误输入应限制为3次尝试 九、故障排除 PAM模块加载失败 : 检查pam_ python.so路径 验证文件权限 短信发送失败 : 检查短信接口配置 验证账户余额和权限 用户手机号获取失败 : 检查/etc/passwd格式 确认GECOS字段包含手机号 SSH连接无响应 : 确认ChallengeResponseAuthentication已启用 检查PAM配置顺序 通过以上步骤,您可以为SSH登录实现一个经济高效的短信双因素认证系统。