在常规的 SSH 登录通知方案中,我们常使用 .bashrc/etc/ssh/sshrc。但在实际生产环境中,这些方案往往会因为 VS Code Remote、SFTP 或多通道连接导致通知瞬间“刷屏”。

为了实现单次会话仅通知一次的精准预警,我们改用 Linux 系统底层的 PAM 模块 进行重构。

一、 重构思路

PAM 方案的逻辑在于:当 sshd 成功验证用户身份并准备开启会话(Session)时,通过 pam_exec.so 调用我们预设的脚本。

  • 触发位置:系统级会话开启时,不依赖用户使用哪种 Shell。
  • 防刷逻辑:PAM 会话逻辑保证了在一个完整的 SSH 连接周期内,通知脚本仅被触发一次。
  • 数据来源:直接调用 PAM 环境变量(如 $PAM_RHOST),获取数据更直接。

二、 落地步骤

1. 编写核心通知脚本

/usr/local/bin/ 目录下创建通知脚本,这种方式可以避开用户家目录权限问题。

Bash

sudo nano /usr/local/bin/ssh_feishu_notify.sh

粘贴以下代码:

Bash

#!/bin/bash

# 只有在开启会话(open_session)时才执行发送逻辑
if [ "$PAM_TYPE" = "open_session" ]; then
  
    # 1. 提取基础信息
    LOGIN_IP=$PAM_RHOST
    LOGIN_USER=$PAM_USER
    HOST_NAME=$(uname -n)
  
    # 2. 修正时区:确保在任何系统设置下,通知均显示北京时间
    FULL_DATE=$(TZ='Asia/Shanghai' date +"%Y-%m-%d %H:%M:%S")
    LOGIN_TIME=$(TZ='Asia/Shanghai' date +"%H:%M:%S")

    # 3. 登录方式识别 (解析 Ubuntu/Debian 常见的 auth.log)
    LAST_AUTH=$(grep "Accepted " /var/log/auth.log 2>/dev/null | grep "$LOGIN_IP" | tail -n 1)
  
    if echo "$LAST_AUTH" | grep -q "publickey"; then
        AUTH_TYPE="SSH 密钥登录 (Key)"
    elif echo "$LAST_AUTH" | grep -q "password"; then
        AUTH_TYPE="账号密码登录 (Password)"
    else
        AUTH_TYPE="SSH 登录 (Session)"
    fi

    # 4. 构建飞书交互式卡片 JSON (符合 Schema 2.0 规范)
    JSON_DATA=$(cat <<EOF
{
  "msg_type": "interactive",
  "card": {
    "schema": "2.0",
    "body": {
      "direction": "vertical",
      "elements": [
        { "tag": "markdown", "content": "${FULL_DATE}" },
        { "tag": "markdown", "content": "**登录成功来源**" },
        { "tag": "markdown", "content": "时间:${LOGIN_TIME}\n设备 IP:${LOGIN_IP}\n登录用户:${LOGIN_USER}\n登录方式:${AUTH_TYPE}" },
        { "tag": "hr" },
        { "tag": "markdown", "content": "<font color='grey'>来自 ${HOST_NAME} (PAM Service)</font>" }
      ]
    },
    "header": {
      "title": { "tag": "plain_text", "content": "【${HOST_NAME}】${LOGIN_IP} 通过 SSH 登录了服务器" },
      "template": "blue"
    }
  }
}
EOF
)

    # 5. 异步推送 Webhook (防止网络延迟阻塞登录速度)
    curl -s -X POST -H "Content-Type: application/json" \
         -d "$JSON_DATA" \
         "https://open.feishu.cn/open-apis/bot/v2/hook/你的机器人密钥" > /dev/null 2>&1 &
fi

2. 配置权限

确保脚本具有可执行权限,否则 PAM 无法调用:

Bash

sudo chmod +x /usr/local/bin/ssh_feishu_notify.sh

3. 挂载到 PAM 配置

编辑 SSH 的 PAM 配置文件:

Bash

sudo nano /etc/pam.d/sshd

在文件末尾添加一行(注意路径准确):

Plaintext

session optional pam_exec.so /usr/local/bin/ssh_feishu_notify.sh

三、 技术要点复盘

1. 为什么不用环境变量锁?

之前的方案尝试在内存中设置环境变量来防抖,但现代 IDE(如 VS Code)会开启完全隔离的会话进程。PAM 方案通过 optional 模式挂载,仅在 sshd 建立主会话时由 root 触发一次,从根本上解决了进程级重复的问题。

2. 时区修正

脚本中显式声明 TZ='Asia/Shanghai' 是最稳妥的做法。很多云服务器或路由器固件(如 iStoreOS)默认使用 UTC,如果不加声明,收到的通知会有 8 小时的偏差。

3. 静默处理

curl 命令末尾添加 > /dev/null 2>&1 & 是性能优化的关键。它将推送任务丢入后台异步执行,即便飞书服务器响应慢,也不会导致你在 SSH 输入完密码后卡顿。


四、 结语

通过 PAM 模块进行的这次重构,我们将原本琐碎、不稳定的登录报警逻辑,提升到了系统服务级别。这不仅让通知更准、更稳,也让服务器的运维管理变得更加专业和优雅。