蜜标 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,告警永不静音。前者保证触发率,后者保证你不会在真正入侵发生的那天,才发现通知早被关掉了。