多数の問題点で躓く

はじめに

HetznerのVPSでcowrie SSHハニーポットを運用している。今回は「ハニーポットにログインが一切記録されない」という問題から始まり、3時間の格闘を経て解決したものの、翌朝また別の問題が発覚するという連続トラブルの記録だ。原因を一つひとつ潰していく過程で、インフラ管理の本質的な難しさを学んだ。


前日:cowrieにログインが記録されない問題

症状

cowrieのログを確認すると、cowrie.login.successが一度も記録されていなかった。攻撃者は毎日何千回もパスワードを試しているはずなのに、ログイン成功どころか失敗すら記録されていない。

sudo cat /home/cowrie/cowrie/var/log/cowrie/cowrie.json | \
  jq -r 'select(.eventid == "cowrie.login.success")'
# → 何も出ない

sudo cat /home/cowrie/cowrie/var/log/cowrie/cowrie.json | \
  jq -r 'select(.eventid == "cowrie.login.failed") | .password' | \
  sort | uniq -c | sort -rn | head -20
# → 何も出ない

調査:切り分けの手順

まずcowrieが正常に動いているか確認した。

sudo systemctl status cowrie
# → active (running) ✅

sudo ss -tlnp | grep XXXX
# → LISTEN 0.0.0.0:XXXX ✅

プロセスもListenも正常。ではパケットが届いているか。MacからXXXX番に接続してtcpdumpで確認した。

sudo tcpdump -i eth0 port XXXX -n

パケットは届いていた。SYNが来ている。しかしSYN-ACKが返っていない。cowrieまで届いていない状態だ。


バグ1:cowrie.cfgのSSHリスナーポートが間違っていた

設定ファイルを確認すると、根本的な設定ミスが見つかった。

sudo grep -n "listen_endpoints" /home/cowrie/cowrie/etc/cowrie.cfg
213: listen_endpoints = tcp:XXXX:interface=0.0.0.0  ← 本物のSSHと同じポート!
637: listen_endpoints = tcp:XXXX:interface=0.0.0.0  ← 本来ここに書くべきポート

iptablesがXXXX番→XXXX番(cowrieのリスナー)に転送しているのに、cowrieは別のポートをListenしていた。転送されたパケットを誰も受け取っていない状態だった。

213行目の設定値をcowrieのリスナーポートに修正し、再起動した。


バグ2:ufwとHetznerファイアウォールの両方にルールがなかった

ポートを修正してもまだ繋がらない。tcpdumpでパケットが届いているのにSYN-ACKが返っていない。

sudo ufw status numbered

OS側のufwにcowrie用ポートのALLOWルールが存在しなかった。さらにHetznerのコンソールで確認すると、クラウド側のファイアウォールにも該当ポートのインバウンドルールがなかった。

iptablesの内部転送より前の段階で、2重にブロックされていたわけだ。

Hetznerコンソールでインバウンドルールを追加し、あわせてufwにもALLOWを追加することでcowrieへの接続が届くようになった。

注意: これらはテスト用に追加したもの。本番運用ではiptablesが内部転送するため、cowrie用ポートを外部に公開する必要はない。動作確認後に両方とも削除済み。


バグ3:userdb.txtの日本語コメントがASCIIデコードエラーを起こしていた

cowrieへの接続はできるようになったが、パスワードを入力しても弾かれる。cowrie.login.failedすら記録されない。

UserDBを直接テストしてみた。

sudo -u cowrie /home/cowrie/cowrie-env/bin/python3 -c "
import sys
sys.path.insert(0, '/home/cowrie/cowrie/src')
from cowrie.core.auth import UserDB
db = UserDB()
print(db.checklogin(b'root', b'1234', '127.0.0.1'))
"
Traceback (most recent call last):
  ...
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe6 in position 2: ordinal not in range(128)

原因が判明した。userdb.txtに日本語のコメントが含まれていて、cowrieのPythonパーサーがASCIIとして読もうとしてクラッシュしていた。UserDB全体がロードされないため、どんなパスワードも認証できない状態になっていた。

# 日本語コメントと空行を全削除
sudo sed -i '/^#/d' /home/cowrie/cowrie/etc/userdb.txt
sudo sed -i '/^$/d' /home/cowrie/cowrie/etc/userdb.txt
sudo systemctl restart cowrie

教訓: userdb.txtにはASCII文字のみ。日本語コメントは禁止。


バグ4:etc_pathが相対パスだった

UserDBのテストをしていて、もう一つの問題に気づいた。

sudo -u cowrie python3 -c "
import sys, os
sys.path.insert(0, '/home/cowrie/cowrie/src')
from cowrie.core.config import CowrieConfig
etc = CowrieConfig.get('honeypot', 'etc_path')
print('cwd:', os.getcwd())
print('full path:', os.path.join(os.getcwd(), etc, 'userdb.txt'))
print('exists:', os.path.exists(os.path.join(os.getcwd(), etc, 'userdb.txt')))
"
cwd: /home/XXXX/projects/XXXX/notes/XXXX
full path: /home/XXXX/projects/XXXX/notes/XXXX/etc/userdb.txt
exists: False

etc_path = etcという相対パスの設定が、実行ディレクトリに依存してしまっていた。別の作業ディレクトリから実行していたため、全く関係のない場所を参照していた。

# 修正前
etc_path = etc

# 修正後
etc_path = /home/cowrie/cowrie/etc

動作確認:ついにログイン成功

4つのバグを修正した後、cowrieに接続してパスワードを入力すると:

root@prod-db01:~#

偽の環境に入れた。cowrieのホスト名prod-db01が表示されている。

sudo tail -20 /home/cowrie/cowrie/var/log/cowrie/cowrie.json | \
  jq '{eventid, username, password}'
{
  "eventid": "cowrie.login.success",
  "username": "root",
  "password": "1234"
}

cowrie.login.successが記録された。約3時間かけて、4つのバグが同時に重なっていた問題を解決した。


翌朝:また攻撃が来ていない

症状

翌朝、cowrie.jsonを確認すると外部からの攻撃がほぼゼロになっていた。

sudo cat /home/cowrie/cowrie/var/log/cowrie/cowrie.json | \
  jq -r 'select(.eventid == "cowrie.session.connect") | .src_ip' | \
  sort | uniq -c | sort -rn

自分のMacからのテスト接続しか記録されていない。昨日あれだけ苦労して直したのに、なぜまた動かないのか。

一方でauth.logを見ると、本物のSSHへの攻撃は相変わらず活発だった。パケットはサーバーに届いている。ではなぜcowrieに来ないのか。


原因:再起動でiptablesルールが消えた

iptablesのルールを確認した。

sudo iptables -t nat -L PREROUTING -n -v
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination

空だった。XXXX番→XXXX番のリダイレクトルールが消えていた。

原因はシンプルだった。以前、iptables-persistentufwが競合する問題があったためiptables-persistentを削除していた。これ自体は正しい判断だったが、代替の永続化手段を用意しないまま運用を続けていた。iptablesのルールはメモリ上にしか存在しないため、再起動すると消える。

ログで再起動時刻を確認すると:

reboot   system boot  Sat Feb 28 02:10   still running

02:10 UTCはバンクーバー時間(UTC-8)で前日18時10分。「昨日の夕方に再起動した」という記憶と一致した。cowrieの問題を解決した直後の再起動で、iptablesのルールが消えていたのだ。

ちなみにこのログを見て気づいたことがあった。サーバーはフィンランドにあるためログはUTC表記になっている。02:10を見て「深夜2時の再起動」と思いそうになるが、実際は前日夕方だった。ログを読む際はタイムゾーンに注意が必要だ。


修正:iptablesルールの復元と永続化

まずルールを手動で復元:

sudo iptables -t nat -A PREROUTING -p tcp --dport XXXX -j REDIRECT --to-port XXXX
sudo iptables -t nat -L PREROUTING -n -v
# → ルールが1行表示されればOK

次に永続化:systemdサービスを作成する

iptables-persistentの代わりに、起動時にiptablesコマンドを実行するsystemdサービスを作成した。

sudo vim /etc/systemd/system/cowrie-redirect.service
[Unit]
Description=iptables redirect port XXXX to cowrie
After=network.target

[Service]
Type=oneshot
ExecStart=/sbin/iptables -t nat -A PREROUTING -p tcp --dport XXXX -j REDIRECT --to-port XXXX
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable cowrie-redirect
sudo systemctl start cowrie-redirect
sudo systemctl status cowrie-redirect
● cowrie-redirect.service
     Active: active (exited)

active (exited)は正常。oneshotサービスはコマンドを1回実行して終了するためexitedが正しい状態だ。enabledになっているので次回再起動後も自動復元される。

ルール重複に注意:

手動でルールを追加した後にsystemdサービスでも追加したため、ルールが2つ重複した。1つ削除して解消した。

sudo iptables -t nat -D PREROUTING -p tcp --dport XXXX -j REDIRECT --to-port XXXX
sudo iptables -t nat -L PREROUTING -n -v
# → 1行だけ表示されればOK

全体を通じた学び

複数のバグが重なっていた

昨日の問題は4つが同時に重なっていた。1つ直しても別の問題が残るため、症状が変わるたびに原因を再調査する必要があった。「直したはずなのにまだ動かない」という状況はこういうことで、複数のバグが重なっているときは特に一つひとつの切り分けが重要になる。

「動いている」と「再起動後も動く」は別物

今回最も印象に残った教訓がこれだ。昨日苦労して直したcowrieは確かに動いていた。しかし再起動という普通のオペレーションで、また壊れた。

何かを削除・変更したとき、「再起動したらどうなるか」を常に意識する必要がある。今回はiptables-persistentを削除した時点で「次の再起動でルールが消える」という時限爆弾を抱えていた。

IPベースの防御には限界がある

AbuseIPDBへの報告やufwでのIPブロックはコミュニティへの貢献になるが、根本的な解決にはならない。攻撃者はIPアドレスを次々と変えながら攻撃を続け、クラウドサービスで新しいインスタンスを数ドルで即座に作れる。

本質的な防御は別のところにある。

  • 鍵認証のみ(パスワード認証無効)
  • rootログイン禁止
  • fail2ban

この3つはIPが何万個変わっても突破できない壁だ。cowrieはその上で「どうせ入れないなら観察しよう」という発想のツールで、防御というよりインテリジェンス収集に近い。


まとめと正直な現状

2日間で合計5つの問題を修正した。

問題原因対処
cowrieにパケットが届かないlisten_endpointsのポートが誤りcowrie.cfgを修正
ufwとHetznerがcowrieをブロック両方にALLOWルールなしテスト用に追加(本番では削除)
UserDBがロードされないuserdb.txtに日本語コメントASCII文字のみに変更
userdb.txtが見つからないetc_pathが相対パス絶対パスに変更
再起動後にcowrieへ攻撃が来ないiptablesルールが消えるsystemdサービスで永続化

一通り動くようにはなった。しかし正直に言うと、まだ不安が残っている。

ネットワーク、iptables、ufw、systemd、Pythonの挙動——これだけの要素が絡み合う構成を、ゼロから独力で設計したわけではない。問題が起きるたびに調べながら対処してきた結果として今の構成がある。「他にまだ気づいていない穴があるのではないか」という感覚は常にある。

それ以上に怖いのは、本番環境への侵入だ。cowrieはXXXX番ポートへの攻撃を受け止めているが、本物のSSHが動いているXXXX番は鍵認証のみで守っている。理屈の上では安全なはずだが、毎日4000回以上叩かれているログを見ると、「本当に大丈夫か」という気持ちになる。

セキュリティの世界では「完全に安全な状態」は存在しない。今できることを積み重ねながら、理解を深めていくしかない。このブログもその記録の一つだ。