通过对 ssh 登录进行二次身份验证,以强化服务器远程管理安全

809 阅读5分钟

基于 Linux PAM(Pluggable Authentication Module) 可插拔认证模块,可通过 pam 机制对 ssh 登录进行二次验证, 其中 Pam-Python 是一款开源的 python 模块,它将需要使用 C 语言编写的 PAM 模块转换为可以使用 python 编写,并且将认证流程 python 函数化,开发者只需在框架内编写少量业务代码即可调用 PAM 模块进行用户认证;pam 各模块的动态链接库: /lib64/security/*.so ,各模块对应的配置文件目录:/etc/pam.d/

编译安装

1、源码编译安装 Pam-Python 扩展认证模块

# 系统环境
> CentOS Linux release 7.9.2009 (3.10.0-1160.el7.x86_64)
# 安装编译环境
yum -y install gcc make pam pam-devel python-devel

# 编译安装 pam 模块
wget -O pam-python-1.0.7.tar.gz https://sourceforge.net/projects/pam-python/files/pam-python-1.0.8-1/pam-python-1.0.8.tar.gz/download
tar zxf pam-python-1.0.8.tar.gz && cd pam-python-1.0.8/src
# 修改源文件 pam_python.c ,将 `#include <Python.h>` 移到 `#include <security/pam_modules.h>` 上方; 否则 make 时会报错
make pam_python.so
cp build/lib.linux-x86_64-2.7/pam_python.so /usr/lib64/security/

认证脚本模板

1、与企业微信交互的认证脚本 /usr/lib64/security/pam_wechat_auth.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys
import pwd
import json
import string
import syslog
import random
import hashlib
import httplib
import datetime
import platform

def auth_log(msg):
    """写入日志"""
    syslog.openlog(facility=syslog.LOG_AUTH)
    syslog.syslog("MultiFactors Authentication: " + msg)
    syslog.closelog()


def action_wechat(content, touser=None, toparty=None, totag=None):
    """微信通知"""
    host = "qyapi.weixin.qq.com"

    # 企业微信设置, 依次为: 企业ID、应用ID、应用的凭证密钥
    corpid = ""
    agentid = ""
    secret = ""
    headers = {
        'Content-Type': 'application/json'
    }

    # 获取 token
    access_token_url = '/cgi-bin/gettoken?corpid={id}&corpsecret={crt}'.format(id=corpid, crt=secret)
    try:
        httpClient = httplib.HTTPSConnection(host, timeout=10)
        httpClient.request("GET", access_token_url, headers=headers)
        response = httpClient.getresponse()
        token = json.loads(response.read())['access_token']
        httpClient.close()
    except Exception as e:
        auth_log('get wechat token error: %s' % e)
        return False

    # 发送告警信息
    send_url = '/cgi-bin/message/send?access_token={token}'.format(token=token)
    data = {
        "msgtype": 'text',
        "agentid": agentid,
        "text": {'content': content},
        "safe": 0
    }
    # 发送验证码到指定用户、部分、具有特定标签的用户
    if touser:
        data['touser'] = touser
    if toparty:
        data['toparty'] = toparty
    if toparty:
        data['totag'] = totag
    try:
        httpClient = httplib.HTTPSConnection(host, timeout=10)
        httpClient.request("POST", send_url, json.dumps(data), headers=headers)
        response = httpClient.getresponse()
        result = json.loads(response.read())
        if result['errcode'] != 0:
            auth_log('Failed to send verification code using WeChat: %s' % result)
            return False
    except Exception as e:
        auth_log('Error sending verification code using WeChat: %s' % e)
        return False
    finally:
        if httpClient:
            httpClient.close()
    auth_log('Send verification code using WeChat successfully.')
    return True

def get_user_comment(user):
    """获取用户描述信息"""
    try:
        comments = pwd.getpwnam(user).pw_gecos
    except:
        auth_log("No local user (%s) found." % user)
        comments = ''
    return comments

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

def gen_key(pamh, user, length):
    """生成 PIN 码并发送到用户"""
    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)

    hostname = platform.node().split('.')[0]
    content = "[MFA] %s 使用 %s 正在登录 %s, 验证码为【%s】, 1分钟内有效。" % (pamh.rhost, user, hostname, pin)
    touser = get_user_comment(user)
    result = action_wechat(content, touser=touser)

    pin_time = datetime.datetime.now()
    return get_hash(pin), pin_time

def pam_sm_authenticate(pamh, flags, argv):
    PIN_LENGTH = 6  # PIN 码长度
    PIN_LIVE = 60   # PIN 存活时间, 超出时间验证失败
    PIN_LIMIT = 3   # 限制错误尝试次数
    EMERGENCY_HASH = '\xba2S\x87j\xedk\xc2-Jo\xf5=\x84\x06\xc6\xad\x86A\x95\xed\x14J\xb5\xc8v!\xb6\xc23\xb5H\xba\xea\xe6\x95m\xf3F\xec\x8c\x17\xf5\xea\x10\xf3^\xe3\xcb\xc5\x14y~\xd7\xdd\xd3\x14Td\xe2\xa0\xba\xb4\x13'  # 预定义验证码 123456 的 hash, 用于紧急认证

    try:
        user = pamh.get_user()
    except pamh.exception as e:
        return e.pam_result

    auth_log("login_ip: %s, login_user: %s" % (pamh.rhost, user))

    if get_user_comment(user) == '':
        msg = pamh.Message(pamh.PAM_ERROR_MSG, "[Warning] You need to set the Qiyi WeChat username in the comment block for user %s." % (user))
        pamh.conversation(msg)
        return pamh.PAM_ABORT

    pin, pin_time = gen_key(pamh, user, PIN_LENGTH)
    # 限制错误尝试次数
    for attempt in range(0, PIN_LIMIT):
        msg = pamh.Message(pamh.PAM_PROMPT_ECHO_OFF, "Verification code:")
        resp = pamh.conversation(msg)
        resp_time = datetime.datetime.now()
        input_interval = resp_time - pin_time
        if input_interval.seconds > PIN_LIVE:
            msg = pamh.Message(pamh.PAM_ERROR_MSG, "[Warning] Time limit exceeded.")
            pamh.conversation(msg)
            return pamh.PAM_ABORT
        resp_hash = get_hash(resp.resp)
        if resp_hash == pin or resp_hash == EMERGENCY_HASH:  # 用户输入与生成的验证码进行校验
            return pamh.PAM_SUCCESS
        else:
            continue

    msg = pamh.Message(pamh.PAM_ERROR_MSG, "[Warning] Too many authentication failures.")
    pamh.conversation(msg)
    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

认证脚本配置

1、根据个人情况,登录企业微信 web 端,配置如下信息

# web 端企业微信后台 --> 我的企业 --> 企业信息  --> 企业 ID
# web 端企业微信后台 --> 应用管理 --> 应用  --> 创建或选择已创建应用 --> AgentId 、Secret
corpid = ""     # 企业ID
agentid = ""    # 应用ID
secret = ""     # 应用的凭证密钥

2、生成预定义 验证码,用于替换默认验证码(用于未收到验证码时的默认验证码)

# 系统执行 python 进入交互时环境
python
>>> key_hash = hashlib.sha512()
>>> key_hash.update("111111")
>>> key_hash.digest()
"\xb0A%\x97\xdc\xea\x816UWM\xc5J[t\x96|\xf8S\x17\xf03*%\x91\xbeyS\xa0\x16\xf8\xdeV \x0e\xb3}[\xa5\x93\xb1\xe4\xaa'\xce\xa5\xca'\x10\x0f\x94\xdc\xcd[\x04\xba\xe5\xca\xddDT\xdb\xa6}"
>>>

3、出于安全考虑,可调整验证码长度、验证码存活时间、尝试次数

def pam_sm_authenticate(pamh, flags, argv):
    PIN_LENGTH = 6  # PIN 码长度
    PIN_LIVE = 60   # PIN 存活时间, 超出时间验证失败
    PIN_LIMIT = 3   # 限制错误尝试次数
    ...

相关配置修改

1、修改系统及 sshd 配置

# 禁用 selinux, 按如下配置需重启系统以生效
sed -i 's/SELINUX=enforcing/SELINUX=disabled'/g /etc/selinux/config

# 修改系统系统 root 的注释为企业微信成员账号标识,当登录 root 账号时将向指定企业微信账号发生二次验证码
usermod -c 'chris' root
cat /etc/passwd
> root:x:0:0:chris:/root:/bin/bash

# sshd 相关配置修改
sed -i 's#^ChallengeResponseAuthentication no#ChallengeResponseAuthentication yes#' /etc/ssh/sshd_config
echo 'auth       requisite    pam_python.so   /usr/lib64/security/pam_wechat_auth.py' >> /etc/pam.d/sshd
chmod +x /usr/lib64/security/pam_wechat_auth.py
systemctl restart sshd

登录验证

1、登录验证实列,日志输出如下

# ssh 登录时依次提示输入用户密码、以及发送到企业微信成员的验证码
ssh root@192.168.31.161
Password:
Verification code:

# 认证过程日志如下
tail -f /var/log/messages
#> Aug 11 17:23:35 t1 sshd: MultiFactors Authentication: login_ip: 192.168.31.201, login_user: root
#> Aug 11 17:23:36 t1 sshd: MultiFactors Authentication: Send verification code using WeChat successfully.
#> Aug 11 17:28:14 t1 systemd-logind: New session 51 of user root.
#> Aug 11 17:28:14 t1 systemd: Started Session 51 of user root

2、版权声明

作者: 运维技术帮
链接: zhihu.com/people/ywjsbang.com
来源: 运维技术帮
著作权归作者所有。商业转载请联系作者获得授权,非商业转载需在文章首尾部分明确注明文章出处及网址