Docker上でApache Guacamoleを踏み台サーバーとして構築:2要素認証(2FA)によるセキュアなWebベースSSH・RDP環境の実現

Security tutorial - IT technology blog
Security tutorial - IT technology blog

深夜のアラート:従来の踏み台サーバーが限界を迎えた理由

ある火曜日の午前2時14分、PagerDutyのアラートが鳴り響きました。インターネットに公開されていたジャンプサーバー(踏み台)が、毎分45,000回にも及ぶSSH認証試行の猛攻を受けて過負荷状態に陥っていたのです。/var/log/auth.logは4時間足らずで38GBを超えて肥大化し、ルートパーティションを枯渇させ、実行中だった3つのバックグラウンドジョブを停止させました。このインシデントをきっかけに、私たちは自社で運用する34台のクラウドインスタンスへのリモートアクセス方式を根本から見直すことになりました。

従来の踏み台サーバー(Bastion Host)運用には、絶え間ない運用のオーバーヘッドが伴います。エンジニアは各自のノートPCでSSH秘密鍵の管理に追われ、無数のProxyJumpディレクティブが書かれた壊れやすいOpenSSH設定ファイルとの格闘を強いられます。

さらに、Wi-Fiネットワークを切り替えるたびにデスクトップVPNクライアントの接続が切断される問題もあります。Windows環境では、3389番ポートのRDPをインターネットに公開すれば、数分と経たずにランサムウェアによる自動スキャンの標的になります。私たちが求めていたのは、すべての内部管理ポートを隠蔽し、多要素認証(MFA)を強制でき、ローカルへのエージェントやプラグインのインストールなしに最新ブラウザから直接利用できる統合アクセスゲートウェイでした。

Apache Guacamoleによるクライアントレスアクセスの仕組み

Apache Guacamoleは、クライアントレスのリモートデスクトップゲートウェイとして機能します。エンジニアのPC上でネイティブのRDPやSSHクライアントを実行する代わりに、セッションはHTML5 Webアプリケーション内で動作します。サーバー側が生のリモートデスクトッププロトコルを、標準のHTTPS(ポート443)およびWebSocket経由で最適化されたCanvas描画更新とDOMイベントのストリームへと変換します。

このスタックは主に3つのコンポーネントで構成されています。

  • guacd(Guacamole Proxy Daemon): プライベートサーバーへのアップストリームSSH、RDP、VNC接続を確立する軽量なネイティブCデーモン。ネイティブのグラフィックやターミナルバッファをGuacamoleプロトコルの命令へと変換します。
  • guacamole-client: Apache Tomcat上で動作するJavaサーブレット。管理ダッシュボードのレンダリング、ユーザー認証、guacdへのストリームデータの中継を担います。
  • PostgreSQL + TOTP拡張機能: 暗号化された接続パラメータ、ユーザー権限、監査ログを保存し、ログイン時に標準的な6桁の時間ベースワンタイムパスワード(TOTP)を要求します。

最前線にはNginxリバースプロキシを配置してTLS終端を処理します。外部トラフィックは2FAで保護されたHTTPSエンドポイントにのみ接触します。一方で、アップストリームのSSH(ポート22)やRDP(ポート3389)は、隔離されたDockerブリッジネットワークやAWS/GCPのプライベートサブネット内に安全に封じ込められます。

デプロイ手順:Docker、PostgreSQL、TOTPの設定

ベアメタル環境でGuacamoleをソースからコンパイルしようとすると、各種Cライブラリの特定、Cairoグラフィック依存関係のビルド、TomcatのXML設定ファイルの調整など多大な手間がかかります。しかしDocker Composeを使用すれば、クリーンで再現性の高い環境を構築できます。以下は、私たちの本番環境で実際に稼働している設定です。

1. ディレクトリの作成とデータベースの初期化

ディレクトリ構造を作成し、公式イメージから直接PostgreSQLのデータベーススキーマを生成します。

mkdir -p /opt/guacamole/{init,data}
cd /opt/guacamole

# 公式のPostgreSQLスキーマファイルを生成
docker run --rm guacamole/guacamole:1.5.5 \
  /opt/guacamole/bin/initdb.sh --postgresql > ./init/01-initdb.sql

2. docker-compose.ymlの作成

/opt/guacamole配下にdocker-compose.ymlを作成し、プロキシデーモン、データベース、Webインターフェース、リバースプロキシを連携させます。

services:
  guacd:
    image: guacamole/guacd:1.5.5
    container_name: guacamole-daemon
    restart: always
    networks:
      - guac-network

  postgres:
    image: postgres:15-alpine
    container_name: guacamole-db
    restart: always
    environment:
      POSTGRES_DB: guacamole_db
      POSTGRES_USER: guacamole_user
      POSTGRES_PASSWORD: StrongProductionPasswordGoesHere!
    volumes:
      - ./init:/docker-entrypoint-initdb.d:ro
      - ./data/postgres:/var/lib/postgresql/data
    networks:
      - guac-network

  guacamole:
    image: guacamole/guacamole:1.5.5
    container_name: guacamole-web
    restart: always
    depends_on:
      - guacd
      - postgres
    environment:
      GUACD_HOSTNAME: guacd
      GUACD_PORT: 4822
      POSTGRESQL_HOSTNAME: postgres
      POSTGRESQL_DATABASE: guacamole_db
      POSTGRESQL_USER: guacamole_user
      POSTGRESQL_PASSWORD: StrongProductionPasswordGoesHere!
      # TOTP 2要素認証の設定
      TOTP_ENABLED: "true"
      TOTP_ISSUER: "ITFromZero-Gateway"
      TOTP_DIGITS: 6
      TOTP_PERIOD: 30
      TOTP_MODE: "sha1"
    networks:
      - guac-network

  nginx:
    image: nginx:alpine
    container_name: guacamole-nginx
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - guacamole
    networks:
      - guac-network

networks:
  guac-network:
    driver: bridge

3. WebSocket向けNginxの設定

Guacamoleで低レイテンシなキーストローク入力とスムーズなカーソル追従を実現するには、WebSocketが不可欠です。以下の設定をnginx.confとして保存します。

server {
    listen 80;
    server_name bastion.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name bastion.example.com;

    ssl_certificate /etc/nginx/certs/fullchain.pem;
    ssl_certificate_key /etc/nginx/certs/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location / {
        proxy_pass http://guacamole:8080/guacamole/;
        proxy_buffering off;
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $http_connection;
        access_log off;
    }
}

4. スタックの起動と2要素認証の有効化

デタッチモードでコンテナを起動します。

docker compose up -d
docker compose logs -f guacamole

ブラウザでhttps://bastion.example.comにアクセスします。初期管理者のログイン情報はguacadmin / guacadminです。

  1. Bitwarden、1Password、Aegisなどの認証アプリを使用して、登録用のQRコードをスキャンします。
  2. すぐにSettings > Users > guacadminに移動してデフォルトパスワードを変更し、エンジニア各自の個別アカウントを作成します。
  3. 個人の管理者アカウントとそのTOTPが正常に動作することを確認したら、guacadminアカウントを無効化します。

5. SSHおよびRDP対象ホストの追加

内部ノードの設定は、サーバー1台あたり約30秒で完了します。

  • Linuxノード(SSH): Settings > Connections > New Connectionに移動し、SSHを選択します。プライベートIP(例:10.0.1.15)、ポート22、ユーザー名を入力します。デプロイ用の秘密鍵をWeb UIに直接貼り付けます。これにより、エンジニアの端末に生の秘密鍵を保持させることなく、即座にホストへのアクセスを提供できます。
  • Windowsノード(RDP): 接続タイプでRDPを選択して追加します。内部IPとポート3389を入力します。社内の自己署名証明書を使用している場合はIgnore certにチェックを入れ、セキュリティモードをNLA (Network Level Authentication)に設定します。

運用現場で得られた知見とメリット

リモートアクセスをGuacamoleに集約したことで、チームのオンボーディング期間は従来の2日間のローカル環境構築からわずか5分に短縮されました。新しい開発者はポータルにサインインし、2要素認証を完了するだけで、即座にターミナルやデスクトップへアクセスできます。ローカルのOpenSSH設定や社内VPNクライアントのトラブルシューティングに頭を悩ませる必要はもうありません。

監査ログ機能も大きな成果の1つです。Guacamoleにはセッション記録機能が組み込まれており、圧縮された再生用ファイルをディスクに書き出せます。対象サーバーに侵略的なエンドポイント監視エージェントを導入することなく、本番環境で行われた作業の正確な画面操作やキーストロークを後から再生・検証できます。

負荷時のメモリ使用量には注意が必要です。アクティブなSSH接続1本あたりのguacdでのRAM消費量は約15MBに過ぎません。しかし、フル1080pのRDPセッションでは、色深度や画面更新頻度に応じて1接続あたり約120MB〜200MBを消費します。ホストVMに4GBのRAMを割り当てることで、18セッションの同時接続でもキーストロークのレイテンシを30ms未満に抑えて安定稼働できました。もし管理ポートを生のままインターネットに公開しているサーバーがあるなら、このコンテナ化されたゲートウェイを導入することで、わずか半日の作業で攻撃対象領域(アタックサーフェス)を劇的に縮小できます。

Share: