ABEJA Tech Blog

中の人の興味のある情報を発信していきます

余ったPCで検証用のプライベートクラウドを構築してネットワークで遊んでみた

※ 本記事は一部 AI を用いて執筆しております。
内容に関しましては基本的に筆者が慎重にレビューや動作確認、ファクトチェックを行なっておりますが、万が一問題と思われる点ございましたら、コメント欄にてご指摘くださいませ。

1. イントロダクション:なぜ今更、自前で環境を構築するのか

1.1 プライベートクラウド回帰の潮流とセキュリティの課題

パブリック クラウド サービス、みなさん使っていますか?

勉強のために色々と検証用の VM 沢山立てたりしたいですよね?

でも一方でリソースを作りすぎ、使いすぎでコストが爆発したりするのも怖いですよね....?

本記事ではそんな慎重派の方でも簡単にクラウド環境構築の学習を、 Proxmox VE (Virtual Environment) という OSS のサーバー仮想化プラットフォームを利用上で Nginx を動かし、さらに Suricata を導入して頑健性のある環境に仕立てあげてみましょう。

(余談ですが、AWS サービスをローカル環境で再現する LocalStack という OSS もあります 。今回はよりベアメタルサーバーに近い環境の検証構築をするために Proxmox VE を選択しています。)

実は近年コストの圧縮、データの主権性、そしてレイテンシの制御といった観点から、オンプレミス環境やコロケーション環境における「プライベートクラウド」の価値が見直されており、海外では移行した事例を紹介する記事がちらほら出てたりします*1 *2

一方で、プライベートクラウドではセキュリティに関してパブリッククラウドより慎重にならなければなりません。

特に、K8s のようなコンテナオーケストレーションシステムを導入する場合、接続パターンが膨大かつ動的に変化するため、従来の境界防御モデル (FirewallによるIP/Port制限) を適用しただけでは適切なアクセルコントロールを実現することは困難で、マイクロサービス間の通信、外部からのIngressトラフィック、そして暗号化されたHTTPS通信の中に潜む脅威を検知・遮断する必要があります。

そのため自前で IDPS を用意して、よりセキュアな環境を構築して(自前でそのような環境を用意するのがどれぐらい大変か体感して)みましょう。

1.2 本記事の目的

本稿は、Proxmox VE上に構築されたNginxに対し、商用アプライアンスに匹敵する「エンタープライズレベル」の可用性とセキュリティを目指したものになります。

具体的には、以下の技術スタックを統合し、単一障害点(SPOF)のない、かつ多層防御を実現するアーキテクチャを設計・構築します。

Keepalived: VRRPを用いた高可用性(HA)の確保。

HAProxy: 高度なレイヤー4/7ロードバランシングとSSLオフロード。

Suricata: 次世代IDS/IPSエンジンによる、復号済みトラフィックのインライン検査。

Nginx: アプリケーションへのルーティングとProxy Protocolの処理。

2. アーキテクチャ設計論:防御と可用性の融合

2.1 全体像

┌─────────────────────────────────────────────────────────────────────────────┐
│                           Proxmox VE ホスト                                  │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                      vmbr0 (Linux Bridge)                           │    │
│  │                    192.168.1.0/24 ネットワーク                        │    │
│  └──────┬──────────────┬──────────────┬──────────────┬─────────────────┘    │
│         │              │              │              │                       │
│    ┌────┴────┐    ┌────┴────┐    ┌────┴────┐    ┌────┴────┐                 │
│    │  lb-1   │    │  lb-2   │    │  web-1  │    │  web-2  │                 │
│    │ VM 101  │    │ VM 102  │    │ VM 201  │    │ VM 202  │                 │
│    │ .11     │    │ .12     │    │ .21     │    │ .22     │                 │
│    └─────────┘    └─────────┘    └─────────┘    └─────────┘                 │
│                                                                              │
│                        VIP: 192.168.1.100                                   │
└─────────────────────────────────────────────────────────────────────────────┘

環境

  • Proxmox VE: 8.x
  • ゲストOS: Ubuntu 24.04 LTS (Cloud-Init イメージ使用)
  • ストレージ: local-lvm
VM ID hostname IP role vCPU RAM
101 lb-1 192.168.1.11 ロードバランサー1(MASTER) 2 2GB
102 lb-2 192.168.1.12 ロードバランサー2(BACKUP) 2 2GB
201 web-1 192.168.1.21 Webサーバー1 2 2GB
202 web-2 192.168.1.22 Webサーバー2 2 2GB
- VIP 192.168.1.100 仮想IP(VRRP) - -

また、構築するシステムの論理的なトラフィックフローは以下です。

ステップ コンポーネント アクション プロトコル/状態 詳細解説
1 Client リクエスト送信 HTTPS (Encrypted) インターネットからのアクセス。
2 Keepalived VIP受付 VRRP 稼働中のGatewayノードへトラフィックを誘導。
3 HAProxy (Frontend) 受信 HTTP 今回はただのLBとして扱いますが、HTTPSの通信を扱う場合には、ここでSSL/TLSを解除し、パケットの中身(ヘッダ、ボディ)を露出させます。
4 Netfilter (iptables) キューイング NFQUEUE HAProxyからバックエンドへ向かうパケットをカーネルレベルで捕捉。
5 Suricata インライン検査 IPS Mode ユーザー空間でパケットを検査。シグネチャマッチでDrop/Passを判定。
6 HAProxy (Backend) 転送 HTTP + Proxy Protocol 検査済みパケットにクライアントIP情報を付与して送信。
7 Nginx ルーティング HTTP Proxy Protocolを解釈し、正しい送信元IPでログ記録・転送。

2.2 コンポーネント選定の技術的根拠

Proxmox VE:インフラの基盤

ベアメタルハイパーバイザーとしての効率性と、Debianベースの運用しやすさが選定理由です。特に、virtioドライバによるネットワークI/Oの準ネイティブなパフォーマンスは、パケット処理を主とするGatewayノードでも比較的高い性能を発揮してくれるでしょう。

Keepalived:シンプルかつ堅牢なHA

Pacemaker/Corosyncのような重量級クラスタリングソフトウェアと比較し、KeepalivedはVRRP(Virtual Router Redundancy Protocol)に特化しており、設定がシンプルで誤動作のリスクが低い点が強みです。HAProxyのプロセス監視機能(vrrp_script)と組み合わせることで、OSが生きていてもサービスが死んでいる場合に即座にフェイルオーバーを実現できます。

HAProxy:高性能ロードバランサ

Nginxもロードバランサとして優秀ですが、HAProxyは「ロードバランシング専業」として設計されており、キューイングの制御、詳細なヘルスチェック、管理画面(Stats Page)の視認性において優位性があります。

Suricata:マルチスレッドIPS

Snort(特に2.x系)がシングルスレッドであったのに対し、Suricataは当初からマルチスレッドアーキテクチャを採用しており、現代のマルチコアCPUの性能を最大限に引き出せます。また、NFQUEUE モードにおける安定性と、L7プロトコル解析(HTTP, DNS, TLS等)の精度の高さから、インラインIPSとして利用されるのを目にします。また、AWSのNetwork FirewallにはSuricata互換のIPSルールを設定できたりしますので、ネットワークを構築する方は一度眺めてみておくと良いかもしれません。


3. インフラストラクチャ構築

3.0 物理サーバー確保

街を歩いていると、なぜか生えてきます。

私は以下の構成の物理サーバーが気がついたら生えてきていました。

  • サーバー本体: NUC8i3BEH
  • メモリ: SO-DIMM DDR 2666MHz 16GB *3
  • ストレージ: SanDisk Ultra 3D SSD *4

3.1 Proxmoxネットワーク構成

Proxmoxでは標準のLinux Bridge(vmbr)またはOpen vSwitch(OVS)が利用可能ですが、複雑なSDN(Software Defined Networking)を構築しない限り、カーネルネイティブなLinux Bridgeの方がオーバーヘッドが少なく、構築やトラブルシューティングも比較的容易なので、今回は1つのLinux Bridgeで配下で全てのVMを繋げます。

3.1.1 Linux Bridgeの確認・設定

Proxmox VEのWebUI → ノード → System → Network で vmbr0 が存在することを確認します。

存在しない場合、または追加のブリッジが必要な場合は、SSHでProxmoxホストに接続して設定します。

/etc/network/interfaces の例:

# Proxmoxホストで実行
cat << 'EOF' > /etc/network/interfaces
auto lo
iface lo inet loopback

# 物理NIC
iface enp0s3 inet manual

# VMネットワーク用ブリッジ
auto vmbr0
iface vmbr0 inet static
    address 192.168.1.1/24
    gateway 192.168.1.254
    bridge-ports enp0s3
    bridge-stp off
    bridge-fd 0
EOF

# ネットワーク設定を適用
ifreload -a

注意: enp0s3 は実際のNIC名に置き換えてください。ip link で確認できます。


3.2 VMの作成

VM を効率的に作成するため、Ubuntu Cloud-Init テンプレートを作成します。

3.2.1 Cloud-Initイメージのダウンロード

# Proxmoxホストで実行
cd /var/lib/vz/template/iso/

# Ubuntu 24.04 Cloud イメージをダウンロード
wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img

# QEMUのディスクイメージサイズを拡張(7GB)
qemu-img resize noble-server-cloudimg-amd64.img 7G

3.2.2 テンプレートVMの作成

# テンプレートVM作成(VM ID: 9000)
qm create 9000 --name "ubuntu-2404-cloudinit-template" \
    --ostype l26 \
    --memory 2048 \
    --cores 2 \
    --cpu host \
    --net0 virtio,bridge=vmbr0 \
    --scsihw virtio-scsi-pci \
    --serial0 socket \
    --vga serial0 \
    --agent enabled=1

# ディスクをインポート
qm set 9000 --scsi0 local-lvm:0,import-from=/var/lib/vz/template/iso/noble-server-cloudimg-amd64.img

# Cloud-Initドライブを追加
qm set 9000 --ide2 local-lvm:cloudinit

# ブートデバイスを設定
qm set 9000 --boot order=scsi0

# Cloud-Initの基本設定
qm set 9000 --ipconfig0 ip=dhcp
qm set 9000 --ciuser ubuntu
qm set 9000 --cipassword "ChangeMe123!"  # 変更してください

# テンプレートに変換
qm template 9000

3.2.3 テンプレートの確認

qm list | grep template

これで、テンプレートが完成しました!

これをクローンしてどしどしVMを作っていきましょう。

3.3: VMのクローンと作成

3.3.1 ロードバランサーVM の作成

# lb-1 (VM ID: 101)
qm clone 9000 101 --name lb-1 --full
qm set 101 --memory 2048 --cores 2
qm set 101 --ipconfig0 ip=192.168.1.11/24,gw=192.168.1.254
qm set 101 --ciuser ubuntu
qm set 101 --cipassword "LB1Pass123!"

# lb-2 (VM ID: 102)
qm clone 9000 102 --name lb-2 --full
qm set 102 --memory 2048 --cores 2
qm set 102 --ipconfig0 ip=192.168.1.12/24,gw=192.168.1.254
qm set 102 --ciuser ubuntu
qm set 102 --cipassword "LB2Pass123!"

3.3.2 WebサーバーVM の作成

# web-1 (VM ID: 201)
qm clone 9000 201 --name web-1 --full
qm set 201 --memory 2048 --cores 2
qm set 201 --ipconfig0 ip=192.168.1.21/24,gw=192.168.1.254
qm set 201 --ciuser ubuntu
qm set 201 --cipassword "Web1Pass123!"

# web-2 (VM ID: 202)
qm clone 9000 202 --name web-2 --full
qm set 202 --memory 2048 --cores 2
qm set 202 --ipconfig0 ip=192.168.1.22/24,gw=192.168.1.254
qm set 202 --ciuser ubuntu
qm set 202 --cipassword "Web2Pass123!"

3.3.3 全VMの起動

# 全VMを起動
qm start 101
qm start 102
qm start 201
qm start 202

# 起動確認
qm list

4. アプリケーションのセットアップ

4.1 Webサーバー(Nginx)のセットアップ

4.1.1 web-1, web-2 へ SSH 接続

# Proxmoxホストまたは同一ネットワークから接続
ssh ubuntu@192.168.1.21  # web-1
ssh ubuntu@192.168.1.22  # web-2

4.1.2 Nginx のインストール(両方のWebサーバーで実行)

# rootに昇格
sudo -i

# パッケージ更新とNginxインストール
apt update && apt upgrade -y
apt install nginx -y

# サービス有効化
systemctl enable nginx
systemctl start nginx

4.1.3 テストページの作成

web-1 で実行:

cat << 'EOF' > /var/www/html/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Server 1</title>
    <style>
        body {
            font-family: 'Segoe UI', Arial, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
        }
        .container {
            text-align: center;
            padding: 50px;
            background: rgba(255,255,255,0.15);
            border-radius: 20px;
            backdrop-filter: blur(10px);
            box-shadow: 0 8px 32px rgba(0,0,0,0.3);
        }
        h1 { font-size: 2.5em; margin-bottom: 20px; }
        .server-info { 
            background: rgba(0,0,0,0.2);
            padding: 15px;
            border-radius: 10px;
            margin-top: 20px;
        }
        .status { color: #00ff88; font-weight: bold; }
    </style>
</head>
<body>
    <div class="container">
        <h1>🖥️ Web Server 1</h1>
        <div class="server-info">
            <p><strong>ホスト名:</strong> web-1</p>
            <p><strong>IP:</strong> 192.168.1.21</p>
            <p><strong>ステータス:</strong> <span class="status">● Active</span></p>
        </div>
        <p style="margin-top:20px; font-size:0.9em;">
            HAProxy + Keepalived + Suricata + Nginx
        </p>
    </div>
</body>
</html>
EOF

web-2 で実行:

cat << 'EOF' > /var/www/html/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Server 2</title>
    <style>
        body {
            font-family: 'Segoe UI', Arial, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
            background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
            color: white;
        }
        .container {
            text-align: center;
            padding: 50px;
            background: rgba(255,255,255,0.15);
            border-radius: 20px;
            backdrop-filter: blur(10px);
            box-shadow: 0 8px 32px rgba(0,0,0,0.3);
        }
        h1 { font-size: 2.5em; margin-bottom: 20px; }
        .server-info { 
            background: rgba(0,0,0,0.2);
            padding: 15px;
            border-radius: 10px;
            margin-top: 20px;
        }
        .status { color: #00ff88; font-weight: bold; }
    </style>
</head>
<body>
    <div class="container">
        <h1>🖥️ Web Server 2</h1>
        <div class="server-info">
            <p><strong>ホスト名:</strong> web-2</p>
            <p><strong>IP:</strong> 192.168.1.22</p>
            <p><strong>ステータス:</strong> <span class="status">● Active</span></p>
        </div>
        <p style="margin-top:20px; font-size:0.9em;">
            HAProxy + Keepalived + Suricata + Nginx
        </p>
    </div>
</body>
</html>
EOF

4.1.4 動作確認

# 各Webサーバーで実行
curl http://localhost
systemctl status nginx

4.2: ロードバランサー(HAProxy + Keepalived + Suricata)のセットアップ

4.2.1 lb-1, lb-2 へ SSH 接続

ssh ubuntu@192.168.1.11  # lb-1
ssh ubuntu@192.168.1.12  # lb-2

4.2.2 パッケージのインストール(両方のLBで実行)

sudo -i

apt update && apt upgrade -y
apt install haproxy keepalived suricata -y

4.3 HAProxy の設定

4.3.1 HAProxy設定(lb-1, lb-2 共通)

cat << 'EOF' > /etc/haproxy/haproxy.cfg
#---------------------------------------------------------------------
# Global settings
#---------------------------------------------------------------------
global
    log /dev/log    local0
    log /dev/log    local1 notice
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
    stats timeout 30s
    user haproxy
    group haproxy
    daemon

#---------------------------------------------------------------------
# Defaults
#---------------------------------------------------------------------
defaults
    log     global
    mode    http
    option  httplog
    option  dontlognull
    option  forwardfor
    option  http-server-close
    timeout connect 5000
    timeout client  50000
    timeout server  50000
    errorfile 400 /etc/haproxy/errors/400.http
    errorfile 403 /etc/haproxy/errors/403.http
    errorfile 408 /etc/haproxy/errors/408.http
    errorfile 500 /etc/haproxy/errors/500.http
    errorfile 502 /etc/haproxy/errors/502.http
    errorfile 503 /etc/haproxy/errors/503.http
    errorfile 504 /etc/haproxy/errors/504.http

#---------------------------------------------------------------------
# Statistics page
#---------------------------------------------------------------------
listen stats
    bind *:8404
    stats enable
    stats uri /stats
    stats refresh 10s
    stats auth admin:HaProxyAdmin123  # 本番環境では変更

#---------------------------------------------------------------------
# Frontend: HTTP (port 80)
#---------------------------------------------------------------------
frontend http_front
    bind *:80
    option tcplog
    default_backend http_back

#---------------------------------------------------------------------
# Backend: Nginx Web Servers
#---------------------------------------------------------------------
backend http_back
    balance roundrobin
    option httpchk GET /
    http-check expect status 200
    
    server web-1 192.168.1.21:80 check inter 2000 rise 2 fall 3
    server web-2 192.168.1.22:80 check inter 2000 rise 2 fall 3
EOF

# 設定確認
haproxy -c -f /etc/haproxy/haproxy.cfg

# サービス有効化・起動
systemctl enable haproxy
systemctl restart haproxy
systemctl status haproxy

4.4 Keepalived の設定

4.4.1 カーネルパラメータの設定(lb-1, lb-2 両方)

# VIPバインド用の設定
cat << 'EOF' >> /etc/sysctl.conf
# Allow binding to non-local IP addresses
net.ipv4.ip_nonlocal_bind = 1
EOF

sysctl -p

4.4.2 lb-1(MASTER)の設定

cat << 'EOF' > /etc/keepalived/keepalived.conf
# lb-1 keepalived config (MASTER)

global_defs {
    router_id LB1
    script_user root
    enable_script_security
}

vrrp_script check_haproxy {
    script "/usr/bin/killall -0 haproxy"
    interval 2
    weight 2
    fall 2
    rise 2
}

vrrp_instance VI_1 {
    state MASTER
    interface eth0   # ここでエラーが発生する場合には、 `ip a` を実行して正しい名前確認してください
    virtual_router_id 51
    priority 100
    advert_int 1
    
    authentication {
        auth_type PASS
        auth_pass K33p@l1v3d!         # 適宜変更してください
    }
    
    unicast_src_ip 192.168.1.11
    unicast_peer {
        192.168.1.12
    }
    
    virtual_ipaddress {
        192.168.1.100/24
    }
    
    track_script {
        check_haproxy
    }
}
EOF

systemctl enable keepalived
systemctl restart keepalived

4.4.3 lb-2(BACKUP)の設定

cat << 'EOF' > /etc/keepalived/keepalived.conf
# lb-2 keepalived config (BACKUP)

global_defs {
    router_id LB2
    script_user root
    enable_script_security
}

vrrp_script check_haproxy {
    script "/usr/bin/killall -0 haproxy"
    interval 2
    weight 2
    fall 2
    rise 2
}

vrrp_instance VI_1 {
    state BACKUP
    interface eth0   # ここでエラーが発生する場合には、 `ip a` を実行して正しい名前確認してください
    virtual_router_id 51
    priority 99
    advert_int 1
    
    authentication {
        auth_type PASS
        auth_pass K33p@l1v3d!         # 適宜変更してください
    }
    
    unicast_src_ip 192.168.1.12
    unicast_peer {
        192.168.1.11
    }
    
    virtual_ipaddress {
        192.168.1.100/24
    }
    
    track_script {
        check_haproxy
    }
}
EOF

systemctl enable keepalived
systemctl restart keepalived

4.4.4 ネットワークインターフェース名の確認

# 実際のインターフェース名を確認
ip link show

# 通常、Proxmox Cloud-Init VMでは ens18 または eth0
# 必要に応じて keepalived.conf の interface を修正

4.5 Suricata(IDS)の設定

4.5.1 基本設定(lb-1, lb-2 両方)

# バックアップ
cp /etc/suricata/suricata.yaml /etc/suricata/suricata.yaml.bak

# 主要な設定を編集
sudo cat << 'EOF' > /etc/suricata/suricata.yaml
%YAML 1.1
---

vars:
  address-groups:
    HOME_NET: "[192.168.1.0/24]"
    EXTERNAL_NET: "!$HOME_NET"
    HTTP_SERVERS: "[192.168.1.21/32, 192.168.1.22/32]"
    DNS_SERVERS: "$HOME_NET"
    SMTP_SERVERS: "$HOME_NET"
    SQL_SERVERS: "$HOME_NET"
    TELNET_SERVERS: "$HOME_NET"
    AIM_SERVERS: "$EXTERNAL_NET"
    DC_SERVERS: "$HOME_NET"
    DNP3_SERVER: "$HOME_NET"
    DNP3_CLIENT: "$HOME_NET"
    MODBUS_CLIENT: "$HOME_NET"
    MODBUS_SERVER: "$HOME_NET"
    ENIP_CLIENT: "$HOME_NET"
    ENIP_SERVER: "$HOME_NET"

  port-groups:
    HTTP_PORTS: "80"
    SHELLCODE_PORTS: "!80"
    ORACLE_PORTS: 1521
    SSH_PORTS: 22
    DNP3_PORTS: 20000
    MODBUS_PORTS: 502
    FILE_DATA_PORTS: "[$HTTP_PORTS,110,143]"
    FTP_PORTS: 21
    GENEVE_PORTS: 6081
    VXLAN_PORTS: 4789
    TEREDO_PORTS: 3544

default-log-dir: /var/log/suricata/

stats:
  enabled: yes
  interval: 8

outputs:
  - fast:
      enabled: yes
      filename: fast.log
      append: yes

  - eve-log:
      enabled: yes
      filetype: regular
      filename: eve.json
      pcap-file: false
      community-id: true
      community-id-seed: 0
      types:
        - alert:
            tagged-packets: yes
            payload: yes
            payload-buffer-size: 4kb
            payload-printable: yes
            packet: yes
            metadata: yes
            http-body: yes
            http-body-printable: yes
        - anomaly:
            enabled: yes
            types:
              decode: yes
              stream: yes
              applayer: yes
        - http:
            extended: yes
            custom: [accept, accept-charset, accept-encoding, accept-language,
                    accept-datetime, authorization, cache-control, cookie,
                    from, max-forwards, origin, pragma, proxy-authorization,
                    range, te, via, x-requested-with, x-forwarded-for,
                    x-authenticated-user]
            dump-all-headers: both
        - dns:
            enabled: yes
            version: 2
        - tls:
            extended: yes
            session-resumption: yes
        - files:
            force-magic: yes
            force-hash: [md5, sha256]
        - drop:
            enabled: yes
        - smtp:
            extended: yes
            custom: [received, x-mailer, x-originating-ip, relays,
                    reply-to, bcc]
        - flow:
            enabled: yes
        - netflow:
            enabled: yes
        - stats:
            totals: yes
            threads: yes
            deltas: yes

  - pcap-log:
      enabled: yes
      filename: packets.pcap
      limit: 100mb
      max-files: 10
      compression: none
      mode: normal
      use-stream-depth: no
      honor-pass-rules: no

  - alert-debug:
      enabled: no
      filename: alert-debug.log

af-packet:
  - interface: eth0
    threads: auto
    cluster-id: 99
    cluster-type: cluster_flow
    defrag: yes
    use-mmap: yes
    tpacket-v3: yes
    ring-size: 2048
    block-size: 32768

pcap:
  - interface: eth0

pcap-file:
  checksum-checks: auto

app-layer:
  protocols:
    http:
      enabled: yes
      libhtp:
        default-config:
          personality: IDS
          request-body-limit: 100kb
          response-body-limit: 100kb
          request-body-minimal-inspect-size: 32kb
          request-body-inspect-window: 4kb
          response-body-minimal-inspect-size: 40kb
          response-body-inspect-window: 16kb
          response-body-decompress-layer-limit: 2
          http-body-inline: auto
          double-decode-path: yes
          double-decode-query: yes
    tls:
      enabled: yes
      detection-ports:
        dp: 443
      ja3-fingerprints: yes
    ssh:
      enabled: yes
    smtp:
      enabled: yes
      mime:
        decode-mime: yes
        decode-base64: yes
        decode-quoted-printable: yes
    dns:
      tcp:
        enabled: yes
      udp:
        enabled: yes

run-mode: workers

default-rule-path: /var/lib/suricata/rules
rule-files:
  - suricata.rules
  - local.rules

classification-file: /etc/suricata/classification.config
reference-config-file: /etc/suricata/reference.config

stream:
  memcap: 64mb
  checksum-validation: yes
  inline: no
  reassembly:
    memcap: 256mb
    depth: 1mb
    toserver-chunk-size: 2560
    toclient-chunk-size: 2560
    randomize-chunk-size: yes

host:
  hash-size: 4096
  prealloc: 1000
  memcap: 32mb

file-store:
  version: 2
  enabled: yes
  dir: /var/log/suricata/files
  write-fileinfo: yes
  force-filestore: no
EOF

4.5.2 ルールの更新

カスタムルールの作成

# ファイルを初期化
sudo rm -f /var/lib/suricata/rules/local.rules
sudo touch /var/lib/suricata/rules/local.rules

# SQL Injection
echo 'alert http any any -> $HTTP_SERVERS $HTTP_PORTS (msg:"LOCAL SQL Injection SELECT"; flow:to_server,established; content:"select"; nocase; http_uri; classtype:web-application-attack; sid:1000001; rev:2;)' | sudo tee -a /var/lib/suricata/rules/local.rules > /dev/null

echo 'alert http any any -> $HTTP_SERVERS $HTTP_PORTS (msg:"LOCAL SQL Injection UNION"; flow:to_server,established; content:"union"; nocase; http_uri; classtype:web-application-attack; sid:1000002; rev:2;)' | sudo tee -a /var/lib/suricata/rules/local.rules > /dev/null

echo 'alert http any any -> $HTTP_SERVERS $HTTP_PORTS (msg:"LOCAL SQL Injection quote"; flow:to_server,established; content:"|27|"; http_uri; classtype:web-application-attack; sid:1000005; rev:2;)' | sudo tee -a /var/lib/suricata/rules/local.rules > /dev/null

# XSS
echo 'alert http any any -> $HTTP_SERVERS $HTTP_PORTS (msg:"LOCAL XSS script tag"; flow:to_server,established; content:"<script"; nocase; http_uri; classtype:web-application-attack; sid:1000010; rev:2;)' | sudo tee -a /var/lib/suricata/rules/local.rules > /dev/null

echo 'alert http any any -> $HTTP_SERVERS $HTTP_PORTS (msg:"LOCAL XSS javascript"; flow:to_server,established; content:"javascript"; nocase; http_uri; classtype:web-application-attack; sid:1000011; rev:2;)' | sudo tee -a /var/lib/suricata/rules/local.rules > /dev/null

echo 'alert http any any -> $HTTP_SERVERS $HTTP_PORTS (msg:"LOCAL XSS encoded"; flow:to_server,established; content:"%3Cscript"; nocase; http_uri; classtype:web-application-attack; sid:1000013; rev:2;)' | sudo tee -a /var/lib/suricata/rules/local.rules > /dev/null

# Directory Traversal
echo 'alert http any any -> $HTTP_SERVERS $HTTP_PORTS (msg:"LOCAL Dir Traversal"; flow:to_server,established; content:"../"; http_uri; classtype:web-application-attack; sid:1000020; rev:2;)' | sudo tee -a /var/lib/suricata/rules/local.rules > /dev/null

echo 'alert http any any -> $HTTP_SERVERS $HTTP_PORTS (msg:"LOCAL etc passwd access"; flow:to_server,established; content:"/etc/passwd"; http_uri; classtype:web-application-attack; sid:1000022; rev:2;)' | sudo tee -a /var/lib/suricata/rules/local.rules > /dev/null

# Command Injection
echo 'alert http any any -> $HTTP_SERVERS $HTTP_PORTS (msg:"LOCAL Cmd Injection pipe"; flow:to_server,established; content:"|7c|"; http_uri; classtype:web-application-attack; sid:1000030; rev:2;)' | sudo tee -a /var/lib/suricata/rules/local.rules > /dev/null

echo 'alert http any any -> $HTTP_SERVERS $HTTP_PORTS (msg:"LOCAL Cmd Injection semicolon"; flow:to_server,established; content:"|3b|"; http_uri; classtype:web-application-attack; sid:1000031; rev:2;)' | sudo tee -a /var/lib/suricata/rules/local.rules > /dev/null
# suricata-update をインストール
apt install suricata-update -y

# ルールソースの追加と更新
suricata-update update-sources

# disable.conf を作成(産業用プロトコルはエラーになるので全て除外)
sudo cat << 'EOF' > /etc/suricata/disable.conf
# Industrial protocols - not needed for web server
group:modbus
group:dnp3
group:enip
group:cip
re:modbus
re:dnp3
re:enip
re:cip
2250009
2250010
2250011
2270000
2270001
2270002
EOF

# ルールを再更新 (失敗したら、手動でリトライを試してみてください)
suricata-update --disable-conf=/etc/suricata/disable.conf

# 設定テスト
suricata -T -c /etc/suricata/suricata.yaml

# サービス有効化・起動
systemctl enable suricata
systemctl restart suricata
systemctl status suricata

4.6 動作確認とテスト

4.6.1 サービス状態の確認

lb-1, lb-2 で実行:

# 各サービスの状態
systemctl status haproxy
systemctl status keepalived
systemctl status suricata

# VIPの確認(MASTERノードのみ)
ip addr show | grep 192.168.1.100

4.6.2 VIPの確認

# lb-1で確認(MASTERの場合、VIPが表示される)
ip addr show eth0

# 出力例:
# inet 192.168.1.11/24 brd 192.168.1.255 scope global eth0
# inet 192.168.1.100/24 scope global secondary eth0  ← VIP

5. テスト・検証

構築が完了したら、実際にセキュリティ機能が動作しているか検証します。

5.1.1 HTTPアクセステスト

# VIP経由でアクセス(任意のホストから)
curl http://192.168.1.100

# ラウンドロビン確認(複数回実行)
for i in {1..10}; do 
    curl -s http://192.168.1.100 | grep "Web Server"
done

5.1.2 HAProxy統計ページの確認

ブラウザで以下にアクセス:

http://192.168.1.100:8404/stats
ユーザー名: admin
パスワード: HaProxyAdmin123

5.1.3 フェイルオーバーテスト

# lb-1でHAProxyを停止
ssh ubuntu@192.168.1.11
sudo systemctl stop haproxy

# lb-2でVIPが引き継がれたか確認
ssh ubuntu@192.168.1.12
ip addr show | grep 192.168.1.100

# アクセス継続確認
curl http://192.168.1.100

# lb-1のHAProxyを再起動
ssh ubuntu@192.168.1.11
sudo systemctl start haproxy

5.1.6 Suricataログの確認

# アラートログ
tail -f /var/log/suricata/fast.log

# JSON詳細ログ
tail -f /var/log/suricata/eve.json | jq .

# 攻撃シミュレーション(テスト用)
curl "http://192.168.1.100/?id=1' OR '1'='1"

6. 実運用のイメージ

6.1 EVE JSONログの活用

Suricataは /var/log/suricata/eve.json に非常に詳細なイベントログを出力します。これにはAlertだけでなく、HTTPアクセスのメタデータ(URL、User-Agent、DNSクエリ等)やTLSハンドシェイクの詳細も含まれます。

実際の運用では、このログをローカルで見続けるのは不可能なので、以下のスタックによる可視化を試してみるのもありですね。

  • Log Shipper: Filebeat または Promtail をGateway VMに導入。
  • Storage/Search: Elasticsearch または Loki。
  • Visualization: Kibana または Grafana。

これにより、「どの国からの攻撃が多いか」「どのルールの検知頻度が高いか」をリアルタイムダッシュボードで監視できるようになります。


6.2 ルールの自動更新

脅威情報は日々更新されます。suricata-update をcronジョブに登録し、毎日自動更新する設定を行いましょう。

ただし、自動更新で新しいルールが追加され、それが誤検知を引き起こすリスクもあるため、重要な環境ではステージング環境でのテストを経てから本番適用するパイプラインが理想的です。


7. まとめ

Proxmox VE上に以下の高可用性構成を構築しました:

[クライアント]
      │
      ▼
[VIP: 192.168.1.100]
      │
      ▼
┌─────┴─────┐
│ Keepalived │ (VRRP フェイルオーバー)
└─────┬─────┘
      │
      ▼
┌─────┴─────┐
│  HAProxy   │ (ロードバランシング)
└─────┬─────┘
      │
      ▼
┌─────┴─────┐
│  Suricata  │ (IDS/侵入検知)
└─────┬─────┘
      │
      ▼
┌─────┴─────┐
│  Nginx     │ (Webサーバー)
│ (web-1/2)  │
└───────────┘

今回の記事では、構築したアーキテクチャは商用製品に勝るとも劣らない?(は言い過ぎですね)環境を構築できました!

この構成を自前で作成してみて、いかに普段のマネージドなサービスがありがたいかを再確認できました。

しかし、ブラックボックス化したマネージドサービスに依存せず、自らの手でパケットの挙動を完全に制御・把握できる環境がこれで手に入りましたので、思う存分遊んでみましょう。

We Are Hiring!

ABEJAは、テクノロジーの社会実装に取り組んでいます。 技術はもちろん、技術をどのようにして社会やビジネスに組み込んでいくかを考えるのが好きな方は、下記採用ページからエントリーください! (新卒の方やインターンシップのエントリーもお待ちしております!) careers.abejainc.com

特に下記ポジションの募集を強化しています!ぜひ御覧ください!

トランスフォーメーション領域:データサイエンティスト

トランスフォーメーション領域:データサイエンティスト(ミドル)

トランスフォーメーション領域:データサイエンティスト(シニア)