对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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本