
コンテナ化されたネットワークデコイ(ハニーポット)で、SSH、RDP、SMB をアドバタイズし、eBPF で全ての受信接続を観測し、各セッションを隔離されたデコイコンテナにリバースプロキシします。
設計では2つの関心事を分離しています:
これは、あなたが所有している、または監視する権限があるネットワーク上の不正アクティビティを検出・調査するための防御ツールです。その権限がある環境でのみ展開してください。
flowchart TB
A["攻撃者 / スキャナ"]
subgraph host["デコイホスト"]
direction TB
NIC["broker eth0<br/>公開ポート: 22, 3389, 445"]
subgraph brk["broker コンテナ"]
direction TB
E["eBPF TC クラシファイア<br/>全ての SYN をログ<br/>実際の送信元 IP を確認"]
P["リバースプロキシ<br/>バックエンドに CONNECT"]
L["構造化 JSON ログ"]
end
subgraph dec["decoynet (内部、ホストルートなし)"]
direction LR
S["ssh-decoy<br/>OpenCanary ssh<br/>ポート 2222"]
D["rdp-decoy<br/>OpenCanary rdp<br/>ポート 3389"]
M["smb-decoy<br/>Impacket SMB サーバー<br/>ポート 445"]
end
end
A --> NIC
NIC --> E
NIC --> P
E --> L
P --> S
P --> D
P --> M
合計4つのコンテナ:
デコイは、ホストや外部へのルートがない internal Docker ネットワーク (decoynet) 上に存在します。ブローカーのみが到達できます。攻撃者がデコイ内部で行うことはすべて、ホストネットワークに直接到達できません。
ブローカーはポート 22, 3389, 445 をホストに公開しているため、受信パケットはブローカーの eth0 に到着します。その後、各パケットに対して2つの処理が行われます:
broker/bpf/decoy.bpf.c) がイーサネット、IP、TCP ヘッダーを解析し、新しい接続試行 (SYN セット、ACK クリア) ごとに conn_event をリングバッファに書き込みます: 送信元 IP とポート、宛先ポート、TCP フラグ、そのポートがアドバタイズされたサービスかどうか。パケットはそのまま通過します (TC_ACT_OK)。CONNECT を実行し、双方向にバイトを中継します。advertised_ports eBPF マップは起動時に config.yaml から設定されるため、クラシファイアはプローブが提供ポートにヒットしたのか、それとも未承諾のものかをタグ付けできます。これにより、3つのポートのみがプロキシされているにもかかわらず、水平ポートスキャンが可視化されます。
「すべてのポートが開いている」とアドバタイズし、任意の宛先ポートをブローカーに送り込みたい場合は、クラシファイアを拡張して宛先ポートを書き換えるか、TPROXY / bpf_sk_assign リダイレクトを使用します。現在のバージョンではパケットパスを変更せず、観測に限定しており、これがより安全なデフォルトです。
cyber-decoy/
├── README.md
├── docker-compose.yml # 4コンテナスタック
├── docker-compose.override.yml # ローカル macOS 開発: eBPF 機能なし、ポート22 リマップ
├── Makefile # ビルド / up / down / bpf ヘルパー
├── LICENSE
├── scripts/
│ └── setup.sh # ホスト事前チェック
├── broker/
│ ├── Dockerfile # eBPF オブジェクト + Go バイナリをコンパイル
│ ├── config.yaml # アドバタイズするサービス (設定可能)
│ ├── go.mod
│ ├── main.go # エントリポイント
│ ├── bpf/
│ │ └── decoy.bpf.c # eBPF TC クラシファイア
│ └── internal/
│ ├── config/config.go # 設定ローダー
│ ├── proxy/proxy.go # TCP リバースプロキシ
│ └── bpf/loader.go # eBPF をロード・アタッチ、イベントストリーム
└── decoys/ # 3つとも OpenCanary を実行
├── ssh/
│ ├── Dockerfile
│ └── opencanary.conf # ssh モジュール、ポート 2222
├── rdp/
│ ├── Dockerfile
│ └── opencanary.conf # rdp モジュール、ポート 3389
└── smb/
├── Dockerfile # 単一 Python プロセス、非 root
├── smb_decoy.py # Impacket SimpleSMBServer + JSON ログ
└── requirements.txt # impacket (固定バージョン)
docker-compose.override.yml を使用する場合は v2.24+ 。これは !reset / !override タグに依存しています)。sudo mount -t bpf bpf /sys/fs/bpf。ブローカーイメージはビルド時に自身のアーキテクチャを検出し、適切な __TARGET_ARCH_* マクロを clang に渡すため、x86_64 と aarch64 (Apple Silicon、Graviton) の両方でビルドできます。gcc-multilib は意図的にインストール しない ことに注意してください。これは x86 のみのパッケージで arm64 の候補がなく、arm64 でビルドを apt 終了コード 100 で破壊するためです。eBPF オブジェクトのコンパイルには clang と libbpf-dev のみが必要です。
Docker Desktop on macOS はコンテナをホストカーネルではなく LinuxKit VM 内で実行するため、TC/TCX eBPF アタッチは一般に 動作しません。これは致命的ではありません。eBPF はベストエフォート設計のため、ブローカーは ebpf disabled: attach failed とログに記録し、リバースプロキシと3つのデコイは正常に動作しログを記録します。プロキシパス全体をローカルで開発・テストし、Linux ホストにデプロイする際に実際の eBPF 観測を得ることができます。
docker-compose.override.yml は自動的に読み込まれ、これを快適にします。eBPF 機能 (VM では無効) を削除し、ホストポート 22 を 2022 にリマップします(Mac の sshd が 22 を所有しているため)。
docker compose up --build # ローカル開発、オーバーライド適用
docker compose -f docker-compose.yml up -d # 本番デプロイ、オーバーライドをバイパス
最初に事前チェックを実行:
./scripts/setup.sh
# 1. 4つのイメージを全てビルド (ブローカーイメージ内で eBPF オブジェクトをコンパイル)
make build
# 2. スタックを起動
make up
# 3. 動作を監視
make logs
その後、別のマシン (またはローカルホストでのスモークテスト) からプローブ:
ssh -p 22 user@DECOY_HOST # SSH デコイにヒット
nc DECOY_HOST 3389 # RDP デコイにヒット
nc DECOY_HOST 445 # SMB デコイにヒット
nc DECOY_HOST 8080 # アドバタイズされていない: eBPF が観測、プロキシなし
ブローカーは eBPF プローブイベントとプロキシセッションの JSON を出力し、各デコイは OpenCanary JSON イベントを出力します。クレデンシャルの着地を監視:
docker compose logs -f ssh-decoy | grep 4002
バナーだけのスタブとは異なり、ssh -p 22 user@DECOY_HOST は実際の鍵交換を完了し、パスワードを要求します。すべての試行がキャプチャされます。サービスフィンガープリントがバージョン検出に耐えることを確認:
nmap -sV -p 22,3389,445 DECOY_HOST
停止:
make down
サービスは broker/config.yaml で定義します。各エントリは独立して有効/無効にでき、ポートのマッピングも変更可能です:
services:
- name: ssh
enabled: true
listen_port: 22
backend: ssh-decoy:2222
サービスを追加するには、ここにエントリを追加し、docker-compose.yml でポートを公開し、デコイコンテナを追加します。無効にするには、enabled: false に設定し (オプションで公開ポートも削除します)。
ホストポート 22 は通常、ホストの実際の SSH デーモンによって使用されています。ラボでは、docker-compose.yml で公開側をリマップできます。例: "2022:22" のように設定し、スキャナーをそのポートに向けます。
3つのデコイはすべて OpenCanary (Thinkst) を実行し、各コンテナが1つのモジュールのみを有効にするように設定されています。ログは JSON として stdout に出力されるため、docker compose logs や任意の SIEM シッパーが追加の配管なしで動作します。
OpenCanary は各イベントに数値の logtype をタグ付けします。ここで見られるもの:
キャプチャされた SSH クレデンシャルの例:
{"dst_port": 2222, "logtype": 4002, "node_id": "decoy-ssh",
"src_host": "10.0.0.66", "src_port": 42958,
"logdata": {"USERNAME": "admin", "PASSWORD": "Passw0rd123"}}
これはブローカーアーキテクチャの直接的な結果であり、これらのログを読む際に理解すべき最も重要な点です。
ブローカーは攻撃者の TCP 接続を終了し、デコイに対して 新しい 接続を開きます。したがって、OpenCanary から見ると、クライアントはブローカーです。デコイイベント内のすべての src_host は、decoynet 上のブローカーのアドレスであり、実際の送信元ではありません。
本当の送信元 IP は別の場所でキャプチャされます:
したがって、属性を特定するには、ブローカーログとデコイログをタイムスタンプとサービスで結合する必要があります。ブローカーは各セッションの remote (実際の攻撃者アドレス) と backend をログに記録しており、これにより結合が可能になります:
docker compose logs broker | grep 'session opened' # 誰が
docker compose logs ssh-decoy | grep '"logtype": 4002' # 何を試したか
デコイ内部で実際の IP が必要な場合、オプションは PROXY プロトコルを送信する (デコイは解析しないのでパッチが必要)、または元の送信元アドレスを保持する透過リダイレクト (TPROXY または eBPF bpf_sk_assign) でユーザースペースプロキシを置き換えることです。両方ともロードマップに記載されています。それまでは、「誰が」についてはブローカーを、「何を」についてはデコイを信頼の源として扱ってください。
SSH や RDP とは異なり、このデコイは OpenCanary を使用しません。OpenCanary の smb モジュールはログ監視ツールに過ぎません。実際の Samba サーバーが出力する smbd_audit 行をファイルから tail してパースするもので、これは Samba と rsyslog と opencanaryd を supervisord で実行する必要があり、5つのリンクのチェーンでどのリンクも静かに失敗する可能性がありました。
smb-decoy はこれらすべてを、Impacket の SimpleSMBServer に基づいた単一の Python プロセスに置き換えます。これは SMB1/2/3 の Pure-Python 実装です。ポート 445 にバインドし、読み取り専用のベイト共有を提示し、SMB2/3 ネゴシエーションに応答するため (nmap -sV が実際のサービスと認識)、接続と NTLM 認証試行を各行に1つの JSON オブジェクトとして stdout にログ出力します。攻撃者の認証メッセージからキャプチャされたユーザー名、ドメイン、ワークステーションがクレデンシャルキャプチャの成果です。
セキュリティ上の注意: Impacket の smbserver には重大なパストラバーサル、CVE-2021-31800 があり、特にハニーポットに影響しました。0.9.23 で修正されています。requirements.txt は現在のリリースを固定しており、それ以下にダウングレードしてはいけません。また、コンテナは非 root、読み取り専用で実行され、NET_BIND_SERVICE を除くすべてのケーパビリティが削除されています。
各デコイは opencanary.conf を所有しています (/etc/opencanaryd/ にインストール)。有用な設定項目:
decoys/ssh/opencanary.conf の ssh.version。現在は SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1 を主張しています。偽装している OS に合わせて変更してください。Windows と主張しているボックスに Ubuntu バナーは怪しまれます。decoys/smb/smb_decoy.py 内の addShare(...) 呼び出し、および decoys/smb/Dockerfile で作成されるベイトファイル。共有名とファイル名が餌です。broker/config.yaml の backend と一致するようにします。別の OpenCanary モジュール (ftp, telnet, mysql, vnc, redis など) を有効にするには、<module>.enabled と <module>.port を設定し、デコイコンテナを追加し、broker/config.yaml に対応するサービスを追加します。
ssh-decoy は名前付きボリュームを /var/lib/opencanary (ssh.key_path) にマウントするため、生成されたホスト鍵は再起動後も保持されます。これがないと OpenCanary は起動ごとに新しい鍵を生成し、フィンガープリントが変化するのは明らかな怪しさです。
SMB デコイは単一プロセスになったため、トラブルシューティングは簡単です。
docker compose logs -f smb-decoy
各行は JSON です。起動時に smb_decoy_start、クライアントが操作するにつれて smb_connect、smb_auth_attempt、smb_tree_connect のイベントが表示されるはずです。ホストから任意の SMB クライアントでテスト:
# macOS Finder: Go > Connect to Server
open 'smb://guest@localhost/HR-Payroll'
# or from Linux
smbclient -L //localhost -p 445 -N
一般的な問題:
smb_decoy_start 行がなくコンテナが終了する場合: requirements.txt が正常にインストールされているか確認してください。Impacket は Python 3.8+ が必要です。イメージは 3.12 を使用しています。smb_auth_attempt がない場合: 一部のクライアントは認証せずに匿名で共有を列挙します。それでも smb_connect と smb_tree_connect は生成されます。ユーザー名を指定して共有をマップし、強制的に認証させてください。src_host はブローカーのアドレスであり、実際の攻撃者ではありません。タイムスタンプでブローカーログと照合してください。NET_ADMIN (および最近のカーネルでは BPF / PERFMON) が必要です。Compose ファイルはこれらのスコープ指定されたケーパビリティを要求します。ホストまたは Docker のバージョンがそれらを拒否する場合、フォールバックとしてブローカーサービスに privileged: true を使用します。これはより広範であり、スコープ指定されたケーパビリティが機能しない場合にのみ使用すべきです。internal ネットワーク上にあります。その状態を維持してください。すべてのデコイコンテナを潜在的に侵害されているものとして扱ってください。bpf_sk_assign により、攻撃者の送信元 IP をデコイに保存し、ブローカーログとデコイログの相関を不要にする。MIT。 LICENSE を参照してください。
| コンテナ | 役割 | ネットワーク |
|---|
broker | パブリックフロントドア: eBPF 観測 + リバースプロキシ | edge + decoynet |
ssh-decoy | OpenCanary ssh モジュール (実際のハンドシェイク、クレデンシャル取得) | decoynet のみ |
rdp-decoy | OpenCanary rdp モジュール (NLA 模倣、ユーザー名取得) | decoynet のみ |
smb-decoy | Impacket SimpleSMBServer (実際の SMB2/3、認証情報取得) | decoynet のみ |
| コンテナ | OpenCanary モジュール | リスンポート | 実際の動作 |
|---|
ssh-decoy | ssh | 2222 | twisted.conch による実際の SSH 鍵交換。すべてのユーザー名/パスワードペアをキャプチャ。 |
rdp-decoy | rdp | 3389 | NLA 対応サーバーを模倣し、常にログイン失敗を返し、mstshash ユーザー名を抽出。 |
smb-decoy | Impacket | 445 | Pure-Python SMB2/3 サーバー。ベイト共有を提示し、接続と NTLM 認証試行を JSON としてログに記録。 |
| logtype | opencanary/logger.py 内の定数 | 意味 |
|---|
| 1000 | LOG_BASE_BOOT | デーモン起動 |
| 4000 | LOG_SSH_NEW_CONNECTION | SSH 接続開始 |
| 4001 | LOG_SSH_REMOTE_VERSION_SENT | クライアントがバージョン文字列を送信 |
| 4002 | LOG_SSH_LOGIN_ATTEMPT | SSH ログイン試行 (USERNAME と PASSWORD を含む) |
| 5000 | LOG_SMB_FILE_OPEN | SMB ファイルが開かれた (USER, SHARENAME, FILENAME を含む) |
| 14001 | LOG_RDP | RDP 接続 / ログイン試行 |
| レイヤー | 本当の送信元 IP を知っているか? | 何が試行されたかを知っているか? |
|---|
eBPF クラシファイア (probe observed) | はい | いいえ、SYN メタデータのみ |
ブローカープロキシ (session opened) | はい | いいえ、バイト数のみ |
OpenCanary デコイ (logtype 4002) | いいえ | はい、クレデンシャル/ファイル |