蜜标 Canary Token 实战:入侵告警配置

蜜标 Canary Token 是一种「假资产 + 真告警」的入侵检测手段:在服务器、代码仓库、内网共享目录里投放一批看起来有价值、实际无人使用的诱饵——假备份文件、假 .env 配置、假 AK/SK、假数据库连接串——正常业务和监控探针永远不会碰它们,一旦被读取就说明有攻击者已经拿到了访问权限。本文给出 Linux + Nginx 环境下从零部署蜜标、把触发事件汇成结构化日志并接入 webhook 告警的完整配置,全部命令可直接复制执行。

与动辄需要独立网段和独立虚拟机的蜜罐不同,蜜标的部署成本几乎为零:几个文件、一段 Nginx location、一个常驻脚本。它填补的是「边界被突破之后」这段盲区——WAF 看的是入口流量,蜜标看的是内网横向移动。

适用场景

  • 已上线但没有任何内网横向移动检测能力的 Web 业务,尤其是共享主机、多站点同机部署的环境。
  • 代码仓库或运维文档曾经泄露,需要监测泄露内容被二次利用的情况。
  • 等保 2.0「入侵防范」条款中对异常访问检测的要求,需要一个可落地、可留证的补充手段。
  • 中小团队没有预算上 NDR 或 SOC 平台,用零成本诱饵把「有没有人进来」这个最基础的问题回答掉。
  • 外包运维、临时开发人员交接期间,需要确认对方没有越界访问生产目录。

前置条件

  • Linux 内核 2.6.13 以上(inotify 支持),inotify-tools 已安装。
  • Nginx 1.18 以上,具备修改 conf.d 并 reload 的权限。
  • Python 3.8 以上,告警聚合脚本只用标准库,无需 pip 安装任何依赖。
  • 一个可接收 HTTP POST 的告警地址:企业微信机器人、钉钉机器人或自建接口均可。
  • 与业务不冲突的诱饵路径前缀,例如 /backup/、/db/、/old/。
  • 访问日志留存不少于 90 天,蜜标日志必须单独存放,避免被 logrotate 一并清掉。

原理说明

蜜标按形态分三类,触发点各不相同:

  • 路径型:一个只要被访问就该报警的 URL,例如 /backup/db.sql。靠 Nginx access_log 捕获,最灵敏,也最容易被扫描器刷到。
  • 文件型:磁盘上的假文件,靠 inotify 监听 open、attrib 事件捕获,能发现已经拿到 shell 的攻击者在翻目录。
  • 凭证型:假的云 AK/SK、假数据库密码,攻击者拿到后必然尝试调用,调用请求会落到我们自己的探测端点上。

三类蜜标共用一条判定原则:任何一次访问都视为异常,不做白名单降噪。这一点和业务告警完全相反——业务告警追求低误报,蜜标追求零漏报,宁可让运维多看几条通知,也不能让真实入侵淹没在过滤规则里。

告警链路是「采集 → 归一 → 去重 → 推送」四段。采集靠 Nginx 日志和 inotify;归一成统一 JSON,字段固定为时间、文件、事件、主机、来源 IP;去重按「文件 + 5 分钟窗口」合并,避免脚本一次遍历就打出几十条;推送走 webhook。整条链路只用系统自带工具搭建,不依赖任何商业组件,也不会因为某个 SaaS 到期而失效。

需要明确的是蜜标的能力边界:它只能回答「有人碰了不该碰的东西」,不能回答「他是怎么进来的」。因此蜜标必须和入口侧的日志(WAF、Nginx 全量日志、SSH 登录记录)配合使用,前者提供确凿的失陷信号,后者提供入侵路径。

操作步骤

步骤 1:规划诱饵目录与文件内容

诱饵文件名必须像真的,否则攻击者连看都不看。四类高价值命名:数据库导出(db_backup_2026.sql)、环境变量(.env.bak)、历史私钥(id_rsa_old)、生产配置(config.prod.yaml)。

sudo mkdir -p /srv/canary/files /srv/canary/log
sudo chown -R www-data:www-data /srv/canary
sudo chmod 755 /srv/canary /srv/canary/files

# 批量生成诱饵文件,内容为可识别的假凭证
for f in .env.bak db_backup_2026.sql id_rsa_old config.prod.yaml backup.tar.gz; do
  printf 'CANARY-TOKEN: %s\nAWS_AK: AKIAIOSFODNN7CANARY\nDB_PASS: Fake#2026\n' \
    "$(cat /proc/sys/kernel/random/uuid)" | sudo tee "/srv/canary/files/$f" >/dev/null
done

sudo ls -la /srv/canary/files

文件权限用 644 而不是 600。644 保证低权限的 Web 进程也能读到,攻击者用 www-data 身份翻目录就会触发;设成 600 反而会因为读取失败而漏报。诱饵内容全部是假数据,本身不需要保密。

步骤 2:Nginx 暴露诱饵路径并写独立日志

# /etc/nginx/conf.d/canary.conf
log_format canary '$remote_addr - [$time_local] "$request" $status '
                  'ua="$http_user_agent" ref="$http_referer" '
                  'xff="$http_x_forwarded_for" host="$host"';

server {
    listen 80 default_server;
    server_name _;

    # 诱饵路径:返回 200 与假内容,绝不返回 404
    location ~ ^/(backup|db|old|\.env|\.git|\.svn|phpmyadmin) {
        access_log /var/log/nginx/canary.log canary;
        root /srv/canary/files;
        try_files $uri /db_backup_2026.sql =200;
        add_header X-Robots-Tag "noindex, nofollow" always;
    }

    location = /healthz {
        access_log off;
        return 200 "ok";
    }
}

关键点:诱饵必须返回 200,不能返回 404。主流扫描器遇到 404 会直接跳过,遇到 200 会继续深挖同目录,实测触发率相差十倍以上。同时给响应加 X-Robots-Tag: noindex,避免被搜索引擎收录后引来正常流量,把告警彻底刷废。

sudo nginx -t && sudo systemctl reload nginx

步骤 3:文件型蜜标的实时监听

sudo tee /usr/local/bin/canary-watch.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
DIR=/srv/canary/files
LOG=/srv/canary/log/trigger.jsonl
WEBHOOK="${CANARY_WEBHOOK:?need CANARY_WEBHOOK env}"

inotifywait -m -r -e open,attrib,modify --format '%w%f|%e|%T' \
  --timefmt '%Y-%m-%dT%H:%M:%S%z' "$DIR" |
while IFS='|' read -r path ev ts; do
  printf '{"ts":"%s","file":"%s","event":"%s","host":"%s"}\n' \
    "$ts" "$path" "$ev" "$(hostname -f)" | tee -a "$LOG"
  curl -s -m 5 -H 'Content-Type: application/json' \
    -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"[蜜标触发] ${path} ${ev} @ $(hostname -f)\"}}" \
    "$WEBHOOK" >/dev/null || true
done
EOF
sudo chmod 755 /usr/local/bin/canary-watch.sh

事件类型只订阅 open、attrib、modify。如果同时订阅 access,一次 cat 会打出三条记录,告警噪音翻倍,反而拖慢响应速度——告警系统的可信度来自信噪比,不来自条数。

步骤 4:告警聚合与去重

直接在 shell 里推送,同一文件被反复读取时会刷屏。用一段 Python 做 5 分钟窗口合并,顺便补齐主机名与时间字段,让告警内容可以直接粘进工单。

sudo tee /usr/local/bin/canary-aggregate.py >/dev/null <<'EOF'
#!/usr/bin/env python3
import json, os, time, urllib.request

LOG = "/srv/canary/log/trigger.jsonl"
HOOK = os.environ["CANARY_WEBHOOK"]
seen = {}
WINDOW = 300  # 5 分钟去重窗口

def push(text):
    body = json.dumps({"msgtype": "text", "text": {"content": text}}).encode()
    req = urllib.request.Request(HOOK, data=body,
                                 headers={"Content-Type": "application/json"})
    try:
        urllib.request.urlopen(req, timeout=5).read()
    except Exception as e:
        print("push fail:", e, flush=True)

with open(LOG, "r", encoding="utf-8") as f:
    f.seek(0, 2)
    while True:
        line = f.readline()
        if not line:
            time.sleep(1)
            continue
        try:
            rec = json.loads(line)
        except json.JSONDecodeError:
            continue
        key = rec.get("file", "?")
        now = time.time()
        if now - seen.get(key, 0) < WINDOW:
            continue
        seen[key] = now
        push("[蜜标触发] %s | %s | %s | %s"
             % (rec["file"], rec["event"], rec["host"], rec["ts"]))
EOF
sudo chmod 755 /usr/local/bin/canary-aggregate.py

步骤 5:systemd 常驻与开机自启

sudo tee /etc/systemd/system/canary-watch.service >/dev/null <<'EOF'
[Unit]
Description=Canary token file watcher
After=network.target

[Service]
Type=simple
Environment=CANARY_WEBHOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY
ExecStart=/usr/local/bin/canary-watch.sh
Restart=always
RestartSec=5
User=root
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/srv/canary/log

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now canary-watch
sudo systemctl status canary-watch --no-pager

服务里显式写了 ProtectSystem=strict 和 ReadWritePaths=/srv/canary/log,只放开日志目录的写权限。这样即使监听脚本本身存在命令注入缺陷,攻击者也无法借此改写系统文件。

配置验证

四步验证,缺一不可。任何一步没输出预期结果,后面的告警都不可信。

# 1. 路径型蜜标是否返回 200
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/backup/db.sql
# 期望输出 200

# 2. 独立日志是否落盘(应看到刚才的请求与 UA)
sudo tail -n 2 /var/log/nginx/canary.log

# 3. 文件型蜜标是否触发
sudo cat /srv/canary/files/.env.bak >/dev/null
sleep 3
sudo tail -n 2 /srv/canary/log/trigger.jsonl

# 4. 常驻服务与 webhook 是否可用
systemctl is-active canary-watch
journalctl -u canary-watch -n 20 --no-pager

第 3 步如果 trigger.jsonl 为空,先确认 inotifywait 是以 root 运行的——普通账号虽然能读到 644 的诱饵文件,但 inotify 的递归监听在某些发行版上对无权限子目录会静默跳过。第 4 步若 webhook 推送失败,服务里的 || true 会让它继续运行,只写日志不中断采集,这是刻意设计:采集链路比推送链路重要,推送断了可以补,事件丢了就补不回来。

常见问题

FAQ 1:蜜标被搜索引擎或安全扫描器误触发怎么办?

分两层处理。源头抑制:给诱饵路径加 X-Robots-Tag: noindex 并在 robots.txt 里 Disallow,避免正常收录。告警侧降级:对已知扫描器网段只记 JSON、不发 webhook。切忌把扫描器 IP 加进业务路径的白名单——那等于把诱饵的边界拆了,攻击者只要借道同一网段就能静默通过。

FAQ 2:蜜标触发了,第一步该做什么?

按顺序确认三件事。第一,来源 IP 是否内网:内网地址意味着主机已失陷,优先查横向移动与持久化,而不是急着封 IP。第二,请求 UA 是否是 curl、python、wget:脚本化说明是自动化工具而非人工误点。第三,同一 IP 是否在 5 分钟内访问了多个诱饵:多点命中即为目录扫描。三步定级之后,再决定封禁、抓包还是直接隔离主机。

FAQ 3:蜜标和蜜罐(Cowrie 之类)能互相替代吗?

不能,两者覆盖的阶段不同。蜜罐模拟完整服务诱捕交互,能拿到攻击者的完整命令序列,但需要独立端口、独立实例,成本高、数量少,通常只部署在边界。蜜标是单个假对象,可以撒几十上百个,成本几乎为零。合理做法是边界放少量蜜罐做深度捕获,内网各主机撒蜜标做广度覆盖,两者是纵深关系而非替代关系。

FAQ 4:诱饵文件被勒索软件加密了,算不算触发?

算,而且是最高优先级。步骤 3 里订阅 modify 事件就是为了这个场景:勒索软件遍历目录时会改写诱饵文件,触发事件即代表加密行为已经开始扩散。此时应立即断网隔离而不是尝试清理,同时确认备份仓库是否开启了只追加模式——这一步决定了后续能不能恢复。

总结

蜜标的价值不在技术复杂度,而在于把「有没有人进来」这个问题从主观判断变成一条可验证的告警。整套方案落地成本是三个文件、一段 Nginx 配置、一个常驻服务,半小时内可以完成。落地之后要守两条纪律:诱饵路径永不返回 404,告警永不静音。前者保证触发率,后者保证你不会在真正入侵发生的那天,才发现通知早被关掉了。