安全にDockerからChromeをホストPCに表示する方法：xhost +を使わないアプローチ

## はじめに

`xhost +`を使うことなしに、安全にDockerで起動したChromeをホストPCに表示する方法をまとめる。

### 実現方法のまとめ

「ホストPCのユーザID」と「DockerでChromeを実行するユーザID」を揃えることで`.Xauthority`を使いDockerのChromeを`xhost +`なしでホストPCに表示する

#### 1.適当なディレクトリに Dockerfile を作成する

```Dockerfile: Dockerfile
FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive
ENV DISPLAY=:0
ENV PULSE_SERVER=unix:/run/user/1000/pulse/native

# 必要なパッケージをインストール
RUN apt-get update && \
    apt-get install -y wget pulseaudio socat alsa-base \
    libcanberra-gtk-module libcanberra-gtk3-module \
    fonts-ipafont-gothic fonts-ipafont-mincho fonts-noto-cjk \
    language-pack-ja dbus-x11 && \
    wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && \
    apt-get install -y ./google-chrome-stable_current_amd64.deb --no-install-recommends && \
    apt-get install -y x11-apps mesa-utils sudo &&\
    apt-get clean && rm -rf /var/lib/apt/lists/*

RUN echo "#!/bin/bash\n\
set -e\n\
sudo /etc/init.d/dbus start || { echo 'Failed to start dbus'; exit 1; }\n\
export XDG_RUNTIME_DIR=/run/user/\$(id -u)\n\
sudo mkdir -p \$XDG_RUNTIME_DIR || { echo 'Failed to create XDG_RUNTIME_DIR'; exit 1; }\n\
sudo chmod 700 \$XDG_RUNTIME_DIR || { echo 'Failed to chmod XDG_RUNTIME_DIR'; exit 1; }\n\
sudo chown \$(id -un):\$(id -gn) \$XDG_RUNTIME_DIR || { echo 'Failed to chown XDG_RUNTIME_DIR'; exit 1; }\n\
export DBUS_SESSION_BUS_ADDRESS=unix:path=\$XDG_RUNTIME_DIR/bus\n\
dbus-daemon --session --address=\$DBUS_SESSION_BUS_ADDRESS --nofork --nopidfile --syslog-only &\n\
exec \"\$@\"" > /start.sh && chmod +x /start.sh

# ホストのUIDとGIDを利用してユーザーを作成する
ARG USER_ID
ARG GROUP_ID

# 既存のグループを確認して、なければ作成
RUN if ! getent group $GROUP_ID >/dev/null; then \
        groupadd -g $GROUP_ID hostgroup; \
    fi

# 既存のUIDに対応するユーザーがいない場合、新規ユーザーを作成して、PWなしでsudoを許可
RUN USER_NAME=$(getent passwd $USER_ID | cut -d: -f1) && \
    if [ -z "$USER_NAME" ]; then \
        USER_NAME="hostuser"; \
        useradd -u $USER_ID -g $GROUP_ID -m $USER_NAME; \
    fi && \
    echo "$USER_NAME ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

# google-chrome を $USER_ID で実行
USER $USER_ID
CMD ["/start.sh", "google-chrome", "--disable-dev-shm-usage"]
```

#### 2.Dockerイメージを作成する

`ubuntu24_chrome3`という名前でDockerイメージを作成する

```bash:ホストPCのユーザIDとグループIDを指定してDockerイメージを作成する
docker build --build-arg USER_ID=$(id -u) --build-arg GROUP_ID=$(id -g) -t ubuntu24_chrome3 .
```

#### 3.Dockerコンテナを起動する

Chromeのサンドボックスに対応するため特権コンテナとして実行し、Chromeが起動することを確認する

```bash
docker run -it --rm \
  --name ubuntu24_chrome3 \
  --env="DISPLAY=$DISPLAY" \
  -v /tmp/.X11-unix:/tmp/.X11-unix \
  --env="PULSE_SERVER=unix:${XDG_RUNTIME_DIR}/pulse/native" \
  --volume="$HOME/.Xauthority:/home/ubuntu/.Xauthority:rw" \
  --volume="${XDG_RUNTIME_DIR}/pulse/native:${XDG_RUNTIME_DIR}/pulse/native" \
  --volume="/etc/machine-id:/etc/machine-id:ro" \
  --volume="/run/user/$(id -u)/pulse:/run/user/$(id -u)/pulse" \
  --network=host \
  --device /dev/dri \
  --cap-add=SYS_ADMIN --security-opt seccomp=unconfined \
  ubuntu24_chrome3
```

![youtube2024.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/9661c54c-6c40-217a-ad5c-ded01bddbe81.gif)

---

## `xhost +`の危険性

### `xhost +`がよく使われる理由

前回までの記事では `xhost +`を使い手軽にDockerでGUIアプリをホストPCに表示させた。コンテナからホストのXサーバーに接続する場合、`xhost +`は、この接続を簡単に許可できる。これを使えば、設定なしでどんなコンテナからもアプリを表示できます。手軽さから、開発やテストでよく使われる

### `xhost +`の危険性

`xhost +`は便利だが、セキュリティに大きな穴を開ける。全ての接続を許可するため、誰でもホストPCの画面にアクセスできる。

- **セッション乗っ取りのリスク**
- **データの盗難**
- **システムの破壊**

これらのリスクを避けるため、開発やテスト以外では`xhost +`は使わない方が良い。

---


## `xhost +`を使わずGUIアプリをホストPCで表示する

### 前回の記事で`xhost +`を使わない場合

https://qiita.com/rairaii/items/398541a16dccc608fc9a

前回の記事のDockerファイルを利用して作成したイメージを実行した場合、`xhost +`を使わないと下記のように、X serverが見つからないとのエラーが発生する

```bash: Missing X server or $DISPLAY
[1:1:0816/051448.624023:ERROR:ozone_platform_x11.cc(244)] Missing X server or $DISPLAY
```

### 解決策1: `xhost +local:`

ホストPCで `xhost +` の代わりに、`xhost +local:`を実行する。リスクは残るがシンプルな解決策である。

```bash:ローカルマシン上のユーザに限ってアクセスを許可
xhost +local:
```

`xhost +local:`は、ローカルマシン上のユーザー間でXサーバーにアクセスを許可する場合に使用される。ネットワーク経由でのアクセスが制限されるため、リスクは`xhost +`よりも低いが、ローカルマシンの他のユーザーがXサーバーにアクセスできるリスクは残る。

```bash: xhost +local: を取り消す
xhost -local:
```

### 解決策2: .Xauthorityを利用する

.Xauthorityを利用しする場合、コンテナ内のユーザーとホストPCのユーザーの設定が一致しなければならない。

#### 用意する Dockerfile
```Dockerfile
FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive
ENV DISPLAY=:0
ENV PULSE_SERVER=unix:/run/user/1000/pulse/native

# 必要なパッケージをインストール
RUN apt-get update && \
    apt-get install -y wget pulseaudio socat alsa-base \
    libcanberra-gtk-module libcanberra-gtk3-module \
    fonts-ipafont-gothic fonts-ipafont-mincho fonts-noto-cjk \
    language-pack-ja dbus-x11 && \
    wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && \
    apt-get install -y ./google-chrome-stable_current_amd64.deb --no-install-recommends && \
    apt-get install -y x11-apps mesa-utils sudo &&\
    apt-get clean && rm -rf /var/lib/apt/lists/*

RUN echo "#!/bin/bash\n\
set -e\n\
sudo /etc/init.d/dbus start || { echo 'Failed to start dbus'; exit 1; }\n\
export XDG_RUNTIME_DIR=/run/user/\$(id -u)\n\
sudo mkdir -p \$XDG_RUNTIME_DIR || { echo 'Failed to create XDG_RUNTIME_DIR'; exit 1; }\n\
sudo chmod 700 \$XDG_RUNTIME_DIR || { echo 'Failed to chmod XDG_RUNTIME_DIR'; exit 1; }\n\
sudo chown \$(id -un):\$(id -gn) \$XDG_RUNTIME_DIR || { echo 'Failed to chown XDG_RUNTIME_DIR'; exit 1; }\n\
export DBUS_SESSION_BUS_ADDRESS=unix:path=\$XDG_RUNTIME_DIR/bus\n\
dbus-daemon --session --address=\$DBUS_SESSION_BUS_ADDRESS --nofork --nopidfile --syslog-only &\n\
exec \"\$@\"" > /start.sh && chmod +x /start.sh

# ホストのUIDとGIDを利用してユーザーを作成する
ARG USER_ID
ARG GROUP_ID

# 既存のグループを確認して、なければ作成
RUN if ! getent group $GROUP_ID >/dev/null; then \
        groupadd -g $GROUP_ID hostgroup; \
    fi

# 既存のUIDに対応するユーザーがいない場合、新規ユーザーを作成して、PWなしでsudoを許可
RUN USER_NAME=$(getent passwd $USER_ID | cut -d: -f1) && \
    if [ -z "$USER_NAME" ]; then \
        USER_NAME="hostuser"; \
        useradd -u $USER_ID -g $GROUP_ID -m $USER_NAME; \
    fi && \
    echo "$USER_NAME ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

# google-chrome を $USER_ID で実行
USER $USER_ID
CMD ["/start.sh", "google-chrome", "--disable-dev-shm-usage"]
```

#### Dockerfileの変更点

| 項目 | 前回| 今回 | 変更理由と影響 |
|:-|:-|:-|:-|
| **追加パッケージ**  | `google-chrome-stable`、`pulseaudio`などの基本パッケージをインストール | 追加で`x11-apps`、`mesa-utils`、`sudo`をインストール。 | - `x11-apps`と`mesa-utils`の追加により、X11およびOpenGLアプリケーションのデバッグや動作確認が可能にした <br> - `sudo`の追加で、ユーザーが管理者権限でコマンドを実行可能にした。 |
| **DBus設定スクリプトの改良**| Chrome実行時のエラー解決の設定 | Chromeの実行ユーザが`root`でない場合に対応 | `sudo`の使用により、ルート権限が必要な操作を安全に実行できる|
| **ホストのUID/GIDに基づくユーザー作成** | n/a | ホストのUID/GIDを使用してコンテナ内にユーザーを作成 |.Xauthorityを用いてXサーバーに接続するため|
| **`google-chrome`の実行ユーザ** | root | ホストのUID| - .Xauthorityを用いてXサーバーに接続するため　<br> - Chromeを-no-sandboxなしで実行する|

#### Dockerイメージの作成

`ubuntu24_chrome3`という名前をつけてイメージを作成する

```bash:ホストPCのユーザIDとグループIDを指定してDockerイメージを作成する
docker build --build-arg USER_ID=$(id -u) --build-arg GROUP_ID=$(id -g) -t ubuntu24_chrome3 .
```

#### Dockerコンテナの起動の失敗

DockerでChromeを前回と同様に起動しようとするとエラーが発生する。

```bash: Chromeの起動はエラー
docker run -it --rm \
  --name ubuntu24_chrome3 \
  --env="DISPLAY=$DISPLAY" \
  -v /tmp/.X11-unix:/tmp/.X11-unix \
  --env="PULSE_SERVER=unix:${XDG_RUNTIME_DIR}/pulse/native" \
  --volume="$HOME/.Xauthority:/home/ubuntu/.Xauthority:rw" \
  --volume="${XDG_RUNTIME_DIR}/pulse/native:${XDG_RUNTIME_DIR}/pulse/native" \
  --volume="/etc/machine-id:/etc/machine-id:ro" \
  --volume="/run/user/$(id -u)/pulse:/run/user/$(id -u)/pulse" \
  --network=host \
  --device /dev/dri \
  ubuntu24_chrome3
```

```bash: エラー内容
 * Starting system message bus dbus [ OK ] 
Failed to move to new namespace: PID namespaces supported, Network namespace supported, but failed: errno = Operation not permitted
[1:1:0816/055842.050671:FATAL:zygote_host_impl_linux.cc(201)] Check failed: . : Operation not permitted (1)
```

#### Dockerコンテナの起動失敗の原因の推察

Chromeは、セキュリティのために「サンドボックス」機能を使用しており、プロセスを分離するために名前空間を使用する。しかし、Dockerコンテナ内では、これらの操作に必要な権限が制限されているため、Chromeのサンドボックス機能が適切に動作せず、エラーが発生しているものと考えられる。
Chromeの起動するユーザをrootからホストPCのユーザと同一のユーザIDに変更した際に、DockerfileのCMDで`--no-sandbox`を削除したことが原因だと考えられる。


#### Xeyesなら起動は成功

```bash:Xeyesは起動する
docker run -it --rm \
  --name ubuntu24_chrome3 \
  --env="DISPLAY=$DISPLAY" \
  -v /tmp/.X11-unix:/tmp/.X11-unix \
  --env="PULSE_SERVER=unix:${XDG_RUNTIME_DIR}/pulse/native" \
  --volume="$HOME/.Xauthority:/home/ubuntu/.Xauthority:rw" \
  --volume="${XDG_RUNTIME_DIR}/pulse/native:${XDG_RUNTIME_DIR}/pulse/native" \
  --volume="/etc/machine-id:/etc/machine-id:ro" \
  --volume="/run/user/$(id -u)/pulse:/run/user/$(id -u)/pulse" \
  --network=host \
  --device /dev/dri \
  ubuntu24_chrome3 /usr/bin/xeyes
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/50110523-bcac-1c53-7936-98677d35b898.png)

#### 特権をつけてDockerコンテナを起動

Chromeのサンドボックスに対応するため特権コンテナとして実行し、Chromeが起動することを確認する

```bash
docker run -it --rm \
  --name ubuntu24_chrome3 \
  --env="DISPLAY=$DISPLAY" \
  -v /tmp/.X11-unix:/tmp/.X11-unix \
  --env="PULSE_SERVER=unix:${XDG_RUNTIME_DIR}/pulse/native" \
  --volume="$HOME/.Xauthority:/home/ubuntu/.Xauthority:rw" \
  --volume="${XDG_RUNTIME_DIR}/pulse/native:${XDG_RUNTIME_DIR}/pulse/native" \
  --volume="/etc/machine-id:/etc/machine-id:ro" \
  --volume="/run/user/$(id -u)/pulse:/run/user/$(id -u)/pulse" \
  --network=host \
  --device /dev/dri \
  --privileged \
  ubuntu24_chrome3
```

![youtube2024.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/9661c54c-6c40-217a-ad5c-ded01bddbe81.gif)

## まとめ

DockerでGUIアプリを使うとき、`xhost +`は便利なのだが、非常に危険でもある。ある程度のリスクが許容できるなら `xhost +local:`を選択、そうでない場合には `/.Xauthority` を使うことを考えると良い。

ただし、Chromeのように特権が必要な場合には、コンテナ内で悪意のあるコードが実行された場合、ホストシステム全体に影響を及ぼす可能性があり、別のセキュリティの懸念が発生する。

`--privileged`の代わりに`--cap-add=SYS_ADMIN --security-opt seccomp=unconfined`を指定することで、部分的な特権付与でセキュリティの懸念を減じるほうが良いだろう。


## 蛇足

`--volume="$HOME/.Xauthority:/home/ubuntu/.Xauthority:rw"`でマウントするユーザのホームディレクトリ(/home/ubuntu/)は下記のようにして確認できる

```bash
docker run -it --rm \
  --name ubuntu24_chrome3 \
  ubuntu24_chrome3 /bin/bash -c 'echo $HOME'
```