CLOVER🍀

That was when it all began.

CockroachDBでセキュアなクラスタをDocker Composeで作る

これは、なにをしたくて書いたもの?

CockroachDBを使うにあたって、SSLTLS証明書を扱うセキュアなクラスタを作るのは少し面倒そうかな?と思って
あとで見ようと思っていたのですが。

先にこちらをクリアしておいた方が良さそうだなぁと思いまして。

たとえばCluster APIを使うためにはセッショントークンを作る必要があるのですが、

All endpoints except /health and /login require authentication using a session token.

Cluster API v2.0 / Requirements

セッショントークンを得るためにはユーザーID、パスワードで認証できる必要があります。

ですが、そもそもユーザーを作成するためにはセキュアなクラスタである必要があります。

CREATE USER | CockroachDB Docs

デフォルトで作成されるrootユーザーにはパスワードがないため、そのままでは使えません。また、非セキュアな
クラスタではrootユーザーのパスワードを変更することすらできません。

この一方でDockerでのGetting Startedなどを見ていると、非セキュアなクラスタになっていたりもするので。

Start a Cluster in Docker (Insecure) | CockroachDB Docs

ここは、最初のうちにクリアしておいた方が良さそうだなと思いまして。

お題

やりたいことは、こちら。

  • 自分がCockroachDBの勉強に使うための、簡易的なDocker Composeの定義を作成する
  • セキュアなCockroachDBクラスタが構築できること
  • クラスタの初期化は、別ステップにする

また、以下はやりません。

  • データのホスト側への保存は考慮しない(使い捨てを前提にする)
  • DB Consoleの証明書についてはカバーしない

あくまで、勉強目的ですね。

環境

今回の環境は、こちらです。

$ docker version
Client: Docker Engine - Community
 Version:           20.10.8
 API version:       1.41
 Go version:        go1.16.6
 Git commit:        3967b7d
 Built:             Fri Jul 30 19:54:27 2021
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.8
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.16.6
  Git commit:       75249d8
  Built:            Fri Jul 30 19:52:33 2021
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.4.9
  GitCommit:        e25210fe30a0a703442421b0f60afac609f950a3
 runc:
  Version:          1.0.1
  GitCommit:        v1.0.1-0-g4144b63
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0


$ docker-compose version
docker-compose version 1.29.2, build 5becea4c
docker-py version: 5.0.0
CPython version: 3.7.10
OpenSSL version: OpenSSL 1.1.0l  10 Sep 2019

CockroachDBは21.1.7を使用します。

セキュアなクラスタを作る

Docker Compose…の前に、そもそもセキュアなクラスタをどうやって作るのか?について見ておいた方が良い気がします。

セキュアなクラスタとは、CockroachDBのノード間の通信やクライアントとの通信をSSLTLSで行うことがベースになります。
そのためにSSLTLS証明書を作成します。

非セキュアなクラスタには、cockroachコマンドでアクセスする際に--insecureオプションがつきまとうことになります。
…といっても、セキュアなクラスタにすると今度は--certs-dirオプションがつきまとうのですが。

作り方自体は、Get Startedに書いています。

Step 1. Generate certificates

また、セキュリティの認証に関するページにもセキュアなクラスタで使うSSLTLS証明書についての記載があります。

Authentication | CockroachDB Docs

ですが、Get Startedの内容はひとつのノードに3つのCockroachDBプロセスを起動してクラスタを構築しているので、
ノードがホストごとに分かれた手順でも確認したいな、と。

こんなことをする必要があります。

  • CA証明書と鍵を作成する
  • サーバー証明書と鍵を作成する
  • クライアント証明書(接続ユーザーごとに作成)と鍵を作成する

Authentication / Using digital certificates with CockroachDB

いずれもcockroach cert [コマンド]で作成でき、クラスタ内の各ノードで参照できる必要があります。

こんな感じです。

## CA証明書と鍵を作成する

$ cockroach cert create-ca \
  --certs-dir=[証明書を作成するディレクトリ] \
  --ca-key=[CAキーを配置するディレクトリ]/ca.key


## サーバー証明書と鍵を作成する
$ cockroach cert create-node \
  localhost \
  [ホスト名(複数可)] \
  --certs-dir=[証明書を作成するディレクトリ] \
  --ca-key=[CAキーを配置するディレクトリ]/ca.key


## クライアント証明書(接続ユーザーごとに作成)と鍵を作成する
$ cockroach cert create-client \
  [ユーザー名] \
  --certs-dir=[証明書を作成するディレクトリ] \
  --ca-key=[CAキーを配置するディレクトリ]/ca.key

たとえば、まとめて/cockroach/cockroach-certsディレクトリ内に証明書とCA鍵を作成するようにしてみましょう。

$ sudo mkdir -p /cockroach/cockroach-certs/{certs,ca}
$ sudo chown -R `whoami`:`whoami` /cockroach/cockroach-certs

ノードはnode1、node2、node3の3つ分、ユーザーはrootmyuserの2つとします。

CA証明書と鍵の作成。

$ cockroach cert create-ca \
  --certs-dir=/cockroach/cockroach-certs/certs \
  --ca-key=/cockroach/cockroach-certs/ca/ca.key

作成されたファイル。

$ find /cockroach/cockroach-certs -type f
/cockroach/cockroach-certs/certs/ca.crt
/cockroach/cockroach-certs/ca/ca.key

次に、ノード証明書と鍵を作成します。--certs-dir--ca-keyは、CA証明書と鍵を作った時と同じものを指定します。

$ cockroach cert create-node \
  localhost node1 node2 node3 \
  --certs-dir=/cockroach/cockroach-certs/certs \
  --ca-key=/cockroach/cockroach-certs/ca/ca.key

node.crtnode.keyが作成されます。

$ find /cockroach/cockroach-certs -type f
/cockroach/cockroach-certs/certs/node.key
/cockroach/cockroach-certs/certs/ca.crt
/cockroach/cockroach-certs/certs/node.crt
/cockroach/cockroach-certs/ca/ca.key

localhostを含め4つのホスト名を指定したのですが、できあがったファイルはひとつです。

これは、SAN(Subject Alternative Name)を使っているからですね。

Authentication / Using cockroach cert or openssl commands

ワイルドカードにも対応していそうではあります。

node.crt must have CN=node and the list of IP addresses and DNS names listed in the Subject Alternative Name field. CockroachDB also supports wildcard notation in DNS names

最後にクライアント証明書と鍵を作成します。こちらは、ユーザー単位に作成します。

$ cockroach cert create-client \
  root \
  --certs-dir=/cockroach/cockroach-certs/certs \
  --ca-key=/cockroach/cockroach-certs/ca/ca.key


$ cockroach cert create-client \
  myuser \
  --certs-dir=/cockroach/cockroach-certs/certs \
  --ca-key=/cockroach/cockroach-certs/ca/ca.key

最終的には、これだけのファイルができあがります。

$ find /cockroach/cockroach-certs -type f
/cockroach/cockroach-certs/certs/client.root.key
/cockroach/cockroach-certs/certs/client.root.crt
/cockroach/cockroach-certs/certs/client.myuser.key
/cockroach/cockroach-certs/certs/client.myuser.crt
/cockroach/cockroach-certs/certs/node.key
/cockroach/cockroach-certs/certs/ca.crt
/cockroach/cockroach-certs/certs/node.crt
/cockroach/cockroach-certs/ca/ca.key

あとは、これらのファイルをクラスタを構成する各ノードに配って--certs-dirで指定してCockroachDBプロセスを
起動すればOKです。

$ cockroach start \
  --certs-dir=/cockroach/cockroach-certs/certs \
  --store=[データの保存ディレクトリ] \
  --join=node1 \
  --join=node2 \
  --join=node3

起動後はinitクラスタを初期化します。

$ cockroach init --certs-dir=/cockroach/cockroach-certs/certs

Docker Composeで書く

と、ここまで書いた内容をDocker Composeで一気に行うようにします。

定義は、このようになりました。

docker-compose.yml

version: "3.9"
services:
  cockroach_seed_node:
    image: cockroachdb/cockroach:v21.1.7
    ports:
      - "26257:26257"
      - "8080:8080"
    volumes:
      - certs_volume:/cockroach/cockroach-certs:rw
    # hostname: node1
    networks:
      cockroach_cluster_network:
        aliases:
          - node1
    entrypoint: >-
      bash -c "
        test -d /cockroach/cockroach-certs/certs || mkdir /cockroach/cockroach-certs/certs &&
        test -d /cockroach/cockroach-certs/ca || mkdir /cockroach/cockroach-certs/ca &&
        rm -rf /cockroach/cockroach-certs/certs/* /cockroach/cockroach-certs/ca/* &&
        cockroach cert create-ca \
          --certs-dir=/cockroach/cockroach-certs/certs \
          --ca-key=/cockroach/cockroach-certs/ca/ca.key \
          &&
        cockroach cert create-node \
          localhost node1 node2 node3 \
          --certs-dir=/cockroach/cockroach-certs/certs \
          --ca-key=/cockroach/cockroach-certs/ca/ca.key \
          &&
        cockroach cert create-client \
          root \
          --certs-dir=/cockroach/cockroach-certs/certs \
          --ca-key=/cockroach/cockroach-certs/ca/ca.key \
          &&
        cockroach cert create-client \
          myuser \
          --certs-dir=/cockroach/cockroach-certs/certs \
          --ca-key=/cockroach/cockroach-certs/ca/ca.key \
          &&
        cockroach start \
          --certs-dir=/cockroach/cockroach-certs/certs \
          --store=/cockroach/cockroach-data \
          --advertise-addr=node1 \
          --join=node1 \
          --join=node2 \
          --join=node3
      "

  cockroach_node2:
    image: cockroachdb/cockroach:v21.1.7
    depends_on:
      - cockroach_seed_node
    volumes:
      - certs_volume:/cockroach/cockroach-certs:ro
    # hostname: node2
    networks:
      cockroach_cluster_network:
        aliases:
          - node2
    entrypoint: >-
      bash -c "
        sleep 3 &&
        cockroach start \
          --certs-dir=/cockroach/cockroach-certs/certs \
          --store=/cockroach/cockroach-data \
          --advertise-addr=node2 \
          --join=node1 \
          --join=node2 \
          --join=node3
      "

  cockroach_node3:
    image: cockroachdb/cockroach:v21.1.7
    depends_on:
      - cockroach_seed_node
    volumes:
      - certs_volume:/cockroach/cockroach-certs:ro
    # hostname: node3
    networks:
      cockroach_cluster_network:
        aliases:
          - node3
    entrypoint: >-
      bash -c "
        sleep 3 &&
        cockroach start \
          --certs-dir=/cockroach/cockroach-certs/certs \
          --store=/cockroach/cockroach-data \
          --advertise-addr=node3 \
          --join=node1 \
          --join=node2 \
          --join=node3
      "

networks:
  cockroach_cluster_network:
    driver: bridge

volumes:
  certs_volume: {}

証明書は、ボリュームで共有するようにします。

volumes:
  certs_volume: {}

最初に、証明書を作るノードを起動します。このノードだけが、証明書を共有するためのボリュームに書き込みます。

  cockroach_seed_node:
    image: cockroachdb/cockroach:v21.1.7
    ports:
      - "26257:26257"
      - "8080:8080"
    volumes:
      - certs_volume:/cockroach/cockroach-certs:rw
    # hostname: node1
    networks:
      cockroach_cluster_network:
        aliases:
          - node1
    entrypoint: >-
      bash -c "
        test -d /cockroach/cockroach-certs/certs || mkdir /cockroach/cockroach-certs/certs &&
        test -d /cockroach/cockroach-certs/ca || mkdir /cockroach/cockroach-certs/ca &&
        rm -rf /cockroach/cockroach-certs/certs/* /cockroach/cockroach-certs/ca/* &&
        cockroach cert create-ca \
          --certs-dir=/cockroach/cockroach-certs/certs \
          --ca-key=/cockroach/cockroach-certs/ca/ca.key \
          &&
        cockroach cert create-node \
          localhost node1 node2 node3 \
          --certs-dir=/cockroach/cockroach-certs/certs \
          --ca-key=/cockroach/cockroach-certs/ca/ca.key \
          &&
        cockroach cert create-client \
          root \
          --certs-dir=/cockroach/cockroach-certs/certs \
          --ca-key=/cockroach/cockroach-certs/ca/ca.key \
          &&
        cockroach cert create-client \
          myuser \
          --certs-dir=/cockroach/cockroach-certs/certs \
          --ca-key=/cockroach/cockroach-certs/ca/ca.key \
          &&
        cockroach start \
          --certs-dir=/cockroach/cockroach-certs/certs \
          --store=/cockroach/cockroach-data \
          --advertise-addr=node1 \
          --join=node1 \
          --join=node2 \
          --join=node3
      "

ENTRYPOINTで、証明書を作成してcockroach startまで行います…。

証明書は、毎回作成し直しています(残っていたら、削除します)。

CockroachDBのデータは、コンテナ内にしか持っていません。ホスト側に残したい場合は、ホスト側のディレクトリを
ボリュームとしてマウントするようにしてください。

ノード数やノード名、ユーザー名も固定なので、こちらもお好みで…。

ノードのホスト名は、networksエイリアスで指定しました。

    networks:
      cockroach_cluster_network:
        aliases:
          - node1

この場合、CockroachDBノード間で通信する時に--advertise-addrで明示的にホスト名を指定してあげないと
うまくいかないので注意してください。

        cockroach start \
          --certs-dir=/cockroach/cockroach-certs/certs \
          --store=/cockroach/cockroach-data \
          --advertise-addr=node1 \
          --join=node1 \
          --join=node2 \
          --join=node3

なお、hostnameでホスト名を指定した場合は--advertise-addrの指定は不要です…。

このノードに関しては、ホスト側からlocalhostで接続できた方が良いかなと思い、portsも指定しています。

    ports:
      - "26257:26257"
      - "8080:8080"

create-nodeの時にlocalhostを含めているのは、ホスト側からの利用も意図しています。

その他のノードは、最初のノードが起動した後に証明書の作成を待ってからcockroach startさせます。
※3秒固定で待っているだけですが

  cockroach_node2:
    image: cockroachdb/cockroach:v21.1.7
    depends_on:
      - cockroach_seed_node
    volumes:
      - certs_volume:/cockroach/cockroach-certs:ro
    # hostname: node2
    networks:
      cockroach_cluster_network:
        aliases:
          - node2
    entrypoint: >-
      bash -c "
        sleep 3 &&
        cockroach start \
          --certs-dir=/cockroach/cockroach-certs/certs \
          --store=/cockroach/cockroach-data \
          --advertise-addr=node2 \
          --join=node1 \
          --join=node2 \
          --join=node3
      "

確認してみる

では、作成したDocker Composeの定義で動作確認してみます。

起動。

$ docker-compose up

起動したら、シードノードに入ってみましょう。

$ docker-compose exec cockroach_seed_node bash

クラスタを初期化します。

# cockroach init --certs-dir=/cockroach/cockroach-certs/certs --host=node1

次に、ユーザーを作成しましょう。

# cockroach sql --certs-dir=/cockroach/cockroach-certs/certs --host=node1

ユーザーを作成して、DB Consoleを操作できるようにadmin権限も与えておきます。ここで作成するユーザーの名前は、
クライアント証明書を作った時のユーザー名と合わせるようにしてください。

root@node1:26257/defaultdb> create user myuser password 'password';
CREATE ROLE

Time: 151ms total (execution 150ms / network 1ms)


root@node1:26257/defaultdb> grant admin to myuser;
GRANT

Time: 200ms total (execution 200ms / network 0ms)

1度SQL Shellを抜けて

root@node1:26257/defaultdb> exit

作成したユーザーで接続。

# cockroach sql --certs-dir=/cockroach/cockroach-certs/certs --host=node1 --user=myuser
#
# Welcome to the CockroachDB SQL shell.
# All statements must be terminated by a semicolon.
# To exit, type: \q.
#
# Server version: CockroachDB CCL v21.1.7 (x86_64-unknown-linux-gnu, built 2021/08/09 17:55:28, go1.15.14) (same version as client)
# Cluster ID: abe5df0f-9f5d-4d0c-a41b-34b6d3d1446d
#
# Enter \? for a brief introduction.
#
myuser@node1:26257/defaultdb> 

セッショントークンを作って、Cluster APIも使ってみましょう。

CA証明書と、作成したユーザー名・パスワードでログインAPIを呼び出すと

# curl -d 'username=myuser&password=password' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --cacert /cockroach/cockroach-certs/certs/ca.crt \
  https://node1:8080/api/v2/login/

セッショントークンが得られます。

{"session":"[セッショントークン]"}

あとは、こちらを使ってCluster APIにアクセス。

# curl -H 'X-Cockroach-API-Session: [セッショントークン]' \
  --cacert /cockroach/cockroach-certs/certs/ca.crt \
  https://node1:8080/api/v2/nodes/

最後に、DB Consoleにもアクセスしてみましょう。https://localhost:8080で、自己署名証明書の警告を無視すれば繋がります。

f:id:Kazuhira:20210811002516p:plain

なお、DB Consoleの証明書を作りたい場合は、パブリックCAの証明書を使ってくださいとのことで…。

Authentication / Using a public CA certificate to access the DB Console for a secure cluster

DB Consoleへのログインは、先ほど作成したユーザー名、パスワードでログイン可能です。

f:id:Kazuhira:20210811002749p:plain

OKですね。

そういえば、クライアント証明書を使っていませんね。こちらは、JDBCなどで接続する際に別途利用しましょう。

まとめ

簡単な内容ですが、CockroachDBでセキュアなクラスタをDocker Composeで作ってみました。

このまま実際の環境で使えるようなものではないですが、CockroachDBの勉強用としては良いかなと思います。

今後はこれを使って進めていきましょう。