TerraformのTLS Providerを試す
TerraformのProviderを眺めていたらTLS Providerというものを発見したので試してみる。まず、TLS Providerが提供するリソースは4種類ある。 1. tls_private_key秘密鍵をPEM形式で生成するリソース。RSAとECDSAをアルゴリズムとして選択できる。注意点として、これによって生成された秘密鍵はtfstateファイルに 暗号化されずに 保存される。Production環境での利用は推奨していない。
-
tls_self_signed_cert 自己署名付きTLS証明書をPEM形式で生成するリソース。
-
tls_cert_request 証明書署名要求(CSR)をPEM形式で生成するリソース。
-
tls_locally_signed_cert 証明書署名要求(CSR)を用いてTLS証明書を生成し、提供された認証局(CA)の秘密鍵で署名するリソース。 なるほど、結構便利そうだ。軽く試してみよう。今回は、自己署名したルートCAと、それを使ってSSL証明書を生成する。その後、SSL証明書を読み込ませたHTTPSサーバーへリクエストを投げてみよう。まずはルートCAを作る。allowed_usesのパラメータには気をつける必要がある。Provider側の文字列とマッチしていない場合、設定されないようだ。
resource “tls_private_key” “root” {
algorithm = “RSA”
}
resource “tls_self_signed_cert” “root” {
key_algorithm = “RSA”
private_key_pem = “${tls_private_key.root.private_key_pem}”
validity_period_hours = 26280
early_renewal_hours = 8760
is_ca_certificate = true
allowed_uses = [“cert_signing”]
subject {
common_name = "hello2018.repl.info"
}
}
resource “local_file” “root_ca_pem” {
filename = “root_ca.pem”
content = “${tls_self_signed_cert.root.cert_pem}”
}
resource “local_file” “root_ca_key” {
filename = “root_ca.key”
content = “${tls_private_key.root.private_key_pem}”
}
terraform applyすると、秘密鍵と証明書がファイルとして出力されているはずだ。
$ ls root_ca.*
root_ca.key root_ca.pem
root_ca.pemを見てみる。tfファイルで設定した内容が反映された証明書になっていることがわかる。
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
db:d7:70:35:81:54:70:11:e8:cc:7d:92:d9:7b:56:7f
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=hello2018.repl.info
Validity
Not Before: Aug 29 08:03:01 2018 GMT
Not After : Aug 28 08:03:01 2021 GMT
Subject: CN=hello2018.repl.info
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:c3:b3:12:27:39:e2:5d:6f:ac:ec:c1:92:b6:3f:
77:3e:19:bb:56:14:80:0c:03:5a:15:a3:21:80:55:
c3:61:93:5f:33:a4:c9:ae:ab:68:91:80:50:e0:f0:
89:4c:88:02:17:d8:f0:f3:c1:2f:93:b4:27:cc:35:
76:8f:7d:c7:7e:ef:8d:0d:01:8a:ad:8e:1b:d2:69:
be:e1:5f:76:bf:d9:90:86:97:89:21:f4:dc:5b:f5:
7f:5c:1d:5f:7c:cb:cc:c2:e5:0a:71:51:0b:39:a5:
f0:ca:7b:5c:d8:d7:9e:2f:5f:ff:c1:ff:f5:bf:85:
2e:a4:5a:a3:0e:ee:2f:81:81:82:fc:4f:fd:7f:a0:
80:59:73:6e:57:03:42:74:d2:3a:b4:57:b7:ad:1c:
22:20:7d:28:8c:47:34:72:46:22:2b:34:ab:7b:45:
26:7d:fd:86:1e:b9:fc:6b:9e:f5:a2:1a:fa:23:16:
9a:69:b0:1d:a1:f5:6e:fb:43:03:22:8b:68:d5:cd:
a8:8f:52:89:0b:57:af:bf:24:79:2c:7a:fe:ab:74:
6b:5a:31:e6:7a:2d:55:0c:37:d6:f1:d8:eb:0a:c6:
df:60:cd:04:1b:11:42:d9:86:65:a8:8a:7f:cc:d9:
6e:f4:54:e2:d4:75:5a:19:36:e2:1e:bd:56:5b:87:
d0:e1
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
Certificate Sign
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Subject Key Identifier:
8F:F3:3D:D0:54:63:37:62:2E:CA:97:D8:BC:EF:0B:4F:D9:37:A0:51
Signature Algorithm: sha256WithRSAEncryption
49:31:d0:04:d3:27:43:ae:d9:4b:46:a6:90:34:b2:cd:fa:b1:
e6:a6:ed:38:92:ca:96:82:cf:2d:90:e7:36:f4:b6:77:1b:40:
f5:0a:06:06:a4:d8:61:9c:fc:6d:02:30:b1:41:50:2a:59:ef:
00:db:a0:d6:5c:03:50:9e:0d:70:80:b9:ce:46:29:91:99:12:
45:9a:fa:f5:c2:4f:71:60:6b:ce:bb:b0:0f:04:f6:e3:d8:63:
e6:d4:70:bd:a0:99:14:71:3e:40:a1:bb:e1:46:79:77:ef:23:
b7:ec:27:1b:80:ad:33:ef:93:92:ae:04:70:15:c0:f2:b2:34:
63:81:92:88:4f:a1:78:6f:74:f3:f0:64:09:e2:d5:e1:ed:77:
d3:26:d5:b2:6e:2f:3a:81:5c:00:f8:ec:4b:e3:36:45:7b:ec:
26:92:b9:42:28:f5:38:bf:0b:49:8c:eb:d6:47:28:ca:d9:41:
63:e1:51:01:b5:05:e6:a3:3d:93:7f:02:9e:f4:8a:60:86:13:
9a:4a:df:ef:38:1c:4c:63:bc:4d:38:30:f3:d3:01:3c:2b:2a:
77:d7:73:b5:55:3c:53:69:d9:99:8a:ce:90:cd:d0:10:56:c5:
76:d7:49:b2:a8:35:2b:5a:9e:f2:5b:9d:2a:83:3f:ce:e0:8f:
40:6a:b4:4c
さて、次はSSLサーバー証明書を作ってみる。
resource "tls_private_key" "foo" {
algorithm = "RSA"
}
resource "tls_cert_request" "foo" {
key_algorithm = "${tls_private_key.foo.algorithm}"
private_key_pem = "${tls_private_key.foo.private_key_pem}"
subject {
common_name = "foo.hello2018.repl.info"
}
dns_names = [
"localhost",
]
}
resource "tls_locally_signed_cert" "foo" {
ca_key_algorithm = "${tls_private_key.root.algorithm}"
cert_request_pem = "${tls_cert_request.foo.cert_request_pem}"
ca_private_key_pem = "${tls_private_key.root.private_key_pem}"
ca_cert_pem = "${tls_self_signed_cert.root.cert_pem}"
validity_period_hours = 12
allowed_uses = [
"server_auth",
]
}
resource "local_file" "foo_cert_pem" {
filename = "foo.pem"
content = "${tls_locally_signed_cert.foo.cert_pem}"
}
resource "local_file" "foo_cert_key" {
filename = "foo.key"
content = "${tls_private_key.foo.private_key_pem}"
}
これで、証明書と秘密鍵がファイルとして生成されているはずだ。
$ ls foo*
foo.key foo.pem
foo.pemの中身も見てみよう。
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
c3:55:e7:99:94:8b:41:3c:f2:86:e9:67:2f:d4:ea:7a
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=hello2018.repl.info
Validity
Not Before: Aug 29 13:04:46 2018 GMT
Not After : Aug 30 01:04:46 2018 GMT
Subject: CN=foo.hello2018.repl.info
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:d2:49:e6:14:a7:7b:4a:f5:51:1c:d6:27:62:05:
2d:c1:43:3a:53:44:f6:5d:01:91:1a:64:a7:82:6e:
e0:ae:de:ad:48:52:9e:ea:d8:10:99:d6:9a:6a:1f:
38:16:42:7a:5d:9a:a7:29:4f:03:d6:20:71:5b:2b:
e4:2e:42:ee:eb:6f:f0:dc:2b:bd:5c:45:6f:fe:41:
49:36:de:9a:0e:71:e4:95:15:e5:1a:61:9f:4d:9d:
77:b7:2a:44:a8:f1:95:cf:e5:35:97:53:83:08:1b:
ff:14:4c:78:c8:18:74:a4:19:7f:4a:c7:cd:e0:21:
0e:ef:c8:98:2a:8b:8d:d2:8b:39:1c:14:8b:b8:18:
23:56:23:e3:3c:60:2d:3d:5e:d6:d8:2e:65:1e:05:
b6:1d:16:4c:f3:41:5c:50:6d:e7:aa:a4:04:8a:f3:
3c:bc:32:b5:a1:8f:c0:c0:ff:ce:9e:f7:25:f8:d1:
ab:50:18:a4:bb:0b:61:6b:c5:dd:2e:3d:49:7d:e7:
e0:4e:92:17:96:88:dc:08:9c:f9:03:74:9c:17:8d:
95:a7:75:61:f4:41:87:6b:69:78:ed:5d:16:7a:d6:
e5:11:b3:7f:a0:0e:8e:64:a2:47:a3:65:45:84:2e:
a9:07:57:7f:f5:39:c5:fd:34:77:9c:d9:8e:57:fc:
44:73
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Authority Key Identifier:
keyid:8F:F3:3D:D0:54:63:37:62:2E:CA:97:D8:BC:EF:0B:4F:D9:37:A0:51
X509v3 Subject Alternative Name:
DNS:localhost
Signature Algorithm: sha256WithRSAEncryption
46:4a:39:3e:bb:cd:fa:2d:ba:98:81:38:30:7b:68:a8:15:35:
56:3c:36:1a:b1:95:f2:a2:f4:e5:c6:0b:38:ce:e8:bf:8a:12:
63:b5:55:9a:27:c8:91:f8:ee:29:b4:3e:9c:80:7a:34:73:49:
cc:6a:0c:8d:1f:9a:f9:4f:f4:ce:1a:0e:26:86:fb:fb:f8:95:
25:5a:dd:8d:db:ec:7b:bf:90:59:aa:83:d9:7d:62:5a:f1:70:
13:52:b4:a6:38:c1:8e:30:49:2d:9d:c1:43:71:7e:8b:76:28:
88:5a:5b:3b:1a:e3:cf:7e:42:3d:04:c6:db:8a:9b:16:3e:92:
77:f4:d7:b1:fa:a3:7e:1a:b7:f1:39:f7:b1:0b:f6:fb:1a:e1:
7e:81:e6:29:fb:b4:21:b4:73:fc:8a:2e:b4:78:b9:ae:9e:82:
4b:0a:ca:53:4a:d6:26:89:7c:a9:c2:0b:40:c1:3b:fc:76:ff:
af:b1:d8:cb:9b:4f:16:2a:37:7a:3a:fe:05:71:27:bc:0d:9e:
8c:45:73:59:07:76:b5:5a:7e:48:5a:b2:04:95:98:ed:42:cd:
95:ed:93:d9:65:fd:c4:64:d6:24:be:a4:ae:8a:b6:8d:57:e8:
fb:4c:cc:4a:e4:d1:b2:42:7f:2f:a6:d4:28:eb:d2:b1:5c:6d:
ca:ab:e3:71
nginxなどを使ってもいいが、今回はRubyでHTTPSサーバーを動かすことにする。 ローカルサーバでSSLしようとして試行錯誤の上結局WEBrick使った話のコードを使わせてもらっています。
require 'webrick'
require 'webrick/https'
cert = ARGV[0]
key = ARGV[1]
server = WEBrick::HTTPServer.new(
:BindAddress => '127.0.0.1',
:Port => 3000,
:DocumentRoot => '.',
:SSLEnable => true,
:SSLCertificate => OpenSSL::X509::Certificate.new(open(cert).read),
:SSLPrivateKey => OpenSSL::PKey::RSA.new(open(key).read))
Signal.trap('INT') { server.shutdown }
server.start
curlでcacertを指定してアクセス。証明書エラーにならずに無事繋がった。
$ curl -v --cacert ./root_ca.pem https://localhost:3000/
* Trying ::1...
* TCP_NODELAY set
* Connection failed
* connect to ::1 port 3000 failed: Connection refused
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 3000 (#0)
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
* CAfile: ./root_ca.pem
CApath: /usr/local/etc/openssl/certs
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server did not agree to a protocol
* Server certificate:
* subject: CN=foo.hello2018.repl.info
* start date: Aug 29 13:04:46 2018 GMT
* expire date: Aug 30 01:04:46 2018 GMT
* subjectAltName: host "localhost" matched cert's "localhost"
* issuer: CN=hello2018.repl.info
* SSL certificate verify ok.
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.60.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/html; charset="UTF-8"
< Server: WEBrick/1.4.2 (Ruby/2.5.1/2018-03-29) OpenSSL/1.0.2o
< Date: Wed, 29 Aug 2018 13:08:52 GMT
< Content-Length: 2417
< Connection: Keep-Alive
<
なかなか使いどころが限られるProviderだと思うが、うまく使えば証明書生成・管理を楽に行えそうである。tls_private_keyリソースの説明に書かれている通り、秘密鍵がtfstateファイルに含まれるため取り扱いには注意が必要だろう。本番環境では別途読み込むとか、Vault Providerを使うようにする、という工夫が必要になりそうだ。Kubernetesのクラスターを扱おうとすると証明書の管理どうすんの、という話になるけどどうやるのがいいのかよく分からない。Terraformを使ってインスタンスを扱う場合はこういうProviderを使うのも一つの手なのだろうか…