Docker Desktop for WindowsのTLSを有効にして安全な環境でWSLから接続する

タイトルは半分ウソでDocker Desktop for Windows自体のTLSを有効にしているのではなく `Expose daemon on tcp://localhost:2375 without TLS`に**チェックを入れずに（TLSなしにしないで）**WSLからdockerを使用する方法というのがこの記事の正しい内容です。（でも検索する人はTLSを有効にしたいと思って検索するでしょう？）TLSなしにすることがなぜ危険かは「[本当は怖いDocker for Windows のExpose daemon on tcp://localhost:2375 without TLS]
(https://qiita.com/shioshiota/items/fe0e085826bc2f19f154)」などを参照して下さい。

この記事はDocker Desktop for Windows 2.1.0.5で動作検証しています。またこの記事ででてくるWSLとはWSL1のことです。WSL2やDocker Desktop WSL 2では状況が違うかもしれません。

やり方は２通り

1. **npiperelay & docker-relay** を使用する。（Unix socket + Named pipe）
2. **docker-remote-api-tls** を使用する。（TLS + Unix socket）

1.はTLSそのものを使いません。TLSを使用したい場合は2.の方法を使います。今回はローカルマシンで使いたかっただけなので試していないですが、2.だと正しく設定すればネットワークごしに使えるはずです。その分セキュリティには気をつける必要があります。両方を併用することも可能です。

**2020-01-22 追記** TLSを有効にできるかもしれない。詳細は「3. TLSを有効にする？」で

## 1. npiperelay & docker-relay

 * https://devblogs.microsoft.com/commandline/cross-post-wsl-interoperability-with-docker/
 * https://github.com/jstarks/npiperelay

### 仕組み

```
Docker client on WSL 
      ↓ Unix socket
docker-relay(socat) 
      ↓ stdio
npiperelay.exe
      ↓ Named pipe
Docker Desktop for Windows
```

docker-relay（socatを使ったシェルスクリプト）はサンプル的な扱いで、dockerだけではなくsocatで対応してるUnix socketやシリアルポート等をNamed pipeにリレーすることができます。他にはMySQLやシリアルコンソールのサンプルが付属しています。

### 使い方

バイナリは提供されてないのでビルドが必要です。基本的に公式の[README.md](https://github.com/jstarks/npiperelay)の通りですが少し簡略化してます。Ubuntu on WSL前提です。

```console
$ # 準備
$ # Linux版のdockerをあらかじめインストールしておく（手順省略）
$ sudo adduser $USER docker # すでに自分をdockerグループに追加しているなら不要
$ sudo apt install golang socat # すでにインストールされているなら不要

$ # ビルド (ソースコードは、$HOME/go/src/github.com/jstarks/npiperelay以下にダウンロードされます）
$ GOOS=windows GOARCH=amd64 go get -d github.com/jstarks/npiperelay
$ GOOS=windows go build -o /mnt/c/Users/<ユーザー名>/bin/npiperelay.exe github.com/jstarks/npiperelay

$ # インストール
$ sudo ln -s /mnt/c/Users/<ユーザー名>/bin/npiperelay.exe /usr/local/bin/npiperelay.exe
$ sudo install $HOME/go/src/github.com/jstarks/npiperelay/scripts/docker-relay /usr/local/bin/docker-relay

$ # docker-relay起動
# unset DOCKER_HOST # 念の為。環境変数を設定していなければ不要
$ sudo docker-relay &
$ docker version # dockerが実行できてるはず
```

注意点

* `npiperelay.exe` はWindows側のディレクトリ（`/mnt/c`以下）に生成する必要がある。
* `docker-relay` (`socat`) を終了したい場合は `sudo kill <socatのPID>`。
* （`docker-relay`が起動してないのに）`socat[439] E "/var/run/docker.sock" exists` のようなエラーが出て`docker-relay`が起動できない場合は、前回の起動で正しく終了しておらず `/var/run/docker.sock` が残っているためなので手動で削除する。

### 改良（任意）

基本的には上記の方法で良いのですが、このままだとOS再起動時に手動で`docker-relay`を実行しなければいけないので、bash起動時に自動的に実行するようにします。

```sh:/usr/local/bin/docker-relay
#!/bin/sh -eu
PIDFILE=/var/run/docker-relay.pid
SOCK=/var/run/docker.sock
pgrep -F "$PIDFILE" -f "socat UNIX-LISTEN:$SOCK" >/dev/null 2>&1 && exit
pkill -f "socat UNIX-LISTEN:$SOCK" ||:
rm -f "$SOCK"
exec socat UNIX-LISTEN:$SOCK,fork,group=docker,umask=007 EXEC:"npiperelay.exe -ep -s //./pipe/docker_engine",nofork &
echo $! > "$PIDFILE"
```

```console
$ # sudo docker-relay でパスワードを聞かれないようにする
$ echo '%docker ALL=(ALL) NOPASSWD: /usr/local/bin/docker-relay' | sudo tee /etc/sudoers.d/docker-relay 

$ # .bashrcで自動起動するようにする
$ echo "sudo docker-relay" >> ~/.bashrc
```

※気のせいかもしれないですが、たまに docker-relay が落ちる気がします。その場合は`sudo docker-relay`で再起動できます。改良版のdocker-relayはコマンド最後の `&` は不要です。docker-relay の多重起動防止や正しく終了しなかった場合のケアもしてるのでそのまま実行できるはずです。

## 2. docker-remote-api-tls

* https://github.com/kekru/docker-remote-api-tls

### 仕組み

```
Docker client on WSL
      ↓ TLS
docker-remote-api-tls (nginx on docker container)
      ↓ Unix socket
Docker server on DockerDesktopVM
```

docker-remote-api-tls は TLS 通信を nginx で`proxy_pass unix:/var/run/docker.sock`するだけの docker コンテナ


### 使い方

TLS 通信を行うには Docker 用に（オレオレ）証明書が必要です。それを簡単に生成するツール(`create-certs.sh`)も docker-remote-api-tls に含まれています。さらに自動生成もしてくれます。なので適当なディレクトリに以下のようなYAMLを作成してdocker-composeで起動するだけです。

```yaml:docker-compose.yml
version: "3.4"
services:
  remote-api:
    image: kekru/docker-remote-api-tls:v0.2.0
    ports:
     - 2376:443
    environment:
     - CREATE_CERTS_WITH_PW=<任意のパスワード>
     - CERT_HOSTNAME=localhost
    volumes:
     - ./certs:/data/certs
     - /var/run/docker.sock:/var/run/docker.sock:ro
```

```cmd
C:\適当なディレクトリ>docker-compose up -d
```

`C:\適当なディレクトリ\certs` 以下に証明書が作成されます。（以下はWSLから見た場合）

```
/mnt/c/適当なディレクトリ/certs
├── ca-cert.pem
├── ca-key.pem
├── client
│   ├── ca.pem
│   ├── cert.pem
│   └── key.pem
├── server-cert.pem
└── server-key.pem
```

WSLで以下の環境変数を設定します。（.bashrcに追加しましょう）

```sh
export DOCKER_CERT_PATH="/mnt/c/適当なディレクトリ/certs/client"
export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://localhost:2376"
```

WSLで`docker`コマンドが使えるはずです。（Linux用のdockerをWSL上に別途インストールして下さい）

```
$ docker version
省略
```

## 3. TLSを有効にする？

**2020-01-22 追記** 試してないです。ただのメモです。

1. Windows用のDockerサーバーの設定ファイル[daemon.json](https://docs.docker.com/engine/reference/commandline/dockerd/#on-windows)にTLSの設定項目があります。
2. Dockerサーバーの仮想マシン（DockerDesktopVM）は再起動すると大部分の変更はリセットされますが、`/var/lib`以下だけはデータが残ります。
3. つまり`/var/lib`以下にTLS関連のファイルを置いて`daemon.json`でそのファイルを指定すれば・・・

あとこれ [Adding TLS certificates](https://docs.docker.com/docker-for-windows/#adding-tls-certificates)（これはクライアントの設定の話だよなぁ？もしかしてサーバー側も設定できたりするの？）

面倒なので今更やる気は起きませんが

## おまけ

### ここまで至る道

**1. Docker Desktop for Windows上でTLSを有効にしたいと思い立つ。**

* [本当は怖いDocker for Windows のExpose daemon on tcp://localhost:2375 without TLS]
(https://qiita.com/shioshiota/items/fe0e085826bc2f19f154)
* [Docker コンテナの設定不備を悪用し仮想通貨発掘マルウェアを拡散する攻撃を確認](https://blog.trendmicro.co.jp/archives/19773)

**2. 証明書を作成すれば動くんじゃね？と気軽に考える。**

→ そう単純な話ではなかった。

**3. docker-relayを見つける。**

* https://devblogs.microsoft.com/commandline/cross-post-wsl-interoperability-with-docker/
* https://github.com/jstarks/npiperelay

→ が、ちゃんと手順通りにしておらず動かなかったので、さらに検索

**4. docker-machineでHyperV上にDockerサーバーを作ってみる。**

→ 動いた。TLS通信もできた。だけどDocker Desktop for Windowsとは別のDockerサーバーを使ってる。

**5. そもそもDocker Desktop for Windowsはどのような仕組みで動いているのか？**

→ HyperV上のLinux仮想マシン（DockerDesktopVM）上でDockerサーバーが動いている。

**6. DockerDesktopVMにログインしてみる。**

→ 仮想マシン名をダブルクリックしても仮想マシンに接続できない。
→ Dockerを利用してDockerDesktopVMにログインする方法を見つける。

* https://blog.jongallant.com/2017/11/ssh-into-docker-vm-windows/
* https://forums.docker.com/t/how-can-i-ssh-into-the-betas-mobylinuxvm/10991/7

パスとか変わってるようなので、今はこれ

```sh
docker run --privileged -it --rm -v /var/run/docker.sock:/var/run/docker.sock -v /usr/local/bin/docker:/usr/local/bin/docker alpine sh
docker run --net=host --ipc=host --uts=host --pid=host -it --security-opt=seccomp=unconfined --privileged --rm -v /:/host alpine /bin/sh
chroot /host
```

**2020/01/22 追記** 中を見るだけならこれで良い

```sh
docker run -it --rm -v /:/vm alpine chroot /vm
```


**7. ここに証明書を置けば良いのか？**

→ ファイルを置いてもDockerDesktopVMを再起動したら初期化される。
→ そもそも TLSポート（TCP:2376）が開いていない。

あとで気づいたが「Expose daemon on tcp://localhost:2375 without TLS」を有効にしても、Dockerサーバーは TCP:2375 ポートを開かない（開くのは `unix:///var/run/docker.sock` のみ）Dockerサーバーへはポートフォワーディングで接続してるのではなく、`com.docker.proxy.exe`（こいつがTCP:2375をオープンする）経由で接続していた。

**8. Docker Desktop for WindowsでTLSポートのオープンや証明書を配置する設定を探す。**

→ （現時点では）ない。できない事がわかるIssueは見つけた。

 * https://github.com/docker/for-win/issues/453
 * https://github.com/docker/for-win/issues/1953

**9. 上記のIssueより docker-remote-api-tls を発見。**

→ 動いた

**10. 再度 docker-relay に挑戦。**

→ 動いた