Rook + Ceph & Postgres Operator で、証明書認証しか許可しない PostgresQL インスタンスを立ち上げる

この記事は、
[softdeviceのみんなでゆるく記事を書く。 Advent Calendar 2020](https://adventar.org/calendars/5794)
の12/3分の記事として公開しています。

# はじめに

以前、[SSL証明書接続が必要な PostgresQL サーバーを docker-compose で立ち上げる](https://qiita.com/soumi/items/9856e2fc9787e3226056)という記事を書いたのですが、その環境を丸々[先日]()作成した  `Rook + Ceph` & `Postgres Operator` な環境に移行する必要が出てきました。

`Postgres Operator` にはそういったケースにも対応できるコマンドが用意されており、簡単に移行が出来ましたので、ご紹介します。

# 前提

[以前書いた記事]()を参照して、`Rook + Ceph` と `Postgres Operator` が完了しているとします。

# 証明書を用意

まずは、SSL証明書を作成します。
ここの手順は、丸々[以前の記事](https://qiita.com/soumi/items/9856e2fc9787e3226056#%E8%A8%BC%E6%98%8E%E6%9B%B8%E4%BD%9C%E6%88%90)と同じになります。

```sh
# サーバー証明書
mkdir cert && cd cert
openssl req -new -nodes -text -out ca.csr -keyout ca-key.pem -subj "/CN=certificate-authority"
openssl x509 -req -in ca.csr -text -extfile /etc/ssl/openssl.cnf -extensions v3_ca -signkey ca-key.pem -out ca-cert.pem
openssl req -new -nodes -text -out server.csr -keyout server-key.pem -subj "/CN=pg-server"
openssl x509 -req -in server.csr -text -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem

# クライアント証明書
openssl req -new -nodes -text -out client.csr -keyout client-key.pem -subj "/CN=postgres"
openssl x509 -req -days 36500 -in client.csr -text -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem

# 証明書一覧
ls

ca-cert.pem  ca.csr      client-cert.pem  client-key.pem   server.csr
ca-cert.srl  ca-key.pem  client.csr       server-cert.pem  server-key.pem
```

[元記事](https://qiita.com/soumi/items/9856e2fc9787e3226056#%E8%A8%BC%E6%98%8E%E6%9B%B8%E4%BD%9C%E6%88%90)には証明書を100年有効にする方法も記載していますので、必要であればご参照下さい。

# 証明書をシークレットとして登録

作成した証明書を `kubernetes` 内で使用するためには、一度シークレットとして登録する必要があります。

CA証明書を `pg-ca` として、

```sh
kubectl create secret generic pg-ca \
   --from-file=ca.crt=ca-crt.pem \
   -n pgo
```

TLS証明書を `pg-tls-keypair ` として登録します。

```sh
kubectl create secret tls pg-tls-keypair \
   --cert=server-cert.pem\
   --key=server-key.pem \
   -n pgo
```

`Postgres Operator` で使用するため、 `pgo`　名前空間に作成します。

# PostgresQL クラスタを立ち上げ

`pgo` コマンドで新しいクラスタを立ち上げますが、`pgo` コマンドにはTLS用のオプションが設けられているので、そのオプションを使用してクラスタを作成します。

(別ウィンドウで開いておく)

```sh
kubectl -n pgo port-forward svc/postgres-operator 9443:8443
```

```sh
pgo create cluster pg-with-ssl \
  --tls-only \
  --server-ca-secret=pg-tls-keypair \
  --server-tls-secret=pg-ca

created cluster: pg-with-ssl
workflow id: 23116517-08fd-404f-a190-e390ab235545
database name: pg-with-ssl
users:
        username: testuser password: )wDfe(jH5yqFwtA]XlGZIc?a
```

作成完了するまで待ちます。

```sh
pgo test pg-with-ssl

cluster : pg-with-ssl
        Services
                primary (10.106.120.45:5432): UP
        Instances
                primary (pg-with-ssl-54f6cf6df9-xffmj): UP
```

作成が完了したら、外部からアクセスできるように設定を変更します。

```sh
kubectl edit svc pg-with-ssl -n pgo

(省略)
spec:
  clusterIP: 10.102.60.6
  ports:
  - name: sshd
    port: 2022
    protocol: TCP
    targetPort: 2022
  - name: postgres
    nodePort: 30001 # <- 追加
    port: 5432
    protocol: TCP
    targetPort: 5432
  selector:
    pg-cluster: test
    role: master
  sessionAffinity: None
  type: NodePort # <- ClusterIP から変更
```

**nodePortは任意ですが、30000～32767である必要があります。**

## 証明書認証を強制

ここまでの状態で接続すると…

```sh
docker run --rm -it postgres psql postgres -h <ノードのIP> -U testuser --password -p 30001

psql (13.1 (Debian 13.1-1.pgdg100+1), server 12.4)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=>
```

パスワードで接続できてしまいます。
それも当然で、ここまでの設定では内容がSSL化されているだけで、認証に関しては何も設定していない状態です。
認証方式を編集するには、通常の `PotgresQL` と同様に `pg_hba` を編集する必要があります。 `Postgres Operator` では、 `Kubernetes` の `ConfigMap` として保存されているので、そこを編集します。

```sh
kubectl edit cm pg-with-ssl-pgha-config -n pgo

(省略)
      pg_hba:
      - local all postgres peer
      - local all crunchyadm peer
      - hostssl replication primaryuser 0.0.0.0/0 md5
      - hostssl all primaryuser 0.0.0.0/0 reject
      - hostssl all all 0.0.0.0/0 cert # <- md5から変更
```

変更を保存してエディタを終了すると書き込まれますが、これを反映するにはクラスタの再起動が必要です。

```sh
pgo restart pg-with-ssl-pgha-config -n pgo
```

再度接続テストをします。

```sh
docker run --rm -it postgres psql postgres -h <ノードのIP> -U testuser --password -p 30001

psql: error: FATAL:  connection requires a valid client certificate
FATAL:  no pg_hba.conf entry for host "192.168.197.212", user "testuser", database "postgres", SSL off
```

証明書が無いので接続拒否されました。想定通りの挙動です。

## 証明書接続の環境用意

SSL証明書認証を使った接続テストをするのですが、ポータビリティを高めたいので、`docker-compose` を使用したいと思います。
`Dockerfile` を使用せずに接続出来たらスマートだったのですが、証明書ファイルの権限が `600` である必要があるため、イメージを作る事にしました。
以下の構成のディレクトリを作成します。

```
- postgresql
    - postgresql.crt
    - postgresql.key
    - root.crt
- docker-compose.yaml
- Dockerfile
- postgres.env
```

それぞれの中身は以下の通りです。

### postgresqlフォルダ

「証明書を用意」 のセクションで作ったものを、以下のようにリネームして配置します。

| src              | dst               |
|:--               |:--              |
| client-cert.pem  | postgresql.crt  |
| client-key.pem   | postgresql.key  |
| ca-cert.pem      | root.crt        |

### docker-compose.yaml

```yaml
version: "3.5"
services:
  postgres:
    build: .
    ports:
      - 5432:5432
    env_file: 
      - postgres.env
    tty: true
    user: root
```

### Dockerfile

```Dockerfile
FROM postgres:12.2-alpine
ADD postgresql /root/.postgresql
RUN chmod -R 600 /root/.postgresql
```

### postgres.env

```
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
```

## 証明書接続のテスト

ファイルの用意が出来たら、 `docker-compose` でバックグラウンド起動します。

```sh
docker-compose up -d
```

この状態で以下のコマンドを使用すると、先程作成したクラスタに接続できるはずです。

```sh
docker-compose exec postgres psql "sslmode=require host=<ノードのIP> port=30001 dbname=postgres user=postgres"

psql (12.2, server 12.4)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=#
```

# おわりに

終わって振り返ってみると、おおまかな作業手順は普通に `PostgresQL` をSSL接続 & 証明書認証化する時とほぼ同じでした。`pgo` コマンドがTLS接続の設定に対応していてくれた事も大きいですね。

同じような実装が必要になる要件は少ないように思いますが、何かの参考になれば幸いです。