用 Anubis 保护网站免受恶意访问,以 GitLab 为例

git.nju.edu.cn 从2015年11月开始得到了 AI bot 的热爱,CPU 根本扛不住,临时限制了校外的网络带宽,但这导致校外慢的几乎无法使用。所以趁着元旦,用 Anubis 对网站进行保护,把 bot 拒之门外。

反向代理模式

Anubis 默认以反向代理的模式运行,所有的流量都经过 Anubis ,Anubis 再将流量反代给后端服务。这种情况下前面还要有一个 HTTPd 如 nginx 实现 TLS 终结再将流量反向代理给 Anubis。在这种情况下怎么将客户端的真实IP传递给最后的应用尤为关键。

当正常访问者的IP有可能为 RFC 1918 私网IP,如校园网环境,校园网内用户使用私网IP访问校园网内服务器是非常正常的行为。但是当这种情况下,Anubis 的处理就有点不尽如人意了。

Anubis 使用 X-Real-IP 和 X-Forwarded-For 处理客户端真实IP,与之相关的环境变量有两个 CUSTOM_REAL_IP_HEADER 和 XFF_STRIP_PRIVATE。

Anubis 会对 X-Forwarded-For 进行处理

  • 当 XFF_STRIP_PRIVATE=true (默认值)时从右向左扫描将第一个公网IP配置为新的 X-Forwarded-For ,如果所有的客户端都是公网 IP 这没有问题,但是如果客户端是公网和私网 IP 混合的模式就是麻烦了。对于私网 IP 的客户端,Anubis 的日志中 X-Forwarded-For 就变空了,如果后端服务使用 X-Forwarded-For 识别客户端真实IP( GitLab 中配置 nginx[‘real_ip_header’] = ‘X-Forwarded-For’ ),结果后端记录的都是前端 nginx 的 IP,这里的 nginx IP 也是私网IP。
  • 当 XFF_STRIP_PRIVATE=false 时从右向左扫描将第一个公网或私网IP配置为新的 X-Forwarded-For ,结果 Anubis 的日志中 X-Forwarded-For 均为前端 nginx 的IP,后端记录的也都是前端 nginx 的 IP。

Anubis 要求前端反向代理将客户端真实IP放在 X-Real-IP 中,如果请求头名字不是这个可以用 CUSTOM_REAL_IP_HEADER 来配置,但是必须有否则会报错。如果后端服务使用 X-Real-IP 识别客户端真实IP( GitLab 中配置 nginx[‘real_ip_header’] = ‘X-Real-IP’ ),结果后端记录的都是 Anubis 的 IP,估计是反代给后端的时候就没有 X-Real-IP ,但在 Anubis 的日志中是正常的,具体我没有探究,在群友的提醒下马上改用 sidecar 模式了。

当然也不是没有解决办法,可以过两次 nginx 。第一次 nginx 把 X-Real-IP 和 X-Forwarded-For 用其它名字的请求头再保存一次,然后将流量反代给 Anubis 。Anubis处理完后将流量反代给 nginx,第二次 nginx 用第一次保存的其它名字做恢复,然后将流量反代给后端服务。配置看起来像下面这样,还是赶快用边车模式吧。

#第一层nginx
  listen 443 ssl;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Real-IP-Frontend $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-For-Frontend $proxy_add_x_forwarded_for;
  proxy_pass http://anubis:8923;
#anubis
  BIND=":8923"
  TARGET=: "http://nginx:3923"
#第二层nginx
  listen 3923;
  proxy_set_header X-Real-IP $http_x_real_ip_frontend;
  proxy_set_header X-Forwarded-For $http_x_forwarded_for_frontend;
  proxy_pass http://gitlab:80;

不太理解 Anubis 为什么要去这样修改 X-Real-IP 和 X-Forwarded-For,正常的反向代理应该从网络层获取客户端IP然后添加到 X-Forwarded-For 后面。

sidecar 模式

对所有的请求,nginx 都会内部发起子请求 auth_request 到 /.within.website/x/cmd/anubis/api/check ,如果认证服务(anubis)返回 200,则请求继续反代给 gitlab 。如果认证服务(anubis)返回 401,则返回客户端 307 重定向到 /.within.website/?redir=……,引导客户端通过 anubis 验证,成功后再跳回原来的链接。

nginx 配置片段

  • /.within.websit 是需要反代给 Anubis 的内容,不包括请求体,只包括请求头,通过请求头传递客户端IP
  • @redirectToAnubis 是给客户端的重定向,让客户端通过 Anubis 验证
  • / 是主入口,auth_request 把所有的请求先给 /.within.website/x/cmd/anubis/api/check ,如果返回200则继续,如果返回401则转给上面的@redirectToAnubis
  • /gitlab-lfs/objects/ 是 LFS 路径,在子请求模式下会因为卡上传而出错
    location /.within.website/ {
        proxy_pass http://anubis:8923;
	proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_pass_request_body off;
        proxy_set_header content-length "";
        auth_request off;
    }
    location @redirectToAnubis {
        return 307 /.within.website/?redir=$scheme://$host$request_uri;
        auth_request off;
    }
    location / {
        auth_request /.within.website/x/cmd/anubis/api/check;
        error_page 401 = @redirectToAnubis;
        include conf.d/proxy_git;
    }
    location ~ /gitlab-lfs/objects/ {
	auth_request off;
        include conf.d/proxy_git;
    }

其中 proxy_git 的内容

        proxy_connect_timeout 600;
        proxy_send_timeout 600;
        proxy_read_timeout 600;
        proxy_buffering off;
        proxy_request_buffering off;
        client_max_body_size 0;
        proxy_redirect off;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
	proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Ssl on;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_pass http://gitlab:80;

anubis 策略配置片段

  • 为了能够让 nginx 区别被拦截的请求,需要对 anubis 的拒绝策略更改 HTTP 状态码为403
  • 对于生产环境建议持久化数据,而不是使用内存
status_codes:
  CHALLENGE: 200
  DENY: 403
store:
  backend: bbolt
  parameters:
    path: /data/bbolt/anubis.bdb

anubis 容器配置片段

  • 挂载配置文件和持久化目录,注意持久化目录对于1000:1000要可写
  • BIND 为接受请求的端口,在 sidecar 模式下 TARGET 为空格
  • 持久化存储需要固定一个64个字符的16进制私钥(可用 openssl rand -hex 32 生成)
  • Anubis 输出的日志是标准的 ndjson
    volumes:
      - ./anubis/policy.yaml:/data/cfg/botPolicy.yaml:ro
      - ./anubis/bbolt:/data/bbolt:rw
    environment:
      BIND: ":8923"
      TARGET: " "
      COOKIE_DOMAIN: "git.nju.edu.cn"
      CUSTOM_REAL_IP_HEADER: "X-Real-IP"
      ED25519_PRIVATE_KEY_HEX: "123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234"
      POLICY_FNAME: "/data/cfg/botPolicy.yaml"
    labels:
      - co.elastic.logs/json.target=""
      - co.elastic.logs/json.add_error_key=true

以上的 nginx 和 GitLab 均在容器中运行,anubis 容器镜像为 ghcr.io/techarohq/anubis:v1.24.0

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理