traefik 可以帮你:

  1. 自动续证书
  2. 反代(自动处理 ws、tls 证书、反代地址、压缩 etc.)
  3. 使用插件代替 waf
  4. 负载均衡(可以使用多个后端、k8s)
  5. 支持 mTLS 认证(如果你使用的是 Cloudflare 的 CDN)

前言

本文是对traefik的一个简单介绍,主要介绍如何使用 traefik 来代替 waf(使用 CrowdSec)、nginx、acme.sh、mtls-auth 等。

traefik会是你服务器的入口,所有的请求都会先经过 traefik,然后再转发到后端服务。

后端服务也不用配置绑定的端口和ip了(例如docker.port: 127.0.0.1:8080:8080,然后nginx反代,每个服务都要记住端口很麻烦),traefik 会自动处理。

info

主要贴出配置,注意多看注释

IMPORTANT

CrowdSec 的工作原理之后会说

info

简单说一下 mtls 的好处

  1. 如果其他人通过你的域名扫描全网 ip,没有 mtls 很容易就扫到你的服务器 ip 了,然后被定点攻击
  2. 如果你使用了 mtls 认证,其他人就算扫到你的 ip 也无法访问你的服务,除非他有 cf 的客户端证书,这个只有 cf 有私钥,无法伪造

traefik 配置

此项配置了之后可以

  1. 自动帮你续证书
  2. 如果你用的是 cf 的 cdn,还可以自动 mtls 认证。
  3. 使用 CrowdSec 作为 WAF(之后再配置 CrowdSec)
  4. 扫 443 和 80 端口的请求直接丢弃
配置结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
traefik/
├── acme.json
├── docker-compose.yml
├── cf-cert/
│ └── cloudflare-ca.pem
├── dynamic_conf/
│ ├── ban.html
│ ├── cloudflare-mtls.yml
│ ├── compressor.yml
│ ├── crowdsec-middleware.yml
│ └── drop-ip-access.yml
└── logs/
└── traefik.log

ERROR

ban.html 我就不贴了,需要的自己 ai 写一个

IMPORTANT

  1. 如果要自动帮你续证书,记得acme.json的权限要设置为600,否则 traefik 无法写入证书。
  2. 如果你使用的是 cf 的 cdn,记得在添加你的 cf 证书。到这里下载证书。
  3. 记得事先创建traefik-net网络,或者在docker-compose.yml中注释掉网络部分。
docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
services:
traefik:
image: traefik:latest
container_name: traefik
restart: unless-stopped


command:
- "--entrypoints.websecure.http.middlewares=crowdsec-bouncer@file,global-compressor@file"

- "--experimental.plugins.crowdsec-bouncer-traefik-plugin.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.crowdsec-bouncer-traefik-plugin.version=v1.4.4"






- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"


- "--entrypoints.web.forwardedheaders.trustedips=173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22"


- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=traefik-net"



- "--log.level=DEBUG"
- "--log.filePath=/var/log/traefik.log"



- "--providers.file=true"
- "--providers.file.directory=/etc/traefik/dynamic_conf"
- "--providers.file.watch=true"

- "--accesslog=true"
- "--accesslog.format=json"



- "--certificatesresolvers.myresolver.acme.httpchallenge=true"
- "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.myresolver.acme.email=xxx"
- "--certificatesresolvers.myresolver.acme.storage=/etc/traefik/acme.json"






ports:
- "80:80"
- "443:443"



volumes:

- "/var/run/docker.sock:/var/run/docker.sock:ro"

- "./acme.json:/etc/traefik/acme.json"

- "./dynamic_conf:/etc/traefik/dynamic_conf:ro"

- "./logs:/var/log:rw"
- "./cf-cert:/etc/traefik/cf-cert:ro"


networks:
- traefik-net

networks:
traefik-net:
name: traefik-net
external: true
cloudflare-mtls.yml
1
2
3
4
5
6
7
8
tls:
options:
cloudflare-mtls:
minVersion: VersionTLS12
clientAuth:
caFiles: - /etc/traefik/cf-cert/cloudflare-ca.pem
clientAuthType: "RequireAndVerifyClientCert"

compressor.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

http:
middlewares:

global-compressor:
compress:



excludedContentTypes:
- "image/png"
- "image/jpeg"
- "image/gif"
- "application/pdf"








minResponseBodyBytes: 1024
crowdsec-middleware.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
http:
middlewares:
crowdsec-bouncer:
plugin:
crowdsec-bouncer-traefik-plugin:
CrowdsecLapiKey: ""
Enabled: "true"
crowdsecMode: "stream"

banHTMLFilePath: "/etc/traefik/dynamic_conf/ban.html"
forwardedHeadersCustomName: "CF-Connecting-IP"
forwardedHeadersTrustedIPs:

- "173.245.48.0/20"
- "103.21.244.0/22"
- "103.22.200.0/22"
- "103.31.4.0/22"
- "141.101.64.0/18"
- "108.162.192.0/18"
- "190.93.240.0/20"
- "188.114.96.0/20"
- "197.234.240.0/22"
- "198.41.128.0/17"
- "162.158.0.0/15"
- "104.16.0.0/13"
- "104.24.0.0/14"
- "172.64.0.0/13"
- "131.0.72.0/22"

- "10.0.10.23/32"
- "10.0.20.0/24"

clientTrustedIPs:
- "192.168.0.0/16"
- "10.0.0.0/8"
- "172.16.0.0/12"


drop-ip-access.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
http:
routers:
catchall-http:
rule: "HostRegexp(`{host:.+}`)"
entryPoints:
- web
service: "blackhole-http-svc"
priority: 1

services:
blackhole-http-svc:
loadBalancer:
servers:
- url: "http://10.255.255.1:80"


tcp:
routers:
catchall-tls:
rule: "HostSNI(`*`)"
entryPoints:
- websecure
service: "blackhole-tcp-svc"
priority: 1
tls:
passthrough: false

services:
blackhole-tcp-svc:
loadBalancer:
servers:

- address: "192.0.2.1:1"

crowdsec 配置

这里配置 CrowdSec,通过 docker 搭建。

整个安全方案由两部分组成:

  1. CrowdSec Agent (crowdsec 服务): 负责分析日志、检测威胁,并管理决策(例如,哪个 IP 应该被禁止)。
  2. Traefik Bouncer (作为 Traefik 插件): 在 Traefik 中运行,负责执行 CrowdSec Agent 的决策,直接在流量入口处阻止恶意 IP。

工作流程

  1. Traefik 接收外部请求,并将访问日志(JSON 格式)输出。
  2. CrowdSec Agent (crowdsec 服务) 通过挂载的 Docker Socket 读取 Traefik 的日志。
  3. Agent 使用 crowdsecurity/traefik collection 中的解析器和场景分析日志,检测到攻击行为(如暴力破解、扫描等)。
  4. 一旦检测到威胁,CrowdSec 会生成一个“决策”,将攻击者的 IP 标记为恶意,并推送到 local API。
  5. local API马上推送到所有Bouncer(如果还配置了其他Bouncer),其中Traefik 中的 CrowdSec Bouncer 插件会获取最新的决策列表。
  6. 当被标记为恶意的 IP 再次尝试访问时,Bouncer 会在 Traefik 层面直接拒绝该请求,从而保护所有后端服务。
crowdsec-bouncer-traefik-plugin.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
services:
crowdsec:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
restart: unless-stopped
environment:
- GID=999
- COLLECTIONS=crowdsecurity/traefik
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./data:/var/lib/crowdsec/data
- ./config:/etc/crowdsec
networks:
- traefik-net

networks:
traefik-net:
external: true

反代配置

主要通过 traefik 的标签(labels)来配置反代。

docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
services:
umami:
image: ghcr.io/umami-software/umami:postgresql-latest


environment:
DATABASE_URL:
DATABASE_TYPE: postgresql
init: true
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"]
interval: 5s
timeout: 5s
retries: 5
networks:
- traefik-net
labels:
- traefik.enable=true
- traefik.http.routers.umami.rule=Host(`stats.xingpingcn.top`)
- traefik.http.services.umami.loadbalancer.server.port=3000
- traefik.http.routers.umami.entrypoints=websecure
- traefik.http.routers.umami.tls=true
- traefik.http.routers.umami.tls.certresolver=myresolver
- traefik.http.routers.umami.tls.options=cloudflare-mtls@file

networks:
traefik-net:
name: traefik-net
external: true