小規模なメモリの Debian サーバーでは、以前 CrowdSec を使って SSH と Web のログを解析し、Firewall Bouncer を通じて封禁の決定を nftables に同期していました。

この構成は攻撃nese behaviorに応じて IP を動的に封禁できますが、メモリが数百 MiB しかなく、複数の個人용サービスを実行しているサーバーでは、リソースのトレードオフを再検討する必要があります。最終的には次の構成を採用しました。

  • Web:Caddy の GeoIP と手動 IP ホワイトリストを使用し、サイトごとに CDN からのオリジン接続を許可するかどうかを選択します。
  • SSH:Fail2ban が systemd journal を読み取り、異常な送信元を nftables で封禁します。

本稿では、改造の全体手順と、導入時に発生したログソースおよび起動順序の問題を記録します。すべてのドメイン、アドレス、ホスト識別子には例示用の値に置き換えています。コマンドは root ユーザーで実行します。

一、先に保護機能の変化を整理する

項目旧構成新構成
Web アクセス制御CrowdSec がログを解析し、封禁の決定を生成Caddy が送信元の地域または IP に応じて許可
SSH の異常ログインCrowdSec の SSH シナリオFail2ban の SSH jail
Web の挙動検知導入済みの Collection と Scenario 次第本稿では維持していません
コミュニティの脅威インテリジェンスCrowdSec のコミュニティリストを使用可能本稿では接続していません
Web リクエストの拒否段階Firewall Bouncer のファイアウォールルールCaddy の HTTP リクエスト処理段階

地域ホワイトリストは、挙動検知と同等の代替にはなりません。許可された地域からの悪意あるリクエストもアプリケーションに到達する可能性があります。また、Caddy の GeoIP ルールは、Caddy を通過しない他の公開ポートには適用されません。

二、メモリ使用量を確認するときは、RSS と Swap の両方を調べる

まず全体的情况を確認します。

free -h

systemctl show crowdsec crowdsec-firewall-bouncer \
    -p Id -p MemoryCurrent

ps -eo pid,comm,rss,args --sort=-rss | head -n 15

RSS だけを見ると、プロセスによるメモリ使用量を過小評価する可能性があります。一部のアノニeletteメモリがすでにSwapアウトされているためです。RSS と Swap の合計で並べ替えることもできます。

for status_file in /proc/[0-9]*/status; do
    awk '
        /^Name:/   { name=$2 }
        /^VmRSS:/  { rss=$2 }
        /^VmSwap:/ { swap=$2 }
        END {
            if (rss+swap > 0)
                printf "%12d %12d %12d %s\n", rss+swap, rss, swap, name
        }
    ' "$status_file" 2>/dev/null
done | sort -nr | head -n 5

4 列は依次に、合計 KiB、RSS KiB、Swap KiB、プロセス名です。これはプロセス単位のランキングです。同じサービスが複数のプロセスを実行している場合は、さらにまとめる必要があります。

今回の調査では、CrowdSec の RSS は約 42 MiB、VmSwap は約 225 MiB で、合計はおおよそ 267 MiB でした。したがって、indeed に簡略化する価値のあるコンポーネントですが、267 MiB 全体を物理メモリとみなすことはできません。

また、「すでに Swap を使用している状態」と「現在も頻繁にページングしている状態」は区別する必要があります。

vmstat 1 10

1 行目以降のサンプリング数据显示、si と so はそれぞれ毎秒の Swap イン量と Swap アウト量です。Swap 使用量が多いこと自体は、現在継続的なページングが発生していることを意味しません。

その他の高使用量プロセスについては、PID でサービスを特定できます。

systemctl status 实际PID --no-pager --full

用途を確認してから、停止する対象を選びます。

systemctl disable --now 实际服务名.service

三、CrowdSec をバックアップして停止する

変更開始前に設定を保存することを推奨します。可能であれば、新しい構成を先に検証してから旧構成を停止することで、保護が途切れる時間を短縮できます。

以下のバックアップディレクトリは、初回移行用です。同じ操作を繰り返す場合は別のディレクトリを使い、最初の状態を上書きしないようにしてください。

install -d -m 0700 /root/security-migration-backup

cp -a /etc/crowdsec /root/security-migration-backup/
cp -a /etc/caddy /root/security-migration-backup/
cp -a /usr/bin/caddy /root/security-migration-backup/caddy.original

nft list ruleset > /root/security-migration-backup/nftables.before.nft

systemctl list-unit-files --no-pager 'crowdsec*' \
    > /root/security-migration-backup/crowdsec-units.before.txt

ここでは、元の Caddy バイナリが /usr/bin/caddy にあるものとしています。実際の場所が異なる場合は、まずサービス定義を確認します。

systemctl cat caddy

3.1 タイマーと常駐サービスを停止する

まず CrowdSec 関連のユニットを確認します。

systemctl list-unit-files --no-pager 'crowdsec*'

以下のループは Bash を使用し、検出された CrowdSec のタイマーを停止します。

while read -r crowdsec_timer _; do
    if [ -n "$crowdsec_timer" ]; then
        systemctl disable --now "$crowdsec_timer"
    fi
done < <(
    systemctl list-unit-files \
        --type=timer --no-legend --no-pager 'crowdsec*'
)

Bouncer を先に停止し、その後でエンジンを停止します。再起動されないよう、両方のユニットをマスクします。

systemctl disable --now crowdsec-firewall-bouncer.service
systemctl disable --now crowdsec.service

systemctl mask crowdsec.service crowdsec-firewall-bouncer.service

結果を確認します。

systemctl is-active crowdsec crowdsec-firewall-bouncer
systemctl is-enabled crowdsec crowdsec-firewall-bouncer

それぞれ inactive と masked になることが期待されます。SSH、Caddy、Nginx の Collection はすべて CrowdSec エンジンが実行するため、それぞれを個別に停止する必要はありません。

3.2 ファイアウォールの残留を確認する

nft list tables

デフォルトの設定では、通常 table ip crowdsec と table ip6 crowdsec6 が使用されます。停止後も残っている場合は、/etc/crowdsec/bouncers/ 以下の実際の設定と照合し、Bouncer 専用のテーブルであることを確認してから削除します。

nft delete table ip crowdsec
nft delete table ip6 crowdsec6

実際に存在し、CrowdSec に属すると確認できたテーブルに対応するコマンドだけを実行してください。nft flush ruleset は実行せず、nftables サービスも停止しないでください。

以前、CrowdSec のルールを手動で /etc/nftables.conf またはその参照ファイルに書き込んでいた場合は、該当する永続化定義も削除します。

ロールバックできるように、パッケージと設定は一時的に保持してください。サービスを停止するだけで、実行中のリソースは解放できます。

四、Caddy に GeoIP モジュールを追加する

本稿ではサードパーティモジュール porech/caddy-maxmind-geolocation を使用します。このモジュールは、MaxMind 互換のローカル MMDB データベースを使用してクライアントの送信元を照合します。

4.1 既存のバージョンとプラグインを記録する

/usr/bin/caddy version
/usr/bin/caddy list-modules --packages --versions
systemctl cat caddy

既存の DNS やレート制限などのプラグインも維持する必要があります。GeoIP モジュールだけを含む新しいバイナリで元のファイルを直接置き換えないでください。

4.2 複製したバイナリにモジュールを追加する

小規模なメモリのサーバーでは、Caddy のオンラインビルドダウンロード機能を利用すると、ローカルで Go によるコンパイルを行う必要がありません。

install -d -m 0755 /usr/local/lib/caddy

install -m 0755 \
    /usr/bin/caddy \
    /usr/local/lib/caddy/caddy

/usr/local/lib/caddy/caddy add-package \
    github.com/porech/caddy-maxmind-geolocation

add-package は Caddy を最新バージョンに更新し、登録済みの既存プラグインと新しいプラグインを含めて再構築します。元のコアバージョンが保持される保証はありません。 この時点では複製したファイルを変更しているだけで、実行中のサービスと元の /usr/bin/caddy は変更されていません。

コアとプラグインのバージョンを固定する必要がある場合は、リソースが十分な別のマシンで xcaddy を使用して指定バージョンをビルドし、対象システムとアーキテクチャに一致するバイナリをアップロードしてください。

新しいバージョン、プラグイン一覧、既存設定との互換性を確認します。

/usr/local/lib/caddy/caddy version
/usr/local/lib/caddy/caddy list-modules --packages --versions

/usr/local/lib/caddy/caddy validate \
    --config /etc/caddy/Caddyfile \
    --adapter caddyfile

既存プラグインを再ビルドできない場合や、元の設定の検証に失敗した場合は、互換性の問題を先に解決してから切り替えてください。

4.3 Country データベースをインストールする

MaxMind 公式サイト から GeoLite2 Country の MMDB データベースを取得します。ダウンロードして展開後、ファイルを /tmp/GeoLite2-Country.mmdb に置いたものとします。

install -d -o root -g caddy -m 0750 /var/lib/caddy/geoip

install -o root -g caddy -m 0640 \
    /tmp/GeoLite2-Country.mmdb \
    /var/lib/caddy/geoip/GeoLite2-Country.mmdb

runuser -u caddy -- test -r \
    /var/lib/caddy/geoip/GeoLite2-Country.mmdb

最後のコマンドが正常に終了すれば、Caddy ユーザーがデータベースを読み取れることを示しています。ここではサービスの実行ユーザーとグループがともに caddy であるものとしています。実際の unit 定義に合わせてください。

国または地域だけを確認する場合は、Country データベースで十分です。Xray の geoip.dat は、このモジュールが直接使用できる MMDB ファイルではありません。

五、地域、手動 IP、CDN オリジンのホワイトリストを個別に管理する

5.1 3 つのマッチャー設定を準備する

3 つのファイルを /etc/caddy/ に配置します。各ファイルはサイト単位のマッチャーを定義します。

geo_whitelist.conf は許可する地域を定義します。送信元 IP が CN、HK、JP のいずれにも該当しない場合、@geo_denied に一致します。

# 在站点块中引用此文件。
@geo_denied {
    not {
        maxmind_geolocation {
            db_path /var/lib/caddy/geoip/GeoLite2-Country.mmdb
            allow_countries CN HK JP
        }
    }
}

custom_allowlist.conf は、Caddy が認識するクライアントアドレスに基づいて判定します。空のホワイトリストでは expression true を使用し、この場合、すべてのリクエストが @not_custom_allowed に一致します。手動で許可するアドレスを追加するには、コメント内の not client_ip 行で expression true を置き換え、実際のアドレスを入力します。

# 在站点块中引用此文件。
# 默认手动白名单为空。启用时,用下方的 not client_ip 行替换 expression true。
# 在同一行填写实际 IPv4、IPv6 或 CIDR,以空格分隔。
@not_custom_allowed expression true
# @not_custom_allowed not client_ip 203.0.113.10 198.51.100.0/24 2001:db8::10

cdn_allowlist.conf は remote_ip を使用して、CDN からオリジンサーバーへの接続元を照合します。以下の例示用ネットワークを、CDN ベンダーが提供する実際のオリジン CIDR に置き換えてください。IPv4 と IPv6 は同じ行に記述できます。

@not_cdn_origin not remote_ip 192.0.2.0/24 2001:db8:100::/48

5.2 再利用可能な 2 つの snippet を定義する

以下の 2 つの snippet は、Caddyfile のグローバルオプションブックの後、サイトブロックの外側に配置します。

(access_whitelist) {
    import /etc/caddy/custom_allowlist.conf
    import /etc/caddy/geo_whitelist.conf

    route @not_custom_allowed {
        route @geo_denied {
            respond "Forbidden" 403
        }
    }
}

(access_whitelist_with_cdn) {
    import /etc/caddy/cdn_allowlist.conf
    import /etc/caddy/custom_allowlist.conf
    import /etc/caddy/geo_whitelist.conf

    route @not_cdn_origin {
        route @not_custom_allowed {
            route @geo_denied {
                respond "Forbidden" 403
            }
        }
    }
}
Snippetホワイトリストの検査に合格する条件
access_whitelist手動 IP ホワイトリストまたは許可された地域に一致すること
access_whitelist_with_cdnCDN オリジン、手動 IP ホワイトリスト、許可された地域のいずれかに一致すること

各 route レイヤーは、対応するホワイトリストに一致しないかを検査します。すべてのレイヤーでホワイトリストに一致しない場合にのみ 403 を返します。検査に合格した後は、サイトの業務ルーティングが続行されます。

5.3 サイトブロックから参照する

各サイトで snippet を 1 つ選択します。両方の snippet は @not_custom_allowed と @geo_denied を定義するため、同じサイトで重複してインポートしないようにしてください。

direct.example.com {
    import access_whitelist

    reverse_proxy 127.0.0.1:8080
}

cdn.example.com {
    import access_whitelist_with_cdn

    reverse_proxy 127.0.0.1:9090
}

この 2 つのサイトでは、Caddy のデフォルトの順序で route が reverse_proxy より先に実行されます。站点が handle、handle_path または redir を使用する場合は、変換後のルート順序を確認し、ホワイトリストの検査が、(Response が先に返る可能性がある業務処理より前に来るようにしてください。

/usr/local/lib/caddy/caddy adapt \
    --config /etc/caddy/Caddyfile \
    --adapter caddyfile \
    --pretty

5.4 CDN オリジン接続とクライアント IP

access_whitelist_with_cdn は、CDN からのオリジンリクエストを独立した許可条件として扱います。ホワイトリストに登録された CDN ノードからオリジンサーバーへリクエストされた場合、オリジンサーバーは手動 IP と GeoIP の検査を省略します。そのため、このオリジンサーバー側のルールは、CDN の背後にいる訪問者の地域を制限しません。CDN からのオリジン接続だけを許可する場合や、CDN 背後の訪問者の地域も検査する場合は、目的に応じた異なるルールを設計してください。

手動ホワイトリストと GeoIP の照合には、Caddy が認識するクライアント IP が使用されます。CDN 背後で訪問者アドレスを検査するには、CDN ベンダーの手順に従って trusted_proxies とクライアント IP ヘッダーを設定してください。ホワイトリストの検査に合格した後は、既存の mTLS やアプリケーション認証が通常どおり実行されます。

六、systemd で独立した Caddy バイナリを使用させる

独立したパスと drop-in を使用することで、後から APT が /usr/bin/caddy を更新した際に、現在使用中のカスタムバイナリが上書きされるのを防げます。カスタムバイナリは、更新も引き続き手動で管理する必要があります。

install -d -m 0755 /etc/systemd/system/caddy.service.d

vim /etc/systemd/system/caddy.service.d/90-geoip-binary.conf

次の内容を書き込みます。

[Service]
ExecStart=
ExecStart=/usr/local/lib/caddy/caddy run --environ --config /etc/caddy/Caddyfile

ExecReload=
ExecReload=/usr/local/lib/caddy/caddy reload --config /etc/caddy/Caddyfile --force

元のサービスが異なる引数、設定パス、またはアダプターを使用している場合は、実際の起動コマンドに合わせて調整してください。

検証してから切り替えます。

/usr/local/lib/caddy/caddy validate \
    --config /etc/caddy/Caddyfile \
    --adapter caddyfile

検証に成功したら、次のコマンドを実行します。

systemctl daemon-reload
systemctl restart caddy

systemctl status caddy --no-pager
journalctl -u caddy -n 50 --no-pager

バイナリを初めて切り替える場合は restart が必要になり、接続が短時間中断されます。その後は設定を変更する場合のみ、次のコマンドを使用します。

systemctl reload caddy

異なる地域の出口、または手動ホワイトリストに含まれないテストサーバーからテストします。

curl -I https://app.example.com/

許可された送信元はバックエンドの処理まで到達し、それ以外の送信元は 403 を返す必要があります。ローカルおよび内部ネットワークのアドレスには通常、国や地域の归属がありません。ローカル監視が必要な場合は、必要な IP を明示的にホワイトリストに追加してください。

七、Fail2ban で SSH を保護する

本稿で実際に検証した Fail2ban のバージョンは、Debian 12 リポジトリが 제공하는 1.0.2 です。

7.1 インストールと確認

apt update
apt install fail2ban python3-systemd nftables

fail2ban-client -V

/usr/sbin/sshd -T | grep '^port '
ss -lntp | grep sshd
journalctl -u ssh.service -n 20 --no-pager

以下の設定は SSH ポートが 22 であることを前提としています。実際のポートが 22 でない場合は、必ず変更してください。

7.2 SSH jail を作成する

vim /etc/fail2ban/jail.d/90-sshd.local

次の内容を書き込みます。

[sshd]
enabled = true

backend = systemd
journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd
filter = sshd[mode=normal]

port = 22
protocol = tcp
banaction = nftables[type=multiport]

usedns = no
ignoreip = 127.0.0.1/8 ::1

findtime = 10m
maxretry = 5
bantime = 1h

同じ送信元から 10 分以内に失敗イベントが 5 件累積すると、SSH ポートが 1 時間封禁されます。一致するイベント数が接続数と等しいとは限りません。

journalmatch の + は、2 つの条件グループ間の OR を表します。systemd バックエンドを使用する場合は、ファイル型の logpath を設定しないでください。固定で信頼できる管理用の送信元がある場合は、必要に応じて ignoreip を追加できます。

.local ファイルは設定のオーバーライドに使用し、パッケージに同梱された .conf ファイルは変更しないようにします。

7.3 検証して起動

fail2ban-client -t

チェックが成功したら:

systemctl enable fail2ban
systemctl restart fail2ban

数秒待ってから、もう一度確認します:

fail2ban-client ping
fail2ban-client status
fail2ban-client status sshd

ping が pong を返し、Jail リストには有効にする必要のある項目だけが含まれていることを確認します。旧設定で有効になっている他の jail がある場合は、対応する .local ファイル内で明示的に無効にしてください。

現在の SSH セッションは維持し、別のターミナルを開いて通常の鍵認証ログインを確認します。

八、2 つの一般的な起動問題

8.1 SSH ログファイルが見つからない

ERROR Failed during configuration: Have not found any log file for sshd jail

通常、現在の jail はファイルログを検索していますが、実際にはシステムが journal を使用しています。まず journalctl -u ssh.service でログの存在を確認し、その後 backend = systemd を設定して python3-systemd をインストールし、再起動します。

8.2 クライアントが socket を見つけられない

ERROR Failed to access socket path: /var/run/fail2ban/fail2ban.sock.
Is fail2ban running?

これはクライアントの接続失敗であり、この行だけからサービスの起動失敗と断定することはできません。今回のデプロイでは、クライアントは起動直後の同じ秒にチェックを開始しましたが、サービスは次の秒に Server ready を報告し,稍後に確認すれば正常です。

トラブルシューティングコマンド:

fail2ban-client -t
systemctl status fail2ban --no-pager --full
journalctl -u fail2ban -b -n 80 --no-pager
tail -n 80 /var/log/fail2ban.log

socket を手動で作成する必要はありません。サービスプロセスによって作成されます。

次の表示が得られた場合:

WARNING 'allowipv6' not defined in 'Definition'. Using default one: 'auto'

これはデフォルト値 auto を使用する際の警告であり、今回のデプロイではこのために起動に失敗したわけではありません。

九、Fail2ban の実際の遮断効果の検証

9.1 まず IPv4、IPv6 のルール生成を検証する

資料で予約されているアドレスを使用してルールの同期をテストします:

fail2ban-client set sshd banip 203.0.113.10
fail2ban-client set sshd banip 2001:db8::10

nft list table inet f2b-table

セット内にアドレスが生成されるか、関連するルールが実際の SSH ポートに一致するかを確認します。デフォルトの nftables アクションは、初回の一時禁止時にのみルールを作成する可能性があるため、まだ禁止がない場合はテーブルが表示されないことがあり、必ずしも障害ではありません。

テストが完了したら、禁止を解除します:

fail2ban-client set sshd unbanip 203.0.113.10
fail2ban-client set sshd unbanip 2001:db8::10

このテストは実行経路を検証しますが、ログ検出条件と実際のネットワーク遮断の両方が検証済みであることを示すものではありません。禁止記録はサービスの再起動後に復元される場合があるため、テスト終了後は明示的に解除してください。

9.2 別の管理対象サーバーから接続を検証する

対象サーバー上で、管理下にある別のサーバーの実際の公衆 IP を禁止します:

fail2ban-client set sshd banip 测试服务器实际公网IPv4

次に、禁止されたサーバー上で、新しい接続を確立します:

nc -4 -vz -w 5 目标服务器实际公网IPv4 22

SSH ハンドシェイクも観察できます:

ssh -4 -vvv \
    -o ConnectTimeout=5 \
    -o ConnectionAttempts=1 \
    -p 22 testuser@目标服务器实际公网IPv4

例中の中国語プレースホルダーとポートは、どちらも実際の値に置き換える必要があります。デフォルトの nftables アクションは reject のため、接続は即座に拒否と表示されることがあります。drop に変更した場合は、通常タイムアウトとして現れます。SSH のホストキーのプロンプトまたは認証段階まで到達できる場合、この新しい接続はその禁止によって阻止されていないことを示します。

必要に応じて、対象サーバーでパケットキャプチャを行い、リクエストが到達したか確認します:

tcpdump -ni any -nn \
    'host 测试服务器实际公网IPv4 and tcp port 22'

tcpdump で SYN が見えることは、ファイアウォールが通過を許可したことを意味しません。

解除後、テストサーバーから同じ接続を繰り返します:

fail2ban-client set sshd unbanip 测试服务器实际公网IPv4

禁止時に接続が失敗し、解除後に接続が成功する ことが、直感的な対比結果になります。明示的な IPv4 アドレスと -4 を使用し、接続が実際には IPv6 を通り、禁止対象は IPv4 だけだった状況を避けます。現在の唯一の管理用接続元を禁止テストに使用しないでください。

十、プラグインのオーバーヘッドとデータベースの保守

確認した時点のプロジェクトのソースコードに基づけば、この GeoIP プラグインは初回の一致処理時にデータベースの Reader を開き、その後は再利用します。マッチャーの実行時にはローカル照会を行い、リモートの GeoIP API リクエストや追加の IP 照会結果キャッシュはありません。

使用している maxminddb-golang は Linux で mmap に対応しています。マッピングファイルも物理メモリを消費しますが、データベースファイルのサイズを追加の Go ヒープメモリ量と直接みなすことはできません。snippet は設定の再利用のためのものであり、すべての위가同じプラグインインスタンスを共有することを保証しません。同じファイルのマップ済みページは、OS に共有させることができます。

個人向けサービスでは追加オーバーヘッドは小さいと予想されますが、この時点では本機上で測定した明確な QPS、照会遅延、プラグインの純増メモリは示されていません。WebSocket などの長接続では、確立後のデータフレームごとにこの GeoIP チェックが再実行されるわけではありません。

Country データベースのみを使用し、通常運用では不要なグローバル debug を無効にすることで、不要なデータ量とリクエストごとのデバッグログを削減できます。

Caddy 全体の実際のメモリ使用量を確認します:

systemctl show caddy -p MemoryCurrent

caddy_pid=$(systemctl show caddy -p MainPID --value)

awk '/^(Rss|Pss|Swap|SwapPss):/' \
    "/proc/$caddy_pid/smaps_rollup"

データベースは継続的に保守する必要があります。更新時は、まず同じディレクトリに新しいファイルを作成し、その後リネームで置き換えてください。mmap で使用中の元のファイルを直接切り詰めないようにします:

install -o root -g caddy -m 0640 \
    /tmp/GeoLite2-Country.mmdb \
    /var/lib/caddy/geoip/GeoLite2-Country.mmdb.new

mv -f \
    /var/lib/caddy/geoip/GeoLite2-Country.mmdb.new \
    /var/lib/caddy/geoip/GeoLite2-Country.mmdb

systemctl reload caddy

ここでの reload は前述の drop-in の --force に依存しており、設定テキストが変更されていない場合でも再読み込みされます。プラグイン自体はデータベースを自動ダウンロードしません。

十一、受け入れとロールバック

受け入れ時に次を確認します:

  • CrowdSec と Firewall Bouncer は停止し、関連 timer は無効になっている。
  • Caddy が実際に実行している新しいバイナリに、GeoIP と既存のプラグインが含まれている。
  • 直接接続するサイトでは、手動ホワイトリストと許可された地域からのリクエストが許可され、それ以外の送信元には 403 が返される。CDN の除外を有効にしたサイトでも、許可リスト内のオリジンノードが許可される。
  • Fail2ban の SSH jail が、新しい journal ログの処理を開始している。
  • IPv4 と IPv6 のルール生成、および実際の接続による禁止/解除の対比テストが成功している。
  • 通常の SSH 鍵認証ログインと既存のアプリケーションへのアクセスに、想定外の影響がない。

サービスの状態と全体のメモリ使用量を確認します:

systemctl is-active caddy fail2ban
systemctl is-active crowdsec crowdsec-firewall-bouncer

free -h

元の構成へ戻す必要がある場合:

systemctl disable --now fail2ban

cp -a /root/security-migration-backup/caddy/. /etc/caddy/

rm /etc/systemd/system/caddy.service.d/90-geoip-binary.conf

systemctl daemon-reload
systemctl restart caddy

systemctl unmask crowdsec.service crowdsec-firewall-bouncer.service
systemctl enable --now crowdsec.service
systemctl enable --now crowdsec-firewall-bouncer.service

この手順により、元の /usr/bin/caddy と CrowdSec の設定は保持されます。以前に timer も有効にしていた場合は、バックアップされたユニット状態に従って該当する項目を復元してください。Bouncer の起動後は判定が再同期されるため、古いファイアウォールのスナップショット全体を無条件に上書きする必要はありません。

参考資料