Let's EncryptのHTTP-01チャレンジをYAMAHA RTX830でいい感じに捌きたい

背景

前回、動的フィルタで捌こうとして(当然)失敗したのでリベンジ。

あれから結局、80番ポートを開けっ放しにして運用してたけど、不正アクセスを試みる通信が結構多くて精神的にあまりよろしくない。 普段は80番ポートは閉じておき、証明書の自動更新をするときだけ開くようにしたい。

概要

certbotから証明書更新用サーバーへの通信をRTX830上で走らせたluaスクリプトで監視。通信を検出したら80番ポートへの通信を開けるルールを投入する。 しばらく待って今度は80番ポートへの通信を遮断するルールを投入する。

前提

$ sudo certbot renew --dry-runが成功することを事前に確認しておく。

手順

証明書を更新する際に、certbotが最初に通信するサーバーを調べる。以下の2つが多分それ。2つ目の方はおそらくテスト用。後でテストするので両方対象にする。

acme-v02.api.letsencrypt.org
acme-staging-v02.api.letsencrypt.org

RTX830のDNS

RTX830のフィルタ設定でFQDNを指定するためには、RTX830に対応するDNSキャッシュを持たせる必要がある。 そのために、自宅サーバーからRTX830に対して定期的にdigで問い合わせをする。自宅サーバーのsystemd timerを使ってサクッと登録。 なお、この手順は自宅サーバーが参照するDNSサーバーとしてRTX830を指定している場合には不要。

$ mkdir -p /home/pi/dig
$ touch /home/pi/dig/dig-bach
$ vim /home/pi/dig/dig-bach
dig-batch
acme-v02.api.letsencrypt.org
acme-staging-v02.api.letsencrypt.org
$ cd /home/pi/.config/systemd/user/
$ touch acme-dig.timer
$ touch acme-dig.service
acme-dig.timer
[Unit]
Description="timer for acme dns request"

[Timer]
OnCalendar=*:0/1

[Install]
WantedBy=timers.target
acme-dig.service
[Unit]
Description="dig for acme"

[Service]
ExecStart=/usr/bin/dig -f /home/pi/dig/dig-batch @(RTX830のIPアドレス) &> /dev/null
Restart=on-failure

[Install]
WantedBy=default.target
$ systemctl --user daemon-reload
$ systemctl --user enable acme-dig.timer
$ systemctl --user list-timers --all
NEXT                        LEFT     LAST                        PASSED UNIT           ACTIVATES
Sun 2025-03-16 11:05:00 JST 53s left Sun 2025-03-16 11:04:04 JST 1s ago acme-dig.timer acme-dig.service

自宅サーバーからログアウト。RTX830にログイン。DNSキャッシュにエントリがあるかを確認

> show dns cache | grep acme
Searching ...
    A    IN    56/240   acme-staging-v02.api.letsencrypt.org (172.65.46.172)
    A    IN   236/240   acme-v02.api.letsencrypt.org (172.65.32.248)

RTX830側でのルール設定

lan2のout側にcertbotからの通信をキャッチするためのルールを登録する。 後々わかりにくくならないように、in側には80番ポートへの通信を遮断するルールをあらかじめ入れておく。

ip lan2 secure filter out 11 20 21 31 22 81 82 98 dynamic 80 81 82 83 84 98 99

ip filter 81 pass-log 自宅サーバーのIPアドレス acme-staging-v02.api.letsencrypt.org,acme-v02.api.letsencrypt.org
ip lan2 secure filter in 10 20 21 30 90 91 92 99 dynamic 90 91 92

ip filter 92 reject * 自宅サーバーのIPアドレス tcpsyn * www

これで自宅サーバーから証明書更新用サーバーに通信が飛ぶと、filter 81でパケットを通過させたログがRTX830のsyslogに出るのでそれをluaスクリプトで拾ってfilter 92のrejectをpass-logに書き換える。ある程度時間がたったらrejectに戻す。

luaスクリプトの登録

さくっと書いてRTX830に転送、RTX830上で走らせたら完了。

lets_encrypt.lua
--------------------------##  設定値  ##--------------------------------
-- 検出したい SYSLOG の文字列パターン。"()”は要エスケープ
ptn = "Passed at OUT%(81%)"

-- メールの設定(これは適宜)
mail_tbl = {
    smtp_address = "環境に合わせて記載",
    smtps = true,
    smtp_auth_name = "環境に合わせて記載",
    smtp_auth_password = "環境に合わせて記載",
    smtp_auth_protocol = "環境に合わせて記載",
    from = "環境に合わせて記載",
    to = "環境に合わせて記載"
}

-- メールの送信に失敗した時に出力する SYSLOG のレベル (info, debug, notice)
log_level = "notice"

----------------------##  設定値ここまで  ##----------------------------
------------------------------------------------------------
-- コマンドの実行結果を出力する関数                       --
------------------------------------------------------------
function exec_command(cmd)
    local rtn, str

    rtn, str = rt.command(cmd)
    if (not rtn) or (not str) then
        str = "execution failure\r\n"
    end

    return rtn, string.format("# %s\r\n%s\r\n", cmd, str)
end
------------------------------------------------------------
-- 現在の日時を取得する関数                               --
------------------------------------------------------------
function time_stamp()
    local t

    t = os.date("*t")
    return string.format("%d/%02d/%02d %02d:%02d:%02d",t.year, t.month, t.day, t.hour, t.min, t.sec)
end
------------------------------------------------------------
-- メインルーチン                                         --
------------------------------------------------------------
local rtn, str

while (true) do
    rtn, str = rt.syslogwatch(ptn)  -- SYSLOG の監視(SYSLOG の出力が行われない間、呼び出し元の Lua タスクはスリープ状態になる)。
    if (rtn) and (str) then
        rtn, str = rt.command("ip filter 92 pass-log * 自宅サーバーのIPアドレス tcpsyn * www")
        rt.sleep(20)
        rtn, str = rt.command("ip filter 92 reject * 自宅サーバーのIPアドレス tcpsyn * www")
        mail_tbl.text = string.format("Detect search string. \r\n search string: \"%s\"\r\n\r\n", ptn)
--      mail_tbl.text = mail_tbl.text .. str[1]
        mail_tbl.subject = string.format("pattern string detected (%s)", time_stamp())
        rtn = rt.mail(mail_tbl)
        rt.syslog(log_level, "executed lets_encrypt.lua")
        if (not rtn) then
            rt.syslog(log_level, "failed to send mail. lets_encrypt.lua")
        end
    end
end

以上

参考