Kubernetes上のCert-Manager:Let’s EncryptからTLSを自動発行・更新 — 証明書の期限切れに悩まない

Security tutorial - IT technology blog
Security tutorial - IT technology blog

cert-managerを5分でインストール — さっそく始めよう

先月、KubernetesクラスタをCert-Managerに移行したのですが、もう手動での証明書管理には戻りたくありません。以前はCertbotをcronジョブで更新に使っていました。ドメインが数個の時はそれで問題なかったのですが、Ingressが数十個に増えてくると本当に混乱してきます。複数ノード間での証明書の同期、各ドメインの有効期限の追跡、cronが正しい時間に動かない時の対応など。

cert-managerはまさにその問題を解決してくれます。クラスタ内で動作し、期限切れ間近の証明書(30日前)を自動で監視して更新し、Kubernetes Secretへの自動injectもしてくれます。サーバーへのSSHも、cronも、手動での管理も一切不要です。

ステップ1:HelmでCert-Managerをインストール

helm repo add jetstack https://charts.jetstack.io
helm repo update

helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set crds.enabled=true

Podが起動しているか確認します:

kubectl get pods -n cert-manager

3つのPodが表示されるはずです:cert-managercert-manager-cainjectorcert-manager-webhook。次のステップに進む前に、すべてがRunning状態になっている必要があります。

ステップ2:Let’s EncryptのClusterIssuerを作成

環境は2種類あります:staging(テスト用、ブラウザに信頼されない証明書)とproductionです。まずStagingでテストすることをお勧めします — Let’s Encryptのレート制限にかかると非常に面倒で、完全に避けられることなので。

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: [email protected]
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
    - http01:
        ingress:
          class: nginx
kubectl apply -f clusterissuer.yaml
kubectl get clusterissuer

ステップ3:証明書を自動発行するためにIngressにAnnotationを設定

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp-ingress
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  tls:
  - hosts:
    - myapp.example.com
    secretName: myapp-tls
  rules:
  - host: myapp.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: myapp-service
            port:
              number: 80

Applyが完了すると、cert-managerが自動的にCertificateリソースを作成し、ACMEチャレンジを実行して、証明書をSecret myapp-tlsに保存します。通常1〜2分かかります。進捗を確認するには:

kubectl describe certificate myapp-tls -n default
kubectl get certificaterequest -n default

深く理解する — cert-managerが裏側でやっていること

IngressにAnnotation cert-manager.io/cluster-issuerを追加すると、cert-managerは順番に一連のリソースを作成します:

  • Certificate — 作成する証明書を定義:ドメイン、Secret名、Issuer
  • CertificateRequest — Let’s EncryptにCSR(Certificate Signing Request)を送信
  • Order — CAとのACMEオーダーを追跡
  • Challenge — ドメインの所有権を証明するためのHTTP-01またはDNS-01チャレンジを処理

一番気に入っているのは、証明書の有効期限が30日を切ると自動的に更新してくれる点です — renewBeforeで完全に設定可能です。以前はStagingサーバー上のCertbotの証明書を2週間も放置してしまったことがありました。誰も気づかず、別のサービスのWebhookが呼び出した時に初めて証明書が期限切れになっていると発覚したんです。cert-managerを使えばそういったことは起こりません。

HTTP-01 vs DNS-01 チャレンジ

HTTP-01はほとんどのケースに適しています。cert-managerがドメイン上にTemporaryなエンドポイントを作成し、Let’s Encryptが検証します。ドメインがクラスタに向いており、インターネットからポート80が開いている必要があります。

DNS-01はワイルドカード証明書が必要な場合や、外部に公開していないInternalサービスを保護する場合に使用します。cert-managerがDNSプロバイダーにTXTレコードを作成します。Cloudflare、Route53などのDNSプロバイダーとの連携が必要です。

応用:ワイルドカード証明書とInternalサービスのセキュリティ

DNS-01を使ったワイルドカード証明書(Cloudflare)

これは私がProductionで使っている設定です。まず、Cloudflareで特定のゾーンに対してZone:DNS:Edit権限を持つAPIトークンを作成します。Global API Keyは権限が広すぎるため使用しないでください — 不必要なリスクです。

サービスアカウント用のランダムトークンが必要な時は、toolcraft.appのパスワードジェネレーターをよく使っています。ブラウザ上で完全に動作するため、認証情報がサードパーティのサーバーを経由する心配がありません。

kubectl create secret generic cloudflare-api-token \
  --from-literal=api-token=YOUR_CLOUDFLARE_TOKEN \
  -n cert-manager
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-dns
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: [email protected]
    privateKeySecretRef:
      name: letsencrypt-dns-key
    solvers:
    - dns01:
        cloudflare:
          apiTokenSecretRef:
            name: cloudflare-api-token
            key: api-token

全サブドメインで共有するワイルドカード証明書を作成します:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: wildcard-example-com
  namespace: default
spec:
  secretName: wildcard-example-com-tls
  issuerRef:
    name: letsencrypt-dns
    kind: ClusterIssuer
  dnsNames:
  - "*.example.com"
  - "example.com"

パブリックIPのないInternalサービスへのTLS

このユースケースこそ、cert-managerが真価を発揮する場面です。Prometheus、Grafana、ArgoCDは通常VPNBastion Host経由でしかアクセスできませんが、内部ネットワーク上でのCredentialのスニッフィングを防ぐためにHTTPSを使うべきです。

DNS-01はDNSレコードによる検証のみ必要で、外部からのHTTPアクセスは不要です。つまり、InternalサービスでもValidな証明書を取得できます:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: grafana-internal-tls
  namespace: monitoring
spec:
  secretName: grafana-tls
  issuerRef:
    name: letsencrypt-dns
    kind: ClusterIssuer
  dnsNames:
  - "grafana.internal.example.com"

GrafanaのIngressはポート80/443をインターネットに公開していなくても、ValidなHTTPSを使用できます。内部ネットワーク上でのCredentialのスニッフィングがなくなり、ブラウザが自己署名証明書の警告を表示することもなくなります。

運用経験から得た実践的なTips

1. 必ずStagingでテストする

Let’s Encrypt Productionにはレート制限があります:ドメインあたり毎時5回の証明書発行失敗、週50証明書。レート制限にかかると、そのドメインで1週間新しい証明書が発行できなくなります。Server URLを変更して、Staging用のClusterIssuerを別途作成してください:

server: https://acme-staging-v02.api.letsencrypt.org/directory

2. 証明書が発行されない時のデバッグ

私が遭遇したエラーの90%は以下が原因でした:(1) ドメインがクラスタに正しく向いていない、(2) IngressのClass名が間違っている、(3) FirewallがインターネットからPort 80をブロックしている。次のリソースの順番でトレースしてください:

# チェーンの各ステップを確認
kubectl describe certificate <name> -n <namespace>
kubectl describe certificaterequest <name> -n <namespace>
kubectl describe order <name> -n <namespace>
kubectl describe challenge <name> -n <namespace>

# Cert-Managerのログを確認
kubectl logs -n cert-manager deploy/cert-manager -f

3. Prometheusで証明書の有効期限を監視

cert-managerはPrometheusメトリクスをデフォルトで公開しています。証明書の有効期限が7日未満になったらアラートを設定しましょう — cert-managerが自動更新するとはいえ、念のためアラートを設定しておくのが確実です:

certmanager_certificate_expiration_timestamp_seconds
certmanager_certificate_ready_status

4. 証明書を含むKubernetes Secretsのバックアップ

cert-managerはPrivate KeyをKubernetes Secretに保存します。痛い経験をしたことがあります。整理のためにNamespaceをRecreateしたところ、証明書を含むSecretも一緒に削除され、新しい証明書が発行されるまで約10分のダウンタイムが発生しました。それ以来、定期的にVeleroでSecretsをバックアップするか、Namespaceに関係する作業の前に少なくともSecretをエクスポートするようにしています:

kubectl get secret myapp-tls -n default -o yaml > myapp-tls-backup.yaml

5. 複数のIngressでワイルドカード証明書を再利用

ワイルドカード証明書が取得できたら、サブドメインごとに個別の証明書を作成する代わりに、複数のIngressで同じSecretを参照しましょう。Let’s Encrypt APIへの呼び出し回数を減らし、管理も楽になります:

spec:
  tls:
  - hosts:
    - api.example.com
    secretName: wildcard-example-com-tls  # 作成済みのCertificateのSecretを再利用

現在、約30のIngressがあるクラスタでcert-managerを運用していますが、証明書に手動で介入したことは一度もありません。Ingressが増えても管理コストは増えません。Kubernetesを使っていてまだどこかにcronジョブで証明書を更新しているなら、今が変え時です。

Share: