Vault ACME on minikube
今年、Vault 1.14がリリースされ、ACMEの機能が追加されました。
普段からVaultを使っているものの、ACMEが追加されたことに最近気づきました。
もっと大々的に機能紹介してよいものだと個人的には思っています。
ゼットラボ社では社内自己署名サーバ証明書の発行に、Smallstep Labsのstep-caでACMEを社内に提供しています。
主に社内KubernetesのIngressサーバ証明書の用途に利用されており、Ingressとの連携には、cert-managerを導入しています。
また、社内SecretStore, PKIとしてVaultを利用しています。
今回VaultにACMEの機能が追加されたので、社内にもVaultがあることから、本記事を通して動作確認をします。
公式サイトには、VaultとCaddyをDocker上で動かすTutorialが公開されています。
上記を参考に、本記事では、minikube上にVault, cert-managerを構築し、ACMEの動作確認をします。
検証環境
以下バージョンで検証しています。
- Mac: Ventura / Intel
- minikube: v1.32.0 Hyperkit利用
- Ingress: minikube addons ingress(ingress-nginx/controller: v1.9.4)
- Kubernetes: v1.28.3
- Vault: v1.15.2
- cert-manager: v1.13.2
検証構成概要
コンポーネントを様々省略しておりますが、簡易的には上記の通りとなります。
以下の手順で構築します。
- minikube起動・Ingress nginxのデプロイ
- Vaultのデプロイ
- CoreDNSの設定追加(HTTP-01 Challenge時に名前解決用途)
- cert-managerのデプロイ
- テスト用Webサーバのデプロイ
- 動作確認
minikube起動・Ingress nginxのデプロイ
$ minikube start
$ minikube addons enable ingress
Vault のデプロイ
今回は、VaultのエンドポイントをTLS化して検証したいと思います。
minikube上でVaultのTLS対応およびシークレットストアをセットアップする方法は以下公式チュートリアルがまとまっているので、こちらをコピペしてデプロイします。
今回は、PKIでの利用ですので、チュートリアル中のシークレットストアの設定は不要です。もちろん、実施していただいても問題ありません。
公式手順の内、一点だけ修正します。
のちほどcert-managerがVault ACMEエンドポイントにhttpsでアクセスするのですが、この時、ワイルドカードのサーバ証明書をうまく検証しないようなので、直接サービスクラスタドメインをCSRに追加しておきます。
※ 実装上の問題なのかどうか原因は調査できてないのであしからず
[req]
default_bits = 2048
prompt = no
encrypt_key = yes
default_md = sha256
distinguished_name = kubelet_serving
req_extensions = v3_req
[ kubelet_serving ]
O = system:nodes
CN = system:node:vault.vault.svc.cluster.local
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth, clientAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = *.vault-internal
DNS.2 = *.vault-internal.vault.svc.cluster.local
DNS.3 = *.vault
### 以下を追加
DNS.4 = vault.vault.svc.cluster.local
IP.1 = 127.0.0.1
チュートリアル通りにセットアップが終わると、以下のようなファイル構成・環境変数になっており、Vaultのサーバ証明書を検証するvault.ca(kubeletのRoot CA)
が手に入っているはずです。
$ tree $WORKDIR
/some-workdir
├── cluster-keys.json
├── csr.yaml
├── overrides.yaml
├── vault-csr.conf
├── vault.ca
├── vault.crt
├── vault.csr
└── vault.key
0 directories, 8 files
$ env
...
VAULT_K8S_NAMESPACE=vault
VAULT_HELM_RELEASE_NAME=vault
VAULT_SERVICE_NAME=vault-internal
K8S_CLUSTER_NAME=cluster.local
WORKDIR=/some-workdir
CLUSTER_ROOT_TOKEN=<Secret>
ACMEでは、cert-managerがVaultにアクセスする際、このRoot CAを使い、サーバ証明書を検証します。
続いて、ACMEのセットアップを行います。
基本的には、以下公式チュートリアルをベースにセットアップします。
手順は以下の通りです。まずは、PKIのセットアップをし、ACMEを有効化します。
Vaultはminikube上にデプロイされているので、Terminalからport-forwardでVaultに接続してから、スクリプトを実行します。
# 別ターミナルで実施
$ kubectl -n vault port-forward service/vault 8200:8200
$ export VAULT_ADDR=https://127.0.0.1:8200
$ export VAULT_CACERT=${WORKDIR}/vault.ca
$ vault login $CLUSTER_ROOT_TOKEN
WARNING! The VAULT_TOKEN environment variable is set! The value of this
variable will take precedence; if this is unwanted please unset VAULT_TOKEN or
update its value accordingly.
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token <secret>
token_accessor Xg45I3MT7547YfNnuUUoXums
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]
チュートリアルのenable_engines.sh
はDocker上のVaultのエンドポイントが指定されています。
今回のminikubeに合わせ、以下のように修正し、実行します。
#!/bin/bash
set -euxo pipefail
VAULT_ENDPOINT="https://vault.vault.svc.cluster.local"
vault secrets enable pki
vault secrets tune -max-lease-ttl=87600h pki
# Root CA pki のセットアップ
vault write -field=certificate pki/root/generate/internal \
common_name="learn.internal" \
issuer_name="root-2023" \
ttl=87600h > root_2023_ca.crt
vault write pki/config/cluster \
path=${VAULT_ENDPOINT}:8200/v1/pki \
aia_path=${VAULT_ENDPOINT}:8200/v1/pki
vault write pki/roles/2023-servers \
allow_any_name=true \
no_store=false
vault write pki/config/urls \
issuing_certificates={{cluster_aia_path}}/issuer/{{issuer_id}}/der \
crl_distribution_points={{cluster_aia_path}}/issuer/{{issuer_id}}/crl/der \
ocsp_servers={{cluster_path}}/ocsp \
enable_templating=true
# 以降、中間CA pki_intのセットアップ
vault secrets enable -path=pki_int pki
vault secrets tune -max-lease-ttl=43800h pki_int
vault write -format=json pki_int/intermediate/generate/internal \
common_name="learn.internal Intermediate Authority" \
issuer_name="learn-intermediate" \
| jq -r '.data.csr' > pki_intermediate.csr
vault write -format=json pki/root/sign-intermediate \
issuer_ref="root-2023" \
csr=@pki_intermediate.csr \
format=pem_bundle ttl="43800h" \
| jq -r '.data.certificate' > intermediate.cert.pem
vault write pki_int/intermediate/set-signed certificate=@intermediate.cert.pem
# 中間CAのエンドポイント, aia_pathはOCSPのAIA処理用途
vault write pki_int/config/cluster \
path=${VAULT_ENDPOINT}:8200/v1/pki_int \
aia_path=${VAULT_ENDPOINT}:8200/v1/pki_int
# ACME用のRole作成。ACMEでは、no_store=falseが必須
vault write pki_int/roles/learn \
issuer_ref="$(vault read -field=default pki_int/config/issuers)" \
allow_any_name=true \
max_ttl="720h" \
no_store=false
# 証明書発行・閲覧するためのエンドポイントの設定
vault write pki_int/config/urls \
issuing_certificates={{cluster_aia_path}}/issuer/{{issuer_id}}/der \
crl_distribution_points={{cluster_aia_path}}/issuer/{{issuer_id}}/crl/der \
ocsp_servers={{cluster_path}}/ocsp \
enable_templating=true
上記設定の中で、Root CAは以下の名前で作成しています。
common_name="learn.internal"
issuer_name="root-2023"
# スクリプト内で利用
$ export VAULT_TOKEN=$CLUSTER_ROOT_TOKEN
$ ./enable_engines.sh
上記スクリプトによって、Root CA pkiおよび、中間CA pki_intが有効化されます。
今回のACMEは中間CAとして振る舞う設定となります。
$ vault secrets list
Path Type Accessor Description
---- ---- -------- -----------
cubbyhole/ cubbyhole cubbyhole_24e7edb4 per-token private secret storage
identity/ identity identity_cb75ac73 identity store
pki/ pki pki_af8d3cb7 n/a
pki_int/ pki pki_d0a3c742 n/a
secret/ kv kv_e5f7aa22 n/a
sys/ system system_3b039b5e system endpoints used for control, policy and debugging
成功すると以下のようなACMEエンドポイントが登録されます。
$ vault read pki_int/config/cluster
Key Value
--- -----
aia_path https://vault.vault.svc.cluster.local:8200/v1/pki_int
path https://vault.vault.svc.cluster.local:8200/v1/pki_int
ACMEのリクエストヘッダー、レスポンスヘッダーを設定します。
$ vault secrets tune \
-passthrough-request-headers=If-Modified-Since \
-allowed-response-headers=Last-Modified \
-allowed-response-headers=Location \
-allowed-response-headers=Replay-Nonce \
-allowed-response-headers=Link \
pki_int
Success! Tuned the secrets engine at: pki_int/
ACMEを有効化します。
$ vault write pki_int/config/acme enabled=true
Key Value
--- -----
allow_role_ext_key_usage false
allowed_issuers [*]
allowed_roles [*]
default_directory_policy sign-verbatim
dns_resolver n/a
eab_policy not-required
enabled true
これでVaultのセットアップは完了です。
CoreDNSの設定追加(HTTP-01 Challenge時に名前解決用途)
ACMEのHTTP-01 Challengeでは、指定したドメインでクラスタ内のcert-managerにACMEサーバがアクセスできる必要があります。
そのため、ACMEサーバがアクセスするDNSが名前解決できるようにする必要があります。
今回はローカルで検証しており、ACMEのサーバ証明書に利用するドメインは外部DNSには登録しないようにします。
/etc/hosts
を書き換えるなどの対応方法は色々ありますが、簡単な方法として、K8s内のCoreDNSにルールを追加して対応します。
今回は、test-vault-acme.k8s
というドメインを対象にします。
こちらに対してクラスタ内のPodがlookupした際、ingress-nginx-controller
にアクセスするように以下変更します。
$ kubectl edit cm coredns --namespace kube-system
apiVersion: v1
data:
Corefile: |
.:53 {
log
errors
health {
lameduck 5s
}
ready
rewrite name test-vault-acme.k8s ingress-nginx-controller.ingress-nginx.svc.cluster.local
kubernetes cluster.local in-addr.arpa ip6.arpa {
...
保存して反映します。
configmap/coredns edited
cert-managerのデプロイ
cert-managerは証明書の管理をしてくれる、非常に便利なソフトウェアです。
公式ドキュメントを参考にデプロイします。
$ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.2/cert-manager.yaml
$ kubectl get pod -A
NAMESPACE NAME READY STATUS RESTARTS AGE
cert-manager cert-manager-7d75f47cc5-5rbp7 1/1 Running 0 27s
cert-manager cert-manager-cainjector-c778d44d8-llx4x 1/1 Running 0 27s
cert-manager cert-manager-webhook-55d76f97bb-4wjwq 1/1 Running 0 27s
ingress-nginx ingress-nginx-admission-create-nmmvz 0/1 Completed 0 46m
ingress-nginx ingress-nginx-admission-patch-m8hkb 0/1 Completed 1 46m
ingress-nginx ingress-nginx-controller-7c6974c4d8-fv4qm 1/1 Running 0 46m
kube-system coredns-5dd5756b68-f69kr 1/1 Running 0 48m
kube-system etcd-minikube 1/1 Running 0 49m
kube-system kube-apiserver-minikube 1/1 Running 0 49m
kube-system kube-controller-manager-minikube 1/1 Running 0 49m
kube-system kube-proxy-q4lm9 1/1 Running 0 48m
kube-system kube-scheduler-minikube 1/1 Running 0 49m
kube-system storage-provisioner 1/1 Running 1 (48m ago) 49m
vault vault-0 1/1 Running 0 36m
vault vault-1 1/1 Running 0 36m
vault vault-2 1/1 Running 0 36m
vault vault-agent-injector-5477bfd7d8-xd4jb 1/1 Running 0 36m
続いて、CertManagerにClusterIssuerを設定し、Vault ACMEサーバとingress-nginx-controllerを介して、HTTP-01チャレンジを行うよう設定します。
caBundleにVault ACMEのエンドポイントを検証するRootCAを設定してください。
あえて設定しない場合は、サーバ証明書検証で失敗するため、skipTLSVerify: true
を指定してください。
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: test-cluster-issuer
namespace: default
spec:
acme:
caBundle: <cat vault.ca | base64 の結果>
server: https://vault.vault.svc.cluster.local:8200/v1/pki_int/acme/directory
privateKeySecretRef:
name: test-cluster-issuer-account-key
solvers:
- http01:
ingress:
ingressClassName: nginx
テスト用Webサーバのデプロイ
テスト用のWebサーバはなんでもよいのですが、K8s公式ドキュメントで使われている動作確認用webサーバを起動します。
以下Deployment, Service, Ingressを適用します。
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: web
name: web
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- image: gcr.io/google-samples/hello-app:1.0
imagePullPolicy: IfNotPresent
name: hello-app
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
labels:
app: web
name: web
namespace: default
spec:
ports:
- name: https
port: 443
protocol: TCP
targetPort: 8080
selector:
app: web
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
namespace: default
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$1
cert-manager.io/cluster-issuer: "test-cluster-issuer"
spec:
# minikube ingress-nginxの場合
ingressClassName: nginx
tls:
- hosts:
# 今回TLS化するドメインを指定
- test-vault-acme.k8s
# 証明書、秘密鍵を動的にcert-managerが以下のSecretに保存します
secretName: selfsigning-cert
rules:
- host: test-vault-acme.k8s
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 443
Ingressリソースのannotationsで、前述した cert-manager.io/cluster-issuer: "test-cluster-issuer"
を設定すれば、cert-managerが動的に80ポートでサーバを起動し、HTTP-01チャレンジのエンドポイントを生やしてくれます。
Webサーバをデプロイします。
$ kubectl apply -f web
動作確認
Terminalから中間CAでサーバ証明書を検証して、Ingressにアクセスしてみましょう。
名前解決できるように、/etc/hosts
を書き換えます。
$ minikube ip
192.168.xx.xx
$ sudo vim /etc/hosts
...
# minikube IPの結果を追記
192.168.xx.xx test-vault-acme.k8s
実際にTerminalからアクセスしてみます。中間CAを使ってアクセスします。
$ curl --cacert $WORKDIR/root_2023_ca.crt -v https://test-vault-acme.k8s/
...
* Server certificate:
* subject: [NONE]
* start date: Dec 12 05:36:09 2023 GMT
* expire date: Jan 13 05:36:39 2024 GMT
* subjectAltName: host "test-vault-acme.k8s" matched cert's "test-vault-acme.k8s"
* issuer: CN=learn.internal Intermediate Authority
* SSL certificate verify ok.
* using HTTP/2
...
Hello, world!
Version: 1.0.0
Hostname: web-57f46db77f-nfvr8
* Connection #0 to host test-vault-acme.k8s left intact
無事に接続できました。
VaultのGUIからも発行されたサーバ証明書を確認してみましょう。
port-forwardしていれば、 https://127.0.0.1:8080/ でアクセスできるはずです。
Root Tokenでログインし、pki_intの証明書一覧を見ると登録されていることが確認できます。
今回取得したサーバ証明書は、Secretに保存されています。
以下より確認すると、GUIで確認した証明書と一致することがわかります。
$ kubectl --namespace default get secrets selfsigning-cert \
-o=jsonpath="{.data.tls\.crt}" | base64 -D | openssl x509 -text -noout
...
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
3e:bd:23:69:67:ac:d3:a7:ef:6c:af:1d:f1:51:d9:68:64:82:30:f2
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=learn.internal Intermediate Authority
Validity
Not Before: Dec 12 08:01:21 2023 GMT
Not After : Jan 13 08:01:51 2024 GMT
...
まとめ
今回、Vault ACMEをminikube上で動作確認しました。
Vaultに簡単な設定を追加するだけでACMEサーバにすることが可能ということを確認しました。
今回はACMEで広く使われているHTTP-01 チャレンジで検証しましたが、Vaultのドキュメントには、DNS-01, TLS-ALPN-01もサポートしていると記載があります。
Kubernetesでは、cert-managerと連携すると、Ingressのサーバ証明書管理が非常に楽になります。
すでにVault PKIを利用し、サーバ証明書をアプリケーション開発者に提供しているのであれば、本ACME機能は、アプリケーション開発者にって、とても魅力的な機能となりうるのではないでしょうか。