DockerのPostgreSQL公式イメージにSSL証明書を適用する方法

[Dockerのpostgres公式イメージ](https://hub.docker.com/_/postgres)にはSSL/TLS設定を行う手段に公式には対応していないが、
独自のイメージを作らずに何とかできないかと調べた結果をメモする。
なお、できるにはできたが苦肉の策なのであまり推奨はしない。(Entrypointの可読性が酷い。。。)

## 前提条件

* 他のコンテナとかも同じ証明書や秘密鍵で済ませたいので、Dockerホスト側に事前に作成した証明書の格納ディレクトリをコンテナにマウントさせる。<br>
  ※以下の記載におけるDockerホスト上の証明書と秘密鍵の場所は`/etc/ssl/certs`配下にあるものとする。
* Dockerホスト側にはpostgresユーザはいないので、この証明書と秘密鍵のオーナーはpostresユーザではない。(今回はroot)

```shell-session

-rw-r--r--. 1 root root 1562 May 21 17:38 /etc/ssl/certs/docker/server.crt
-rw-------. 1 root root 1704 May 21 17:38 /etc/ssl/certs/docker/server.key
```

* 公式イメージベース(FROM:postgres)で独自イメージを作って、証明書をイメージの中に含めると更新できないので起動時のパラメータ設定で何とかできるようにする。
* 以下の試したことに記載する"postgres-data"は事前に作成したデータボリュームを意味する。<br>
  `docker volume create --driver local postgres-data`

## 前提知識

* postgres(デーモンの起動プログラム)のオプションに`-c name=value`といった形式で名前付きパラメータを指定することができ、
  postgresql.confにSSL設定をしなくても以下のように起動オプションを付けることでSSL設定を行うことができる。<br>
```
postgres -c ssl=on -c ssl_cert_file=${サーバー証明書のパス} -c ssl_key_file=${秘密鍵のパス}
```

## 試したことその１（失敗）

Dockerホストの証明書と秘密鍵をそのままマウントさせて起動オプションに↑の前提知識の記載内容のオプションを指定して起動してみる。

### 起動コマンド

```sh
docker run -d --name postgres \
  -v postgres-data:/var/lib/postgresql/data \
  -v /etc/ssl/certs/docker:/etc/ssl/certs/postgres \
  -e POSTGRES_PASSWORD="postgres-passw0rd" \
  postgres:11 \
     -c ssl=on \
     -c ssl_cert_file=/etc/ssl/certs/postgres/server.crt \
     -c ssl_key_file=/etc/ssl/certs/postgres/server.key
```

### 結果

```shell-session
[test_user@docker-host ~]$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                     PORTS               NAMES
ff22f668ed21        postgres:11         "docker-entrypoint.s…"   7 seconds ago       Exited (1) 4 seconds ago                       postgres
[test_user@docker-host ~]$ docker logs postgres
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

・・・省略・・・

PostgreSQL init process complete; ready for start up.

2019-05-22 07:57:44.136 UTC [1] FATAL:  could not load private key file "/etc/ssl/certs/postgres/server.key": Permission denied
2019-05-22 07:57:44.136 UTC [1] LOG:  database system is shut down
[test_user@docker-host ~]$
```

`could not load private key file "/etc/ssl/certs/postgres/server.key": Permission denied`にあるように秘密鍵のパーミッションが600でpostgresユーザのものではないため、起動できない。。。（当たり前だった）

この結果を踏まえて、ホストOSにpostges(uid=70)ユーザを作って証明書と秘密鍵のオーナーを合わせればうまくいったが、前提条件に記載した通り他のコンテナでも同じものを利用したいので不都合のため、これを成功とはしない。。。


# 試したことその２（失敗）

[公式イメージのentrypoint](https://github.com/docker-library/postgres/blob/master/11/alpine/docker-entrypoint.sh)では、"/docker-entrypoint-initdb.d"にシェル(*.sh)やSQLスクリプト(*.sql or *.sql.gz)を置くと実行してくれることを思い出して、ホストOS上の以下のシェルを置いた場所を"/docker-entrypoint-initdb.d"にマウントさせてみた。

```shell:copy_cert.sh
#!/bin/bash

set -euo pipefail

cp -p /etc/ssl/certs/postgres/server.* /var/lib/postgresql/
chown postgres:postgres /var/lib/postgresql/server.*
# 念のためkeyを-rw-------にする
chmod 0600 /var/lib/postgresql/server.key
```

## 起動コマンド

```sh
docker run -d --name postgres \
  -v postgres-data:/var/lib/postgresql/data \
  -v /etc/ssl/certs/docker:/etc/ssl/certs/postgres \
  -v $(pwd)/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:ro \
  -e POSTGRES_PASSWORD="postgres-passw0rd" \
  postgres:11 \
     -c ssl=on \
     -c ssl_cert_file=/var/lib/postgresql/server.crt \
     -c ssl_key_file=/var/lib/postgresql/server.key
```

## 結果

```shell-session
[test_user@docker-host postgres]$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                     PORTS               NAMES
e38f27d22a63        postgres:11         "docker-entrypoint.s…"   6 seconds ago       Exited (1) 2 seconds ago                       postgres
[test_user@docker-host postgres]$ docker logs postgres
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

・・・省略・・・

waiting for server to start....2019-05-22 08:05:13.718 UTC [40] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2019-05-22 08:05:13.730 UTC [41] LOG:  database system was shut down at 2019-05-22 08:05:13 UTC
2019-05-22 08:05:13.733 UTC [40] LOG:  database system is ready to accept connections
 done
server started

/usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/copy_cert.sh
cp: cannot open '/etc/ssl/certs/postgres/server.key' for reading: Permission denied
[test_user@docker-host postgres]$
```

これを試してわかったことは、Dockerfileに`USER`指定がないので起動ユーザはrootだと勝手に思い込んでいたが、実際にはシェルのなかで`su-exec postgres $@`としているので、実質的な起動ユーザはpostgresであることがわかった。（シェルだけはrootで実行してほしかったなぁ。。。）

# 試したことその他３（成功＝結論）

しょうがないので苦肉の策として、イメージのentrypointを以下のように上書きしてやることにした。（ようは↑のcopy_cert.shの内容を実行してからオリジナルのdocker-entrypoint.shを実行する）

## 起動コマンド

```shell-session
docker run -d --name postgres \
  -v postgres-data:/var/lib/postgresql/data \
  -v /etc/ssl/certs/docker:/etc/ssl/certs/postgres \
  -e POSTGRES_PASSWORD="postgres-passw0rd" \
  --entrypoint /bin/sh \
  postgres:11 \
     -c 'cp -f /etc/ssl/certs/postgres/server.* /var/lib/postgresql/ && chown postgres:postgres /var/lib/postgresql/server.* && chmod 0600 /var/lib/postgresql/server.key && docker-entrypoint.sh -c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key'
```

余談になるが、どうやらコマンドラインから実行するときの"--entrypoint"オプションにスペースを含んだ文字列を指定するとうまくいかなかったので上のような記載になっている。。

## 結果

```shell-session
[vagrant@docker-host postgres]$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
f1ba7135cd32        postgres:11         "/bin/sh -c 'cp -f /…"   8 seconds ago       Up 6 seconds        5432/tcp            postgres
[vagrant@docker-host postgres]$ docker logs postgres
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

・・・省略・・・

PostgreSQL init process complete; ready for start up.

2019-05-22 08:06:46.826 UTC [9] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2019-05-22 08:06:46.827 UTC [9] LOG:  listening on IPv6 address "::", port 5432
2019-05-22 08:06:46.828 UTC [9] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2019-05-22 08:06:46.839 UTC [53] LOG:  database system was shut down at 2019-05-22 08:06:46 UTC
2019-05-22 08:06:46.843 UTC [9] LOG:  database system is ready to accept connections
[vagrant@docker-host postgres]$
```

SSL接続確認

```shell-session
[vagrant@docker-host postgres]$ docker exec -it postgres bash
root@f1ba7135cd32:/# psql 'postgres://postgres:postgres-passw0rd@localhost:5432/postgres?sslmode=require'
psql (11.3 (Debian 11.3-1.pgdg90+1))
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=# quit
root@f1ba7135cd32:/# exit
```

localhostだが、ssl接続できていることが確認できる。

# 最終版（docker-composeによるまとめ）

```yml:docker-compose.yml
version: '3.5'
services:
  postgres-server:
    image: postgres:11
    container_name: postgres-server
    hostname: postgres-server
    restart: always
    ports:
      - "5432:5432"
    networks:
      - container-link
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      # 初期処理時にentrypoint.shからコールされる初期化シェル・スクリプト群
      - ./services/postgres/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:ro
      # データ永続化はデータボリュームで。
      - postgres-data:/var/lib/postgresql/data
      # ホスト上の共通サーバ証明書/秘密鍵のディレクトリをマウント
      - /etc/ssl/certs/docker:/etc/ssl/postgres:ro
      # Timezoneをホストと一緒にする
      - /etc/localtime:/etc/localtime:ro
    # 1) マウントしたディレクトリの証明書と秘密鍵をコンテナ内にコピー
    # 2) 証明書と秘密鍵のオーナーを変更
    # 3) 念のため、秘密鍵のパーミッションを600にする(ホスト側がなっていれば不要)
    # 4) オリジナルのdocker-entrypoint.shに名前付き引数にSSL設定して実行
    entrypoint: >
        /bin/sh -c "cp -f /etc/ssl/postgres/server.* /var/lib/postgresql/ &&
        chown postgres:postgres /var/lib/postgresql/server.* &&
        chmod 0600 /var/lib/postgresql/server.key &&
        docker-entrypoint.sh -c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key"

  # 接続確認用コンテナ
  postgres-client:
    # alpineベースのpostgres-clientがあったので拝借
    image: jbergknoff/postgresql-client
    container_name: postgres-client
    hostname: postgres-client
    depends_on:
      - postgres-server
    networks:
      - container-link
    volumes:
      # サーバー証明書を証明したルート証明書の場所
      # （${HOME}/.postgresql/root.crtとしておくとCommonNameで検証ができる
      - /etc/ssl/certs/docker:/root/.postgresql:ro
    # このコンテナイメージのEntryPointは"psql"になっているので接続URLを渡してあげる
    command:
      - "postgres://postgres:postgres-passw0rd@postgres-server.docker.internal:5432/postgres?sslmode=verify-full"
    # 落ちないように（後でdocker execで検証するため）
    tty: true

networks:
  # docker-composeで勝手にデフォルトネットワークが生成される予防。（自動で全コンテナが所属する）
  default:
    external:
      name: bridge
  # コンテナ間通信用のネットワークセグメント(各サービスの"${hostname}.docker.internal"でFQDNな感じの文字列でアクセスできる)
  container-link:
    name: docker.internal
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: "172.20.0.0/16"

volumes:
  # postgresコンテナのデータ永続化用のデータボリューム
  postgres-data:
    name: postgres-data
```

### 確認

↑の定義の接続クライアントはpostgres-serverがアクセスできるようになるまで待つ必要があるので、別々に起動する。
depends_onは起動順序は制御できるけど、Postgresサーバーが接続できるようになるまでは待ってくれるわけではない。

```shell-session
[test_user@docker-host test]$ docker-compose up -d postgres-server
Creating network "docker.internal" with driver "bridge"
Creating volume "postgres-data" with default driver
Creating postgres-server ... done
[test_user@docker-host test]$ docker logs -t postgres-server
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

・・・省略・・・

2019-05-22 17:34:00.004 JST [9] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2019-05-22 17:34:00.004 JST [9] LOG:  listening on IPv6 address "::", port 5432
2019-05-22 17:34:00.006 JST [9] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2019-05-22 17:34:00.017 JST [53] LOG:  database system was shut down at 2019-05-22 17:33:59 JST
2019-05-22 17:34:00.020 JST [9] LOG:  database system is ready to accept connections
[test_user@docker-host test]$ docker-compose up -d postgres-client
postgres-server is up-to-date
Creating postgres-client ... done
[test_user@docker-host test]$ docker ps -a
CONTAINER ID        IMAGE                          COMMAND                  CREATED              STATUS              PORTS                    NAMES
88d604f8b9ac        jbergknoff/postgresql-client   "psql postgres://pos…"   4 seconds ago        Up 3 seconds                                 postgres-client
f89b5e18e50d        postgres:11                    "/bin/sh -c 'cp -f /…"   About a minute ago   Up About a minute   0.0.0.0:5432->5432/tcp   postgres-server
[test_user@docker-host test]$ docker exec -it postgres-client psql
psql: could not connect to server: No such file or directory
        Is the server running locally and accepting
        connections on Unix domain socket "/tmp/.s.PGSQL.5432"?
[test_user@docker-host test]$ docker exec -it postgres-client sh
/ # exit
[test_user@docker-host test]$ docker logs -t postgres-client
2019-05-22T08:35:22.554877080Z psql (10.3, server 11.3 (Debian 11.3-1.pgdg90+1))
2019-05-22T08:35:22.554917194Z WARNING: psql major version 10, server major version 11.
2019-05-22T08:35:22.554921391Z          Some psql features might not work.
2019-05-22T08:35:22.554924196Z SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
2019-05-22T08:35:22.554927753Z Type "help" for help.
2019-05-22T08:35:22.554930560Z
[test_user@docker-host test]$ docker exec -it postgres-client sh
/ # psql "postgres://postgres:postgres-passw0rd@postgres-server.docker.internal:5432/postgres?sslmode=verify-full"
psql (10.3, server 11.3 (Debian 11.3-1.pgdg90+1))
WARNING: psql major version 10, server major version 11.
         Some psql features might not work.
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=#
```

## 余談

### サーバー証明書の検証について

psqlでサーバーのドメイン名などの検証する場合は、`sslmode=verify-full`などを指定するが、このときにサーバー証明書を署名したルート証明書は"~/.postgresql/root.crt"に置くということを初めてしった。


### SSL接続の強制などについて。

pg_hba.confにてSSLアクセスを強制させたい場合などについてだが、これについてはpg_hba.confのオーナーはpostgresユーザであるため、"/docker-entrypoint-initdb.d"配下にpg_hba.confをsedとかで編集するシェルを置けばよい。
ちなみにデフォルトのpg_hba.confはdocker-entrypoint.shの中で追記されているのでデフォルト設定とは多少違うみたい。。。