Fail2Ban 封禁 Docke r容器端口的异常访问

环境与需求

因业务需要,使用docker容器部署了ssh跳板机,后续发现容器部署的 ssh跳板机 的IP和端口泄漏,收到许多ssh访问登录攻击。大量未知用户的访问探测,触发 MaxStartups throtting 导致正常访问受到影响。因为是异常IP的异常访问,不想更改MaxStartups 配置,因此需要做一下限制。

主机上使用firewalld防火墙。

这里假设容器内的 SSH服务使用端口22,然后容器的 22端口 映射到了主机2222端口。

分析

异常日志

# sshd 日志 /var/log/auth.log 中的出现的异常访问记录
2025-07-25T11:14:14.410785+08:00 jumps-new sshd[556171]: Invalid user tmp from xx.53.78.68 port 8946
2025-07-25T11:14:14.443162+08:00 jumps-new sshd[556171]: Connection closed by invalid user tmp xx.53.78.68 port 8946 [preauth]

Docker 容器的 /var/log/auth.log 被映射到了主机的/var/log/docker-jumps-auth.log

防火墙规则放哪里

主机上使用firewalld防火墙,容器使用itpables规则。一般来说 未经特殊配置的 firewalld 防火墙管不住docker 的网络流量,所以我们准备使用 fail2ban 进行检测并在 iptables 规则链上做文章。

Docker 不建议我们操作 Docker的几条链,我们的自定义规则应该放在 DOCKER-USER 链上。

fail2ban 配置结构介绍

默认配置目录结构:

/etc/fail2ban/
├── action.d
├── fail2ban.conf
├── fail2ban.d
├── filter.d
├── jail.conf
├── jail.d
├── paths-arch.conf
├── paths-common.conf
├── paths-debian.conf
└── paths-opensuse.conf

5 directories, 6 files

fail2ban 的配置文件通常位于 /etc/fail2ban/jail.conf,不过我们推荐创建一个 jail.local 文件来保留自定义配置(避免在软件更新时覆盖原文件),或者在/etc/fail2ban/jail.d目录中创建自定义配置文件如customisation.local
关系大概是 jail.conf-->jial.local(覆盖conf)-->Filters-->Actions-->FirewallRulesUpdate

正在加载 Flowchart 图表…

file

  • 过滤器 (Filters – 存放在 /etc/fail2ban/filter.d/ 目录): 每个Filter文件里都定义了一堆正则表达式(failregex),用来从特定的日志文件中匹配那些“异常”的行为,比如登录失败、访问不存在的页面、或者尝试进行注入攻击的。它还能识别出“攻击人”的IP地址(通过标签提取),你也可以主动忽略某些异常(使用标签包裹),或者不去匹配这些异常。
  • 动作 (Actions – 存放在 /etc/fail2ban/action.d/ 目录): 检查出异常访问后的处置方案。比如,最常见的动作就是用iptablesfirewalldnftables(Linux防火墙工具)把“攻击人IP”给封禁一段时间,还可以配置成发送邮件通知管理员。
  • 监狱 (Jails – 在 jail.confjail.localjail.d/ 目录下定义): 这就是“总体执行方案”!一个Jail把一个或多个Filter(负责异常行为识别)、一个或多个Action(采取什么应对措施)、以及其他辅助判断和操作的参数(比如监控哪个日志文件logpath,在多长时间内findtime尝试多少次算违规maxretry、封禁多久bantime、使用什么手段进行封禁banaction等)给结合起来,针对特定的服务如SSH进行防护。

这里我们需要做的步骤:

  1. 自定义 Filters,确保 fail2ban 能识别当前场景下的异常日志
  2. 自定义 Actions 配置,启用防火墙规则进行拦截

实际操作

Filters 更新与调试

第一步调整/etc/fail2ban/filter.d/sshd.conf 配置。

Openssh 的默认 Fail2Ban Filter配置

“`
# Fail2Ban filter for openssh
#
# If you want to protect OpenSSH from being bruteforced by password
# authentication then get public key authentication working before disabling
# PasswordAuthentication in sshd_config.
#
#
# "Connection from <HOST> port \d+" requires LogLevel VERBOSE in sshd_config
#

[INCLUDES]

# Read common prefixes. If any customizations available — read them from
# common.local
before = common.conf

[DEFAULT]

_daemon = sshd

# optional prefix (logged from several ssh versions) like "error: ", "error: PAM: " or "fatal: "
__pref = (?:(?:error|fatal): (?:PAM: )?)?
# optional suffix (logged from several ssh versions) like " [preauth]"
#__suff = (?: port \d+)?(?: \[preauth\])?\s*
__suff = (?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*
__on_port_opt = (?: (?:port \d+|on \S+)){0,2}
# close by authenticating user:
__authng_user = (?: (?:invalid|authenticating) user <F-USER>\S+|.*?</F-USER>)?

# for all possible (also future) forms of "no matching (cipher|mac|MAC|compression method|key exchange method|host key type) found",
# see ssherr.c for all possible SSH_ERR_…_ALG_MATCH errors.
__alg_match = (?:(?:\w+ (?!found\b)){0,2}\w+)

# PAM authentication mechanism, can be overridden, e. g. `filter = sshd[__pam_auth='pam_ldap']`:
__pam_auth = pam_[a-z]+

[Definition]

prefregex = ^<F-MLFID>%(__prefix_line)s</F-MLFID>%(__pref)s<F-CONTENT>.+</F-CONTENT>$

cmnfailre = ^[aA]uthentication (?:failure|error|failed) for <F-USER>.*</F-USER> from <HOST>( via \S+)?%(__suff)s$
^User not known to the underlying authentication module for <F-USER>.*</F-USER> from <HOST>%(__suff)s$
<cmnfailre-failed-pub-<publickey>>
^Failed <cmnfailed> for (?P<cond_inv>invalid user )?<F-USER>(?P<cond_user>\S+)|(?(cond_inv)(?:(?! from ).)*?|[^:]+)</F-USER> from <HOST>%(__on_port_opt)s(?: ssh\d*)?(?(cond_user): |(?:(?:(?! from ).)*)$)
^<F-USER>ROOT</F-USER> LOGIN REFUSED FROM <HOST>
^[iI](?:llegal|nvalid) user <F-USER>.*?</F-USER> from <HOST>%(__suff)s$
^User <F-USER>\S+|.*?</F-USER> from <HOST> not allowed because not listed in AllowUsers%(__suff)s$
^User <F-USER>\S+|.*?</F-USER> from <HOST> not allowed because listed in DenyUsers%(__suff)s$
^User <F-USER>\S+|.*?</F-USER> from <HOST> not allowed because not in any group%(__suff)s$
^refused connect from \S+ \(<HOST>\)
^Received <F-MLFFORGET>disconnect</F-MLFFORGET> from <HOST>%(__on_port_opt)s:\s*3: .*: Auth fail%(__suff)s$
^User <F-USER>\S+|.*?</F-USER> from <HOST> not allowed because a group is listed in DenyGroups%(__suff)s$
^User <F-USER>\S+|.*?</F-USER> from <HOST> not allowed because none of user's groups are listed in AllowGroups%(__suff)s$
^<F-NOFAIL>%(__pam_auth)s\(sshd:auth\):\s+authentication failure;</F-NOFAIL>(?:\s+(?:(?:logname|e?uid|tty)=\S*)){0,4}\s+ruser=<F-ALT_USER>\S*</F-ALT_USER>\s+rhost=<HOST>(?:\s+user=<F-USER>\S*</F-USER>)?%(__suff)s$
^maximum authentication attempts exceeded for <F-USER>.*</F-USER> from <HOST>%(__on_port_opt)s(?: ssh\d*)?%(__suff)s$
^User <F-USER>\S+|.*?</F-USER> not allowed because account is locked%(__suff)s
^<F-MLFFORGET>Disconnecting</F-MLFFORGET>(?: from)?(?: (?:invalid|authenticating)) user <F-USER>\S+</F-USER> <HOST>%(__on_port_opt)s:\s*Change of username or service not allowed:\s*.*\[preauth\]\s*$
^Disconnecting: Too many authentication failures(?: for <F-USER>\S+|.*?</F-USER>)?%(__suff)s$
^<F-NOFAIL>Received <F-MLFFORGET>disconnect</F-MLFFORGET></F-NOFAIL> from <HOST>%(__on_port_opt)s:\s*11:
<mdre-<mode>-other>
^<F-MLFFORGET><F-MLFGAINED>Accepted \w+</F-MLFGAINED></F-MLFFORGET> for <F-USER>\S+</F-USER> from <HOST>(?:\s|$)

cmnfailed-any = \S+
cmnfailed-ignore = \b(?!publickey)\S+
cmnfailed-invalid = <cmnfailed-ignore>
cmnfailed-nofail = (?:<F-NOFAIL>publickey</F-NOFAIL>|\S+)
cmnfailed = <cmnfailed-<publickey>>

mdre-normal =
# used to differentiate "connection closed" with and without `[preauth]` (fail/nofail cases in ddos mode)
mdre-normal-other = ^<F-NOFAIL><F-MLFFORGET>(Connection (?:closed|reset)|Disconnected)</F-MLFFORGET></F-NOFAIL> (?:by|from)%(__authng_user)s <HOST>(?:%(__suff)s|\s*)$

mdre-ddos = ^Did not receive identification string from <HOST>
^kex_exchange_identification: (?:read: )?(?:[Cc]lient sent invalid protocol identifier|[Cc]onnection (?:closed by remote host|reset by peer))
^Bad protocol version identification '.*' from <HOST>
^<F-NOFAIL>SSH: Server;Ltype:</F-NOFAIL> (?:Authname|Version|Kex);Remote: <HOST>-\d+;[A-Z]\w+:
^Read from socket failed: Connection <F-MLFFORGET>reset</F-MLFFORGET> by peer
^banner exchange: Connection from <HOST><__on_port_opt>: invalid format
# same as mdre-normal-other, but as failure (without <F-NOFAIL> with [preauth] and with <F-NOFAIL> on no preauth phase as helper to identify address):
mdre-ddos-other = ^<F-MLFFORGET>(Connection (?:closed|reset)|Disconnected)</F-MLFFORGET> (?:by|from)%(__authng_user)s <HOST>%(__on_port_opt)s\s+\[preauth\]\s*$
^<F-NOFAIL><F-MLFFORGET>(Connection (?:closed|reset)|Disconnected)</F-MLFFORGET></F-NOFAIL> (?:by|from)%(__authng_user)s <HOST>(?:%(__on_port_opt)s|\s*)$

mdre-extra = ^Received <F-MLFFORGET>disconnect</F-MLFFORGET> from <HOST>%(__on_port_opt)s:\s*14: No(?: supported)? authentication methods available
^Unable to negotiate with <HOST>%(__on_port_opt)s: no matching <__alg_match> found.
^Unable to negotiate a <__alg_match>
^no matching <__alg_match> found:
# part of mdre-ddos-other, but user name is supplied (invalid/authenticating) on [preauth] phase only:
mdre-extra-other = ^<F-MLFFORGET>Disconnected</F-MLFFORGET>(?: from)?(?: (?:invalid|authenticating)) user <F-USER>\S+|.*?</F-USER> <HOST>%(__on_port_opt)s \[preauth\]\s*$

mdre-aggressive = %(mdre-ddos)s
%(mdre-extra)s
# mdre-extra-other is fully included within mdre-ddos-other:
mdre-aggressive-other = %(mdre-ddos-other)s

# Parameter "publickey": nofail (default), invalid, any, ignore
publickey = nofail
# consider failed publickey for invalid users only:
cmnfailre-failed-pub-invalid = ^Failed publickey for invalid user <F-USER>(?P<cond_user>\S+)|(?:(?! from ).)*?</F-USER> from <HOST>%(__on_port_opt)s(?: ssh\d*)?(?(cond_user): |(?:(?:(?! from ).)*)$)
# consider failed publickey for valid users too (don't need RE, see cmnfailed):
cmnfailre-failed-pub-any =
# same as invalid, but consider failed publickey for valid users too, just as no failure (helper to get IP and user-name only, see cmnfailed):
cmnfailre-failed-pub-nofail = <cmnfailre-failed-pub-invalid>
# don't consider failed publickey as failures (don't need RE, see cmnfailed):
cmnfailre-failed-pub-ignore =

cfooterre = ^<F-NOFAIL>Connection from</F-NOFAIL> <HOST>

failregex = %(cmnfailre)s
<mdre-<mode>>
%(cfooterre)s

# Parameter "mode": normal (default), ddos, extra or aggressive (combines all)
# Usage example (for jail.local):
# [sshd]
# mode = extra
# # or another jail (rewrite filter parameters of jail):
# [sshd-aggressive]
# filter = sshd[mode=aggressive]
#
mode = normal

#filter = sshd[mode=aggressive]

ignoreregex =

maxlines = 1

journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd

# DEV Notes:
#
# "Failed \S+ for .*? from <HOST>…" failregex uses non-greedy catch-all because
# it is coming before use of <HOST> which is not hard-anchored at the end as well,
# and later catch-all's could contain user-provided input, which need to be greedily
# matched away first.
#
# Author: Cyril Jaquier, Yaroslav Halchenko, Petr Voralek, Daniel Black and Sergey Brester aka sebres
# Rewritten using prefregex (and introduced "mode" parameter) by Serg G. Brester.
“`

cmnfailre 这个变量是我们需要关注的,日志前面的 时间 进程相关信息在使用这里的正则匹配时已经剔除了,所以可以用^进行匹配,具体文档参见man jail.conf
异常日志是:

# sshd 日志 /var/log/auth.log 中的出现的异常访问记录
2025-07-25T11:14:14.410785+08:00 jumps-new sshd[556171]: Invalid user tmp from xx.53.78.68 port 8946
2025-07-25T11:14:14.443162+08:00 jumps-new sshd[556171]: Connection closed by invalid user tmp xx.53.78.68 port 8946 [preauth]

开始我们准备匹配后面的Connection closed 这一行,在修改了 cmnfailre 之后使用如下命令进行调试:

# 查看状态
fail2ban-client status
fail2ban-client status docker-sshd
# man fail2ban-regex or fail2ban-regex --help
fail2ban-regex /var/log/docker-jumps-auth.log /etc/fail2ban/filter.d/sshd.conf --print-all-ignored -v
fail2ban-regex /var/log/docker-jumps-auth.log /etc/fail2ban/filter.d/sshd.conf --print-all-missed -v
fail2ban-regex /var/log/docker-jumps-auth.log /etc/fail2ban/filter.d/sshd.conf --print-all-matched -v

fail2ban-client get docker-sshd failregex
fail2ban-client get docker-sshd ignoreregex
fail2ban-client get docker-sshd ignoreip
fail2ban-client get docker-sshd banaction

fail2ban-client status docker-sshd |grep 22.217.95.xxx
iptables -nvL

调试过程中发现目标日志都被Ignore了!进一步查看发现有个F-NOFAIL标签刚好影响到这一行,所以日志被忽略了,因此Action没有执行。那可以选择匹配 Invalid user这一行,遂在 /etc/fail2ban/filter.d/sshd.conf

#cmnfailre = 下面加一行
^[iI]nvalid user <F-USER>.*</F-USER> from <HOST>%(__on_port_opt)s(?: ssh\d*)?$

可以发现现在能正确匹配日志。后面就是启用 Fail2Ban 了。

配置 Fail2Ban 检查ssh日志

使用/etc/fail2ban/jail.local
创建默认配置文件的副本:

cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

# jail.conf默认有许多常见服务的配置,你可以删除或者创建新文件, 或者在 jail.d目录下创建

默认的sshd配

[sshd]

# To use more aggressive sshd modes set filter parameter "mode" in jail.local:
# normal (default), ddos, extra or aggressive (combines all).
# See "tests/files/logs/sshd" or "filter.d/sshd.conf" for usage example and details.
#mode   = normal
port    = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s

我们创建一个新的块:

[DEFAULT]
ignoreip = 127.0.0.1/8 ::1
bantime  = 48h
findtime  = 2m
maxretry = 2
#destemail = [email protected]
#sender = [email protected]
#action = %(action_mwl)s

[docker-sshd]
enabled = true
mode    = aggressive
port    = 2222
filter  = sshd
chain   = DOCKER-USER
banaction = iptables-multiport
logpath  = /var/log/docker-jumps-auth.log
maxretry = 2
bantime  = 30m
findtime = 2m

常用参数说明:

  • ignoreip = 127.0.0.1/8 ::1:添加 IP 白名单,避免自己被封锁(生产环境很重要!)。
  • mode = aggressive 模式
  • enabled = true:启用 SSH 防护。
  • port = 2222:指定 SSH 使用的端口(通常是 22,如果你使用自定义端口,可以修改这里)。
  • logpath = /var/log/docker-jumps-auth.log:指定 SSH 登录日志文件的路径。
  • filter = sshd:指定用于匹配 SSH 登录失败的日志过滤器,该过滤器在 filter.d 目录中以sshd.conf的形式存在,可以在这里修改自定义的 正则表达式规则,或者在这里创建新的xxxfilter.conf。
  • maxretry = 2:设定最大登录失败次数为 2 次。
  • bantime = 48h:设置 IP 地址被禁止的时间为 xxx 秒(或者 40m 2h ),-1为永久封禁。
  • findtime = 2m:设置检查的时间窗口为 2 分钟,意味着在这个时间段内超过 2 次失败的登录会触发封禁。
  • chain = DOCKER-USER 指定 防火墙规则使用的链名称

启动或者重启重载fail2ban 你会发现它没有能正确进行拦截!原因如下:

限制容器请使用 DOCKER-USER
数据包进入 DOCKER-USER链时 **已经完成了 NAT**,所以使用2222端口是错误的,应该使用容器里ssh服务的22端口!
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1
bantime  = 48h
findtime  = 2m
maxretry = 2
#destemail = [email protected]
#sender = [email protected]
#action = %(action_mwl)s

[docker-sshd]
enabled = true
mode    = aggressive
port    = 22  # <<<<< 注意这里
filter  = sshd
chain   = DOCKER-USER # <<<<< 注意这里
banaction = iptables-multiport
logpath  = /var/log/docker-jumps-auth.log
maxretry = 2
bantime  = 30m
findtime = 2m

这样所有容器的22端口都受到影响了,我们给它再限制一下范围。这个操作是通过banaction指定的。

Actions 自定义

Action配置保存在 /etc/fail2ban/action.d/ 目录,进来之后复制一下原有iptables-multiport.confiptables-docker-multiport.conf,再进行编辑 vim iptables-docker-multiport.conf
观察发现这里用的iptables进行的操作,那只要指定一下 destination IP即可,在需要的地方都加一下 -d 。比如:

Click_to_view_details

“`ini
# Fail2Ban configuration file
#
# Author: Cyril Jaquier
# Modified by Yaroslav Halchenko for multiport banning
#

[INCLUDES]

before = iptables-common.conf

[Definition]

# Option: actionstart
# Notes.: command executed on demand at the first ban (or at the start of Fail2Ban if actionstart_on_demand is set to false).
# Values: CMD
#
actionstart = <iptables> -N f2b-<name>
<iptables> -A f2b-<name> -j <returntype>
<iptables> -I <chain> -p <protocol> -m multiport –dports <port> -j f2b-<name>

# Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
# Values: CMD
#
actionstop = <iptables> -D <chain> -p <protocol> -m multiport –dports <port> -j f2b-<name>
<actionflush>
<iptables> -X f2b-<name>

# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \t]'

# Option: actionban
# Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>

# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>

[Init]

root@proxy1:/etc/fail2ban# cat action.d/iptables-docker-multiport.conf
# Fail2Ban configuration file
#
# Author: Cyril Jaquier
# Modified by Yaroslav Halchenko for multiport banning
#

[INCLUDES]

before = iptables-common.conf

[Definition]

# Option: actionstart
# Notes.: command executed on demand at the first ban (or at the start of Fail2Ban if actionstart_on_demand is set to false).
# Values: CMD
#
actionstart = <iptables> -N f2b-<name>
<iptables> -A f2b-<name> -j <returntype>
<iptables> -I <chain> -p <protocol> -m multiport –dports <port> -d <destip> -j f2b-<name>

# Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
# Values: CMD
#
actionstop = <iptables> -D <chain> -p <protocol> -m multiport –dports <port> -d <destip> -j f2b-<name>
<actionflush>
<iptables> -X f2b-<name>

# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \t]'

# Option: actionban
# Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionban = <iptables> -I f2b-<name> 1 -s <ip> -d <destip> -j <blocktype>

# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionunban = <iptables> -D f2b-<name> -s <ip> -d <destip> -j <blocktype>

[Init]

“`

完事之后再在jail.local使用我们新创建并修改的 action,并传入参数 destip=容器IP重启fail2ban 即可生效。

[DEFAULT]
ignoreip = 127.0.0.1/8 ::1   # 还应该有 公司的 可信IP
bantime  = 48h
findtime  = 2m
maxretry = 2
#destemail = [email protected]
#sender = [email protected]
#action = %(action_mwl)s

[docker-sshd]
enabled = true
mode    = aggressive
port    = 22  # <<<<< 注意这里
filter  = sshd
chain   = DOCKER-USER # <<<<< 注意这里
#banaction = iptables-multiport
banaction = iptables-docker-multiport[destip=10.100.0.199] # <<<<< 注意这里
logpath  = /var/log/docker-jumps-auth.log
maxretry = 2
bantime  = 30m
findtime = 2m

至此完成 使用fail2ban 封禁容器端口的异常访问。

用一杯咖啡支持我们,我们的每一篇[文档]都经过实际操作和精心打磨,而不是简单地从网上复制粘贴。期间投入了大量心血,只为能够真正帮助到您。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇