Loading

对ansible主控节点做双因子认证安全加固--python实现

引言

  当部署ansible的服务器为控制端时,大量的服务器可能已经做了免密,此时的服务器对被控端的服务器ssh权限过大,这时可能就需要对这个控制端的服务器做个一个安全加固进行防御。

实现

用户名+动态口令+密码方式

引用Google双因子实现

TOTP 是时间同步,基于客户端的动态口令和动态口令验证服务器的时间比对,一般每60秒产生一个新口令,要求客户端和服务器能够十分精确的保持正确的时钟,客户端和服务端基于时间计算的动态口令才能一致

代码和依赖包

#!/usr/bin/python
# -*- coding=utf-8 -*-
import pwd, syslog, time
import logging
import json
import pyotp
import threading
import os
LOCK = threading.Lock()

# 拒绝root用户, 默认调试模式,默认为False, 调试结束最好改为 True
NOT_ROOT = False
# 密钥
KEYS = "O5XSAYLLEB4WS3RANRUWC3Q="
# 设置白名单ip, * 表示所有主机都可以连接; ["10.10.0.1", "10.12.0.2"] 表示为只允许列表出现的主机连接
WHITELIST = ["*"]
# 设置重试等待时间 s
TIME_OUT = 30*60
# 设置最多允许错误次数
TRY_AGAIN = 3
# 用户白名单,可以忽略PIN验证
USER_LIST = [""]
# 管理员PIN
ADMIN_PIN = "louwenjun"
# 日志和登录历史的文件路径
PAM_FILE = "/tmp/.pam/"

if not os.path.exists(PAM_FILE):
    os.system("mkdir -p {}".format(PAM_FILE))

LOGIN_HISTORY_FILE = "{}login_history.json".format(PAM_FILE)

logging.basicConfig(level=logging.DEBUG,
                    filename='{}pam_python_auth.log'.format(PAM_FILE),
                    filemode='a',
                    format='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s'
                    )


def auth_log(msg):
    syslog.syslog("IPCPU-PAM-AUTH: " + msg)
    syslog.closelog()
    logging.info("PAM_PYTHON-AUTH: " + msg)


def PIN_verify(secret_key):
    """Extract user's phone number for pw entry"""
    totp = pyotp.TOTP(KEYS)
    return totp.verify(secret_key)


def update_login_history_file(user, host, data, is_del=None, is_error=None):
    error_user_data = data
    defult_data = {"num": 1, "time": -1}

    if is_error is True:
        if host in error_user_data and user in error_user_data[host]:
            # 当用户登入错误次数超出上限则进行添加延时等待
            if error_user_data[host][user]["num"] >= TRY_AGAIN and error_user_data[host][user]["time"] == -1:
                error_user_data[host][user]["time"] = int(time.time())
            error_user_data[host][user]["num"] += 1
        elif host in error_user_data:
            error_user_data[host][user] = defult_data
        else:
            error_user_data[host] = {user: defult_data}

    if is_del is True and host in error_user_data and user in error_user_data[host]:
        del error_user_data[host][user]

    auth_log("==update_login_history_json:%s" % data)

    with open(LOGIN_HISTORY_FILE, "w") as f:
        f.write(json.dumps(data))


def get_login_history_file():
    try:
        with open(LOGIN_HISTORY_FILE, "r") as f:
            history_json = json.loads(f.read())
    except:
        history_json = {}

    auth_log("==login_history_json:%s" % history_json)
    return history_json


def check_user_ip_is_true(user, host, data):
    status, msg = -1, "Permission denied"

    # 校验登入ip是否是白名单中
    if "*" not in WHITELIST and WHITELIST and host not in WHITELIST:
        return status, msg
    # 校验用户是否被锁,如果被锁返回需要等待解锁时间,否则解除用户锁
    if host in data and user in data[host]:
        user_data = data[host][user]
        if user_data["num"] <= TRY_AGAIN:
            return 0, ""

        time_left = int(time.time()) - user_data.get("time", 0)

        if time_left <= TIME_OUT:
            time_array = time.localtime(TIME_OUT - time_left)
            reciprocal = time.strftime("%M:%S", time_array)
            auth_log("==time_left:%s" % reciprocal)
            time.sleep(3)
            return status, msg + "  Please try again in %s time Or Please contact the administrator" % reciprocal
        else:
            update_login_history_file(user, host, data, is_del=True)

    return 0, ""


def pam_sm_authenticate(pamh, flags, argv):
    try:
        LOCK.acquire()
        try:
            user = pamh.get_user()
            host = getattr(pamh, "rhost")
        except pamh.exception as e:
            return e.pam_result
        auth_log("user: %s, host: %s" % (user, host))

        if user in USER_LIST:
            return pamh.PAM_SUCCESS
        # 获取当前登入的用户名, 并且记录(用户名以及登入的ip)
        login_history_json = get_login_history_file()

        update_login_history_file(user, host, login_history_json, is_error=True)

        status, error = check_user_ip_is_true(user, host, login_history_json)

        if status == -1:
            msg = pamh.Message(pamh.PAM_ERROR_MSG, error)
            pamh.conversation(msg)
            return pamh.PAM_AUTH_ERR

        # 禁止root用户登入
        if user == "root" and NOT_ROOT:
            return pamh.PAM_AUTH_ERR

        auth_log("=====start====")
        msg = pamh.Message(pamh.PAM_PROMPT_ECHO_OFF, "Enter Your PIN: ")
        resp = pamh.conversation(msg)
        auth_log("=====resp: %s====" % resp.resp)
        # PIN码校验成功, 清除登入主机用户错误次数
        if PIN_verify(resp.resp):
            update_login_history_file(user, host, login_history_json, is_del=True)
            return pamh.PAM_SUCCESS

        # PIN校验失败
        return pamh.PAM_AUTH_ERR
    except Exception as e:
        auth_log("====Accident ERROR: %s====" % e)
        msg = pamh.Message(pamh.PAM_PROMPT_ECHO_OFF, "Enter Administrator PIN: ")
        resp = pamh.conversation(msg)
        if resp.resp == ADMIN_PIN:
            return pamh.PAM_SUCCESS
    finally:
        auth_log("====end====")
        LOCK.release()

"""
#以下都是默认函数
"""


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

下面是实现的案例部署

  PS:

  在部署之前,请检查服务器系统时区是否和客户端同步,如果没有同步,请进行同步时区之后再进行部署

# 在部署包pam放到/root/,并进入pam目录,相关依赖包都在pam文件夹中


# 解压pam_install.tar.gz,并得到一个pam依赖包的repodata

tar xzvf pam_install.tar.gz

# 需要创建/etc/yum.repo.d/pam.repo 文件并配置一个yum源地址

[pam_install]

name=pam_install             

baseurl=file:///root/pam/pam_install    # rpm包路径

enabled=1                 

gpgcheck=0

 

#安装pam相关依赖

yum clean all

yum makecache

yum install pam pam-devel -y

 

# 安装pyotp模块

tar xzvf pyotp-2.3.0.tar.gz

cd pyotp-2.3.0

python setup.py install

cp -rf /usr/lib/python2.7/site-packages/pyotp-2.3.0-py2.7.egg/pyotp /usr/lib64/python2.7/

 

# 拷贝pam_python.so文件到/lib64/security/

cp pam_python.so  /lib64/security/

 

#将auth.py脚本放入/lib/security/

mkdir /lib/security

chmod 600 /lib/security

cp auth.py /lib/security/

 

#修改/etc/pam.d/sshd,在最前面新增一行,如下

auth       requisite    pam_python.so auth.py

 

 

#修改 /etc/ssh/sshd_config

ChallengeResponseAuthentication yes

 

# 重启ssh服务

systemctl restart sshd

 

# 查看ssh日志文件

tailf /var/log/secure

 

# 查看安全加固脚本日志

tailf /tmp/.pam/pam_python_auth.log

 

# 查看或修改所有访问连接错误的主机和用户的记录

cat /tmp/.pam/login_history.json

验证

下载Google Authenticator,配置用户和密钥,保存就会生成动态PIN

[root@IPCPU-11 ~]# ssh test@192.168.110.11

Enter Your PIN:
Password:
Last login: Mon Mar 21 00:04:37 2016 from 192.168.110.11

posted @ 2021-01-27 18:49  知无不言~  阅读(259)  评论(0编辑  收藏  举报