背景と必要性
深夜2時、アラート通知が鳴り止まない事態に見舞われました。/var/log/auth.logは数百MBにまで肥大化し、見知らぬ無数のIPレンジから60,000件を超えるSSHブルートフォース攻撃が記録されていました。その時痛感したのは、「SSHポートを変更し、パスワード認証を無効化するだけでは不十分だ」という現実でした。
ポート22を2222や58222といったランダムなポートに変更するだけでは、単なる気休めのセキュリティ(Security through obscurity)に過ぎません。Masscanを使えば、わずか3分足らずで全65,535ポートのスキャンが完了してしまいます。その直後からボットネットによる大量のパケットが押し寄せ、帯域幅を圧迫し、システムログを無駄に肥大化させます。
ファイアウォール(UFW/iptables)によるIP制限はどうでしょうか?この対策は、固定IPを持つオフィスなどで作業する場合には有効です。しかし実際のところ、DevOpsエンジニアはカフェで作業したり、ネットワークを頻繁に切り替えたり、スマートフォンのテザリングを利用したりすることが日常茶飯事です。IPアドレスが頻繁に変わる環境で、手動でホワイトリストを更新し続けるのは非現実的です。
そこで登場するのが、Knockdデーモンを利用したポートノッキング(Port Knocking)です。この手法は、高いセキュリティと運用の柔軟性を完璧に両立させます。
仕組みは非常にシンプルです。ファイアウォール上でSSHポートは常にDROP状態に設定されています。攻撃者やボットネットがポートスキャンを実行しても、何のサービスも稼働していないかのように完全に不可視化されます。
一方、Knockdはバックグラウンドでネットワークインターフェースを監視しています。特定のポートとプロトコル(TCP/UDP)の順序で構成されたパケットシーケンス(ノックシーケンス)を送信すると(扉をモールス信号でノックするように)、Knockdがiptablesを呼び出し、送信元IPに対してのみSSHポートを一時的に開放します。そして一定時間(例:30秒)が経過すると、ポートは自動的に再び閉じられます。
本番環境のクラスターで6ヶ月運用した結果、SSHブルートフォースのログは完全にゼロになりました。接続時の追加の手間も、ノック処理にかかるわずか1秒程度です。
インストール
Knockdは非常に軽量なデーモンであり、メモリ消費量はわずか3〜4MB程度です。主要なLinuxディストリビューションのほとんどで利用可能です。
UbuntuまたはDebianの場合、knockdパッケージとファイアウォールルール永続化ツールのiptables-persistentをインストールします:
sudo apt update
sudo apt install knockd iptables-persistent -y
CentOS、RHEL、AlmaLinuxの場合は、EPELリポジトリ経由でインストールします:
sudo dnf install epel-release -y
sudo dnf install knock-server -y
サーバーにアクセスするクライアント端末側にも、knockクライアントをインストールしておきます:
# Ubuntu/Debianクライアントの場合
sudo apt install knockd -y
# macOSの場合(Homebrewを使用)
brew install knock
詳細な設定手順
構築手順は、ベースラインとなるファイアウォールの設定、Knockdでのノックシーケンス定義、サービスの起動の3ステップです。
1. iptablesによるSSHポート遮断の設定
作業を始める前に、現在開いているSSHセッションはそのまま維持してください。設定ミスによる締め出し(ロックアウト)を防ぐため、設定が完了するまでターミナルを絶対に閉じないでください。
# ループバックおよび確立済みの接続を継続して許可
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A INPUT -i lo -j ACCEPT
# ポート22へのすべての新規接続を遮断
sudo iptables -A INPUT -p tcp --dport 22 -j DROP
# サーバー再起動後もルールを保持するために保存
sudo netfilter-persistent save
2. /etc/knockd.conf の設定
Knockdのメイン設定ファイルを開きます:
sudo nano /etc/knockd.conf
デフォルトの内容を以下の設定に置き換えます:
[options]
UseSyslog
Interface = eth0
[opencloseSSH]
sequence = 7412:tcp,8523:tcp,9631:tcp
seq_timeout = 10
tcpflags = syn
start_command = /sbin/iptables -I INPUT -s %IP% -p tcp --dport 22 -j ACCEPT
cmd_timeout = 30
stop_command = /sbin/iptables -D INPUT -s %IP% -p tcp --dport 22 -j ACCEPT
各設定パラメータの意味は以下の通りです:
- Interface: サーバーのインターネット向けネットワークインターフェース名(
ip -br aコマンドで確認、通常はeth0やens3など)。 - sequence: ノックするポートの順序。推測されないよう独自のランダムなポート番号に変更してください。TCPとUDPを混在させることも可能です(例:
7412:udp,8523:tcp,9631:tcp)。 - seq_timeout: ノックシーケンスを完了するまでの制限時間(秒)。ここでは10秒以内に完了する必要があります。
- tcpflags = syn: TCP SYNパケットのみを検知し、浮遊する無関係なパケットによる誤作動を防ぎます。
- start_command: 正しいシーケンスを受信した際に実行するコマンド。
%IP%変数にはノック元のIPアドレスが自動的に代入されます。 - cmd_timeout: ポート開放後、Knockdが
stop_commandを実行して開放ルールを削除するまでの待機時間(秒)。事前に接続を確立したSSHセッションは、前述のESTABLISHED,RELATEDルールにより切断されることなく維持されます。
3. Knockdサービスの有効化
Ubuntuの場合、デーモンの自動起動を許可するために/etc/default/knockdを編集します:
sudo nano /etc/default/knockd
次の2行を設定・確認します:
START_KNOCKD=1
KNOCKD_OPTS="-i eth0"
設定後、サービスを再起動して自動起動を有効化します:
sudo systemctl restart knockd
sudo systemctl enable knockd
sudo systemctl status knockd
動作確認と運用
クライアント端末から動作確認を行います。
初期状態でのポート閉鎖確認
まずはNmapを使用してサーバーのポート22をスキャンしてみます:
nmap -Pn -p 22 198.51.100.24
実行結果としてfilteredが返されるはずです。この状態でssh [email protected]を実行しても、応答がなくOperation timed outになります。
ノックの実行と接続
設定したシーケンス通りにノックコマンドを実行します:
knock 198.51.100.24 7412:tcp 8523:tcp 9631:tcp
コマンドは一瞬(約0.3秒)で完了します。直後にSSH接続を試行します:
ssh [email protected]
即座に接続が成功します。30秒経過するとファイアウォールによりポートは再び閉じられますが、すでに確立されたSSHセッションは切断されることなく快適に作業を継続できます。
~/.ssh/configによる自動化
接続のたびに手動でknockコマンドを実行するのは手間がかかります。クライアント側の~/.ssh/configに設定を記述することで自動化できます:
Host my-server
HostName 198.51.100.24
User ubuntu
IdentityFile ~/.ssh/id_rsa
ProxyCommand knock %h 7412 8523 9631 && sleep 1 && nc %h %p
これで、通常通りssh my-serverと入力するだけで、クライアントが自動的にノックパケットを送信し、1秒待機してからSSH接続を確立してくれます。
サーバー側でのKnockdログ監視
状態の確認や、ノックしてもポートが開かない場合のトラブルシューティングには、systemdのジャーナルログを確認します:
sudo journalctl -u knockd -f
正常なノックシーケンスを受信した場合、次のようなログが出力されます:
knockd[1234]: 203.0.113.50: opencloseSSH: Stage 1
knockd[1234]: 203.0.113.50: opencloseSSH: Stage 2
knockd[1234]: 203.0.113.50: opencloseSSH: Stage 3
knockd[1234]: 203.0.113.50: opencloseSSH: OPEN SESAME
knockd[1234]: opencloseSSH: running command: /sbin/iptables -I INPUT -s 203.0.113.50 -p tcp --dport 22 -j ACCEPT
knockd[1234]: 203.0.113.50: opencloseSSH: command timeout
knockd[1234]: opencloseSSH: running command: /sbin/iptables -D INPUT -s 203.0.113.50 -p tcp --dport 22 -j ACCEPT
もしログがStage 1やStage 2で止まり、OPEN SESAMEまで到達しない場合は、公共Wi-Fiや経由するプロキシによってTCP SYNパケットがドロップされている可能性があります。その場合は、シーケンスをUDPプロトコルに変更するか、seq_timeoutの値を15秒程度に延長して通信の安定性を高めてみてください。

