前言
大概一年前,我写了一篇关于自建服务的文章,其中介绍了一些基本的服务器安全配置,以及我在使用的一些自建服务。这段时间,我的自建服务结构发生了一些改变,我开始用 Crowdsec 监控服务日志并封禁恶意 ip,并用 Traefik 替代 Caddy 作为反向代理服务器。在这篇文章里,我会比较详细地介绍这些工具的部署过程,以及必要的配置选项。我还会穿插介绍一些这段时间我学到的 docker-compose 部署时的小技巧,可以让容器部署过程更安全、更方便。
部署之前
我主要使用 docker-compose 来部署服务,所以在部署之前,需要安装并配置好 docker-ce 与 docker-compose,并了解 docker-compose 的基本用法,除此之外,我还习惯将当前用户加入 docker 用户组,这样在每次运行 docker 命令时就不用加 sudo 了,至于 docker 的安装与配置我这里不再赘述,具体可以查看 官方文档。
为了方便管理,我打算将 Traefik 与需要被反向代理开放到公网的容器放在同一个 docker 网络,不需要被反向代理的容器放在另一个 docker 网络,我将其命名为 frontend 与 backend,在部署服务之前,先手动创建这两个网络:
docker network create frontend --subnet 172.25.0.0/24
docker network create backend
在这里我给 frontend 网络指定一个固定的网段,是为了方便后面添加防火墙规则,而 backend 网络就没有要求,不指定网段让它自动管理。
关于 docker-compose 文件的存放路径,我比较习惯在当前用户的家目录下面创建一个 containers 目录,再在其中创建各个子目录用来存放 compose 文件,比如 ~/containers/crowdsec
是 Crowdsec 容器,~/containers/traefik
是 Traefik 容器。
Crowdsec 部署
Crowdsec 是一个类似于 Fail2ban 的安全引擎,可以监控服务日志并封禁恶意 ip,不过与 Fail2ban 不同的是它支持从互联网订阅 ip 黑名单,以及可以在网页面板中查看封禁记录。
我打算用 Crowdsec 来监控 SSH 日志,以及后面要部署的 Traefik 的日志。
docker-compose 示例
在部署 Crowdsec 之前,需要先确认当前 Linux 发行版将 SSH 的登录日志存放在了哪里,一般来说有两种情况,一种是存放在 /var/log
目录下单独的日志文件中,另一种是由 journald 管理。如果是单独的日志文件,只要把对应文件映射进容器内部即可,若是由 journald 管理,就需要一些额外配置了。我用过的 Linux 发行版里面,Debian 11 将 SSH 登录日志存放在了 /var/log/auth.log
文件里;而 Debian 12 已经改用了 journald;我在下文中用作演示的 AlmaLinux 9 则是默认将 SSH 登录日志存放在了 /var/log/secure
文件里,其他基于 RHEL 的发行版应该也都差不多。对于其他的发行版,在部署前需要先确定日志的存放位置。
首先创建并进入 ~/containers/crowdsec
目录,本节的内容中的操作都是在这个目录下进行的,参考 Crowdsec 官方的示例,创建如下的 docker-compose.yaml
文件:
---
services:
crowdsec:
# 使用 journald 的发行版需要用 latest-debian tag。
# image: crowdsecurity/crowdsec:latest-debian
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
restart: unless-stopped
environment:
COLLECTIONS: "crowdsecurity/linux crowdsecurity/iptables crowdsecurity/sshd"
ENROLL_KEY: ${ENROLL_KEY}
TZ: Asia/Shanghai
GID: "${GID-1000}"
volumes:
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- ./config:/etc/crowdsec
- ./data:/var/lib/crowdsec/data/
# 需要根据当前发行版存放日志路径来更改
- /var/log/ecure:/var/log/auth.log:ro
# 使用 journald 的发行版需要取消下面一行注释
# - /var/log/journal:/var/log/host:ro
ports:
- "127.0.0.1:8080:8080"
networks:
frontend:
networks:
frontend:
external: true
关于镜像的 tag,因为默认 latest tag 的镜像是基于 Alpine Linux 的,不支持 systemd 与 journald,想要读取 journald 日志就需要用基于 Debian 的 latest-debian tag,不过镜像体积会大一些。
环境变量里需要关心的有两个,一个是 COLLECTIONS,这个变量定义了容器部署时自动安装的规则集,规则集包含了日志监控规则与封禁规则等,这里我添加了三个规则集,前两个是 Linux 系统的通用规则,最后一个是针对 SSH 服务的规则,在后面我会添加更多的规则,在这里可以查看所有可用规则;
另一个环境变量 ENROLL_KEY,这个是与 Crowdsec 官网绑定的密钥,打开这个网页,创建账号并登陆后,拉到最下面有个 Connect with the Console 部分,把最后面的密钥复制下来。

密钥类的信息不太推荐直接明文写进 docker-compose.yaml 文件里,比较推荐的是像我这样,将环境变量定义成 ENROLL_KEY: ${ENROLL_KEY}
,然后在 docker-compose.yaml 相同目录下新建一个 .env
的隐藏文件,再将环境变量写入这个文件。假如我的密钥是 mysupersecretkey
,那 .env
文件的内容就是:
ENROLL_KEY=mysupersecretkey
这样在启动容器时,docker-compose 会自动将 compose 文件中的 ${ENROLL_KEY}
替换为 .env 中定义的值。
文件系统映射中需要关注有三个,一个是 - /var/log/journal:/var/log/host:ro
,只在要读取 journald 日志时才需添加,用来将宿主机的 journald 日志映射到容器内;
另一个是 - /var/log/ecure:/var/log/auth.log:ro
,用来读取存放在文件里的日志,我这里是将 AlmaLinux 的 SSH 日志文件映射到容器内的 /var/log/auth.log
,具体使用时,需要读取哪个日志文件就映射哪个文件;
最后是 acquis.yaml,这个配置文件控制了 Crowdsec 从哪里读取日志,需要在容器启动前编辑好。如果 SSH 服务日志是存放在单独的文件里,那么 acquis.yaml 就这么写:
filenames:
- /var/log/auth.log
labels:
type: syslog
需要注意其中的 filenames
后面填写的文件路径应该是容器内的路径,而不是宿主机的路径。
如果 SSH 的日志由 journald 管理,acquis.yaml 需要这么写:
source: journalctl
journalctl_filter:
- "--directory=/var/log/host/"
- "_SYSTEMD_UNIT=ssh.service"
labels:
type: syslog
其中的 _SYSTEMD_UNIT=ssh.service
是需要读取日志的 systemd 服务名称,对于 SSH 服务,在 Debian 12 上,这个服务是 ssh.service,但是有些发行版可能是 sshd.service,可以用 journalctl -u service_name
来查看对应服务的日志,哪个服务有日志就用哪个。
网络方面,因为后面打算将 Traefik 连接到 Crowdsec,所以我把它放进了 frontend 网络。端口开放了一个 8080 端口,用以与后面安装到宿主机上的修复组件(remediation component)连接,对于不需要开放到公网的服务,端口映射可以加一个 127.0.0.1
,比如上面的 127.0.0.1:8080:8080
,表示 8080 端口只对本机开放,不对其他网络开放,这样即使没有配置防火墙,其他主机也无法访问这个端口,也算是一层额外的防护。
对于使用 journald 的发行版,在容器启动前,按照官方的示例,需要配置 iptables 规则:
iptables -A INPUT -j LOG --log-prefix "iptables: "
不过 iptables 工具目前已经停止了维护,可以用 nftables 工具来代替 iptables,首先安装 iptables-nft
软件包,它相当于是用 nftables 实现的 iptables,安装时会一并安装 nftables 并卸载 iptables,之后用 iptables-nft
代替 iptables
命令:
iptables-nft -A INPUT -j LOG --log-prefix "iptables: "
然后运行 iptables-nft-save
来持久化规则,保证在重启后规则不会重置。
一切都配置好后,就可以启动容器了,在 docker-compose.yaml 文件相同目录,运行 docker compose up -d
,便可以拉取镜像并启动了。这时回到 Crowdsec 网站的控制台,会看到一个 Security Engines enroll request,点击 Accept enroll
,便可将当前服务器添加到网页控制台了。

验证运行效果
Crowdsec 提供了一个 cli 工具,叫 cscli,可以用来控制 Crowdsec 的各种行为。要在容器中使用 cscli,需要运行 docker exec crowdsec cscli
,方便起见我在 .bashrc
里添加了一个 alias cscli="docker exec crowdsec cscli"
,这样就可以直接用 cscli 了。如果没有特别说明的话,后面内容中出现的 cscli 命令实际上都是 docker exec crowdsec cscli
。
运行 cscli metrics
便可查看 Crowdsec 的运行状态,如果在输出结果里面看到如下 Acquisition Metrics
的内容,说明 Crowdsec 已经能够成功读取日志文件了。如果没有,就断开 SSH 连接,重新登录几次,如果还是没有,就仔细检查一下之前的步骤有没有出错。

Crowdsec 能够读取日志还不够,我们还需要确认 Crowdsec 能过滤出恶意扫描的行为并记录下 ip,由于此时还没有配置对应的修复组件(remediation component),所以不用担心自己被挡在服务器门外,可以尝试模拟一下恶意 SSH 扫描的行为,比如使用不存在的用户名登陆到服务器,按照 Crowdsec 默认的规则,在 60 秒内有超过 10 次失败的登陆,或者在 10 秒内有 5 次失败的登陆,就会把 ip 拉黑 4 小时,如果手速够快,理论上就可以触发。运行 cscli decisions list
就可以查看当前被拉黑的 ip,可以看到我的 ip 已经被拉黑了 4 小时,不过由于没有修复组件,我还能正常连接到服务器。

要想手动解除拉黑,只需运行 cscli decisions delete -i ip
,把命令中的 ip 替换为刚刚被封禁的 ip,就可以解除拉黑了。
关于 cscli 的更多用法,可以查看官方文档。
修复组件
像上面那样配置好 Crowdsec 后,虽然已经可以监控日志并记录下恶意 ip 了,但是目前还不能阻止恶意 ip 连接到我们的服务器,因为这个任务是交给修复组件(remediation components)来完成的。修复组件可以简单理解为保镖(bouncers),它从 Crowdsec 那里拿到 ip 的黑名单,再根据黑名单决定是否放行 ip,修复组件需要根据自己使用的系统和软件来单独安装配置,在这里可以查看所有可用的修复组件。
如果要保护 SSH 服务,可以用防火墙组件,首先需要添加 Crowdsec 的软件源:
curl -s https://install.crowdsec.net | sudo sh
然后使用系统包管理器安装,我这里安装的是适用于 nftables 的版本:
sudo dnf install crowdsec-firewall-bouncer-nftables
因为没有配置与 Crowdsec 连接的密钥,安装后它会报错,这是正常的。接下来就将其连接到 Crowdsec,首先运行:
cscli bouncers add firewall
会生成下面的一个 api 密钥,这个这个密钥只会展示一次,所以一定要保存好。

然后编辑 crowdsec-firewall-bouncer 的配置文件 /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml
,主要需要关心两个值,api_url
和 api_key
,这两个分别是与 Crowdsec 认证的地址与密钥,我的 Crowdsec 运行在本地,端口是 8000,将刚才复制的密钥填进去,大概是这样:
# 前后内容省略,只要改这两行
api_url: http://127.0.0.1:8080/
api_key: fgV6ByUQFzpjpLaFB5i1YeVVvtP+oPPk+RfYZTrgR+Q
有些发行版的包管理器可能会在软件更新后覆盖掉这个配置文件,为了防止修改的配置被覆盖,也可以不修改原配置文件,而是新建一个 /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml.local
,把需要修改的部分写进这个文件,这样 crowdsec-firewall-bouncer 就会优先读取这个文件。
修改完配置文件,重启相关服务 sudo systemctl restart crowdsec-firewall-bouncer
,如果一切都没出错的话,防火墙组件就能成功连接到 Crowdsec 了。要想检查是否连接成功,运行 cscli bouncers list
,如果输出的结果里面 Valid
下面有一个对钩,而且 Last Api pull
下面有时间显示,就说明已经连接成功了。

修改默认规则
默认情况下,Crowdsec 会把恶意 ip 拉黑 4 小时,4 小时到了就会重新放行这个 ip,如果觉得这个时间太短,其实也可以修改。在 docker-compose.yaml 里面。我定义了一个 config 文件夹的映射,这个文件夹里存放的是各种配置文件,这一节我要修改的文件,都是在这个文件夹里面,要想修改 ip 拉黑的时长,可以编辑 config/profiles.yaml
这个文件,找到下面几行:
decisions:
- type: ban
duration: 4h
把 duration
后面的时长改成希望的时长,比如想要封禁 48 小时就改成 duration: 48h
。需要注意这个时长要改两处,一个是单个 ip 的封禁时长,一个是 ip 段的封禁时长,修改完成后需要重新启动 Crowdsec 容器才会生效。docker compoe up -d --force-recreate
命令可以强制重新创建容器,不用每次都 docker compose down
再 docker compose up -d
了。
如果 SSH 服务禁用了 root 登录和密码验证,只用密钥验证,并且将端口改成了不常用的端口,那么理论上其实已经挺安全了,观察 SSH 日志的话会发现平时几乎没有陌生 ip 尝试登录,但并不是完全没有,偶尔还是会有一两个 ip 会扫描到我开放的端口,我甚至还发现有些恶意 ip 非常鸡贼,每隔一两小时才会尝试登录一次,这样的话就完全不会触发 Crowdsec 的封禁规则。
(补充一些题外的内容,我发现 AlmaLinux 这样使用了 SELinux 的发行版在修改了 SSH 端口后会直接启动失败,这是因为 SELinux 默认情况下不允许 SSH 服务开放除了默认端口以外的端口,想要解决这个问题,需要让 SELinux 放行对应的端口 sudo semanage port -a -t ssh_port_t -p tcp 33000
,把最后面的端口号改成自己要开放的 SSH 端口,如果提示 semanage 命令不存在,就需要安装对应软件包 sudo dnf install policycoreutils-python-utils
,另外也可以把 SELinux 的模式改成 permissive,或是直接禁用 SELinux。)
为了能够封禁这些 ip,我们可以把规则改的激进一些。Crowdsec 的 ip 封禁规则保存在 config
文件夹下的 scenarios
里面,在这里面我们可以创建一个新的配置文件,比如说就叫 ssh-veryslow-bf.yaml
,我直接抄的官方的 ssh-bf 规则,稍微改了一下:
# ssh bruteforce
type: leaky
name: ssh-veryslow-bf
description: "Detect ssh very slow bruteforce"
filter: "evt.Meta.log_type == 'ssh_failed-auth'"
leakspeed: "300m"
references:
- http://wikipedia.com/ssh-bf-is-bad
capacity: 2
groupby: evt.Meta.source_ip
blackhole: 1m
reprocess: true
labels:
service: ssh
confidence: 3
spoofable: 0
classification:
- attack.T1110
label: "SSH Very Slow Bruteforce"
behavior: "ssh:bruteforce"
remediation: true
---
# ssh user-enum
type: leaky
name: ssh-veryslow-bf_user-enum
description: "Detect ssh very slow user enum bruteforce"
filter: evt.Meta.log_type == 'ssh_failed-auth'
groupby: evt.Meta.source_ip
distinct: evt.Meta.target_user
leakspeed: 300m
capacity: 2
blackhole: 1m
labels:
service: ssh
remediation: true
confidence: 3
spoofable: 0
classification:
- attack.T1589
behavior: "ssh:bruteforce"
label: "SSH Very Slow User Enumeration"
主要就是改了 leakspeed
和 capacity
两个值,分别代表持续的时间和允许的最大失败次数。我这里改的是在 5 小时内登录失败 2 次,就会被封禁。当然我这里改的有些过于激进了,要是自己一不小心登录失败两次就坏了,为了保险起见还可以把自己的 ip 加入白名单,按照官方文档的方法,在 config/parsers/s02-enrich/
目录里面创建一个 mywhitelists.yaml
文件:
name: my/whitelist
description: "Whitelist events from my ip addresses"
whitelist:
reason: "my ip ranges"
ip:
- "80.x.x.x"
在里面填上自己的 ip 或 ip 段,这样自己的 ip 就不会误封了。同样修改配置后要重启容器才能生效。
更多 Crowdsec 的配置方法,可以查看他们的官方文档。
Traefik 部署
Traefik 是一个反向代理服务器,支持自动申请和续签 ssl 证书,并且对 docker 有很好的支持,可以为部署在 docker 容器中的服务自动配置反向代理,十分方便。
Traefik 官方的文档我觉得写的有些杂乱,我的配置主要参考了 Youtube 上这个视频,如果觉得我下面的内容写的不够清楚,可以去看一下这个视频。
docker-compose 示例
创建并进入 ~/containers/traefik
目录,没有特别说明,本节所有操作都在这个目录下进行,在其中新建一个 docker-compose.yaml
文件:
---
services:
traefik:
# The official v3 Traefik docker image
image: traefik:v3
container_name: traefik
ports:
- "80:80"
- "443:443"
environment:
- CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
networks:
frontend:
ipv4_address: 172.25.0.250
volumes:
- /etc/localtime:/etc/localtime:ro
# 读取 docker socket
- /var/run/docker.sock:/var/run/docker.sock
# 静态配置文件
- ./config/traefik.yaml:/etc/traefik/traefik.yaml:ro
# 证书存储路径
- ./data/certs:/var/traefik/certs:rw
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik-secure.entrypoints=https"
# 用来访问 Traefik 后台的域名,需要改成自己域名
- "traefik.http.routers.traefik-secure.rule=Host(`traefik.example.com`)"
- "traefik.http.routers.traefik-secure.tls=true"
- "traefik.http.routers.traefik-secure.tls.certresolver=cloudflare"
# 申请通配符域名证书,也要记得改成自己的域名
- "traefik.http.routers.traefik-secure.tls.domains[0].main=example.com"
- "traefik.http.routers.traefik-secure.tls.domains[0].sans=*.example.com"
- "traefik.http.routers.traefik-secure.service=api@internal"
certsdump:
image: ldez/traefik-certs-dumper
container_name: dumper
depends_on:
- traefik
entrypoint: sh -c 'apk add jq;
while ! [ -e /data/acme.json ] || ! [ `jq ".[] | .Certificates | length" /data/acme.json` != 0 ];
do
sleep 300;
done &&
traefik-certs-dumper file --version v2 --domain-subdir --clean=false --watch --source ./acme.json --dest .'
volumes:
- ./data/certs:/data
restart: 'unless-stopped'
networks:
- backend
working_dir: /data
networks:
frontend:
external: true
backend:
external: true
其中的环境变量 CF_DNS_API_TOKEN
是 Cloudflare DNS 的 api 密钥。从 Let's Encrypt 申请通配符证书需要进行 DNS 挑战,简单来说就是在申请证书的时候给自己的域名创建一个指定的 DNS 记录,用来验证自己拥有这个域名,这个过程可以手动进行,但是要想自动申请和续签,就需要使用 DNS 提供商的 API 密钥来自动进行这个过程,Traefik 支持的 DNS 提供商可以在这里查询,如果自己的域名提供商不在这个列表里,那就需要把自己域名的解析服务器迁移到列表中的提供商。我这里用的是 Cloudflare,需要在其官网申请 API Token。
在 Cloudflare 官网登陆账号,打开这个页面,点击 Create Token;然后选择最下面的 Custom Token,点击 Get Started;给 Token 一个名字,Permissions 那里需要两个,一个是 Zone / Zone / Read
,另一个是 Zone / DNS / Edit
,Zone Resources 里面可以指定这个 API Token 可以作用于哪一个域名;一切配置后好,点击 Continue;然后是确认页面,确认无误后点击 Create;最后就会显示申请的 API Token 了,这个 Token 只会显示一次,所以一定要复制下来保存好。





这个 Token 我同样是放在了 .env
文件里:
CF_DNS_API_TOKEN=mysupersecrettoken
docker 的文件映射主要需要关心三个,- /var/run/docker.sock:/var/run/docker.sock
是为了让 Traefik 能够读取 docker socket,从而为 docker 容器自动配置反向代理;- ./data/certs:/var/traefik/certs:rw
是存放证书的位置;config/traefik.yaml
是 Traefik 的静态配置文件,需要在容器启动前创建好,下面是一个配置示例:
global:
checkNewVersion: false
sendAnonymousUsage: false
api:
dashboard: true
debug: true
entryPoints:
http:
address: ":80"
http:
redirections:
entryPoint:
to: https
scheme: https
https:
address: :443
serversTransport:
insecureSkipVerify: true
certificatesResolvers:
cloudflare:
acme:
# 下面填入自己的邮箱,用来接收证书过期提醒
email: "email@example.com"
storage: /var/traefik/certs/acme.json
caServer: https://acme-v02.api.letsencrypt.org/directory
# caServer: https://acme-staging-v02.api.letsencrypt.org/directory
keyType: EC256
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "1.0.0.1:53"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
其中的 entryPoints
部分定义了 Traefik 监听的端口,这里配置了监听 80 和 443 端口分别作为 http 和 https,同时我还为 http 端口配置了一个 redirections
,用来将所有 http 的访问全部重定向到 https,因为一些浏览器比如 Firefox 有 https only 模式,可以把 http 自动升级成 https,如果只有自己用并且浏览器打开了 https only 功能,这个重定向我个人觉得有点可有可无,为了安全考虑可以删掉这个端口,另外要记得一并修改 docker-compose.yaml 里面的端口。
certificatesResolvers
这一部分定义了证书申请的配置,storage
定义了证书存储的路径,这个路径应该对应 docker-compose.yaml 里面的证书存储路径;caServer
是向 Let's Encrypt 申请证书时使用的服务器,默认是 https://acme-v02.api.letsencrypt.org/directory
,但如果在短时间内申请证书次数过多,这个服务器会限制你的访问。如果是在测试环境,可以将服务器改成 https://acme-staging-v02.api.letsencrypt.org/directory
,这个服务器不会限制访问次数,但申请到的证书也会被浏览器提示为不安全,等配置稳定了,要切换到生产环境的话,再把服务器改回去。
providers
部分定义了 Traefik 反向代理的服务来源,这里我只配置了一个 docker,并且把 exposedByDefault
设为了 false,防止 Traefik 默认将所有的 docker 服务都反向代理出去,只有我手动指定的服务才会被反向代理。除了 docker,Traefik 还支持其他很多服务来源,这就超出本文的讨论范围了。
docker-compose.yaml 文件里还有一个 labels
部分,这个其实是 Traefik 的动态配置,可以为每个 docker 容器单独定义动态配置,这里配置的是将 Traefik 自带的网页控制台通过反向代理开放到公网。traefik.enable=true
代表为这个容器启用反向代理,后面 traefik.http.routers
开头的都是反向代理的路由配置,traefik-secure
是路由的名称,这个可以随意修改。- "traefik.http.routers.traefik-secure.entrypoints=https"
表示将控制台服务反向代理到 https 端口上;- "traefik.http.routers.traefik-secure.rule=Host(
traefik.example.com)"
表示将服务反向代理到 traefik.example.com
这个域名上;- "traefik.http.routers.traefik-secure.tls=true"
和 - "traefik.http.routers.traefik-secure.tls.certresolver=cloudflare"
,表示为这个服务启用 tls,并且证书的来源是 cloudflare
,这个名字需要与静态配置文件里的 certificatesResolvers
部分保持一致;- "traefik.http.routers.traefik-secure.tls.domains[0].main=example.com"
和 - "traefik.http.routers.traefik-secure.tls.domains[0].sans=*.example.com"
表示为 example.com
和通配符域名 *.example.com
同时申请证书,记得要在域名提供商那里为自己的域名创建一条通配符 DNS 记录并指向自己的服务器 ip;- "traefik.http.routers.traefik-secure.service=api@internal"
则表示反向代理的服务是 Traefik 内部的 api@internal
。
在 Traefik 容器下面我还添加了一个 certsdump 容器,因为 Traefik 申请到的证书是以 json 格式保存的,而不是平常熟悉的证书和密钥文件,如果你有其他的服务想要单独使用这个证书,就需要将其转换,traefik-certs-dumper 这个容器就可以将 Traefik 的 json 格式证书转换为证书和密钥文件,具体用法可以看作者的 github,如果你不需要,也可以把这部分删掉。
网络方面,Traefik 我让其使用了 frontend 网络,并且把 ip 地址固定为了 172.25.0.250,这是为了方便后面定义防火墙规则;traefik-certs-dumper 容器是基于 Alpine Linux 的,我看它的启动命令里有 apk add jq
,意思是使用 Alpine 的系统包管理器安装 jq 软件包,是需要联网的,不过为了安全起见不和 Traefik 在同一个网络,所以放在了 backend 网络里。
一切都配置好后,docker compose up -d
启动容器,如果防火墙的配置没问题的话,在浏览器输入自己配置的域名,应该就可以看到 Traefik 的控制台了,这个控制台是只读的,在这里可以查看 Traefik 的各种运行状态,但是无法修改配置。

对我个人而言,我不太喜欢把控制台页面就这样开放到默认的端口上,我们可以在 Traefik 的静态配置文件里额外添加一个端口,我用这个随机端口生成工具生成了一个高位数的端口,比如是 59402,在 config/traefik.yaml
的 entryPoints
部分添加如下内容:
traefik:
address: :59402
然后修改 docker-compose.yaml 添加对应的端口映射,并将 - "traefik.http.routers.traefik-secure.entrypoints=https"
改成 - "traefik.http.routers.traefik-secure.entrypoints=traefik"
。重启容器,之后在浏览器中就需要用域名加刚刚定义的端口才能访问控制台。
防火墙规则
如果用 ufw 来管理防火墙的话,会发现 ufw 无法管理 docker 开放出来的端口,即使 ufw 没有放行端口,却还是能够访问 docker 开放出来的端口,这是 ufw 的一个已知问题,我在之前的文章中提到了可以用 ufw-docker 来解决这个问题,不过没有说它的具体用法。在这里就简单提一下它的基本用法。
首先需要安装 ufw-docker 的脚本:
sudo wget -O /usr/local/bin/ufw-docker \
https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker
sudo chmod +x /usr/local/bin/ufw-docker
(另外我还发现一个奇怪的问题,在 AlmaLinux 上,把这个脚本安装到 /usr/local/bin
路径后,可以直接用 root 用户运行 ufw-docker
命令,也可以用普通用户运行这个命令,不过会提示权限不足,但是就是无法用普通用户加 sudo 运行,会提示命令找不到,就很奇怪。解决方法是把脚本安装到 /usr/bin
路径,但这样不太优雅,更好的方法是创建一个从 /usr/loca/bin/ufw-docker
到 /usr/bin/ufw-docker
的软链接。)
之后运行 sudo ufw-docker install
来修改 ufw 的防火墙规则以支持 docker,这样 ufw 就可以管理 docker 的端口了,要想放行端口和以前一样 sudo ufw allow port
就行了,不过 ufw-docker 这个工具提供了一个更好的方法。
比如我想放行前面 Traefik 监听的 80、443 以及 59402 端口,我可以这样:
sudo ufw-docker allow traefik 80
sudo ufw-docker allow traefik 443
sudo ufw-docker allow traefik 59402
上面的 traefik
是 docker 容器的名称,之后运行 sudo ufw status
,会看到多出了三条规则:

意思是只允许这些端口到 Traefik 容器的 ip 的流量,也就是 172.25.0.250,这样的话,比直接开放端口要安全一些。
不过因为这样用 ufw-docker 创建的规则是基于 ip 的,而 docker 容器重启时 ip 可能会改变,导致无法访问,所以之前才给 traefik 容器指定了一个固定 ip,防止容器的 ip 变化。
添加身份认证
如果自己的自建服务没有自带身份认证系统的话,Traefik 还支持为反向代理添加身份认证系统。Traefik 内置的有一个 BasicAuth 的中间件,不过可配置项比较少,也比较简陋,而且 basic auth 的认证方式对与密码管理器的自动填充十分不友好。所以这里我打算使用 Authelia 来为 Traefik 配置身份认证系统。
我打算将 Authelia 的容器和 Traefik 容器放在一起,当然分开放也是没问题的,按照Authelia 文档的介绍,在 traefik 的 docker-compose.yaml 文件里添加如下内容:
authelia:
image: authelia/authelia
container_name: authelia
volumes:
- ./authelia:/config
networks:
- frontend
environment:
- AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE=/config/secrets/JWT_SECRET
- AUTHELIA_SESSION_SECRET_FILE=/config/secrets/SESSION_SECRET
- AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/config/secrets/STORAGE_ENCRYPTION_KEY
- TZ=Asia/Shanghai
labels:
- "traefik.enable=true"
# 跳转到身份认证的域名,改成自己的域名
- "traefik.http.routers.authelia.rule=Host(`auth.example.com`)"
- "traefik.http.routers.authelia.entrypoints=https"
- "traefik.http.routers.authelia.tls=true"
- "traefik.http.routers.authelia.tls.certresolver=cloudflare"
- "traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/authz/forward-auth"
- "traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true"
- "traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email"
restart: 'unless-stopped'
healthcheck:
disable: true
在当前目录创建一个名为 authelia 的文件夹,这个目录存放了 Authelia 的配置文件,因为我的使用情况比较简单,所以我直接参考的官方给出的 lite 示例,首先创建 authelia/configuration.yml
:
---
server:
address: 'tcp://:9091'
log:
level: 'info'
file_path: '/var/log/authelia/authelia.log'
totp:
issuer: 'authelia.com'
theme: 'dark'
identity_validation:
reset_password:
jwt_secret: ''
# duo_api:
# hostname: api-123456789.example.com
# integration_key: ABCDEF
# # This secret can also be set using the env variables AUTHELIA_DUO_API_SECRET_KEY_FILE
# secret_key: 1234567890abcdefghifjkl
authentication_backend:
file:
path: '/config/users_database.yml'
password:
algorithm: argon2
access_control:
default_policy: 'deny'
rules:
# 对所有子域名开启账号密码验证,把域名改成自己的域名
- domain: '*.example.com'
policy: 'one_factor'
session:
# This secret can also be set using the env variables AUTHELIA_SESSION_SECRET_FILE
secret: ''
cookies:
- name: 'authelia_session'
domain: 'example.com' # 应该填自己的根域名
authelia_url: 'https://auth.example.com' # 应该填 Traefik 反代 Authelia 的域名
expiration: '1 hour'
remember_me: '3 months'
# redis:
# host: 'redis'
# port: 6379
# # This secret can also be set using the env variables AUTHELIA_SESSION_REDIS_PASSWORD_FILE
# # password: authelia
regulation:
max_retries: 3
find_time: '2 minutes'
ban_time: '5 minutes'
storage:
encryption_key: ''
local:
path: '/config/db.sqlite3'
notifier:
filesystem:
filename: /config/notification.txt
...
相比于官方的示例,下面是我的一些修改:首先是配置文件里三个密钥 jwt_secret
、session secret
和 encryption_key
给留空了,这些密钥可以通过环境变量读取;接着是 redis 数据库相关的配置被注释掉了,对于只有几个用户的简单使用场景,redis 数据库没有什么必要;还有 notifier
邮件通知部分,我没有配置邮箱 smtp 这些东西,而是直接让通知写进文件里面。
还有一些是需要根据自己情况修改的:access_control
部分可以为每个域名控制不同的认证方式,域名规则也可以正则匹配,我在这里配置的是对 example.com
的所有子域名开启 one_factor 也就是账户密码认证,除此之外还可以配置 two_factor 二次认证;cookies
部分,domain
要填自己的根域名,authelia_url
要填 Traefik 反代 Authelia 的域名。更多的配置,可以看 Authelia 的官方文档。
对于用户配置,我这里配置了将用户数据存放在 users_database.yml
里面,authelia/users_database.yml
的配置如下:
---
users:
demo_user:
disabled: false
displayname: 'demo_user'
# Password is test
password: '$argon2id$v=19$m=65536,t=3,p=4$Ku0DILVaodPmWRb/pI/ZRA$WEB3CbcGH3aMqwDgs1kUCkWm73E/l5YCSwzhGZQnNwE'
# Email 改成自己的
email: 'email@example.com'
groups:
- 'admins'
- 'dev'
...
这里配置了一个名为 demo_user 的用户,密码部分是用 argon2 加密后的密码,要想生成一个加密的密码,可以运行:
docker run --rm -it authelia/authelia:latest authelia crypto hash generate argon2
然后按照提示输入两次密码,就会生成加密后的密码,填入 password
后面即可。
因为配置文件中的三个密钥留空了,需要从环境变量中读取密钥,所以需要创建三个密钥文件,创建 authelia/secrets
文件夹,在里面创建三个文件,分别为 JWT_SECRET
、SESSION_SECRET
和 STORAGE_ENCRYPTION_KEY
,创建三个高强度密码,分别填入这三个文件,文档中推荐密码长度为 64 位,可以用 bitwarden 之类的密码管理器生成。
Authelia 容器的 labels
部分,前面几个表示是用 Traefik 将 Authelia 的服务反向代理到公网,用法前面都介绍过了。后面的三个则比较重要,这三个 labels 定义了一个中间件(middleware),可以让流量先经过 Authelia 的中间件,通过身份认证以后才可以继续访问。
中间件定义好以后,还需要指定服务来使用这个中间件,比如说为 Traefik 控制台添加身份认证,就在 Traefik 容器的 labels 部分添加一条 - "traefik.http.routers.traefik-secure.middlewares=authelia@docker"
。
一切配置好后,重启容器,再次打开 Traefik 控制台,就能看到自动跳转到了 Authelia 的身份认证页面,认证成功后才能打开控制台。

如何反向代理(以 whoogle-search 为例)
写到这里,所有麻烦的部分几乎都完成了,接下来要想用 Traefik 反向代理部署的自建服务就很简单了。只需像往常一样使用 docker-compose 部署自建服务,并对 docker-compose 做几处改动即可。首先为容器指定 Traefik 容器所在的 frontend 网络;因为在同一个网络下的 docker 容器可以直接用容器名作为主机名相互访问,所以如果没有额外的开放端口需求,就把端口映射部分完全删掉;最后再添加几条 label 就可以了。容器运行起来后,Traefik 就会自动为容器配置反向代理,十分方便。
作为演示,下面我会部署一个 whoogle-search 服务,whoogle-search 是一个简单的自建搜索引擎,虽然它也支持使用 basic auth 方式进行身份认证,不过就像我前面所说的,这种身份认证方式对密码管理器不太友好,所以我这里不但要用 Traefik 来自动配置反向代理,还要用 Authelia 来配置更好用的身份认证系统。
想要让 whoogle-search 跑起来,最少只需要一个 docker-compose.yaml 文件就够了。创建并进入 ~/containers/whoogle
,创建 docker-compose.yaml:
---
services:
whoogle-search:
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoogle-search.rule=Host(`search.example.com`)"
- "traefik.http.routers.whoogle-search.entrypoints=https"
- "traefik.http.routers.whoogle-search.tls.certresolver=cloudflare"
- "traefik.http.services.whoogle-search.loadbalancer.server.port=5000"
- "traefik.http.routers.whoogle-search.middlewares=authelia@docker"
image: ${WHOOGLE_IMAGE:-benbusby/whoogle-search}
container_name: whoogle-search
restart: unless-stopped
pids_limit: 50
mem_limit: 256mb
memswap_limit: 256mb
# user debian-tor from tor package
user: whoogle
security_opt:
- no-new-privileges
cap_drop:
- ALL
tmpfs:
- /config/:size=10M,uid=927,gid=927,mode=1700
- /var/lib/tor/:size=15M,uid=927,gid=927,mode=1700
- /run/tor/:size=1M,uid=927,gid=927,mode=1700
# env_file: # Alternatively, load variables from whoogle.env
# - whoogle.env
networks:
- frontend
networks:
frontend:
external: true
我这里基本上就是拿 whoogle-search 仓库里的 docker-compose.yaml 文件修改来的,指定了网络 frontend,删掉了所有开放的端口,并添加了几个 label,自上到下的意思分别是:启用 Traefik 反向代理、反向代理到 search.example.com
域名,反向代理到 https 端口;向配置文件里定义的 cloudflare
证书源申请证书;反向代理的端口是 5000;最后启用 Authelia 的身份认证。
编辑好后,docker compose up -d
启动容器,在浏览器中输入配置好的域名,就可以访问自己的 whoogle-search 服务了。

值得一提的是,如果你的浏览器已经由 Authelia 认证过了且 cookie 没有过期或被删除,这时访问 whoogle-search 是不会提示身份认证的,也就是说只要认证一次,所以服务都可以访问了,十分方便。
很多流行的自建服务项目都在文档里面提供了 Traefik 反向代理相关的示例,大部分时候也只要抄示例配置就行了。
连接 Crowdsec 与 Traefik
现在我们配置好了 Crowdsec 与 Traefik,接下来就需要使用 Crowdsec 来保护 Traefik 了。Traefik 支持插件,其中就有一个 Crowdsec 插件,用来阻止恶意 ip 的连接。
读取日志
在配置 Crowdsec 插件之前,需要先确保 Crowdsec 能够读取到 Traefik 与 Authelia 的日志。
进入 ~/containers/traefik
目录,首先编辑 Traefik 配置文件 config/traefik.yaml
,添加访问日志相关的配置:
accessLog:
filePath: "/var/log/traefik/traefik.log"
接着编辑 Authelia 配置文件 authelia/configuration.yml
,添加日志相关配置:
log:
level: 'info'
file_path: '/var/log/authelia/authelia.log'
为了方便不同容器之间共享文件,我这里创建了两个 docker 卷,分别用来存放 traefik 和 Authelia 的日志:
docker volume create traefik-log
docker volume create authelia-log
编辑 docker-compose.yaml,首先在最后添加如下内容,表示引入外部卷:
volumes:
traefik-log:
external: true
authelia-log:
external: true
之后再为 Traefik 和 Authelia 容器添加对应的文件映射:
Traefik 的:
- traefik-log:/var/log/traefik
Authelia 的:
- authelia-log:/var/log/authelia
重启容器。
然后进入 ~/containers/crowdsec
,编辑 docker-compose.yaml,同样先在最后引入外部卷,然后将两个卷映射到 Crowdsec 容器内部的 /var/log/traefik
和 /var/log/authelia
。最后不要忘了在环境变量 COLLECTIONS 里面添加上 Traefik 和 Authelia 的规则集 crowdsecurity/traefik LePresidente/authelia
。
然后编辑 acquis.yaml,在最下面添加如下内容:
# 上面是之前配置的 sshd 规则
---
filenames:
- /var/log/traefik/*.log
labels:
type: traefik
---
filenames:
- /var/log/authelia/authelia.log
labels:
type: authelia
重启 crowdsec 容器,然后运行 cscli metrics
,看到如下的内容,就说明日志已经被成功读取了,同样,如果没有看到,就在浏览器里访问几次自己的自建服务之后再看一下。

配置 Crowdsec 插件
首先,我们需要为 Traefik 上的 Crowdsec 插件创建一个新的「保镖」,即为 bouncer,运行 cscli bouncers add traefik
创建一个名为「traefik」的 bouncer,并将输出的 api 密钥保存下来。

回到 ~/containers/traefik
目录,首先编辑 traefik 的配置文件 config/traefik.yaml
,在最后添加如下内容:
experimental:
plugins:
crowdsec-bouncer:
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
version: "v1.4.1"
表示为 Traefik 启用插件支持,并安装 Crowdsec 插件。
之后编辑 docker-compose.yaml 文件,在 Traefik 容器的 labels 部分添加如下几行:
- "traefik.http.middlewares.crowdsec.plugin.crowdsec-bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.crowdsec-bouncer.crowdseclapikey=${CROWDSEC_TRAEFIK_BOUNCER_API_KEY}"
- "traefik.http.middlewares.crowdsec.plugin.crowdsec-bouncer.crowdseclapischeme=http"
- "traefik.http.middlewares.crowdsec.plugin.crowdsec-bouncer.crowdseclapihost=crowdsec:8080"
表示启用了一个名为 crowdsec 的中间件,使用的插件是配置文件里定义的 crowdsec-bouncer 插件,同样密钥我用环境变量表示了,把之前保存的密钥写入 .env
文件:
CROWDSEC_TRAEFIK_BOUNCER_API_KEY=vlizBG1/1HN52dkeLsnXfnHusTXdyoTkEUb8ORHhTAM
之后便要让需要被保护的服务使用这个中间件,可以像之前 Authelia 一样为每个容器单独指定,比如要想为上面的 whoogle-search 服务同时指定 crowdsec 和 authelia 两个中间件,可以这样写:
- "traefik.http.routers.whoogle-search.middlewares=crowdsec@docker, authelia@docker"
也可以全局指定,编辑配置文件 config/traefik.yaml
,在 entryPoint 部分进行如下修改:
https:
address: :443
http:
middlewares:
- crowdsec@docker
表示为所有经过 443 端口的服务都启用 crowdsec 中间件,如果你使用了其他端口,也可以按需配置。
不过我发现没办法为 80 端口配置 crowdsec 中间件,会直接报错说中间件不存在,不过之前配置的 80 端口流量都已经重定向到了 443 端口,倒也问题不大,如果真的不需要 80 端口,也可以把这个端口删掉。
总结
我把这篇文章中的 Crowdsec 与 Traefik 的配置,都放到了 github 上,并把我在用的自建服务都更新为了使用 Traefik 的配置,感兴趣的朋友可以参考一下。