1. はじめに
Apache guacamoleはOSSの踏み台サーバーであり、以下のような特徴を持つ。
- ブラウザ経由で接続可能(クライアントにSSH/RDP/VPCクライアントが不要)。実運用では、TLSを有効にし、ポート番号を変えるとか、Client-to-Server VPNと組み合わせるとかはやっておいた方が良さそう。
- SSH/RDP/VNCを利用するバックエンドサーバーに対応。
- 操作履歴を保存可能(SSHならテキスト保存、RDP/VNCなら動画で保存可能)
- SFTPでバックエンドサーバーにファイル転送可能
- 複数人での画面共有が可能
- LDAP/OIDC/SAML等とも連携可能
- 二要素認証も可能
今回は、以下の方針で環境を構築する。
- 色々なパッケージを手動で導入することを避けるため(native installの場合は、非常にたくさんのパッケージをインストールし、コンパイルする必要がある)、dockerで構成する。ただし、一括でコンテナを管理したいため、docker-composeを利用する。
- 外部にはHTTPではなくHTTPSでサービスを公開する。そのため、Nginxをreverse proxyとして構成する。単にApache guacamoleをインストールするとHTTPで通信できてしまうため、Nginxを経由することでHTTPSでアクセスさせる。
- 認証を強化するため、二要素認証を有効にする。
なお、こちらの記事を参考にさせていただきました。
2. 環境確認
環境確認
[root@syasuda-bastion1 ~]# cat /etc/redhat-release
CentOS Stream release 9
[root@syasuda-bastion1 ~]# dnf -y update
[root@syasuda-bastion1 ~]# uname -a
Linux syasuda-bastion1 5.14.0-252.el9.x86_64 #1 SMP PREEMPT_DYNAMIC Wed Feb 1 13:25:18 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
3. dockerおよびdocker-composeのインストール
dockerのインストール
[root@syasuda-bastion1 ~]# curl -fsSL https://get.docker.com -o get-docker.sh
[root@syasuda-bastion1 ~]# sudo sh get-docker.sh
[root@syasuda-bastion1 ~]# systemctl enable docker
[root@syasuda-bastion1 ~]# systemctl start docker
[root@syasuda-bastion1 ~]# docker version
Client: Docker Engine - Community
 Version:           23.0.6
 API version:       1.42
 Go version:        go1.19.9
 Git commit:        ef23cbc
 Built:             Fri May  5 21:19:37 2023
 OS/Arch:           linux/amd64
 Context:           default
Server: Docker Engine - Community
 Engine:
  Version:          23.0.6
  API version:      1.42 (minimum version 1.12)
  Go version:       go1.19.9
  Git commit:       9dbdbd4
  Built:            Fri May  5 21:18:11 2023
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.21
  GitCommit:        3dce8eb055cbb6872793272b4f20ed16117344f8
 runc:
  Version:          1.1.7
  GitCommit:        v1.1.7-0-g860f061
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
docker-composeのインストール
[root@syasuda-bastion1 ~]# curl -SL https://github.com/docker/compose/releases/download/v2.17.4/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
[root@syasuda-bastion1 ~]# chmod +x /usr/local/bin/docker-compose
[root@syasuda-bastion1 ~]# docker-compose version
Docker Compose version v2.17.3
4. Nginxの設定ファイルの作成と配置
TLS証明書用ディレクトリの作成
[root@syasuda-bastion1 ~]# mkdir -p ./nginx/ssl
TLS証明書の作成
[root@syasuda-bastion1 ~]# openssl req -new -x509 -sha256 -newkey rsa:2048 -days 3650 -nodes -subj "/CN=Guacamole bastion" -out ./nginx/ssl/nginx.pem -keyout ./nginx/ssl/nginx.key
[root@syasuda-bastion1 ~]# ls -l ./nginx/ssl/
total 8
-rw-------. 1 root root 1704 May  9 03:07 nginx.key
-rw-r--r--. 1 root root 1135 May  9 03:07 nginx.pem
[root@syasuda-bastion1 ~]# openssl x509 -in nginx/ssl/nginx.pem -text -noout|grep -i -e signature -e public-key -e CN -e Not
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = Guacamole bastion
            Not Before: May  9 07:07:44 2023 GMT
            Not After : May  6 07:07:44 2033 GMT
        Subject: CN = Guacamole bastion
                Public-Key: (2048 bit)
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
構成ファイル用のディレクトリの作成
[root@syasuda-bastion1 ~]# mkdir -p ./nginx/conf.d
./nginx/conf.d/default.conf
server {
    listen 443 ssl;
    server_name  localhost;
    ssl_certificate      /etc/nginx/ssl/nginx.pem;
    ssl_certificate_key  /etc/nginx/ssl/nginx.key;
    location /guacamole/ {
        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;
        client_max_body_size 10g;
        access_log off;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
- 
proxy_buffering offを忘れないように(とmanualにも書かれている)。
- Nginxではデフォルトで1MBのファイルアップロードの制限があるため、client_max_body_sizeも忘れないように(と、これもmanualにも書かれている)
5. PostgreSQLの初期化ファイルを作成
[root@syasuda-bastion1 ~]# mkdir ./pginit
[root@syasuda-bastion1 ~]# chmod -R +x ./pginit
[root@syasuda-bastion1 ~]# docker run --rm guacamole/guacamole /opt/guacamole/bin/initdb.sh --postgres > ./pginit/initdb.sql
6. TOTP(二要素認証)およびrecording storageのためのjar配置
- 本稿執筆時点では、version 1.5.1が最新。常に最新を利用することが推奨される。
 https://guacamole.apache.org/releases/
- 
guacamole-auth-totp-x.x.x.jarが必要。このファイルは、guacamole-auth-totp-x.x.x.tar.gzに含まれている。
[root@syasuda-bastion1 ~]# mkdir -p ./guacamole/extensions
[root@syasuda-bastion1 ~]# curl -L https://apache.org/dyn/closer.lua/guacamole/1.5.1/binary/guacamole-auth-totp-1.5.1.tar.gz?action=download | tar zxvf - -C ./guacamole
[root@syasuda-bastion1 ~]# cp -p ./guacamole/guacamole-auth-totp-1.5.1/guacamole-auth-totp-1.5.1.jar ./guacamole/extensions/.
[root@syasuda-bastion1 ~]# curl -L https://apache.org/dyn/closer.lua/guacamole/1.5.1/binary/guacamole-history-recording-storage-1.5.1.tar.gz?action=download | tar zxvf - -C ./guacamole
[root@syasuda-bastion1 ~]# cp -p ./guacamole/guacamole-history-recording-storage-1.5.1/guacamole-history-recording-storage-1.5.1.jar ./guacamole/extensions/.
[root@syasuda-bastion1 ~]# ls -l ./guacamole/extensions/
total 4724
-rw-r--r--. 1 vpcuser vpcuser 4819523 Apr 10 13:23 guacamole-auth-totp-1.5.1.jar
-rw-r--r--. 1 vpcuser vpcuser   13697 Apr 10 13:23 guacamole-history-recording-storage-1.5.1.jar
7. Reverse Proxy構成
- ここを参照。この構成をしないと、接続履歴画面で、接続元情報が常にNginxのIPアドレスになってしまい、意味をなさないことになる。
- internalProxiesの値としてはReverse Proxy(今回の例ではNginx)のIPアドレスを指定する必要がある。ホスト名は使えない。CIDR表記はできないが、正規表現で書いても良い。
- 今回は、Nginxに192.168.0.2という固定IPアドレスを明示的に指定することにする。
[root@syasuda-bastion1 ~]# docker run --rm guacamole/guacamole cat /usr/local/tomcat/conf/server.xml > ./guacamole/server.xml
./guacamole/server.xml
(途中略)
        <!-- Access log processes all example.
             Documentation at: /docs/config/valve.html
             Note: The pattern used is equivalent to using pattern="common" -->
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t "%r" %s %b" />
        <Valve className="org.apache.catalina.valves.RemoteIpValve"
               internalProxies="192.168.0.2"
               remoteIpHeader="x-forwarded-for"
               remoteIpProxiesHeader="x-forwarded-by"
               protocolHeader="x-forwarded-proto" />
      </Host>
    </Engine>
  </Service>
</Server>
8. recordingログ(SSHやRDPなどのログ)のためのファイル配置
- recordingログはguacdコンテナ上で作成される。よって後から参照するためには永続化が必要である。
- 以下のようにキャプチャーされた動画を参照できるようにするためには、このログはguacamoleコンテナーから読み取る必要がある。guacamoleコンテナーにおけるrecording storage extensionは、デフォルトで/var/lib/guacamole/recordingsを検索する。コンテナ上には/var/lib/guacamoleさえ存在していない。
- 
よって、以下のようなディレクトリ配置にする。 - ホスト上に./guacd/guacamole/recordingsを作成する
- guacdコンテナの/var/lib/guacamoleが、ホスト上の./guacd/guacamoleをマウントするように構成する。
- guacamoleコンテナの/var/lib/guacamoleが、ホスト上の./guacd/guacamoleをマウントするように構成する。
 
- ホスト上に
- 
もう一つ難しいのは、権限の問題である。 - guacdコンテナは、ログをユーザー:guacd(UUID=1000), グループ:guacd(GUID=1000)で書き込む。
- guacamoleコンテナーは、guacamole(UUID=1001), グループ:guacamole(GUID=1001)で読み込もうとするため、そのままでは読めない。
 
[root@syasuda-bastion1 ~]# docker exec -it root-guacamole-1 bash
guacamole@18fe445ac77d:/opt/guacamole$ id
uid=1001(guacamole) gid=1001(guacamole) groups=1001(guacamole)
guacamole@18fe445ac77d:/opt/guacamole$ ls -ltr /var/lib/guacamole/recordings/
total 0
drwxr-x---. 2 1000 1000 115 May 12 03:49 cf97bce1-d5f1-322a-9f02-dde030944c7d
guacamole@18fe445ac77d:/opt/guacamole$ ls -ltr /var/lib/guacamole/recordings/cf97bce1-d5f1-322a-9f02-dde030944c7d
ls: cannot open directory '/var/lib/guacamole/recordings/cf97bce1-d5f1-322a-9f02-dde030944c7d': Permission denied
そのため、以下のような対応を行った(他に良い方法があったら教えてください)。
- ホスト上に各コンテナと同じUID/GIDを持つユーザー・グループを作成する。(-oオプションを付けておけば、既に同じUID/GIDが存在しても重複を許可してユーザ・グループを作成可能)
- ログの書き込みは継続してguacdユーザーが行えるように、ディレクトリーuser ownerはguacdにする。ただし、group ownerはguacamoleにする。
- ディレクトリに対してSGIDを構成する。これにより、その中に作成されるファイルやディレクトリはそのグループを継承することができる。guacamoleコンテナーのユーザーはguacamoleグループに属しているため、そのディレクトリに作成されたディレクトリやファイルにアクセスすることが可能である。
[root@syasuda-bastion1 ~]# docker run --rm guacamole/guacd id
uid=1000(guacd) gid=1000(guacd) groups=1000(guacd)
[root@syasuda-bastion1 ~]# docker run --rm guacamole/guacamole id
uid=1001(guacamole) gid=1001(guacamole) groups=1001(guacamole)
[root@syasuda-bastion1 ~]# groupadd -g 1000 -o guacd
[root@syasuda-bastion1 ~]# groupadd -g 1001 -o guacamole
[root@syasuda-bastion1 ~]# useradd -M -u 1000 -o -g guacd -s /sbin/nologin guacd 
[root@syasuda-bastion1 ~]# useradd -M -u 1001 -o -g guacamole -s /sbin/nologin guacamole
[root@syasuda-bastion1 ~]# mkdir -p ./guacd/guacamole/recordings
[root@syasuda-bastion1 ~]# chown -R guacd:guacamole ./guacd/guacamole
[root@syasuda-bastion1 ~]# chmod -R 2775 ./guacd/guacamole
9. docker-compose.ymlの作成
- Nginxをwell knownポートである443で公開するのではなく、8443で公開する。
- guacamoleコンテナーはport 8080でHTTPアクセスをlistenしているが、docker-composeで構成されるnginxからのみアクセスできればいいので、よくサンプルであるように
    ports:
      - "8080:8080"
のような記述は削除する。
- 先述のセクションで作成した所定のファイルをマウントできるように構成する。
- nginx
- ./nginx/conf.d (構成ファイル)
- ./nginx/ssl (TLS証明書・鍵)
 
- PostgreSQL
- ./pginit (初期化SQL)
- ./pgdata (永続DBデータ)
 
- guacamole
- ./guacamole/extensions (TOTP)
 
 
- nginx
- Guacamoleのserver.xmlでX-Forwarded-forを有効にするためにNginxのIPアドレスを固定しておく必要がある。今回は192.168.0.2を割り当てている。
- 他のコンテナもIPアドレスを割り当てた。というのは、場合によっては他のコンテナが先に起動して192.168.0.2を取得してしまい、Nginxが192.168.0.2が利用できなくなる(Error response from daemon: Address already in useというエラーが出る)可能性があるからである。それだったら、4つぐらいしかないコンテナーなので、全部にIPアドレスを割り当てることにする。
docker-compose.yml
version: "3"
services:
  nginx:
    image: nginx:latest
    restart: unless-stopped
    networks:
      backend:
        ipv4_address: 192.168.0.2
    ports:
      - "8443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
  postgres:
    image: postgres:latest
    restart: unless-stopped
    networks:
      backend:
        ipv4_address: 192.168.0.3
    environment:
      PGDATA: /var/lib/postgresql/data/guacamole
      POSTGRES_DB: guacamole_db
      POSTGRES_PASSWORD: guacamole1234567890
      POSTGRES_USER: guacamole_user
    volumes:
      - ./pginit:/docker-entrypoint-initdb.d
      - ./pgdata:/var/lib/postgresql/data
  guacd:
    image: guacamole/guacd:latest
    restart: unless-stopped
    networks:
      backend:
        ipv4_address: 192.168.0.4
    volumes:
      - ./guacd/guacamole:/var/lib/guacamole
  guacamole:
    image: guacamole/guacamole:latest
    restart: unless-stopped
    networks:
      backend:
        ipv4_address: 192.168.0.5
    environment:
      GUACD_HOSTNAME: guacd
      POSTGRES_DATABASE: guacamole_db
      POSTGRES_HOSTNAME: postgres
      POSTGRES_PASSWORD: guacamole1234567890
      POSTGRES_USER: guacamole_user
      GUACAMOLE_HOME: /etc/guacamole
    volumes:
      - ./guacamole/extensions:/etc/guacamole/extensions
      - ./guacamole/server.xml:/usr/local/tomcat/conf/server.xml
      - ./guacd/guacamole:/var/lib/guacamole
    depends_on:
      - postgres
      - guacd
networks:
  backend:
    ipam:
      driver: default
      config:
        - subnet: 192.168.0.0/24
10. docker-composeによるguacamoleの起動
[root@syasuda-bastion1 ~]# docker-compose up -d
[root@syasuda-bastion1 ~]# docker-compose ps
NAME                IMAGE                        COMMAND                  SERVICE             CREATED             STATUS                             PORTS
root-guacamole-1    guacamole/guacamole:latest   "/opt/guacamole/bin/…"   guacamole           41 seconds ago      Up 39 seconds                      8080/tcp
root-guacd-1        guacamole/guacd:latest       "/bin/sh -c '/opt/gu…"   guacd               41 seconds ago      Up 40 seconds (health: starting)   4822/tcp
root-nginx-1        nginx:latest                 "/docker-entrypoint.…"   nginx               41 seconds ago      Up 40 seconds                      80/tcp, 0.0.0.0:8443->443/tcp, :::8443->443/tcp
root-postgres-1     postgres:latest              "docker-entrypoint.s…"   postgres            41 seconds ago      Up 40 seconds                      5432/tcp
[root@syasuda-bastion1 ~]# docker inspect -f '{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker ps -q)
/root-guacamole-1 - 192.168.0.5
/root-nginx-1 - 192.168.0.2
/root-guacd-1 - 192.168.0.4
/root-postgres-1 - 192.168.0.3
[root@syasuda-bastion1 ~]# ss -anpt
State              Recv-Q             Send-Q                         Local Address:Port                         Peer Address:Port             Process
LISTEN             0                  128                                  0.0.0.0:22                                0.0.0.0:*                 users:(("sshd",pid=824,fd=3))
LISTEN             0                  4096                                 0.0.0.0:111                               0.0.0.0:*                 users:(("rpcbind",pid=671,fd=4),("systemd",pid=1,fd=69))
LISTEN             0                  4096                                 0.0.0.0:8443                              0.0.0.0:*                 users:(("docker-proxy",pid=73719,fd=4))
ESTAB              0                  0                                  10.0.0.31:22                               10.0.0.4:48792             users:(("sshd",pid=72366,fd=4),("sshd",pid=72351,fd=4))
LISTEN             0                  128                                     [::]:22                                   [::]:*                 users:(("sshd",pid=824,fd=4))
LISTEN             0                  4096                                    [::]:111                                  [::]:*                 users:(("rpcbind",pid=671,fd=6),("systemd",pid=1,fd=71))
LISTEN             0                  4096                                    [::]:8443                                 [::]:*                 users:(("docker-proxy",pid=73725,fd=4))
11. Apache guacamoleのコンソールへのアクセス試行
- 
https://<IP address>:8443/guacamole/にアクセスする。
- ID/初期パスワードはguacadmin/guacadmin   
- 初回ログイン時は、二要素認証の登録が必要となる。QRコードをGoogle Authenticatorで読み込む。  
- Google Authenticatorに登録した結果。この認証コードを先ほどの画面に入力する。  
- なお、次回ログインからは以下のようにID/Passwordの入力後に認証コードを要求する画面が現れる。  
12. ログイン後の各種設定
12-1. 一般の設定
- 初期パスワードをguacadminから他のパスワードに変更しておく。  
- あとはユーザーを作ったり、グループを作ったり、接続を作ったり、色々とやっていく。
- Apache guacamoleの設定データはPostgreSQLに保管されるため、./pgdataをバックアップしておけば良い。逆に初期化したい時は、./pgdataを削除してdocker-composeを起動すれば良い。
12-2. Linux接続
- 
SSHのスクリプトログの配置先は、以下を指定。 - タイプスクリプトの保存ディレクトリ: /var/lib/guacamole/recordings/${HISTORY_UUID}Guacamoleコンソールからレコーディングを参照することはまだサポートされていないが、${HISTORY_UUID}を使っておくと将来Guacamoleがサポートする時に対応できるらしい。
- タイプスクリプト名: ${GUAC_USERNAME}-${GUAC_DATE}-${GUAC_TIME}-${GUAC_CLIENT_ADDRESS}.log
- タイプスクリプトの保存ディレクトリを自動的に作成する。: チェックを入れる。(保存先ディレクトリが存在しない場合にディレクトリを作成してくれるが、作成するのは最後のサブディレクトリだけであり、その親ディレクトリは予め存在している必要がある)。
 
- タイプスクリプトの保存ディレクトリ: 
- 
SSHのスクリーンレコーディングは以下を指定(タイプスクリプトが存在するし、ターミナルの動画なんて後から見直したくもないだろうから、取得する必要はないかもしれないが、まぁ今回はテストも兼ねて・・・)。 
12-3. Windows接続
13. ログの確認
[root@syasuda-bastion1 ~]# docker exec root-guacd-1 ls -ltR /var/lib/guacamole/recordings/
/var/lib/guacamole/recordings/:
total 0
drwxr-s---    2 guacd    1001           166 May 13 14:06 f4fe7bcf-81aa-3763-947c-bb9748fb32af
drwxr-s---    2 guacd    1001            57 May 13 14:05 78416a47-bd7f-3d9a-89b3-e460ab668404
/var/lib/guacamole/recordings/f4fe7bcf-81aa-3763-947c-bb9748fb32af:
total 900
-rw-r-----    1 guacd    1001        906287 May 13 14:06 guacadmin-20230513-140602-111.xxx.x.129.dat
-rw-r-----    1 guacd    1001          5226 May 13 14:06 guacadmin-20230513-140602-111.xxx.x.129.log
-rw-r-----    1 guacd    1001           456 May 13 14:06 guacadmin-20230513-140602-111.xxx.x.129.log.timing
/var/lib/guacamole/recordings/78416a47-bd7f-3d9a-89b3-e460ab668404:
total 940
-rw-r-----    1 guacd    1001        959199 May 13 14:05 guacadmin-20230513-140528-111.xxx.x.129.dat
- guacamoleコンテナ上のログ
[root@syasuda-bastion1 ~]# docker exec root-guacamole-1 ls -ltR /var/lib/guacamole/recordings/
/var/lib/guacamole/recordings/:
total 0
drwxr-s---. 2 1000 guacamole 166 May 13 14:06 f4fe7bcf-81aa-3763-947c-bb9748fb32af
drwxr-s---. 2 1000 guacamole  57 May 13 14:05 78416a47-bd7f-3d9a-89b3-e460ab668404
/var/lib/guacamole/recordings/f4fe7bcf-81aa-3763-947c-bb9748fb32af:
total 900
-rw-r-----. 1 1000 guacamole   5226 May 13 14:06 guacadmin-20230513-140602-111.xxx.x.129.log
-rw-r-----. 1 1000 guacamole 906287 May 13 14:06 guacadmin-20230513-140602-111.xxx.x.129.dat
-rw-r-----. 1 1000 guacamole    456 May 13 14:06 guacadmin-20230513-140602-111.xxx.x.129.log.timing
/var/lib/guacamole/recordings/78416a47-bd7f-3d9a-89b3-e460ab668404:
total 940
-rw-r-----. 1 1000 guacamole 959199 May 13 14:05 guacadmin-20230513-140528-111.xxx.x.129.dat
- ホスト上のログ
[root@syasuda-bastion1 ~]# ls -ltRn ./guacd/guacamole/recordings/
./guacd/guacamole/recordings/:
total 0
drwxr-s---. 2 1000 1001 166 May 13 10:06 f4fe7bcf-81aa-3763-947c-bb9748fb32af
drwxr-s---. 2 1000 1001  57 May 13 10:05 78416a47-bd7f-3d9a-89b3-e460ab668404
./guacd/guacamole/recordings/f4fe7bcf-81aa-3763-947c-bb9748fb32af:
total 900
-rw-r-----. 1 1000 1001   5226 May 13 10:06 guacadmin-20230513-140602-111.xxx.x.129.log
-rw-r-----. 1 1000 1001 906287 May 13 10:06 guacadmin-20230513-140602-111.xxx.x.129.dat
-rw-r-----. 1 1000 1001    456 May 13 10:06 guacadmin-20230513-140602-111.xxx.x.129.log.timing
./guacd/guacamole/recordings/78416a47-bd7f-3d9a-89b3-e460ab668404:
total 940
-rw-r-----. 1 1000 1001 959199 May 13 10:05 guacadmin-20230513-140528-111.xxx.x.129.dat












