Caddy 学习笔记 - 反向代理与 IP 封禁

文章目录

镜像版本:caddy:2.11.4h1:XKxkMTgNSizEvKG6QHue6cAsFOteU2qA61w2tKkCWi0=)。低于 2.7 的版本没有 trusted_proxies 全局选项,配置写法会不一样。

简介

Caddy 是一个基于 Go 编写的 HTTP/2、HTTP/3 web 服务器,反向代理、静态托管、TLS 自动化开箱即用。相比 Nginx,Caddy 提供静态二进制文件,零依赖启动,在国产化服务器上不用处理 glibcpcreopensslzlib 的版本适配问题。

功能:正向代理,反向代理,静态托管,IP 封禁。

正向代理:客户端代理,如 VPN。代理客户端。

反向代理:服务器端的代理,代理后端服务。

IP 封禁:基于真实客户端 IP 做黑白名单拦截。

下载与安装

Caddy 官网下载地址:https://caddy.com/download

GitHub Release 地址:https://github.com/caddyserver/caddy/releases

shell 复制代码
# 启动 Caddy
caddy run --config /etc/caddy/Caddyfile --adapter caddyfile

# 后台运行
caddy start --config /etc/caddy/Caddyfile --adapter caddyfile

# 停止 Caddy
caddy stop

# 重新加载配置(admin 未关闭时)
caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile

# 验证配置
caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile

# 格式化 Caddyfile
caddy fmt --overwrite /etc/caddy/Caddyfile

# 查看版本
caddy version

Caddyfile 配置文件结构

Caddyfile 采用就近配置的语法。外层大括号是全局选项,site block 是站点配置。

caddyfile 复制代码
{
    # 全局选项,影响所有站点
    auto_https off
    admin off
    servers {
        trusted_proxies static xx.xx.xx.xx/32
        client_ip_headers X-Forwarded-For X-Real-IP
    }
}

:8528 {
    # 站点配置
    bind 0.0.0.0

    route {
        handle /api/* {
            reverse_proxy 127.0.0.1:8899
        }
    }
}

CIDR 掩码与 trusted_proxies

trusted_proxies 后面跟的是 IP 范围,写法是 CIDR 记法。

复制代码
xx.xx.xx.xx/32   → 只有 xx.xx.xx.xx 这一个 IP
xx.xx.xx.0/24    → xx.xx.xx.0 ~ xx.xx.xx.255,整段 256 个 IP
xx.xx.0.0/16     → xx.xx.0.0 ~ xx.xx.255.255,整段 65536 个 IP
xx.xx.xx.xx      → 不写尾数,Caddy 当作 /32

/数字 是前缀长度,表示子网掩码中 1 的个数:

  • /32 = 255.255.255.255,只匹配 1 个 IP
  • /24 = 255.255.255.0,匹配 256 个 IP
  • /16 = 255.255.0.0,匹配 65536 个 IP
  • /0 = 0.0.0.0,匹配所有 IP
caddyfile 复制代码
servers {
    # 信任直连 Caddy 的上一层代理出口 IP
    trusted_proxies static xx.xx.xx.xx/32

    # 信任整段出口网段
    # trusted_proxies static xx.xx.xx.0/24

    # 信任所有私网段(生产慎用)
    # trusted_proxies static private_ranges

    # 解析真实 IP 的请求头,按顺序取第一个非空
    client_ip_headers X-Forwarded-For X-Real-IP
}

private_ranges 等于 192.168.0.0/16172.16.0.0/1210.0.0.0/8127.0.0.1/8fd00::/8::1

trusted_proxies 的判定逻辑:

  • 源 IP 在列表内 → 从 client_ip_headersclient_ip
  • 源 IP 不在 → client_ip = remote_ip(直连对端 IP)

IP 封禁失效最常见的根因就是 trusted_proxies 配错:来源不在白名单,Caddy 不使用 XFF,client_ip 永远是代理 IP,黑名单命中不到。

静态资源代理

静态页面配置

caddyfile 复制代码
handle_path /m/* {
    root * /var/www/site-m
    try_files {path} {path}/ /index.html
    file_server
}

说明:

  • handle_path 自动剥离 /m 前缀
  • root * 指定文件系统根目录
  • try_files {path} {path}/ /index.html 兜底,找不到文件时回退到 index.html,前端路由不 404
  • file_server 处理 mime 推断、range 请求

handle 与 handle_path

指令 前缀处理 用途
handle /prefix/* 不剥 反向代理、保留前缀的静态
handle_path /prefix/* 剥前缀 静态 SPA,upstream 期望根路径

保留前缀的静态资源:

caddyfile 复制代码
handle /fe/* {
    root * /var/www/site-fe
    try_files {path} {path}/ /index.html
    file_server
}

附件下载

caddyfile 复制代码
handle_path /files/* {
    root * /var/www/files
    file_server
    header Content-Disposition attachment
    header Content-Type application/octet-stream
}

Content-Disposition attachment 让浏览器作为下载处理,Content-Type application/octet-stream 作为兜底 mime,避免文本解析。

反向代理

Http 反向代理

由于使用反向代理,后端服务无法获取用户的真实 IP 地址,所以需要设置 header 信息。

caddyfile 复制代码
handle /api/* {
    uri replace /api /upstream-path
    reverse_proxy 127.0.0.1:8899 {
        header_up X-Real-IP {http.request.header.X-Real-IP}
        header_up X-Forwarded-For {http.request.header.X-Forwarded-For}
    }
}

说明:

  • uri replace /api /upstream-path/api 替换为 /upstream-path
  • reverse_proxy 转发到后端,默认追加 X-Forwarded-ForX-Forwarded-ProtoX-Forwarded-Host
  • header_up 透传客户端原头给后端

常用的 uri 处理指令

指令 作用
uri strip_prefix /prefix 剥前缀
uri strip_suffix .html 剥后缀
uri replace /old /new 替换路径片段
uri replace /old /new 5 限制替换次数

保留前缀的反向代理

caddyfile 复制代码
handle /upstream/* {
    reverse_proxy 127.0.0.1:8527
}

/upstream/users/1 透传给 127.0.0.1:8527/upstream/users/1

header_up 注意事项

不要把 {client_ip} 占位符塞进 header_up。原因:

  • reverse_proxy 自己按 trusted_proxies 清洗 XFF 链
  • 上游(Spring、Express、Go)用各自的 trust proxy 配置读真实 IP
  • 手工塞占位符反而污染代理链

健康检查与负载均衡

caddyfile 复制代码
reverse_proxy 127.0.0.1:8899 127.0.0.1:8898 {
    lb_policy round_robin
    lb_retries 2
    health_uri /health
    health_interval 10s
}

动静分离

  1. 为什么需要动静分离?

Tomcat 主要是用来处理 servlet 请求。处理 css、js、图片这些静态文件的 IO 性能不够好,将静态文件交给 Caddy 处理,可以提高系统的访问速度,减少后端请求次数,给后端服务器降压。

  1. Caddy 配置文件示例:
caddyfile 复制代码
:8528 {
    handle /api/* {
        reverse_proxy 127.0.0.1:8080
    }

    handle /static/* {
        root * /var/www/static
        file_server
    }

    handle / {
        root * /var/www/site
        try_files {path} {path}/ /index.html
        file_server
    }
}
  1. 匹配优先级

Caddy 同一 route 内从上到下匹配,更具体的路径放前面,通用兜底放后面。

caddyfile 复制代码
route {
    # 具体路径先匹配
    handle /files/zhlt_file/* {
        root * /var/www/zhlt_file
        file_server
    }

    # 通用兜底
    handle /files/* {
        root * /var/www/files
        file_server
    }
}

IP 封禁

封禁文件 blocked_ips.caddyfile

一行一条,CIDR 也直接写:

caddyfile 复制代码
client_ip xx.xx.xx.xx
client_ip xx.xx.xx.xx/24

单行多地址:

caddyfile 复制代码
client_ip xx.xx.xx.xx xx.xx.xx.xx xx.xx.xx.xx/24

Caddy 把同一 @blocked 命名匹配器内的多行 client_ip 合并为 OR,命中任一返回 403。

配置文件挂载

caddyfile 复制代码
route {
    @blocked {
        import blocked_ips.caddyfile
    }
    respond @blocked "Forbidden" 403

    # 业务 handle 写在后面
}

client_ip 匹配器 vs 直接比 XFF 头

错误写法(永远不命中):

caddyfile 复制代码
@blocked expression `{http.request.header.X-Forwarded-For} == "xx.xx.xx.xx"`

XFF 一般是 client, proxy1, proxy2 的逗号链,整串等值比较不可能命中。

正确写法:

caddyfile 复制代码
@blocked client_ip xx.xx.xx.xx
respond @blocked "Forbidden" 403

client_ip 匹配器对 Caddy 解析出的单值 IP 做等值或 CIDR 比较。

route 强制前置

Caddy 对指令有内置排序。respond 写在 route 外面、且放在 handle 后面,会被某个业务 handle 提前接管。

caddyfile 复制代码
route {
    @blocked { import blocked_ips.caddyfile }
    respond @blocked "Forbidden" 403
    # 业务 handle 写在后面
}

route 把整段指令按书写顺序锁定执行。封禁检查放最前,业务路径就不会漏过。

白名单

只放行内网:

caddyfile 复制代码
@denied not client_ip private_ranges
abort @denied

管理脚本 run_caddy.sh

Caddy 命令不好记,写了一个管理脚本,封装 start/stop/restart/reload/check/status 六个动作,并自带配置校验。脚本放在 Caddy 二进制同级目录。

bash 复制代码
#!/bin/bash

# Caddy 管理脚本
BASE_DIR="/home/soft/caddy"

CADDY_BIN="$BASE_DIR/caddy"
CADDY_CONFIG="$BASE_DIR/Caddyfile"
CADDY_PIDFILE="$BASE_DIR/caddy.pid"
CADDY_LOGFILE="$BASE_DIR/caddy.log"

# 确保日志和 PID 目录存在
mkdir -p "$(dirname "$CADDY_PIDFILE")" "$(dirname "$CADDY_LOGFILE")"

# 校验配置
check_config() {
    if ! $CADDY_BIN validate --config "$CADDY_CONFIG" >/dev/null 2>&1; then
        echo "ERROR: Caddy 配置文件语法错误!"
        $CADDY_BIN validate --config "$CADDY_CONFIG"
        exit 1
    else
        echo "INFO: Caddy 配置文件语法正确。"
    fi
}

# 获取当前 PID
get_pid() {
    if [ -f "$CADDY_PIDFILE" ]; then
        cat "$CADDY_PIDFILE"
    fi
}

# 检查是否运行
is_running() {
    local pid=$(get_pid)
    if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
        return 0
    else
        return 1
    fi
}

case "$1" in
    start)
        if is_running; then
            echo "WARNING: Caddy 已在运行 (PID: $(get_pid))。"
            exit 0
        fi
        check_config
        echo "INFO: 启动 Caddy..."
        nohup $CADDY_BIN run --config "$CADDY_CONFIG" --adapter caddyfile > "$CADDY_LOGFILE" 2>&1 &
        echo $! > "$CADDY_PIDFILE"
        echo "SUCCESS: Caddy 已启动 (PID: $!)"
        ;;

    stop)
        if ! is_running; then
            echo "WARNING: Caddy 未运行。"
            exit 0
        fi
        echo "INFO: 停止 Caddy..."
        kill $(get_pid)
        rm -f "$CADDY_PIDFILE"
        echo "SUCCESS: Caddy 已停止。"
        ;;

    restart)
        echo "INFO: 重启 Caddy..."
        $0 stop
        sleep 2
        $0 start
        ;;

    reload)
        if ! is_running; then
            echo "ERROR: Caddy 未运行,无法重载。"
            exit 1
        fi
        echo "INFO: 热重载 Caddy 配置..."
        check_config
        kill -USR1 $(get_pid)
        echo "SUCCESS: 配置已重载。"
        ;;

    check)
        check_config
        ;;

    status)
        if is_running; then
            echo "INFO: Caddy 正在运行 (PID: $(get_pid))"
        else
            echo "INFO: Caddy 未运行"
        fi
        ;;

    *)
        echo "用法: $0 {start|stop|restart|reload|check|status}"
        exit 1
        ;;
esac

使用方式:

bash 复制代码
# 启动
./run_caddy.sh start

# 停止
./run_caddy.sh stop

# 重启
./run_caddy.sh restart

# 热重载(admin off 时通过 USR1 信号)
./run_caddy.sh reload

# 校验配置语法
./run_caddy.sh check

# 查看运行状态
./run_caddy.sh status

更新黑名单后:

bash 复制代码
./run_caddy.sh check && ./run_caddy.sh reload

脚本说明:

  • BASE_DIR 为 Caddy 二进制所在目录,按实际部署路径修改
  • startnohup 后台运行,PID 写入 caddy.pid,日志输出到 caddy.log
  • reload 通过 kill -USR1 发信号,Caddy 收到 USR1 会重新加载配置
  • check 在 start/reload 前自动调用,配置语法错误直接退出,不会启动失败进程

日志

caddyfile 复制代码
log {
    output file logs/access.log {
        roll_size 100mb
        roll_keep 10
        roll_keep_for 720h
    }
    format json
}
  • roll_size 单文件超过 100MB 触发切割
  • roll_keep 最多保留 10 个历史文件
  • roll_keep_for 720h 30 天后强制清理
  • format json 方便 ELK / Loki 收

控制台日志全局配置:

caddyfile 复制代码
log {
    output stdout
    level DEBUG
}

access.log 字段说明

json 复制代码
{
  "request": {
    "remote_ip": "yy.yy.yy.yy",
    "client_ip": "xx.xx.xx.xx",
    "method": "GET",
    "uri": "/"
  }
}

remote_ip 是直连 Caddy 的对端,client_ip 是经 trusted_proxies 解出的真实 IP。封禁判定的对象始终是 client_ip

完整 Caddyfile

caddyfile 复制代码
{
    auto_https off
    admin off
    servers {
        trusted_proxies static xx.xx.xx.xx/32
        client_ip_headers X-Forwarded-For X-Real-IP
        listener_wrappers {
            proxy_protocol
        }
    }
    log {
        output stdout
        level DEBUG
    }
}

:8528 {
    bind 0.0.0.0

    route {
        @blocked {
            import blocked_ips.caddyfile
        }
        respond @blocked "Forbidden" 403

        handle /api/v2/* {
            reverse_proxy 127.0.0.1:9680
        }

        handle /api/* {
            uri replace /api /upstream-path
            reverse_proxy 127.0.0.1:8899 {
                header_up X-Real-IP {http.request.header.X-Real-IP}
                header_up X-Forwarded-For {http.request.header.X-Forwarded-For}
            }
        }

        handle /upstream/* {
            reverse_proxy 127.0.0.1:8527
        }

        handle_path /m/* {
            root * /var/www/site-m
            try_files {path} {path}/ /index.html
            file_server
        }

        handle_path /s/* {
            root * /var/www/site-s
            try_files {path} {path}/ /index.html
            file_server
        }

        handle /fe/* {
            root * /var/www/site-fe
            try_files {path} {path}/ /index.html
            file_server
        }

        handle_path /files/* {
            root * /var/www/files
            file_server
            header Content-Disposition attachment
            header Content-Type application/octet-stream
        }
    }

    log {
        output file logs/access.log {
            roll_size 100mb
            roll_keep 10
            roll_keep_for 720h
        }
        format json
    }
}

验证

bash 复制代码
# 验证配置
caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile

# docker 验证,不污染本机
docker run --rm -v "$(pwd):/etc/caddy:ro" caddy:2.11.4 \
  caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile

# 模拟请求,看封禁是否生效
curl -v -H "X-Forwarded-For: xx.xx.xx.xx" http://127.0.0.1:8528/

Empty reply from server → 命中 abort / 403,配置没漏。

返回 200 → 配置没生效,查 trusted_proxies / client_ip_headers / client_ip 匹配器。

常见问题

现象 原因 修法
真实 IP 永远是代理 IP trusted_proxies 没配或填错 把直连 Caddy 的代理出口 IP 加进去
X-Real-IP == xx.xx.xx.xx 不命中 直接比原始头,XFF 是逗号链 改用 client_ip 匹配器
/ 能拦,/api/* 漏过 respond 在 route 外被 handle 接管 把封禁包进 route 钉在最前
duplicate listener 启动失败 servers {} 在多个 import 文件里重复定义 导入文件不要再写 servers
客户端 TLS / 握手失败 listener_wrappers { proxy_protocol } 开了但上游没发 PROXY 头 跟上游对齐,或临时关 wrapper

注意事项

  1. trusted_proxies 范围要小,不要塞 0.0.0.0/0
  2. 黑名单走 client_ip 匹配器,不要直接比 XFF 头
  3. 封禁与业务 handle 包进同一个 route,封禁写最前
  4. admin off 之后,更新配置只能 systemctl restart caddy
  5. 不要将 root 目录配置成 //root
  6. 真实 IP、内网域名、路径前缀不进文档、备份、仓库
bash 复制代码
# 落盘前 sed 替换真实 IP 为占位符
sed -i -E 's/([0-9]{1,3}\.){3}[0-9]{1,3}/xx.xx.xx.xx/g' blocked_ips.caddyfile
相关推荐
谢白羽1 小时前
SGLang的AWQ量化笔记
笔记·python·sglang
噜~噜~噜~2 小时前
操作系统笔记-2.3.5.2 读者-写者问题
笔记·操作系统
m4Rk_2 小时前
【论文阅读】Agent 记忆机制(57):MemSearch-o1——从查询词元生长证据,重组 Deep Search 记忆路径
论文阅读·人工智能·学习·开源·github
闭月之泪舞3 小时前
C++编程学习
c++·学习
MindUp3 小时前
企业私有化文件管理系统选型实录:从部署架构到AI能力的技术调研笔记
人工智能·笔记·架构
2401_862880823 小时前
Linux应用层开发 --- UDP&TCP
linux·运维·tcp/ip·udp
GGMM7893 小时前
西门子BOP20基本操作面板超详细教程(S120/G130专用)
笔记·变频器·变频器维修
网安蟹佬霸4 小时前
2026网络安全每日学习计划:每天学多久?学什么?怎么安排?照着做就行!
网络·学习·安全·web安全·学习路线·网安
一条破秋裤4 小时前
TIM 输入捕获与 PWMI:从边沿到频率、占空比
笔记·stm32·学习