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. 关键功能说明
-
短信发送类(SMSOperation):
- 初始化短信服务参数
- 处理手机号码
- 发送短信验证码
-
辅助函数:
auth_log(): 记录认证日志到/var/log/messagesget_hash(): 使用SHA512哈希算法处理验证码get_user_number(): 从/etc/passwd获取用户手机号gen_key(): 生成随机验证码并发送
-
PAM接口函数:
pam_sm_authenticate(): 主认证函数,处理整个认证流程- 其他PAM标准接口函数保持默认成功返回
3. 配置SSH使用PAM模块
- 修改PAM配置:
vi /etc/pam.d/sshd
添加:
auth requisite pam_python.so Multiauth.py
- 启用ChallengeResponseAuthentication:
vi /etc/ssh/sshd_config
确保有:
ChallengeResponseAuthentication yes
- 重启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
七、安全增强建议
-
短信接口防护:
- 实现发送频率限制
- 添加IP白名单
- 使用HTTPS协议
-
验证码增强:
- 增加验证码有效期检查
- 实现验证码使用后立即失效
-
用户管理:
- 提供备用认证方式
- 实现手机号变更流程
-
审计日志:
- 记录完整的认证过程
- 实现异常登录告警
八、测试与验证
- 使用SSH客户端连接服务器
- 系统应提示输入验证码
- 检查手机是否收到短信
- 输入正确验证码应能成功登录
- 错误输入应限制为3次尝试
九、故障排除
-
PAM模块加载失败:
- 检查pam_python.so路径
- 验证文件权限
-
短信发送失败:
- 检查短信接口配置
- 验证账户余额和权限
-
用户手机号获取失败:
- 检查/etc/passwd格式
- 确认GECOS字段包含手机号
-
SSH连接无响应:
- 确认ChallengeResponseAuthentication已启用
- 检查PAM配置顺序
通过以上步骤,您可以为SSH登录实现一个经济高效的短信双因素认证系统。