コンテンツにスキップ

セキュリティガイド (Security Guide)

バージョン: v3.2.1


Copyright (c) 2026 Masanori Sakai

Licensed under the MIT License


概要

流星検出システムを安全に運用するためのセキュリティガイドラインです。

目次


セキュリティリスク

リスク評価

graph TB
    subgraph "高リスク"
        Risk1["RTSP認証情報の漏洩<br/>影響: カメラへの不正アクセス"]
        Risk2["Webインターフェースへの不正アクセス<br/>影響: 検出結果の閲覧・削除"]
    end

    subgraph "中リスク"
        Risk3["検出結果ファイルの流出<br/>影響: プライバシー侵害"]
        Risk4["Dockerコンテナの脆弱性<br/>影響: システム侵害"]
    end

    subgraph "低リスク"
        Risk5["ログファイルからの情報漏洩<br/>影響: 設定情報の露出"]
    end

    style Risk1 fill:#f8d7da
    style Risk2 fill:#f8d7da
    style Risk3 fill:#ffe3c4
    style Risk4 fill:#ffe3c4
    style Risk5 fill:#fff1bf

脅威モデル

脅威 攻撃者 影響 対策優先度
RTSP認証情報の平文保存 内部者、マルウェア カメラ乗っ取り
Webポートの外部公開 外部攻撃者 データ漏洩
検出画像からの位置特定 第三者 プライバシー侵害
Docker脆弱性の悪用 外部攻撃者 システム侵害
ログファイルの漏洩 内部者 設定情報露出

RTSP認証情報の保護

問題点

デフォルト設定:

streamers ファイル(平文):
rtsp://username:password@10.0.1.25/live

補足: | 昼間画像パス を行末に付けることも可能ですが、画像ファイルにもアクセス制限を推奨します。

⚠️ リスク: - 認証情報が平文で保存 - Gitにコミットすると公開される - ファイルシステムアクセスで閲覧可能


対策1: .envファイルの使用 ★推奨

ステップ1: .envファイルを作成

# .env ファイルを作成
cat > .env << 'EOF'
# RTSP認証情報
RTSP_USER_CAMERA1=username1
RTSP_PASS_CAMERA1=password1

RTSP_USER_CAMERA2=username2
RTSP_PASS_CAMERA2=password2

RTSP_USER_CAMERA3=username3
RTSP_PASS_CAMERA3=password3

# カメラIPアドレス
CAMERA1_IP=10.0.1.25
CAMERA2_IP=10.0.1.3
CAMERA3_IP=10.0.1.11
EOF

# パーミッション制限(重要!)
chmod 600 .env

ステップ2: .gitignoreに追加

echo ".env" >> .gitignore

ステップ3: streamersファイルを環境変数参照に変更

# streamers ファイル(認証情報なし)
cat > streamers << 'EOF'
${CAMERA1_IP}
${CAMERA2_IP}
${CAMERA3_IP}
EOF

※ 昼間画像を指定する場合は | camera1.jpg のように行末に追加します(画像ファイルの権限にも注意)。

ステップ4: generate_compose.pyを修正

カスタムスクリプトで.envから読み込み:

# generate_compose_secure.py
import os
from dotenv import load_dotenv

load_dotenv()  # .envファイルを読み込み

# RTSP URLを構築
rtsp_url1 = f"rtsp://{os.getenv('RTSP_USER_CAMERA1')}:{os.getenv('RTSP_PASS_CAMERA1')}@{os.getenv('CAMERA1_IP')}/live"

または、docker-compose.ymlで直接参照:

services:
  camera1:
    environment:
      - RTSP_URL=rtsp://${RTSP_USER_CAMERA1}:${RTSP_PASS_CAMERA1}@${CAMERA1_IP}/live

対策2: Docker Secrets(本番環境推奨)

ステップ1: Secretsファイルを作成

# secretsディレクトリを作成
mkdir -p secrets
chmod 700 secrets

# 各カメラの認証情報を作成
echo "username1:password1" > secrets/camera1_auth
echo "username2:password2" > secrets/camera2_auth
echo "username3:password3" > secrets/camera3_auth

# パーミッション制限
chmod 600 secrets/*

ステップ2: .gitignoreに追加

echo "secrets/" >> .gitignore

ステップ3: docker-compose.ymlに設定

services:
  camera1:
    secrets:
      - camera1_auth
    environment:
      - RTSP_URL_FILE=/run/secrets/camera1_auth
      - CAMERA_IP=10.0.1.25

secrets:
  camera1_auth:
    file: ./secrets/camera1_auth
  camera2_auth:
    file: ./secrets/camera2_auth
  camera3_auth:
    file: ./secrets/camera3_auth

ステップ4: エントリーポイントスクリプトで読み込み

# entrypoint.sh
#!/bin/bash
AUTH=$(cat /run/secrets/camera1_auth)
USERNAME=$(echo $AUTH | cut -d: -f1)
PASSWORD=$(echo $AUTH | cut -d: -f2)
RTSP_URL="rtsp://${USERNAME}:${PASSWORD}@${CAMERA_IP}/live"

exec python meteor_detector_rtsp_web.py "$RTSP_URL" "$@"

対策3: 認証情報のローテーション

# 定期的にパスワードを変更(推奨: 3ヶ月ごと)

# 1. カメラ側のパスワードを変更
# 2. .envファイルを更新
# 3. コンテナを再起動
./meteor-docker.sh restart

対策4: 読み取り専用ファイルシステム

services:
  camera1:
    read_only: true  # コンテナを読み取り専用に
    tmpfs:
      - /tmp
      - /var/tmp

ネットワークセキュリティ

ポート公開の最小化

デフォルト(全ポート公開)

ports:
  - "8080:8080"  # ダッシュボード
  - "8081:8080"  # カメラ1
  - "8082:8080"  # カメラ2
  - "8083:8080"  # カメラ3
  - "1984:1984"  # go2rtc (WebRTC使用時)

⚠️ リスク: すべてのポートが外部に公開。特に go2rtc の 1984 番ポートを外部公開する場合は、信頼できるネットワークに限定してください。


推奨: ダッシュボードのみ公開

services:
  dashboard:
    ports:
      - "8080:8080"  # ダッシュボードのみ公開

  camera1:
    # ポート公開なし(内部ネットワークのみ)
    expose:
      - "8080"

効果: - カメラコンテナに外部から直接アクセス不可 - ダッシュボード経由のアクセスのみ


高セキュリティ: localhostのみ

ports:
  - "127.0.0.1:8080:8080"  # localhostのみ

効果: - 同一ホストからのみアクセス可能 - リモートアクセスはSSHポートフォワーディング経由

SSHポートフォワーディング:

# リモートからアクセス
ssh -L 8080:localhost:8080 user@meteor-server

# ブラウザで http://localhost:8080/ にアクセス

ファイアウォール設定

UFW(Ubuntu)

# デフォルトポリシー
sudo ufw default deny incoming
sudo ufw default allow outgoing

# SSH許可(重要!ロックアウト注意)
sudo ufw allow 22/tcp

# ダッシュボードポート(信頼できるIPのみ)
sudo ufw allow from 192.168.1.0/24 to any port 8080

# 有効化
sudo ufw enable
sudo ufw status

firewalld(CentOS/RHEL)

# ダッシュボードポート(信頼できるIPのみ)
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" port port="8080" protocol="tcp" accept'

sudo firewall-cmd --reload
sudo firewall-cmd --list-all

VPN経由のアクセス(高セキュリティ)

graph LR
    User["ユーザー"]
    VPN["VPNサーバー"]
    Meteor["流星検出システム"]

    User -->|"暗号化トンネル"| VPN
    VPN -->|"プライベートネットワーク"| Meteor

    style VPN fill:#dce6ff
    style Meteor fill:#e2eafc

推奨VPNソリューション: - WireGuard(高速・軽量) - OpenVPN(汎用性高い) - Tailscale(簡単設定)


HTTPSの導入(リバースプロキシ)

Nginxでリバースプロキシ

# /etc/nginx/sites-available/meteor
server {
    listen 443 ssl http2;
    server_name meteor.example.com;

    ssl_certificate /etc/letsencrypt/live/meteor.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/meteor.example.com/privkey.pem;

    # セキュリティヘッダー
    add_header Strict-Transport-Security "max-age=31536000" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;

    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket対応(MJPEGストリーム用)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

# HTTP -> HTTPS リダイレクト
server {
    listen 80;
    server_name meteor.example.com;
    return 301 https://$server_name$request_uri;
}

Let's Encrypt証明書の取得

# Certbotのインストール
sudo apt install certbot python3-certbot-nginx

# 証明書の取得
sudo certbot --nginx -d meteor.example.com

# 自動更新の設定
sudo systemctl enable certbot.timer

ファイルシステムのセキュリティ

パストラバーサル対策

ダッシュボードは /image/, /detection/, /manual_recording/ 等のエンドポイントでリクエストパスを受け取ります。現行実装では以下の多層防御を適用しています。

  • 許可リストによるサフィックス検証: 手動録画削除 (DELETE /manual_recording/) はリクエストパスの拡張子として .mp4 のみを許可しています。削除実行時は同名の .jpg サムネイルも自動削除されますが、これはサーバー側で制御された処理であり外部からパスを指定することはできません。go2rtcアセット (/go2rtc_asset/) は video-stream.js / video-rtc.js のみを許可しています。
  • manual_recordings ディレクトリ要件: 手動録画パスには manual_recordings パート含有を必須チェックしています。
  • Path.resolve() による境界確認: 解決後の絶対パスが DETECTIONS_DIR 配下に収まっていることを確認し、.. 等を含む上位ディレクトリへのアクセスを拒否しています。
  • URLエンコード統一: dashboard.py の各ルートでは urllib.parse.quote により subpath をエンコードしてから内部ハンドラへ渡し、デコード前の .. 混入を防いでいます。
  • 録画系 API のアクセス制御: v3.2.0 で追加された録画予約 (POST /camera_recording_schedule/<index>) および録画停止 (POST /camera_recording_stop/<index>) 、v3.2.1 で追加された手動録画削除 (DELETE /manual_recording/) はダッシュボード経由でのみ使用可能です。これらのエンドポイントを外部に直接公開しないよう、リバースプロキシやファイアウォールで制御してください。

これらの対策を維持するため、ファイルパスを扱う新規エンドポイントを追加する際は同様の検証ロジックを組み込んでください。

ディレクトリパーミッション

# プロジェクトディレクトリ
chmod 755 /path/to/meteo

# streamersファイル(認証情報あり)
chmod 600 /path/to/meteo/streamers

# .envファイル
chmod 600 /path/to/meteo/.env

# 検出結果ディレクトリ
chmod 755 /path/to/meteo/detections

# スクリプト
chmod 700 /path/to/meteo/meteor-docker.sh

検出結果の匿名化

位置情報の削除

# EXIFデータを削除(位置情報など)
sudo apt install exiftool

# 画像からメタデータを削除
find ./detections -name "*.jpg" -exec exiftool -all= {} \;

自動匿名化スクリプト

# anonymize.sh
#!/bin/bash
DETECTIONS_DIR="./detections"

# 新しい画像を検索
find "$DETECTIONS_DIR" -name "*.jpg" -mmin -5 | while read img; do
    # EXIFデータを削除
    exiftool -all= "$img"
    # 元のファイルを削除
    rm "${img}_original"
done

Dockerセキュリティ

イメージのスキャン

# Trivyのインストール
sudo apt install trivy

# イメージの脆弱性スキャン
trivy image meteo-camera1:latest
trivy image meteo-dashboard:latest

# 重大度HIGH以上のみ表示
trivy image --severity HIGH,CRITICAL meteo-camera1:latest

イメージの署名と検証

# Docker Content Trust有効化
export DOCKER_CONTENT_TRUST=1

# イメージのビルド(署名付き)
docker build --tag myregistry.com/meteo-camera1:latest .
docker push myregistry.com/meteo-camera1:latest

最小権限の原則

services:
  camera1:
    user: "1000:1000"  # 非rootユーザーで実行
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE  # 必要な権限のみ追加
    security_opt:
      - no-new-privileges:true

リソース制限

services:
  camera1:
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M

アクセス制御

Basic認証の追加

Nginxで実装

# パスワードファイルの作成
sudo apt install apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd admin

# Nginx設定
# /etc/nginx/sites-available/meteor
location / {
    auth_basic "Meteor Detection System";
    auth_basic_user_file /etc/nginx/.htpasswd;

    proxy_pass http://localhost:8080;
}

ダッシュボードに認証を追加

dashboard.pyを拡張:

import base64
from functools import wraps

def require_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = self.headers.get('Authorization')
        if not auth:
            self.send_response(401)
            self.send_header('WWW-Authenticate', 'Basic realm="Login Required"')
            self.end_headers()
            return

        # "Basic dXNlcjpwYXNz" から認証情報を取得
        encoded = auth.split(' ')[1]
        decoded = base64.b64decode(encoded).decode('utf-8')
        username, password = decoded.split(':')

        # 認証チェック(環境変数から取得推奨)
        if username == os.getenv('DASHBOARD_USER') and \
           password == os.getenv('DASHBOARD_PASS'):
            return f(*args, **kwargs)

        self.send_response(403)
        self.end_headers()
        return

    return decorated

class DashboardHandler(BaseHTTPRequestHandler):
    @require_auth
    def do_GET(self):
        # ... 既存のコード

IPアドレス制限

docker-compose.ymlでの制限

services:
  dashboard:
    networks:
      meteor-net:
        ipv4_address: 172.20.0.10
    # iptablesルールと組み合わせて制限

iptablesでの制限

# 特定IPからのみ許可
sudo iptables -A INPUT -p tcp --dport 8080 -s 192.168.1.0/24 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 8080 -j DROP

# 永続化
sudo iptables-save > /etc/iptables/rules.v4

セキュリティチェックリスト

デプロイ前

  • [ ] RTSP認証情報を.envファイルに移動
  • [ ] .envファイルのパーミッションを600に設定
  • [ ] .gitignoreに.envとsecretsを追加
  • [ ] streamersファイルから認証情報を削除
  • [ ] docker-compose.ymlから平文パスワードを削除
  • [ ] 不要なポートを閉じる(go2rtc の 1984 番ポートは WebRTC を使用する場合のみ公開)
  • [ ] go2rtc ポート (1984) を外部公開する場合は信頼できるネットワークに限定
  • [ ] ファイアウォール設定を確認
  • [ ] Dockerイメージの脆弱性スキャン

運用中

  • [ ] 定期的なパスワード変更(3ヶ月ごと)
  • [ ] ログファイルの監視(不正アクセスの検知)
  • [ ] Dockerイメージの定期更新
  • [ ] 検出結果のバックアップ
  • [ ] ディスク容量の監視
  • [ ] セキュリティパッチの適用

インシデント対応

  • [ ] 不正アクセスの兆候を確認
  • [ ] ログファイルを保全
  • [ ] 影響範囲を特定
  • [ ] パスワードを変更
  • [ ] システムを一時停止
  • [ ] 原因を調査
  • [ ] 再発防止策を実施

セキュリティインシデント対応

不正アクセスの検知

# アクセスログの確認
docker compose logs dashboard | grep -E "GET|POST|DELETE"

# 異常なアクセスパターンを検索
# 例: 大量のDELETEリクエスト
docker compose logs dashboard | grep DELETE | wc -l

# 外部IPからのアクセス
docker compose logs dashboard | grep -v "127.0.0.1"

インシデント発生時の対応

flowchart TD
    Detect["不正アクセス検知"]
    Stop["システム停止"]
    Preserve["ログ保全"]
    Analyze["影響範囲分析"]
    Change["認証情報変更"]
    Patch["脆弱性修正"]
    Restart["システム再開"]
    Report["インシデントレポート作成"]

    Detect --> Stop
    Stop --> Preserve
    Preserve --> Analyze
    Analyze --> Change
    Change --> Patch
    Patch --> Restart
    Restart --> Report

    style Detect fill:#f8d7da
    style Stop fill:#ffe3c4
    style Restart fill:#dff6e8

ステップ1: 即座に停止

# すべてのコンテナを停止
./meteor-docker.sh stop

# ネットワークを切断(緊急時)
docker network disconnect meteor-net meteor-dashboard

ステップ2: ログの保全

# ログをバックアップ
docker compose logs > incident-$(date +%Y%m%d-%H%M%S).log

# システムログも保存
sudo journalctl -u docker > system-incident-$(date +%Y%m%d-%H%M%S).log

ステップ3: 認証情報の変更

# すべてのRTSPパスワードを変更
vim .env

# Dockerイメージを再ビルド
./meteor-docker.sh build

ステップ4: 脆弱性の修正

# システムの更新
sudo apt update && sudo apt upgrade -y

# Dockerイメージの更新
docker compose pull
./meteor-docker.sh build

セキュリティ監査

定期監査スクリプト

#!/bin/bash
# security-audit.sh

echo "=== セキュリティ監査 ==="
echo "実行日時: $(date)"
echo ""

# 1. ファイルパーミッション確認
echo "[1] ファイルパーミッション"
ls -la .env streamers 2>/dev/null || echo "  .env または streamers が見つかりません"
echo ""

# 2. 平文パスワードの検索
echo "[2] 平文パスワードの検索"
grep -r "rtsp://.*:.*@" . --exclude-dir=.git 2>/dev/null && \
    echo "  ⚠️  平文パスワードが見つかりました" || \
    echo "  ✓ 平文パスワードは見つかりませんでした"
echo ""

# 3. 公開ポートの確認
echo "[3] 公開ポート"
docker compose ps --format "table {{.Name}}\t{{.Ports}}"
echo ""

# 4. 脆弱性スキャン
echo "[4] Dockerイメージの脆弱性"
if command -v trivy &> /dev/null; then
    trivy image --severity HIGH,CRITICAL --quiet meteo-camera1:latest
else
    echo "  trivyがインストールされていません"
fi
echo ""

# 5. ログの異常確認
echo "[5] ログの異常確認(過去24時間)"
ERROR_COUNT=$(docker compose logs --since 24h | grep -i error | wc -l)
echo "  エラー件数: $ERROR_COUNT"
if [ $ERROR_COUNT -gt 10 ]; then
    echo "  ⚠️  エラーが多発しています"
fi
echo ""

echo "=== 監査完了 ==="

関連ドキュメント