
コンテナ化されたネットワークデコイ(ハニーポット)で、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つのコンテナ:
| コンテナ | 役割 | ネットワーク |
|---|---|---|
broker | パブリックフロントドア: eBPF 観測 + リバースプロキシ | edge + decoynet |
ssh-decoy | OpenCanary ssh モジュール (実際のハンドシェイク、クレデンシャル取得) | decoynet のみ |
rdp-decoy | OpenCanary rdp モジュール (NLA 模倣、ユーザー名取得) | decoynet のみ |
smb-decoy | Impacket SimpleSMBServer (実際の SMB2/3、認証情報取得) | decoynet のみ |
デコイは、ホストや外部へのルートがない 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 モジュール | リスンポート | 実際の動作 |
|---|---|---|---|
ssh-decoy | ssh | 2222 | twisted.conch による実際の SSH 鍵交換。すべてのユーザー名/パスワードペアをキャプチャ。 |
rdp-decoy | rdp | 3389 | NLA 対応サーバーを模倣し、常にログイン失敗を返し、mstshash ユーザー名を抽出。 |
smb-decoy | Impacket | 445 | Pure-Python SMB2/3 サーバー。ベイト共有を提示し、接続と NTLM 認証試行を JSON としてログに記録。 |
OpenCanary は各イベントに数値の logtype をタグ付けします。ここで見られるもの:
| 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 接続 / ログイン試行 |
キャプチャされた 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 は別の場所でキャプチャされます: