NomadのHTTP APIをTLS化する

[前回](https://qiita.com/takayoshi-shiraki/items/46fc6a1e4dbc7f5fd500)はConsulのHTTP APIのTLSを有効化し、HTTPSでアクセスするように制限しました。
今回はNomadのHTTP APIをTLS化します。

[公式のチュートリアル](https://learn.hashicorp.com/tutorials/nomad/security-enable-tls)に手順が示されているので、それをベースに若干アレンジしています。

openssl ではなく、チュートリアルでの例のとおり[cfssl](https://github.com/cloudflare/cfssl)を使って、証明書や鍵を生成します。


cfsslのインストールについては[公式ドキュメント](https://github.com/cloudflare/cfssl#installation)等を参照してください。
ここではDebianにapt-getでインストールしています。

```console
$ apt-get install golang-cfssl
```

## 概要

NomadはConsulと異なり、HTTPとHTTPSを別ポートで共存することはできません。
HTTP APIはデフォルトだとポート4646をリッスンしますが、TLSを有効にするとHTTPではアクセスできなくなります。

今回はこのポート4646のHTTP APIをTLS化し、クライアント認証を必須に設定していきます。

注意点としては、Consulと同様にNomadの管理UI画面もHTTP APIと同一ポートで提供されるため、管理UI画面にアクセスする場合にもクライアント認証が必要になります。
またNomadのCLIも裏側ではHTTP APIが使用されるため、Nomad CLIを使用する場合もクライアント認証が必須となります。

　

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


## ルートCAの作成

まず、プライベートなルートCAの証明書と鍵を生成します。

```console
$ cat <<EOF > ca_csr.json
{
    "CN": "My Nomad CA",
    "key": {
        "algo": "ecdsa",
        "size": 256
    },
    "names": [
        {
            "OU": "Your Unit",
            "O":  "Your Company",
            "C":  "JP",
            "ST": "TOKYO",
            "L":  "XXX-KU"
        }
    ]
}
EOF
$ 
$ cfssl gencert -initca ca_csr.json | cfssljson -bare nomad-ca
2021/03/30 06:30:31 [INFO] generating a new CA key and certificate from CSR
2021/03/30 06:30:31 [INFO] generate received request
2021/03/30 06:30:31 [INFO] received CSR
2021/03/30 06:30:31 [INFO] generating key: ecdsa-256
2021/03/30 06:30:31 [INFO] encoded CSR
2021/03/30 06:30:31 [INFO] signed certificate with serial number 88956190062274699214290191130303041727763892227
$ ls
ca_csr.json  nomad-ca.csr  nomad-ca-key.pem  nomad-ca.pem
$ 
$ openssl x509 -text -noout < nomad-ca.pem
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            0f:94:ee:fb:38:e1:b5:9b:fb:e7:5e:65:59:af:9c:ff:b3:a4:c8:03
        Signature Algorithm: ecdsa-with-SHA256
        Issuer: C = JP, ST = TOKYO, L = XXX-KU, O = Your Company, OU = Your Unit, CN = My Nomad CA
        Validity
            Not Before: Mar 30 06:26:00 2021 GMT
            Not After : Mar 29 06:26:00 2026 GMT
        Subject: C = JP, ST = TOKYO, L = XXX-KU, O = Your Company, OU = Your Unit, CN = My Nomad CA
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:f9:fa:90:22:fc:f7:05:7a:e0:af:69:97:9e:80:
                    ...(中略)...
                    21:e2:13:56:de
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Key Usage: critical
                Certificate Sign, CRL Sign
            X509v3 Basic Constraints: critical
                CA:TRUE
            X509v3 Subject Key Identifier:
                82:43:67:03:C2:09:B1:51:94:33:43:7D:B2:54:09:7C:11:A5:F3:9E
    Signature Algorithm: ecdsa-with-SHA256
         30:45:02:21:00:ad:87:3c:5a:70:07:4a:91:3b:63:45:5e:61:
         ...(中略)...
         42:e3:93:d8:33:1a:93:43:f5:d8:7f:f8:49:57:63:fd:bc
```

ca_csr.jsonでSubjectの内容(OU,CN等)や鍵のスペック(ECDSA 256bit)を指定しています。

cfsslを使うと、あっさりと自己署名のルートCA証明書と鍵ファイルを生成できました。
便利ですね。

* nomad-ca.pem: ルートCA証明書
* nomad-ca-key.pem: ルートCAの鍵

以下の2つは中間生成物なので削除して問題ありません。

* ca_csr.json
* nomad-ca.csr


## サーバ証明書、クライアント証明書の作成

ここで一気に以下のファイルを作成します。

* Nomadサーバ用のサーバ証明書と鍵
* Nomadクライアント用のサーバ証明書と鍵
* Nomad CLI/HTTP APIクライアント用のクライアント証明書と鍵
* クライアント証明書と鍵をPKCS#12形式にパッケージングしたもの

流れは以下のスクリプトファイルの通りです。

```create_certs.sh
#!/bin/bash

CA_FILE=./nomad-ca.pem
CA_KEY=./nomad-ca-key.pem

#有効期間：3650日
cat <<EOF > config.json
{
  "signing": {
    "default": {
      "expiry": "87600h",
      "usages": ["signing", "key encipherment", "server auth", "client auth"]
    }
  }
}
EOF

# 管理UI画面へアクセスする際のDNS名をhostsに指定しておく
cat <<EOF > nomad_server.json
{
    "CN": "nomad.pmtmsfw.tk",
    "hosts": [
        "server.global.nomad",
        "localhost",
        "127.0.0.1",
        "nomad.mydomain.tk"
    ],
    "key": {
        "algo": "ecdsa",
        "size": 256
    },
    "names": [
        {
            "OU": "Your Unit",
            "O":  "Your Company",
            "C":  "JP",
            "ST": "TOKYO",
            "L":  "XXX-KU"
        }
    ]
}
EOF

cfssl gencert -ca=$CA_FILE -ca-key=$CA_KEY \
     -config=config.json  nomad_server.json | cfssljson -bare nomad-server

cat <<EOF > nomad_client.json
{
    "CN": "nomad-client.mydomain.tk",
    "hosts": [
        "client.global.nomad",
        "localhost",
        "127.0.0.1"
    ],
    "key": {
        "algo": "ecdsa",
        "size": 256
    },
    "names": [
        {
            "OU": "Your Unit",
            "O":  "Your Company",
            "C":  "JP",
            "ST": "TOKYO",
            "L":  "XXX-KU"
        }
    ]
}
EOF

cfssl gencert -ca=$CA_FILE -ca-key=$CA_KEY \
     -config=config.json  nomad_client.json | cfssljson -bare nomad-client

cat <<EOF > nomad_cli.json
{
    "CN": "nomad-cli.mydomain.tk",
    "hosts": [
        "localhost",
        "127.0.0.1"
    ],
    "key": {
        "algo": "ecdsa",
        "size": 256
    },
    "names": [
        {
            "OU": "Your Unit",
            "O":  "Your Company",
            "C":  "JP",
            "ST": "TOKYO",
            "L":  "XXX-KU"
        }
    ]
}
EOF

cfssl gencert -ca=$CA_FILE -ca-key=$CA_KEY \
     -profile=client nomad_cli.json | cfssljson -bare nomad-cli
```

スクリプトを実行します。

```console
$ chmod +x create_certs.sh
$ ./create_certs.sh
2021/03/30 08:28:40 [INFO] generate received request
2021/03/30 08:28:40 [INFO] received CSR
2021/03/30 08:28:40 [INFO] generating key: ecdsa-256
2021/03/30 08:28:40 [INFO] encoded CSR
2021/03/30 08:28:40 [INFO] signed certificate with serial number 494608314106644591725088700987600713379823876766
2021/03/30 08:28:40 [INFO] generate received request
2021/03/30 08:28:40 [INFO] received CSR
2021/03/30 08:28:40 [INFO] generating key: ecdsa-256
2021/03/30 08:28:40 [INFO] encoded CSR
2021/03/30 08:28:40 [INFO] signed certificate with serial number 639450421206660877889416706379311058516682651023
2021/03/30 08:28:40 [INFO] generate received request
2021/03/30 08:28:40 [INFO] received CSR
2021/03/30 08:28:40 [INFO] generating key: ecdsa-256
2021/03/30 08:28:40 [INFO] encoded CSR
2021/03/30 08:28:40 [INFO] signed certificate with serial number 184117806583655875159914202903602417004142126537
$
$ for F in `ls -X *.pem`; do echo $F;done
nomad-ca-key.pem
nomad-ca.pem
nomad-client-key.pem
nomad-client.pem
nomad-cli-key.pem
nomad-cli.pem
nomad-server-key.pem
nomad-server.pem
$ 
$ openssl pkcs12 -export -out nomad-ui-client.p12 -passout pass: \
  -inkey nomad-cli-key.pem -in nomad-cli.pem -certfile ./nomad-ca.pem
$
$ ls *.p12
nomad-ui-client.p12
```

最後のopensslコマンドで、クライアント証明書と鍵をPKCS#12形式にパッケージングしています。
この「nomad-ui-client.p12」については後述しますが、管理UI画面にアクセスする際に使用します。

ここまでで以下のファイルが出来上がりました。


| ファイル名            | 用途 |
| -------------------- | --- |
| nomad-ca.pem         | Nomad用プライベートルートCAの証明書 |
| nomad-ca-key.pem     | Nomad用プライベートルートCAの鍵 |
| nomad-server.pem     | Nomadサーバ向けのサーバ証明書 |
| nomad-server-key.pem | Nomadサーバ向けの鍵 |
| nomad-client.pem     | Nomadクライアント向けのサーバ証明書 |
| nomad-client-key.pem | Nomadクライアント向けの鍵 |
| nomad-cli.pem        | Nomad CLI/HTTP API向けのクライアント証明書 |
| nomad-cli-key.pem    | Nomad CLI/HTTP API向けの鍵 |
| nomad-ui-client.p12  | Nomad CLI/HTTP API向けのクライアント証明書と鍵を同梱したもの |

ここでも\*.jsonと\*.csrは、中間生成物なので削除して問題ありません。



## Nomadサーバへのサーバ証明書・鍵の配置と設定

Nomadサーバへ配置するファイルは以下のとおりです。
前回、ConsulのHTTP APIをTLS化したので、Consulにクライアント認証有りHTTPSで接続するためのファイルも配置します。

* nomad-ca.pem
* nomad-server.pem
* nomad-server-key.pem
* consul-agent-ca.pem(ConsulのルートCA証明書)
* dc1-cli-consul-0.pem(Consulのクライアント証明書)
* dc1-cli-consul-0-key.pem(Consulのクライアント認証用の鍵)

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

```/etc/nomad.d/nomad_server.hcl
datacenter = "dc1"
data_dir = "/opt/nomad"
bind_addr = "0.0.0.0"

advertise {
  http = "{{ GetInterfaceIP \"eth0\" }}"
  rpc = "{{ GetInterfaceIP \"eth0\" }}"
  serf = "{{ GetInterfaceIP \"eth0\" }}"
}

server {
  enabled          = true
  bootstrap_expect = 3
  raft_protocol = 3
  # Enable gossip encryption
  encrypt = "VUeFI3llUP3EHuHxV/TEOIpMJL5EMgMYIFpaCA8j2dw="
}
client {
  enabled = false
}

# access Consul with TLS
consul {
  ssl       = true
  address   = "127.0.0.1:8501"
  ca_file   = "/path/to/consul-agent-ca.pem"
  cert_file = "/path/to/dc1-cli-consul-0.pem"
  key_file  = "/path/to/dc1-cli-consul-0-key.pem"
}

# Require TLS
tls {
  http = true
  rpc  = true
  ca_file   = "/path/to/nomad-ca.pem"
  cert_file = "/path/to/nomad-server.pem"
  key_file  = "/path/to/nomad-server-key.pem"
  verify_server_hostname = true
  verify_https_client    = true
}
```

consulブロックでlocalhostに配備したconsulにHTTPSでアクセスするための設定を加えています。

tlsブロックのverify_server_hostname はサーバ間通信の際に接続先サーバのホスト名を検証するフラグです。
verify_https_client はサーバに接続するアクセス元にクライアント認証を要求するフラグです。
同一のCAで署名されたクライアント証明書を持たなければ接続を許可しません。

## Nomadクライアントのサーバ証明書・鍵の配置と設定

Nomadサーバへ配置するファイルは以下のとおりです。
Nomadサーバとの違いはNomadクライアント用のサーバ証明書を配置しているところです。

* nomad-ca.pem
* nomad-client.pem
* nomad-client-key.pem
* consul-agent-ca.pem(ConsulのルートCA証明書)
* dc1-cli-consul-0.pem(Consulのクライアント証明書)
* dc1-cli-consul-0-key.pem(Consulのクライアント認証用の鍵)


```/etc/nomad.d/nomad_client.hcl
datacenter = "dc1"
data_dir   = "/opt/nomad"
bind_addr = "0.0.0.0"

advertise {
  http = "{{ GetInterfaceIP \"eth0\" }}"
  rpc = "{{ GetInterfaceIP \"eth0\" }}"
  serf = "{{ GetInterfaceIP \"eth0\" }}"
}

server {
  enabled = false
}

client {
  enabled = true
  network_interface  = "eth0"
}

# adopt TLS for consul
consul {
  ssl       = true
  address = "127.0.0.1:8501"
  ca_file   = "/path/to/consul-agent-ca.pem"
  cert_file = "/path/to/dc1-cli-consul-0.pem"
  key_file  = "/path/to/dc1-cli-consul-0-key.pem"
}

# Require TLS
tls {
  http = true
  rpc  = true
  ca_file   = "/path/to/nomad-ca.pem"
  cert_file = "/path/to/nomad-client.pem"
  key_file  = "/path/to/nomad-client-key.pem"
  verify_server_hostname = true
  verify_https_client    = true
}
```

## Nomad　CLI/HTTP APIの実行方法


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

```console
$ export NOMAD_ADDR=https://nomad.mydomain.tk:443
$ export NOMAD_CACERT=/path/to/nomad-ca.pem
$ export NOMAD_CLIENT_CERT=/path/to/nomad-cli.pem
$ export NOMAD_CLIENT_KEY=/path/to/nomad-cli-key.pem
$
$ nomad server menbers
Name                                   Address      Port  Status  Leader  Protocol  Build  Datacenter  Region
ip-10-10-xx-xx.service.consul.global   10.10.xx.xx  4648  alive   true    2         1.0.3  dc1         global
ip-10-10-xx-xx.service.consul.global   10.10.xx.xx  4648  alive   false   2         1.0.3  dc1         global
ip-10-10-xx-xx.service.consul.global   10.10.xx.xx  4648  alive   false   2         1.0.3  dc1         global
```

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

```console
$ curl -s --cacert ${NOMAD_CACERT} --cert ${NOMAD_CLIENT_CERT} --key ${NOMAD_CLIENT_KEY} ${NOMAD_ADDR}/v1/agent/health | jq
{
  "server": {
    "message": "ok",
    "ok": true
  }
}
```

## Nomad管理UI画面へのアクセス

前述の手順の中で、クライアント証明書と鍵をPKCS#12形式にパッケージングした「nomad-ui-client.p12」を作成しました。
このファイルをPC側のWebブラウザに登録することで管理UI画面をHTTPSでアクセス可能です。
（Windowsの場合、nomad-ui-client.p12をダブルクリックすると「証明書のインポートウイザード」が起動するので、ウィザードに従って登録します）

## まとめ

これでNomadのHTTP APIのTLS化ができました。
NomadはNomadサーバ群を認識するためにConsulに問い合わせますので、ConsulのHTTP APIをTLS化した場合は、Consulのクライアント認証を満たすように設定が必要です。

ここまででConsulとNomadのTLS化ができましたので、次回はNomad上にPrometheusを配置してみます。
そこでConsulとNomadのメトリクス情報をPrometheusで収集してみましょう。

　

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

上記のTLSの有効化では、エージェント間のRPC呼び出しやエージェントへのHTTP API呼び出しを保護します。

一方、Consulの場合もそうでしたが、Nomadサーバ間のゴシップ通信については、別の手段で通信内容を保護する必要があります。

ゴシップ通信の暗号化は[チュートリアル](https://learn.hashicorp.com/tutorials/nomad/security-gossip-encryption)に記載しているとおり、同一の共通鍵をすべてのNomadサーバの設定ファイルに記載します。


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

```console
$ openssl rand -base64 32
VUeFI3llUP3EHuHxV/TEOIpMJL5EMgMYIFpaCA8j2dw=
```

```text:nomad_sever.hcl/nomad_client.hclの例
datacenter = "dc1"
data_dir = "/opt/nomad"
...
encrypt = "VUeFI3llUP3EHuHxV/TEOIpMJL5EMgMYIFpaCA8j2dw="
...
```


## 参考ドキュメント

* https://learn.hashicorp.com/tutorials/nomad/security-enable-tls
* https://www.nomadproject.io/docs/configuration/tls
* https://learn.hashicorp.com/tutorials/nomad/security-gossip-encryption