cert-managerとHashiCorp Vaultを使用して証明書を管理する方法

Jintao Zhang

March 2, 2023

Products

cert-managerが解決する問題

2017年にJETSTACKによってオープンソース化されたcert-managerは、CNCF(Cloud Native Computing Foundation)に寄贈され、サンドボックスレベルのプロジェクトとなりました。2022年10月にはCNCFのインキュベーションプロジェクトとなりました。

cert-managerは、KubernetesとOpenShift内のx.509証明書を自動的に管理できます。これにより、証明書と証明書署名要求がKubernetes上でサポートされる最初のリソースタイプとなり、このプロセスはCRDによって実装されています。さらに、cert-managerは開発者がアプリケーションのアクセスセキュリティを向上させるために迅速に証明書を申請できるようにします。

それでは、cert-managerが登場する前にKubernetesでどのように証明書を管理していたかを見てみましょう。

Kubernetesでの証明書管理方法

Kubernetesでは、主に2つのネイティブな方法でデータを保存できます:

  • ConfigMap
  • Secret

ただし、ConfigMap内のすべての情報はプレーンテキストです。そのため、比較的一般的な設定情報を保存するには適していますが、証明書のような機密情報を保存するには適していません。

Kubernetesが設計された際、証明書などの関連情報を保存するためにSecretを使用することが推奨され、これに対するサポートも提供されています。kubectl create secret tlsを使用して簡単に証明書情報を保存できます。例えば:

➜  ~ kubectl create secret tls moelove-tls --cert=./cert.pem --key=./cert-key.pem
secret/moelove-tls created
➜  ~ kubectl get secret moelove-tls -oyaml
apiVersion: v1
data:
  tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNFekNDQWJtZ0F3SUJBZ0lVVHhCTC9aQkdpOEJCOUFVN2JRWi9jK3c2L1Rzd0NnWUlLb1pJemowRUF3SXcKVFRFTE1Ba0dBMVVFQmhNQ1EwNHhFREFPQmdOVkJBY1RCMEpsYVdwcGJtY3hGVEFUQmdOVkJBb1RERTF2WlV4dgpkbVVnU1U1R1R6RVZNQk1HQTFVRUF4TU1iVzlsYkc5MlpTNXBibVp2TUI0WERUSXlNVEF4T1RBM01UY3dNRm9YCkRUSXpNVEF4T1RBM01UY3dNRm93VFRFTE1Ba0dBMVVFQmhNQ1EwNHhFREFPQmdOVkJBY1RCMEpsYVdwcGJtY3gKRlRBVEJnTlZCQW9UREUxdlpVeHZkbVVnU1U1R1R6RVZNQk1HQTFVRUF4TU1iVzlsYkc5MlpTNXBibVp2TUZrdwpFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRVVTcEFjNGE1UXQwQ0NVa2hGSGY3WnZvR1FReVVPUUxSClJhZG0rSUUrV1ZkOThyWkc5NFpob08ybDZSWkY2MnVPN3FpZ2VsaUJwY0FGQ3FzWU9HNnVLcU4zTUhVd0RnWUQKVlIwUEFRSC9CQVFEQWdXZ01CMEdBMVVkSlFRV01CUUdDQ3NHQVFVRkJ3TUJCZ2dyQmdFRkJRY0RBakFNQmdOVgpIUk1CQWY4RUFqQUFNQjBHQTFVZERnUVdCQlFnS01icnBUb3k4NVcvRy9hMGZtYzlDMUJRbURBWEJnTlZIUkVFCkVEQU9nZ3h0YjJWc2IzWmxMbWx1Wm04d0NnWUlLb1pJemowRUF3SURTQUF3UlFJZ1EzTzhJZ0N2MlRkNUhhV00KcE1LWmRCLzNXdEMreERlSVdPbER6L2hCdzE0Q0lRRExQNG0weFpmSkJvRGc5cERocThGdHN5VDdVZVhVdlZGQQpsS0tReFZNOXFBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
  tls.key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUsyZjZHQlNZQ0R4eVoycnB2bVZ1YW5MNDhxeW9SK1NiWmxiQzNqSUZybzhvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFVVNwQWM0YTVRdDBDQ1VraEZIZjdadm9HUVF5VU9RTFJSYWRtK0lFK1dWZDk4clpHOTRaaApvTzJsNlJaRjYydU83cWlnZWxpQnBjQUZDcXNZT0c2dUtnPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
kind: Secret
metadata:
  creationTimestamp: "2022-10-19T07:24:26Z"
  name: moelove-tls
  namespace: default
  resourceVersion: "2103326"
  uid: 14f86514-a1d1-4d99-b000-9ed8b5189d56
type: kubernetes.io/tls

上記のコマンドにより、Kubernetes内にmoelove-tlsという名前のkubernetes.io/tlsタイプのSecretリソースが作成されます。

このリソースは、アプリケーションが証明書情報を使用する必要がある場合に直接参照できます。ほとんどの場合、Ingress Controllerのシーンで使用されます。例えば:

➜  ~ kubectl create ingress moelove-ing --rule="moelove.info/=moelove:8080,tls=moelove-tls"
ingress.networking.k8s.io/moelove-ing created
➜  ~ kubectl get ing moelove-ing -oyaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  creationTimestamp: "2022-10-19T07:32:43Z"
  generation: 1
  name: moelove-ing
  namespace: default
  resourceVersion: "2104268"
  uid: b90f09f7-8036-4b9f-9744-a247141ea8da
spec:
  rules:
  - host: moelove.info
    http:
      paths:
      - backend:
          service:
            name: moelove
            port:
              number: 8080
        path: /
        pathType: Exact
  tls:
  - hosts:
    - moelove.info
    secretName: moelove-tls
status:
  loadBalancer: {}

上記のコマンドにより、moelove-ingという名前のIngressリソースが作成されます。そのドメイン名はmoelove.infoと宣言され、moelove-tlsを使用してこのドメイン名に証明書保護が追加されます。対応するIngress ControllerコンポーネントがIngressリソースを取得すると、コンポーネントは自動的にこのドメイン名の証明書を設定し、ウェブサイトのセキュリティを向上させます。

遭遇した問題

証明書の発行が煩雑

上記の内容では、証明書の発行方法をデモンストレーションしていません。興味があれば、OpenSSL Documentationを確認してください。証明書の発行プロセスでは、多くの概念を理解する必要があります。さらに、署名プロセスはKubernetesクラスタの外部で行われ、「宣言的」な設定方法では具体的に何が起こったかを理解することはできません。特に、証明書には多くの異なる暗号化アルゴリズムや設定などがあります。

そのため、デフォルトの方法を使用する場合、生成された証明書とキーをKubernetes Secretsに保存することしかできません。

証明書の更新/再署名が煩雑

証明書には有効期限があることは周知の事実です。証明書が期限切れになるか、失効する前に、新しい証明書を準備し、新しい証明書の有効期限が古いものよりも後である必要があります。

Kubernetes Secretsでの証明書管理には改善の余地があります:

  • 有効期限の自動チェックがない:Kubernetesには、証明書が期限切れかどうかに関係なく、任意の証明書を保存できます。

  • 無効なデータのチェックがない:Kubernetes Secretsに保存されたデータが破損しているか無効である場合、Kubernetesでは特別な処理が行われません。

セキュリティの欠如

Kubernetes Secretsに保存された証明書とキー情報は、base64エンコードされているだけです。そのため、データを取得した誰でもbase64デコードして実際のデータを取得できます。例えば:

➜  ~ kubectl get secrets moelove-tls -o jsonpath='{ .data.tls\.crt }' |base64 -d
-----BEGIN CERTIFICATE-----
MIICEzCCAbmgAwIBAgIUTxBL/ZBGi8BB9AU7bQZ/c+w6/TswCgYIKoZIzj0EAwIw
TTELMAkGA1UEBhMCQ04xEDAOBgNVBAcTB0JlaWppbmcxFTATBgNVBAoTDE1vZUxv
dmUgSU5GTzEVMBMGA1UEAxMMbW9lbG92ZS5pbmZvMB4XDTIyMTAxOTA3MTcwMFoX
DTIzMTAxOTA3MTcwMFowTTELMAkGA1UEBhMCQ04xEDAOBgNVBAcTB0JlaWppbmcx
FTATBgNVBAoTDE1vZUxvdmUgSU5GTzEVMBMGA1UEAxMMbW9lbG92ZS5pbmZvMFkw
EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUSpAc4a5Qt0CCUkhFHf7ZvoGQQyUOQLR
Radm+IE+WVd98rZG94ZhoO2l6RZF62uO7qigeliBpcAFCqsYOG6uKqN3MHUwDgYD
VR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNV
HRMBAf8EAjAAMB0GA1UdDgQWBBQgKMbrpToy85W/G/a0fmc9C1BQmDAXBgNVHREE
EDAOggxtb2Vsb3ZlLmluZm8wCgYIKoZIzj0EAwIDSAAwRQIgQ3O8IgCv2Td5HaWM
pMKZdB/3WtC+xDeIWOlDz/hBw14CIQDLP4m0xZfJBoDg9pDhq8FtsyT7UeXUvVFA
lKKQxVM9qA==
-----END CERTIFICATE-----

上記のコマンドにより、証明書に関連する元のデータを取得できます。

一方、証明書とキーデータを更新したい場合、二次確認プロセスなしで直接更新できます。

これは、ほとんどのシナリオでのセキュリティポリシーと一致しません。

次に、cert-managerがこれらの問題をどのように解決するかを見てみましょう。

cert-managerがこれらの問題を解決する方法

自動発行

cert-managerはCRDを通じて開発および拡張され、IssuersClusterIssuersリソースを追加および実装し、証明書のCA(認証局)を表します。

また、さまざまな組み込みタイプをサポートし、外部コンポーネントと簡単に統合できます。例えば:

  • SelfSigned:自己署名証明書

  • CA:発行のためのCAを提供

  • Vault:HashiCorp Vaultを使用して発行

  • Venafi:Venafiを使用して発行

  • External:署名のためにいくつかの外部コンポーネントを使用。例えば:

  • ACME(Automated Certificate Management Environment)

これらのコンポーネントを使用して、簡単に証明書を発行できます。以降の内容では、Vaultを例として具体的に紹介します。

自動更新/再署名

cert-managerでは、cmctlを使用して手動で証明書を更新できます。同時に、cert-managerは証明書の有効期限と完全性を自動的にチェックします。

証明書が期限切れになったり、証明書データが不完全な場合、自動的に証明書の再発行をトリガーし、労力とメンテナンスコストを節約できます。

セキュリティ保証

cert-managerでは、CRD(CustomResourceDefinitions)を通じてsignersリソースが追加され、証明書リクエストを確認し、ApprovedまたはDeniedすることができます。Approveされた後にのみ有効になり、証明書が発行されます。これはより安全な方法です。

APISIX Ingress Controllerがcert-managerと統合する方法

インストール

Apache APISIX Ingress Controllerは、Ingress、カスタムリソース、およびGateway APIを通じてプロキシルールを設定できるKubernetes Ingress Controllerです。

次に、APISIX Ingress Controllerとcert-managerを統合して、エージェントのドメイン名にTLS証明書を追加し、セキュリティを向上させる方法をデモンストレーションします。

同時に、Vaultを使用して証明書を発行します。

APISIX Ingress Controllerのデプロイ

APISIX Ingress Controllerのデプロイは非常に簡単で、以下の手順を実行するだけです:

tao@moelove:~$ helm repo add apisix https://charts.apiseven.com
tao@moelove:~$ helm repo add bitnami https://charts.bitnami.com/bitnami
tao@moelove:~$ helm repo update
tao@moelove:~$ helm install apisix apisix/apisix --set gateway.tls.enabled=true --set gateway.type=NodePort   --set ingress-controller.enabled=true   --set ingress-controller.config.apisix.serviceNamespace=apisix   --namespace apisix   --create-namespace   --set ingress-controller.config.apisix.serviceName=apisix-admin --set ingress-controller.config.ingressPublishService="apisix/apisix-gateway"
NAME: apisix
LAST DEPLOYED: Wed Oct 19 21:33:37 2022
NAMESPACE: apisix
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
1. Get the application URL by running these commands:
  export NODE_PORT=$(kubectl get --namespace apisix -o jsonpath="{.spec.ports[0].nodePort}" services apisix-gateway)
  export NODE_IP=$(kubectl get nodes --namespace apisix -o jsonpath="{.items[0].status.addresses[0].address}")
  echo http://$NODE_IP:$NODE_PORT

すべてのPodが実行状態になると、デプロイが成功します。

tao@moelove:~$ kubectl -n apisix get pods  
NAME                                         READY   STATUS    RESTARTS   AGE
apisix-777c9fdd67-rf8zs                      1/1     Running   0          6m48s
apisix-etcd-0                                1/1     Running   0          6m48s
apisix-etcd-1                                1/1     Running   0          6m48s
apisix-etcd-2                                1/1     Running   0          6m48s
apisix-ingress-controller-568544b554-k7nd4   1/1     Running   0          6m48s

Vaultのデプロイ

Vaultをデプロイする際も、Helmを使用できます。ここでは、--set "server.dev.enabled=true"設定項目を追加して、デプロイ後に追加の操作なしで直接使用できるようにしました。(この設定は本番環境では使用しないでください。)

tao@moelove:~$ helm repo add hashicorp https://helm.releases.hashicorp.com
tao@moelove:~$ helm install vault hashicorp/vault --set "injector.enabled=false" --set "server.dev.enabled=true"
NAME: vault
LAST DEPLOYED: Wed Oct 19 21:53:50 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
Thank you for installing HashiCorp Vault!

Now that you have deployed Vault, you should look over the docs on using
Vault with Kubernetes available here:

https://www.vaultproject.io/docs/


Your release is named "vault". To learn more about the release, try the following:

  $ helm status vault
  $ helm get manifest vault

デプロイが完了すると、PodがRunning状態になり、デプロイが完了したことが示されます。

tao@moelove:~$ kubectl get pods  
NAME      READY   STATUS    RESTARTS   AGE
vault-0   1/1     Running   0          29s
tao@moelove:~$ kubectl get svc
NAME             TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)             AGE
kubernetes       ClusterIP   10.96.0.1      <none>        443/TCP             84m
vault            ClusterIP   10.96.190.88   <none>        8200/TCP,8201/TCP   4m14s
vault-internal   ClusterIP   None           <none>        8200/TCP,8201/TCP   4m14s

次に、Vaultに入り、pki機能を有効にして、対応するポリシーを設定します。

tao@moelove:~$ kubectl  exec -it vault-0 -- sh
/ $ vault secrets enable pki
Success! Enabled the pki secrets engine at: pki/
/ $ vault write pki/root/generate/internal common_name=moelove.info ttl=8760h
Key              Value
---              -----
certificate      -----BEGIN CERTIFICATE-----
MIIDODCCAiCgAwIBAgIUds5uMJV9rOkwFEt6Xof5T2SVFccwDQYJKoZIhvcNAQEL
...
VM4DRVgDkqY9JdHU
-----END CERTIFICATE-----
expiration       1668983612
issuer_id        8df13015-7c70-df9a-7bb7-9b3b4afe7f82
issuer_name      n/a
issuing_ca       -----BEGIN CERTIFICATE-----
MIIDODCCAiCgAwIBAgIUds5uMJV9rOkwFEt6Xof5T2SVFccwDQYJKoZIhvcNAQEL
...
VM4DRVgDkqY9JdHU
-----END CERTIFICATE-----
key_id           c9fcfcb0-3548-a9a7-e706-30510592c797
key_name         n/a
serial_number    76:ce:6e:30:95:7d:ac:e9:30:14:4b:7a:5e:87:f9:4f:64:95:15:c7
/ $
/ $ vault write pki/config/urls issuing_certificates="http://vault.default:8200/v1/pki/ca" crl_distribution_points="http://vault.default:8200/v1/pki/crl"
Success! Data written to: pki/config/urls
/ $ vault write pki/roles/moelove-dot-info allowed_domains=moelove.info allow_subdomains=true max_ttl=72h
Success! Data written to: pki/roles/moelove-dot-info
/ $
/ $ vault policy write pki - <<EOF
> path "pki*"                        { capabilities = ["read", "list"] }
> path "pki/sign/moelove-dot-info"    { capabilities = ["create", "update"] }
> path "pki/issue/moelove-dot-info"   { capabilities = ["create"] }
> EOF
Success! Uploaded policy: pki

次に、Kubernetes認証を設定します:

/ $ vault auth enable kubernetes
Success! Enabled kubernetes auth method at: kubernetes/
/ $ vault write auth/kubernetes/config kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
Success! Data written to: auth/kubernetes/config
/ $ vault write auth/kubernetes/role/issuer  bound_service_account_names=issuer bound_service_account_namespaces=default policies=pki ttl=20m
Success! Data written to: auth/kubernetes/role/issuer

上記の操作が完了したら、次にcert-managerをデプロイします。

cert-managerのデプロイ

これで、Helmを使用してcert-managerをインストールできます。インストールプロセスは比較的簡単です。

tao@moelove:~$ helm repo add jetstack https://charts.jetstack.io
tao@moelove:~$ helm repo update jetstack
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "jetstack" chart repository
Update Complete. ⎈Happy Helming!⎈
tao@moelove:~$ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.10.0/cert-manager.crds.yaml
customresourcedefinition.apiextensions.k8s.io/clusterissuers.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/challenges.acme.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/certificaterequests.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/issuers.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/certificates.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/orders.acme.cert-manager.io created
tao@moelove:~$ helm install \
>   cert-manager jetstack/cert-manager \
>   --namespace cert-manager \
>   --create-namespace \
>   --version v1.10.0

xNAME: cert-manager
LAST DEPLOYED: Wed Oct 19 22:51:06 2022
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
cert-manager v1.10.0 has been deployed successfully!

In order to begin issuing certificates, you will need to set up a ClusterIssuer
or Issuer resource (for example, by creating a 'letsencrypt-staging' issuer).

More information on the different types of issuers and how to configure them
can be found in our documentation:

https://cert-manager.io/docs/configuration/

For information on how to configure cert-manager to automatically provision
Certificates for Ingress resources, take a look at the `ingress-shim`
documentation:

https://cert-manager.io/docs/usage/ingress/

Podのステータスを確認します:

tao@moelove:~$ kubectl -n cert-manager get pods
NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-69b456d85c-znpq4              1/1     Running   0          117s
cert-manager-cainjector-5f44d58c4b-wcd27   1/1     Running   0          117s
cert-manager-webhook-566bd88f7b-7rptf      1/1     Running   0          117s

その後、設定と検証を開始できます。

設定と検証方法

証明書の設定と発行

tao@moelove:~$ kubectl create serviceaccount issuer
serviceaccount/issuer created
tao@moelove:~$ kubectl get secret
NAME                          TYPE                 DATA   AGE
sh.helm.release.v1.vault.v1   helm.sh/release.v1   1      36m
tao@moelove:~$ vim issuer-secret.yaml
tao@moelove:~$ cat issuer-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: issuer-token-moelove
  annotations:
    kubernetes.io/service-account.name: issuer
type: kubernetes.io/service-account-token
tao@moelove:~$ kubectl apply -f issuer-secret.yaml
secret/issuer-token-moelove created
tao@moelove:~$ kubectl get sa,secret
NAME                     SECRETS   AGE
serviceaccount/default   0         118m
serviceaccount/issuer    0         2m11s
serviceaccount/vault     0         38m

NAME                                 TYPE                                  DATA   AGE
secret/issuer-token-moelove          kubernetes.io/service-account-token   3      35s
secret/sh.helm.release.v1.vault.v1   helm.sh/release.v1                    1      38m

Issuerの作成

この設定により、Vaultが証明書認証局として使用され、Vaultで設定されたロールとシークレットを参照して自動発行が行われます。

tao@moelove:~$ cat vault-issuer.yaml
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: vault-issuer
  namespace: default
spec:
  vault:
    server: http://vault.default
    path: pki/sign/moelove-dot-info
    auth:
      kubernetes:
        mountPath: /v1/auth/kubernetes
        role: moelove-dot-info
        secretRef:
          name: issuer-token-moelove
          key: token
tao@moelove:~$ kubectl apply -f vault-issuer.yaml
issuer.cert-manager.io/vault-issuer created

証明書の作成

この設定により、証明書が自動的に発行され、後続の使用中にmoelove-info-tlsを参照できます。

tao@moelove:~$ cat moelove-dot-info-cert.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: moelove-info
  namespace: default
spec:
  secretName: moelove-info-tls
  issuerRef:
    name: vault-issuer
  commonName: www.moelove.info
  dnsNames:
  - www.moelove.info
tao@moelove:~$ kubectl apply -f moelove-dot-info-cert.yaml
certificate.cert-manager.io/moelove-info created

検証

次に、HTTPBINサービスをプロキシして検証します。

まず、HTTPBINアプリケーションを作成し、対応するServiceを作成します。

kubectl run httpbin --image kennethreitz/httpbin
kubectl expose pod httpbin --port=80

次に、以下のリソースを定義してプロキシし、証明書を参照します:

# ApisixTlsオブジェクトを定義
apiVersion: apisix.apache.org/v2
kind: ApisixTls
metadata:
  name: moelove
spec:
  hosts:
  - moelove.info
  secret:
    name: moelove-info-tls

---
# バックエンドにアクセスするためのルートを定義
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
  name: moelove
spec:
  http:
  - name: httpbin
    match:
      paths:
      - /*
      hosts:
      - moelove.info
    backends:
    - serviceName: httpbin
      servicePort: 80

これらのリソースをクラスタに適用します。その後、kubectl port-forwardを使用してAPISIXのポート443をローカルに転送し、テストアクセスを実行します:

$ ~ kubectl port-forward -n ingress-apisix svc/apisix-gateway 8443:443 &
$ ~ curl -sk https://moelove.info:8443/ip --resolve 'moelove.info:8443:127.0.0.1'
{
  "origin": "172.17.18.1"
}

moelove.infoドメイン名にHTTPS証明書が正しく設定され、APISIX Ingress Controllerを通じてプロキシが設定されていることがわかります。

結論

Kubernetesには、証明書を保存するための2つのデフォルトの方法があります:ConfigMapとSecretです。しかし、証明書の発行と更新/再署名は煩雑で、セキュリティも改善が必要です。

cert-managerはこれらの問題を解決し、Kubernetesエコシステム内の証明書発行/管理の事実上の標準となりつつあります。さらに、Vaultなどのツールと統合でき、より安全です。

Apache APISIX Ingress Controllerは、ユーザーフレンドリーなIngress Controllerを作成することを目指しており、早期に完全なcert-manager統合機能を追加しました。ユーザーは、Apache APISIX Ingress ControllerでVaultを使用してcert-managerを通じて証明書を発行し、アプリケーションにHTTPSプロキシを提供できます。

Tags: