ConsulのHTTP APIをTLS化する

今回から何回かに分けて、Hashicorp製品であるConsulとNomad関連のトピックを扱っていきたいと思います。

第１弾は、ConsulのHTTP APIのTLS化です。

Consulではクラスタ内のサーバ間の通信や、外部からの要求を受け付ける際にいくつかポートをリッスンし、要求を受け入れています。
今回は、その一部であるHTTP APIをTLS化し、HTTPSでのAPIコールを受け付けるようにします。
さらにクライアント認証も有効化し、かつ、デフォルトで有効であったHTTPでのアクセスも無効化します。
これにより意図しない箇所からAPIをコールされないように強めのガードをかけることにします。

なお、注意点としては、上記のようにHTTP APIのTLSを有効化し、HTTPを無効にした場合、Consulの管理UI画面へのアクセスもHTTPSに限られます。
これは管理UIがHTTP APIと同じポートで提供されているからです。
またHTTP APIだけでなく、ConsulのCLIを使った操作にも内部でHTTP APIが使用されるため、こちらも必要な証明書、鍵を指定することが必要になります。


TLS有効化に必要な作業ステップは以下のとおりです。

１．各種の鍵、証明書の作成
  1-① ルートCAの証明書と鍵を作成する
  1-② Consulサーバ用サーバ証明書と鍵を作成する
  1-③ Consulクライアント用サーバ証明書と鍵を作成する
  1-④ APIにリクエストを発行するクライアントのためのクライアント証明書と鍵を作成する

２．証明書、鍵の配置
  2-① Consulサーバに必要な証明書や鍵を配置し、Consul設定ファイルに記載する
  2-② Consulクライアントに必要な証明書や鍵を配置し、Consul設定ファイルに記載する
  2-③ CLIやHTTP APIを実行するノードに必要な証明書や鍵を配置する


さて、上記を実現するためにはいくつかの選択肢がありますが、今回は以下のアプローチをとることにします。

まず、１-③、2-②ですが、Consulの「auto-encryption」機能を使うことで、手間を省くことができます。
これはConsul Connect CAの機能を使い、Consulクライアント向けのサーバ証明書と鍵を生成し、自動的にすべてのConsulクライアントに配布する機能です。
今回はこのアプローチを選択します。

　

では、進めていきましょう。

## 1-①ルートCAの証明書と鍵を作成する

Consul Connectで扱うルートCAの証明書と鍵を生成します。これはプライベートなルートCAです。
Consul Connect CAは、ルート証明書がSPIFFE SVID署名証明書である必要があるとのことです。
しかしながらSPIFFE SVID署名証明書としてルートCAに求められる要件が不透明なため、[公式のチュートリアル](https://learn.hashicorp.com/tutorials/consul/gossip-encryption-secure)に示されている手順をベースに進めていきます。

ここではまず、consulのCLIを使ってCAの証明書と鍵を生成します。
CNの項目値を指定できるようなので指定しておきましょう。

```console
$ consul tls ca create -common-name="My Consul CA"
==> Saved consul-agent-ca.pem
==> Saved consul-agent-ca-key.pem
$
$ cat consul-agent-ca.pem
-----BEGIN CERTIFICATE-----
MIICljCCAj2gAwIBAgIQYiBa30DnStxyYStLYGmmdjAKBggqhkjOPQQDAjCBjjEL
MAkGA1.....(中略)...........
.........................kNYxxa2
-----END CERTIFICATE-----
$
$ consul-agent-ca-key.pem
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIHHeRyQ7iv9lCWd12S9N9+WR9j6yliEYJweHR8ZpP/00oAoGCCqGSM49
  ...(中略)...
nFIOOLpdblfmD5U1l4SS1CFIDTvW9NM+og==
-----END EC PRIVATE KEY-----
$
$ openssl x509 -text -noout < consul-agent-ca.pem
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            62:20:5a:df:40:e7:4a:dc:72:61:2b:4b:60:69:a6:76
        Signature Algorithm: ecdsa-with-SHA256
        Issuer: C = US, ST = CA, L = San Francisco, street = 101 Second Street, postalCode = 94105, O = HashiCorp Inc., CN = My Consul CA
        Validity
            Not Before: Mar 29 05:51:10 2021 GMT
            Not After : Mar 28 05:51:10 2026 GMT
        Subject: C = US, ST = CA, L = San Francisco, street = 101 Second Street, postalCode = 94105, O = HashiCorp Inc., CN = My Consul CA
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:a4:e3:08:0c:32:b1:43:a8:0d:a3:0e:59:ea:25:
                    ...(中略)...
                    d6:f4:d3:3e:a2
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Certificate Sign, CRL Sign
            X509v3 Basic Constraints: critical
                CA:TRUE
            X509v3 Subject Key Identifier:
                2E:C5:6A:6E:06:5E:35:FF:4B:52:29:7D:14:2C:1D:FC:AF:C1:07:73:39:67:C9:BE:F1:55:F3:11:CB:BE:D3:AD
            X509v3 Authority Key Identifier:
                keyid:2E:C5:6A:6E:06:5E:35:FF:4B:52:29:7D:14:2C:1D:FC:AF:C1:07:73:39:67:C9:BE:F1:55:F3:11:CB:BE:D3:AD

    Signature Algorithm: ecdsa-with-SHA256
         30:44:02:20:2d:04:19:d6:14:3b:44:82:0a:da:98:03:2b:e7:
                   ...(中略)...
         d1:e5:5b:fb:6b:10:3d:cb:1d:ec:46:43:58:c7:16:b6
$
$ openssl ec -text -noout < consul-agent-ca-key.pem
read EC key
Private-Key: (256 bit)
priv:
    71:de:47:24:3b:8a:ff:65:09:67:75:d9:2f:4d:f7:
                   ...(中略)...
    fd:34
pub:
    04:a4:e3:08:0c:32:b1:43:a8:0d:a3:0e:59:ea:25:
                   ...(中略)...
    d6:f4:d3:3e:a2
ASN1 OID: prime256v1
NIST CURVE: P-256
```

PEM形式のCAの証明書(consul-agent-ca.pem)と鍵(consul-agent-ca-key.pem)ができました。
opensslコマンドで中身を確認しています。

SubjectにはHashicorpの情報が埋められていますが、プライベートCAなので気にしないで行きましょう。
ECDSA 256bitの鍵ペアが作られていて、自己署名されていることがわかります。


## 1-② Consulサーバ用サーバ証明書と鍵を作成する

続いてConsulサーバに配置するサーバ証明書を作成します。
ここではサーバにアクセスするときに使用するDNS名を「-additional-dnsname」で追加しています。

```console
$ consul tls cert create -server \
    -additional-dnsname=consul.mydomain.tk \
    -additional-dnsname=consul.service.consul \
    -ca ./consul-agent-ca.pem -key ./consul-agent-ca-key.pem
==> WARNING: Server Certificates grants authority to become a
    server and access all state in the cluster including root keys
    and all ACL tokens. Do not distribute them to production hosts
    that are not server nodes. Store them as securely as CA keys.
==> Using ./consul-agent-ca.pem and ./consul-agent-ca-key.pem
==> Saved dc1-server-consul-0.pem
==> Saved dc1-server-consul-0-key.pem
$
$ openssl x509 -text -noout < dc1-server-consul-1.pem
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            53:d1:96:b8:e4:44:2a:60:04:c8:11:fc:c8:49:c9:77
        Signature Algorithm: ecdsa-with-SHA256
        Issuer: C = US, ST = CA, L = San Francisco, street = 101 Second Street, postalCode = 94105, O = HashiCorp Inc., CN = My Consul CA
        Validity
            Not Before: Mar 29 06:41:19 2021 GMT
            Not After : Mar 29 06:41:19 2022 GMT
        Subject: CN = server.dc1.consul
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:68:a5:d9:e1:e9:1c:cd:01:c8:3a:af:fd:8a:5e:
                    ...(中略)...
                    30:1e:09:c4:dc
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Server Authentication, TLS Web Client Authentication
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Subject Key Identifier:
                7B:1B:08:C8:CD:7F:B9:82:A0:AE:3B:32:47:00:C3:68:0A:AC:95:67:8F:AE:0E:3E:89:56:C3:2C:64:86:3B:E3
            X509v3 Authority Key Identifier:
                keyid:2E:C5:6A:6E:06:5E:35:FF:4B:52:29:7D:14:2C:1D:FC:AF:C1:07:73:39:67:C9:BE:F1:55:F3:11:CB:BE:D3:AD

            X509v3 Subject Alternative Name:
                DNS:consul.mydomain.tk, DNS:consul.service.consul, DNS:server.dc1.consul, DNS:localhost, IP Address:127.0.0.1
    Signature Algorithm: ecdsa-with-SHA256
         30:45:02:21:00:83:c0:bd:69:17:20:8f:e8:6b:ab:e3:5c:e7:
         --(中略)--
         22:eb:40:69:cd:6b:1f:fe:61:99:65:aa:69:3d:1b:fb:95
```

サーバ証明書「dc1-server-consul-0.pem」とサーバ鍵「dc1-server-consul-0-key.pem」が生成されました。
「Issuer」や「X509v3 Authority Key Identifier:」keyid部分を見ると作成したルートCA(2E:C5:6A:6E:06:5E:～)で署名されているのがわかります。
また、オプションで追加指定したDNS名もSAN(Subject Alternative Name)に設定されているのがわかります。

## 1-③ Consulクライアント用サーバ証明書と鍵を作成する

前述のように、Consulクライアント用サーバ証明書・鍵はConsul側で自動で生成・配置する機能に頼るので、ここでの作業はありません。
次に進みましょう。

## 1-④ APIにリクエストを発行するクライアントのためのクライアント証明書と鍵を作成する

Consul CLIもしくはHTTP APIを実行するクライアントのためにクライアント証明書も作成します。

```console
$ consul tls cert create -cli -ca ./consul-agent-ca.pem -key ./consul-agent-ca-ke
y.pem
==> Using ./consul-agent-ca.pem and ./consul-agent-ca-key.pem
==> Saved dc1-cli-consul-0.pem
==> Saved dc1-cli-consul-0-key.pem
```

クライアント証明書「dc1-cli-consul-0.pem」とクライアント用の鍵「dc1-cli-consul-0-key.pem」が作成できました。

さて、このクライアント証明書はConsulの管理UI画面をhttpsでアクセスする際も要求されることになるので、
Webブラウザ側にこのクライアント証明書と鍵を使うように設定する必要があります。

まず、opensslコマンドを使って、クライアント証明書と鍵をPCKS.12形式にパッケージングします。

```console
$ openssl pkcs12 -export -out consul-ui-client.p12 -passout pass: \
   -in dc1-cli-consul-0.pem -inkey dc1-cli-consul-0-key.pem -certfile ./consul-agent-ca.pem
```

これでパスフレーズなしのPKCS.12ファイル「consul-ui-client.p12」が生成されました。
このファイルをPC側のWebブラウザに登録することで管理UI画面をHTTPSでアクセス可能です。
（Windowsの場合、consul-ui-client.p12をダブルクリックすると「証明書のインポートウイザード」が起動しますので、ウイザードに従って登録します）


それでは生成したファイルをサーバに配置して設定を加えていきましょう。


## 2-① Consulサーバに必要な証明書や鍵を配置し、Consul設定ファイルに記載する

Consulサーバには以下の4つのファイルをサーバに配置します。

* consul-agent-ca.pem（ルートCAの証明書）
* dc1-server-consul-0.pem（サーバ証明書）
* dc1-server-consul-0-key.pem（サーバ証明書のペアとなる鍵ファイル）

以下はConsulサーバの設定ファイルの例です。

```code:consul_server.hcl
data_dir = "/opt/consul"
log_level = "INFO"
datacenter = "dc1"
server = true
bootstrap_expect = 3

bind_addr = "{{ GetInterfaceIP \"eth0\" }}"
client_addr = "0.0.0.0"
retry_join = ["provider=aws tag_key=node_type tag_value=consul_server"]

# Note: http not used
ports {
  http = -1
  https = 8501
  grpc  = 8502
}
connect {
  enabled = true
  ca_provider = "consul"
  ca_config {
    root_cert = <<EOF
-----BEGIN CERTIFICATE-----MIICmTCCAj+gAwIBAgIQUxo8wNlmKsjkAdaVu2cVATAKBggqhkjOPQQDAjCBjzELMAkGA1UEldDEOMAwGA1UE--(中略)--cDba60lZm+J5KyLoC9dzEl7s-----END CERTIFICATE-----
EOF
    private_key =<<EOF
-----BEGIN EC PRIVATE KEY-----MHcCAQEEIItxNhgEb6jSAWqFhRhuRmkGNLMyr7vK3oWlyEOtUCzvoAoGCCqGSM49
AwEHoUQ--(中略)--Ui8EfaeA==-----END EC PRIVATE KEY-----
EOF
  }
}

ui_config {
  enabled = true
}

enable_agent_tls_for_checks = true

# Require TLS (client cert required)
verify_incoming = true,
verify_outgoing = true,
verify_server_hostname = true,
ca_file   = "/opt/tls/consul-agent-ca.pem",
cert_file = "/opt/tls/dc1-server-consul-0.pem",
key_file  = "/opt/tls/dc1-server-consul-0-key.pem",
auto_encrypt {
  allow_tls = true
}
```

ポイントは以下です。

* httpを無効にし、httpsを8501ポートで有効にするため、portsブロックでポートを指定する。
* Consul Connect CAの証明書と鍵をデフォルトから置き換えるため、root_certとprivate_keyにそれぞれのPEMファイルの中身を張り付ける。
* verify_incoming: trueの場合、他のサーバやクライアントからの接続にクライアント認証を要求する
* verify_outgoing: trueの場合、他のサーバへのRPCにTLSを使用する
* verify_server_hostname: trueの場合、他のサーバへのRPC実行時にサーバ証明書のホスト名がserver.<datacenter名>.<domain名>を検証する。
  * datacenter名、domain名はConsulサーバのサーバ証明書作成時に指定したもの（デフォルトでは"dc1"と"consul"が使われる)
* ca_file,cert_file,key_fileで各種証明書ファイル、鍵ファイルの配置パスを指定する。
* サーバ証明書を自動でConsulクライアントへ配置するように、auto_encryptブロックで「allow_tls = true」を指定する。


## 2-② Consulクライアントに必要な証明書や鍵を配置し、Consul設定ファイルに記載する

Consulクライアントには以下のファイルを配置する必要があります。
サーバ証明書と鍵は自動で配置されるので、不要です。

* consul-agent-ca.pem（ルートCAの証明書）

以下はConsulサーバの設定ファイルの例です。

```code:consul_client.hcl
data_dir = "/opt/consul"
datacenter = "dc1"
Log_level = "INFO"
server = false

bind_addr = "{{ GetInterfaceIP \"eth0\" }}"
client_addr = "0.0.0.0"
retry_join = ["provider=aws tag_key=node_type tag_value=consul_server"]
enable_local_script_checks = true

ports {
  http = -1
  https = 8501
  grpc  = 8502
}
connect {
  enabled = true
}
enable_agent_tls_for_checks = true

verify_incoming = false,
verify_outgoing = true,
verify_server_hostname = true,
ca_file = "/opt/tls/consul-agent-ca.pem",
# enable auto_deploy cert
auto_encrypt = {
  tls = true
}
```

クライアント側は基本的にマニュアルに記載の通りの設定です。


## 2-③ CLIやHTTP APIを実行するノードに必要な証明書や鍵を配置する

ConsulのCLIやHTTP APIを実行する際に、以下のファイルを使用します。

* consul-agent-ca.pem（ルートCAの証明書）
* dc1-cli-consul-0.pem（クライアント証明書）
* dc1-cli-consul-0-key.pem（クライアント証明書のペアとなる鍵ファイル）

CLIを実行する場合、規定された環境変数を設定すると便利です。
以下のように設定します。

```console
$ export CONSUL_HTTP_ADDR=https://consul.mydomain.tk:443
$ export CONSUL_CACERT=/path/to/consul-agent-ca.pem
$ export CONSUL_CLIENT_CERT=/path/to/dc1-cli-consul-0.pem
$ export CONSUL_CLIENT_KEY=/path/to/dc1-cli-consul-0-key.pem
$ 
$ #環境変数が使用されるため、特にオプションの指定は不要
$ consul members

curlでHTTP APIを呼び出す場合は、オプションで以下のように指定します。

```console
$ curl --cacert ${CONSUL_CACERT} --cert ${CONSUL_CLIENT_CERT} --key ${CONSUL_CLIENT_KEY} \
 ${CONSUL_HTTP_ADDR}/v1/catalog/datacenters
```

さて以上で、ConsulのHTTP APIをTLS化することができました。
クライアント認証を必須としているため、多少、面倒ではありますが、これで通信の秘匿化とサーバ、クライアントの真正性がチェックできるようになりました。


## おまけ：Gossip通信の暗号化

上記のTLSの有効化では、エージェント間のRPC呼び出しやエージェントへのHTTP API呼び出しを保護します。
一方、エージェント間のゴシップ通信はUDPで行われるため、別の手段で通信内容を保護する必要があります。

ゴシップ通信の暗号化は[チュートリアル](https://learn.hashicorp.com/tutorials/consul/gossip-encryption-secure)に記載しているとおり、同一の共通鍵を全エージェント（Consulサーバ、Consulクライアントのどちらも）の設定ファイルに記載します。


consul keygenで生成した32byteのBase64値を設定ファイルのencryptに指定します。

```console
$ consul keygen
pNb02VRrpzaaLsXMs9U0u9ZbwYsm4o+lcTWBmGEot7U=
```

```text:consul_sever.hcl/consul_client.hclの例
data_dir = "/opt/consul"
datacenter = "dc1"
Log_level = "INFO"
...
encrypt = "pNb02VRrpzaaLsXMs9U0u9ZbwYsm4o+lcTWBmGEot7U=",
...
```

　

今回はConsulのHTTP APIのTLS化を扱いました。
次回はNomadのHTTP APIのTLS化を扱いたいと思います。

## 参考ドキュメント
* https://learn.hashicorp.com/tutorials/consul/gossip-encryption-secure
* https://www.consul.io/docs/connect/ca/consul
* https://www.consul.io/commands/tls/ca
* https://www.consul.io/commands/tls/cert