背景

在Traefik中,Fail2Ban是一个非常强大的插件,可以轻松实现IP黑名单,IP白名单,IP封禁等操作。我们可以根据不同的需求,选择封禁的时长。

比如:我想针对1分钟内请求100次以上的IP封禁1小时。对于比DDOS小很多的某些攻击,可以有效防止。(如果处于DDOS,大部分都是在服务商那就已经将云服务拉入小黑屋了,
此时服务流量基本进不来了,所以这里只是针对小攻击的处理方法,或者某些暴力爬虫等)

对于Fail2Ban的原理,你可以参考Github,基本原理是时间窗口,go语言写这类工具再对接Kubernetes有天生的优势,所以这里就不再赘述了。

环境

traefik: v2.9.10
kubernetes: v1.24.6+k3s1
Fail2Ban: v0.7.1

安装

在K3S中,我们已经内置了Traefik,所以如果想安装插件,我们需要对内置的Traefik进行改造,添加插件。

由于K3S会周期性的执行 /var/lib/rancher/k3s/server/manifests 目录下的所有yaml,而如果我们修改了默认的traefik.yaml,K3S会自动恢复默认文件,所以我们这里需要新建一个yaml文件,用HelmChartConfig的方式修改traefik安装的配置即可。

创建配置文件 nano /var/lib/rancher/k3s/server/manifests/traefik-config.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
additionalArguments:
- "--experimental.plugins.fail2ban.modulename=github.com/tomMoulard/fail2ban"
- "--experimental.plugins.fail2ban.version=v0.7.1"
- "--providers.kubernetescrd=true"
- "--providers.kubernetesingress=true"
experimental:
plugins:
enabled: true
providers:
kubernetesCRD:
allowCrossNamespace: true
image:
name: "rancher/mirrored-library-traefik"
tag: "2.9.10"
  • additionalArguments用于添加插件的启动参数,在启动Traefik时,他会自动下载,需要注意的是,他是自主访问github下载的,如果网络不通畅,下载插件会失败,但Traefik会正常启动,导致所有使用了fail2ban的Middleware全部失效,所以启动后还要确保有没有fail2ban的成功日志信息
  • experimental用于启动插件(新版本好像不配置也可以)
  • allowCrossNamespace允许跨命名空间调用(比如我在A命名空间创建了有关fail2ban的Middleware,但我想在B命名空间同样使用这个Middleware,所以需要允许跨命名空间调用)
  • 因为自带的Traefik版本比较低,所以指定下新版本的镜像,亲测在2.10版本中无法使用,所以只能升级到2.9版本的,在自带的2.6版本同样无法使用,会报plugin: unknown plugin type: sablier的错误,重启也无效

additionalArguments中插件的CLI配置,可以参考每个插件右上角的Install pLugin,里面写了具体的配置,插件地址是:https://plugins.traefik.io/plugins/628c9ebcffc0cd18356a979f/fail2-ban

在配置好之后,我们等待一会,K3S会自动执行该配置文件。

接下来使用 kubectl get jobs -n kube-system 查看下Helm job是否触发

1
2
3
4
root@m3:/var/lib/rancher/k3s/server/manifests# kubectl get jobs -n kube-system
NAME COMPLETIONS DURATION AGE
helm-install-traefik 1/1 7s 13h
helm-install-traefik-crd 1/1 8s 92d

可以看到,已经执行完了,接下来看下Traefik是否正常重启,使用kubectl describe pods -n kube-system traefik-xxxxx,启动参数是否含有上面配置的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Containers:
traefik:
Container ID: docker://6f058c4329b788f51bbea5f6e8debf15217f8067647
Image: rancher/mirrored-library-traefik:2.9.10
Image ID: docker-pullable://rancher/mirrored-library-traefik@sha256:aaec134463b277ca7aa4f888fe724a
Ports: 9100/TCP, 9000/TCP, 8000/TCP, 8443/TCP
Host Ports: 0/TCP, 0/TCP, 0/TCP, 0/TCP
Args:
...
--experimental.plugins.fail2ban.modulename=github.com/tomMoulard/fail2ban
--experimental.plugins.fail2ban.version=v0.7.1
--providers.kubernetescrd=true
--providers.kubernetesingress=true
...

查看Traefik的日志,看看是否有fail2ban的日志信息

1
2024/04/23 14:05:33 Plugin: FailToBan is up and running

接下来我们只需要创建一个Middleware,然后就可以使用Fail2Ban了。

使用

创建Middleware,规则为1分钟内请求100次以上的IP封禁1小时

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: api-protect
namespace: default
spec:
plugin:
fail2ban:
rules:
bantime: 1h
enabled: 'true'
findtime: 1m
maxretry: '100'

这里参数的配置请参考插件README即可,创建好Middle后,我们需要将流量拦截到这个Middleware上。

在你想要配置的Ingress中,添加如下信息:

1
2
3
4
5
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
traefik.ingress.kubernetes.io/router.middlewares: default-api-protect@kubernetescrd

绑定规则:命名空间-中间件名称@kubernetescrd

如果有多个则用英文逗号分隔(traefik.ingress.kubernetes.io/router.middlewares: default-api-protect1@kubernetescrd,default-api-protect2@kubernetescrd)

比如,我想先从http转https,再拦截,就需要两个middleware。

在Traefik的Dashboard中,能看到对应的Middleware和绑定的pod即可,如下图所示:

测试

我们可以把条件改苛刻些测试下,比如5秒内有两次请求则ban1分钟,请求如果返回403或者5xx则证明ban成功了

1
2
3
4
5
6
7
8
spec:
plugin:
fail2ban:
rules:
bantime: 1m
enabled: 'true'
findtime: 5s
maxretry: '2'

更多玩法

白名单

traefik的白名单坑很多,原因在于外网ip取决于X-Real-Ip,而为了透传此参数,下的功夫可不少。

首先,我们需要添加如下配置 nano /var/lib/rancher/k3s/server/manifests/traefik-config.yaml

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
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
...
ports:
# http内部信任列表
web:
forwardedHeaders:
trustedIPs:
- 10.0.0.1/8
proxyProtocol:
trustedIPs:
- 10.0.0.1/8
# https内部信任列表
websecure:
forwardedHeaders:
trustedIPs:
- 10.0.0.1/8
proxyProtocol:
trustedIPs:
- 10.0.0.1/8
# 使用 Local 策略时,客户端的 IP 地址将被保留,也就是所谓的真实ip
service:
spec:
externalTrafficPolicy: Local
# 此处重要,一定要选择一个非master的节点,否则无法获取到客户端真实ip
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- n6
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- n5
- n6
# 这个配置在我这里无效,但也贴出来吧。同理,不要在master上schedule
tolerations:
- key: "CriticalAddonsOnly"
operator: "Exists"
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
additionalArguments:
- "--experimental.plugins.fail2ban.modulename=github.com/tomMoulard/fail2ban"
- "--experimental.plugins.fail2ban.version=v0.8.3"
# 此处重要,允许insecure
- "--entryPoints.web.proxyProtocol.insecure"
- "--entryPoints.web.forwardedHeaders.insecure"
- "--entryPoints.websecure.proxyProtocol.insecure"
- "--entryPoints.websecure.forwardedHeaders.insecure"
experimental:
plugins:
enabled: true
...

创建middleware:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: test-allow-ip
namespace: defalut
spec:
plugin:
fail2ban:
allowlist:
ip:
- '::1'
- 127.0.0.1
- xx.xx.xx.xx #你的ip
logLevel: DEBUG

接下来我们尝试创建一个应用来看看是否生效

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
cat << EOF > whoami.yml
apiVersion: v1
kind: Service
metadata:
name: whoami
spec:
ports:
- protocol: TCP
name: web
port: 80
selector:
app: whoami
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: whoami
labels:
app: whoami
spec:
replicas: 1
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: traefik/whoami
ports:
- name: web
containerPort: 80
EOF

创建应用:

1
kubectl apply -f whoami.yml

接下来我们创建一个ingress,用于访问这个Deployment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cat << EOF > ingress-whoami.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: test.yourdomain.com
namespace: default
annotations:
traefik.ingress.kubernetes.io/router.middlewares: default-test-allow-ip@kubernetescrd #将流量发送至middleware
spec:
rules:
- host: test.yourdomain.com
http:
paths:
- backend:
service:
name: whoami
port:
number: 80
path: /
pathType: Prefix
EOF
1
kubectl apply -f ingress-whoami.yml

访问这个域名:http://test.yourdomain.com,可以看到类似信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Hostname: whoami-54b87bcdc-dpjhl
IP: 127.0.0.1
IP: 10.42.0.251
RemoteAddr: 10.42.7.26:38144
GET / HTTP/1.1
Host: test.yourdomain.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
X-Forwarded-For: 43.xx.xx.xx
X-Forwarded-Host: test.yourdomain.com
X-Forwarded-Port: 80
X-Forwarded-Proto: http
X-Forwarded-Server: traefik-xxx-xxx
X-Real-Ip: 43.xx.xx.xx

其中:X-Real-Ip: 43.xx.xx.xx就是你的真实ip,证明已经生效,且通过该白名单ip可正常访问,其余ip均返回403错误。

经测试,如果traefik部署在master节点,X-Real-Ip 一定为内部ip10.xx.xx.xx,导致无法通过白名单校验,切记不要在master节点部署traefik。

结语

Fail2Ban基本上满足了我对于某些恶意请求的拦截,在安装的时候经常发现不起作用,后来发现是Traefik版本的问题,2.0~2.6安装插件那会,好像还需要去官网注册token,现在已经摒弃了,取而代之的是Traefik的Hub。

白名单踩了2个月的坑,最后发现是主节点不能部署,也算是一个隐藏的坑点吧,具体为什么不能部署,后面理解了再贴吧。

在后续的文章中,我会玩一玩Traefik插件中星星最多的Sablier插件,它能根据流量自动启动pod,没人访问后自动关闭pod。我的测试环境刚好只在我开发的时候才用得到,不用的时候还占用内存(没错,说的就是你这个JVM占好多内存),看我后面的文章吧~

参考资料: