🙄

cert-manager で 自己署名証明書

2021/07/19に公開

はじめに

今回は cert-manager についてです。
cert-manager + Let's Encrypt を使った Ingress の証明書の払い出しを行おうと思ったのですが、Let's Encrypt にはチャレンジなる検証があり、インターネット公開されていないと色々と難しそう・・・。
というわけで、今回は cert-manager を使って自己署名証明書を作って Ingress に証明書を適用しようと思います。

環境情報

  • Kubernetes:v1.20.6
  • nginx-ingress:v0.47(github のコードは helm-chart-3.34.0)
  • helm:v3.5.4
  • cert-manager:v1.4.0

Ingress お試し用に Jenkins をデプロイします。PV はいつもどおり Ceph で。
※Web 画面をちょっと見たいだけなので、他の Web アプリでも代替できます

※Node の前段に nginx を BareMetal LB として用意しています。
 設定ファイルは以下を適用しています。

kubeadm でのクラスタ構築は以下を参考に。

https://qiita.com/t_ume/items/74831283a9eea8098379

nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

error_log /var/log/nginx/error.log warn;

events {
   worker_connections  1024;
}

stream {
   upstream kubernetes {
      server 192.168.10.61:6443 max_fails=2 fail_timeout=30s;
      server 192.168.10.62:6443 max_fails=2 fail_timeout=30s;
      server 192.168.10.63:6443 max_fails=2 fail_timeout=30s;
   }
   server {
      listen 6443;
      proxy_pass kubernetes;
   }
   upstream ingress80 {
      server 192.168.10.71:32080 max_fails=2 fail_timeout=30s;
      server 192.168.10.72:32080 max_fails=2 fail_timeout=30s;
   }
   server {
      listen 80;
      proxy_pass ingress80;
   }
   upstream ingress443 {
      server 192.168.10.71:32443 max_fails=2 fail_timeout=30s;
      server 192.168.10.72:32443 max_fails=2 fail_timeout=30s;
   }
   server {
      listen 443;
      proxy_pass ingress443;
   }
}

nginx-ingress

Ingress を導入していきます。既に何がしかの IngressController があればスキップでお願いします。helm でインストールしていきます。

# github からコード取得
$ git clone https://github.com/kubernetes/ingress-nginx.git -b helm-chart-3.34.0 --depth 1

# 作業ディレクトリに移動
$ cd ingress-nginx/charts/ingress-nginx/

前項の nginx の設定で http:32080、https:32443 でリバースプロキシしているので、値を values.yaml に反映させます。

values.yaml
@@ -433,7 +433,7 @@ controller:
       http: http
       https: https

-    type: LoadBalancer
+    type: NodePort

     # type: NodePort
     # nodePorts:
@@ -442,8 +442,8 @@ controller:
     #   tcp:
     #     8080: 32808
     nodePorts:
-      http: ""
-      https: ""
+      http: 32080
+      https: 32443
       tcp: {}
       udp: {}

それでは早速デプロイします。

$ kubectl create ns ingress-nginx
namespace/nginx-ingress created

$ helm install ingress-nginx . -n ingress-nginx
NAME: ingress-nginx
LAST DEPLOYED: Mon Jul 12 15:20:42 2021
NAMESPACE: ingress-nginx
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
・・・

$ kubectl get po -n ingress-nginx
NAME                                       READY   STATUS    RESTARTS   AGE
ingress-nginx-controller-d9458694b-cjmhw   1/1     Running   0          29s

現在のバージョンで ingress を設定中に以下エラーが起きたので対処します。

Error from server (InternalError): error when creating "ingress.yaml": Internal error occurred: failed calling webhook "validate.nginx.ingress.kubernetes.io": an error on the server ("") has prevented the request from succeeding

https://stackoverflow.com/questions/67061272/kubernetes-ingress-internal-error-occurred-failed-calling-webhook-validate-ng

$ kubectl get -A ValidatingWebhookConfiguration
NAME                      WEBHOOKS   AGE
ingress-nginx-admission   1          119s

$ kubectl delete ValidatingWebhookConfiguration ingress-nginx-admission
validatingwebhookconfiguration.admissionregistration.k8s.io "ingress-nginx-admission" deleted

cert-manager

Ingress の準備ができたので、本題の cert-manager をデプロイします。
こちらも helm からインストールします。

# 作業ディレクトリを移動(ここでは $HOME に)
$ cd ~

# github からコード取得
$ git clone https://github.com/jetstack/cert-manager.git -b v1.4.0 --depth 1
$ cd cert-manager/deploy/charts/cert-manager/

# バージョン指定するために、Chart.tempalte.yaml をコピーして編集
$ cp -p Chart{.template,}.yaml

cert-manager のバージョンを指定するため、appVersion を編集します。

Chart.yaml
@@ -2,7 +2,7 @@
 name: cert-manager
 # The version and appVersion fields are set automatically by the release tool
 version: v0.1.0
-appVersion: v0.1.0
+appVersion: v1.4.0
 description: A Helm chart for cert-manager
 home: https://github.com/jetstack/cert-manager
 icon: https://raw.githubusercontent.com/jetstack/cert-manager/master/logo/logo.png

準備ができたのでデプロイします。

$ kubectl create ns cert-manager
namespace/cert-manager created

# cert-manager で利用する CRD をデプロイする
# URL で指定している「v1.4.0」の箇所はデプロイする cert-manager のバージョンに合わせる
$ kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.4.0/cert-manager.crds.yaml

customresourcedefinition.apiextensions.k8s.io/certificaterequests.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/certificates.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/challenges.acme.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/clusterissuers.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/issuers.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/orders.acme.cert-manager.io created

$ kubectl api-resources | grep -e NAME -e cert-manager
NAME                     SHORTNAMES     APIVERSION                 NAMESPACED   KIND
challenges                              acme.cert-manager.io/v1    true         Challenge
orders                                  acme.cert-manager.io/v1    true         Order
certificaterequests      cr,crs         cert-manager.io/v1         true         CertificateRequest
certificates             cert,certs     cert-manager.io/v1         true         Certificate
clusterissuers                          cert-manager.io/v1         false        ClusterIssuer
issuers                                 cert-manager.io/v1         true         Issuer

# Deploy cert-manager
$ helm install cert-manager . -n cert-manager
NAME: cert-manager
LAST DEPLOYED: Mon Jul 12 15:50:42 2021
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
cert-manager has been deployed successfully!
・・・

$ kubectl get po -n cert-manager
NAME                                     READY   STATUS    RESTARTS   AGE
cert-manager-59c55b4dbd-mpc6f            1/1     Running   0          9m50s
cert-manager-cainjector-5bccf4b7-tdtnm   1/1     Running   0          9m49s
cert-manager-webhook-6fd8ccc59d-hfsv6    1/1     Running   0          9m50s

自己署名証明書発行

cert-manager の準備ができたので自己署名証明書を発行していきます。

今回作成する認証局・証明書のイメージは以下の通りです。

まずは CA(認証局) から作成していきます。
Issuer(発行者)を作成し、ルート証明書を発行します。後続の 自己署名証明書発行の際にここで作成するルート証明書を利用します。
Certificateduration (有効期限)で 50 年を指定しています(h(時間)指定)。

ca.yaml
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned-issuer
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: selfsigned-ca
  namespace: sandbox
spec:
  isCA: true
  commonName: selfsigned-ca
  duration: 438000h
  secretName: selfsigned-ca-cert
  privateKey:
    algorithm: RSA
    size: 2048
  issuerRef:
    name: selfsigned-issuer
    kind: ClusterIssuer
    group: cert-manager.io
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: ca-issuer
  namespace: sandbox
spec:
  ca:
    secretName: selfsigned-ca

作業用の namespace を作成して、CA をデプロイします。

# 作業用 namespace の作成
$ kubectl create ns sandbox
namespace/sandbox created

$ kubectl apply -f ca.yaml
clusterissuer.cert-manager.io/selfsigned-issuer created
certificate.cert-manager.io/selfsigned-ca created
issuer.cert-manager.io/ca-issuer created

$ kubectl describe clusterissuers selfsigned-issuer
Name:         selfsigned-issuer
Namespace:
Labels:       <none>
Annotations:  <none>
API Version:  cert-manager.io/v1
Kind:         ClusterIssuer
・・・
Spec:
  Self Signed:
Status:
  Conditions:
    Last Transition Time:  2021-07-14T17:13:12Z
    Observed Generation:   1
    Reason:                IsReady
    Status:                True
    Type:                  Ready
Events:                    <none>

# ルート証明書の確認
# ※有効期限が発行日時から 50 年後になっている
$ kubectl describe certificate selfsigned-ca -n sandbox
Name:         selfsigned-ca
Namespace:    sandbox
Labels:       <none>
Annotations:  <none>
API Version:  cert-manager.io/v1
Kind:         Certificate
・・・
Spec:
  Common Name:  selfsigned-ca
  Duration:     438000h0m0s
  Is CA:        true
  Issuer Ref:
    Group:  cert-manager.io
    Kind:   ClusterIssuer
    Name:   selfsigned-issuer
  Private Key:
    Algorithm:  RSA
    Size:       2048
  Secret Name:  selfsigned-ca-cert
Status:
  Conditions:
    Last Transition Time:  2021-07-14T17:13:12Z
    Message:               Certificate is up to date and has not expired
    Observed Generation:   1
    Reason:                Ready
    Status:                True
    Type:                  Ready
  Not After:               2071-07-02T17:06:27Z     ★ 50 年後
  Not Before:              2021-07-14T17:06:27Z
  Renewal Time:            2054-11-05T09:06:27Z
Events:                    <none>

$ kubectl describe issuers ca-issuer -n sandbox
Name:         ca-issuer
Namespace:    sandbox
Labels:       <none>
Annotations:  <none>
API Version:  cert-manager.io/v1
Kind:         Issuer
・・・
Spec:
  Ca:
    Secret Name:  selfsigned-ca-cert
Status:
  Conditions:
    Last Transition Time:  2021-07-14T17:13:12Z
    Message:               Signing CA verified
    Observed Generation:   1
    Reason:                KeyPairVerified
    Status:                True
    Type:                  Ready
Events:
  Type    Reason           Age                  From          Message
  ----    ------           ----                 ----          -------
  Normal  KeyPairVerified  103s (x2 over 103s)  cert-manager  Signing CA verified

# Manifest ファイルの「secretName」で指定した名前で、
# Secret にルート証明書と秘密鍵が格納されている
$ kubectl describe secrets selfsigned-ca-cert -n sandbox
Name:         selfsigned-ca-cert
Namespace:    sandbox
Labels:       <none>
Annotations:  cert-manager.io/alt-names:
              cert-manager.io/certificate-name: selfsigned-ca
              cert-manager.io/common-name: selfsigned-ca
              cert-manager.io/ip-sans:
              cert-manager.io/issuer-group: cert-manager.io
              cert-manager.io/issuer-kind: ClusterIssuer
              cert-manager.io/issuer-name: selfsigned-issuer
              cert-manager.io/uri-sans:

Type:  kubernetes.io/tls

Data
====
ca.crt:   1099 bytes
tls.crt:  1099 bytes
tls.key:  1675 bytes

CA の準備ができたので早速 Jenkins 用の 自己署名証明書を発行していきます。
CA 作成時同様にマニフェストファイルを作成して、証明書を発行します。
※適用する URL はdnsNames(SAN)で指定しています。
 参考:https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.Certificate
subject は適宜変更を。
duration を 1 年で指定しています
issuerRef で先程作成した CA の Issuer を指定する

jenkins.yaml
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: jenkins
  namespace: sandbox
spec:
  subject:
    organizations:
    - MyOrg
    countries:
    - Japan
    organizationalUnits:
    - MyUnit
    localities:
    - Sapporo
    provinces:
    - Hokkaido
  commonName: jenkins-cn
  duration: 8760h
  dnsNames:
  - www.jenkins.internal
  secretName: jenkins-certificate
  issuerRef:
    name: ca-issuer
    kind: Issuer
    group: cert-manager.io
  privateKey:
    algorithm: RSA
    size: 2048

デプロイして 自己署名証明書を発行します。

$ kubectl apply -f jenkins.yaml -n sandbox
certificate.cert-manager.io/jenkins created

# 証明書の確認
$ kubectl describe certificate jenkins -n sandbox
Name:         jenkins
Namespace:    sandbox
Labels:       <none>
Annotations:  <none>
API Version:  cert-manager.io/v1
Kind:         Certificate
・・・
Spec:
  Common Name:  jenkins-cn
  Dns Names:
    www.jenkins.internal
  Duration:  8760h0m0s
  Issuer Ref:
    Group:  cert-manager.io
    Kind:   Issuer
    Name:   ca-issuer
  Private Key:
    Algorithm:  RSA
    Size:       2048
  Secret Name:  jenkins-certificate
  Subject:
    Countries:
      Japan
    Localities:
      Sapporo
    Organizational Units:
      MyUnit
    Organizations:
      MyOrg
    Provinces:
      Hokkaido
Status:
  Conditions:
    Last Transition Time:  2021-07-14T17:25:35Z
    Message:               Certificate is up to date and has not expired
    Observed Generation:   1
    Reason:                Ready
    Status:                True
    Type:                  Ready
  Not After:               2022-07-14T17:25:35Z
  Not Before:              2021-07-14T17:25:35Z
  Renewal Time:            2022-03-15T01:25:35Z
  Revision:                1
Events:
  Type    Reason     Age   From          Message
  ----    ------     ----  ----          -------
  Normal  Issuing    35s   cert-manager  Issuing certificate as Secret does not exist
  Normal  Generated  35s   cert-manager  Stored new private key in temporary Secret resource "jenkins-j2vlp"
  Normal  Requested  35s   cert-manager  Created new CertificateRequest resource "jenkins-ln7qf"
  Normal  Issuing    35s   cert-manager  The certificate has been successfully issued

# 作成された Secret (証明書と鍵)を確認
$ kubectl describe secrets jenkins-certificate
Name:         jenkins-certificate
Namespace:    sandbox
Labels:       <none>
Annotations:  cert-manager.io/alt-names: www.jenkins.internal
              cert-manager.io/certificate-name: jenkins
              cert-manager.io/common-name: jenkins-cn
              cert-manager.io/ip-sans:
              cert-manager.io/issuer-group: cert-manager.io
              cert-manager.io/issuer-kind: Issuer
              cert-manager.io/issuer-name: ca-issuer
              cert-manager.io/uri-sans:

Type:  kubernetes.io/tls

Data
====
tls.crt:  1253 bytes
tls.key:  1679 bytes
ca.crt:   1099 bytes

Deploy Jenkins

証明書が発行できたので実際にコンテナを起動、Ingress に設定していきます。

# 作業ディレクトリを移動
$ cd ~

# github からコード取得
$ git clone https://github.com/jenkinsci/helm-charts.git -b jenkins-3.5.2 --depth 1
$ cd helm-charts/charts/jenkins/

values.yaml を以下の様に編集します。

  • tag でイメージを最新に変更
  • Ingress を有効化
  • annotations で IngressController を指定
  • hostName で Ingress で受け取る FQDN を指定
  • secretName で証明書の Secret を指定
  • storageClass で PV のストレージクラスを指定
values.yaml
@@ -19,7 +19,7 @@ controller:
   # Used for label app.kubernetes.io/component
   componentName: "jenkins-controller"
   image: "jenkins/jenkins"
-  tag: "2.289.1-jdk11"
+  tag: "2.302"
   imagePullPolicy: "Always"
   imagePullSecretName:
   # Optionally configure lifetime for controller-container
@@ -383,7 +383,7 @@ controller:
   updateStrategy: {}

   ingress:
-    enabled: false
+    enabled: true
     # Override for the default paths that map requests to the backend
     paths: []
     # - backend:
@@ -398,7 +398,9 @@ controller:
     # For Kubernetes v1.19+, use 'networking.k8s.io/v1'
     apiVersion: "extensions/v1beta1"
     labels: {}
-    annotations: {}
+    annotations:
+      kubernetes.io/ingress.class: nginx
+      ingressClassName: nginx
     # kubernetes.io/ingress.class: nginx
     # kubernetes.io/tls-acme: "true"
     # For Kubernetes >= 1.18 you should specify the ingress-controller via the field ingressClassName
@@ -407,11 +409,11 @@ controller:
     # Set this path to jenkinsUriPrefix above or use annotations to rewrite path
     # path: "/jenkins"
     # configures the hostname e.g. jenkins.example.com
-    hostName:
+    hostName: www.jenkins.internal
     tls:
-    # - secretName: jenkins.cluster.local
-    #   hosts:
-    #     - jenkins.cluster.local
+    - secretName: jenkins-certificate
+      hosts:
+        - www.jenkins.internal

   # often you want to have your controller all locked down and private
   # but you still want to get webhooks from your SCM
@@ -724,7 +726,7 @@ persistence:
   ##   set, choosing the default provisioner.  (gp2 on AWS, standard on
   ##   GKE, AWS & OpenStack)
   ##
-  storageClass:
+  storageClass: rook-ceph-block
   annotations: {}
   accessMode: "ReadWriteOnce"
   size: "8Gi"

Jenkins をデプロイします。

$ helm install jenkins . -n sandbox

$ kubectl get po -n sandbox
NAME        READY   STATUS    RESTARTS   AGE
jenkins-0   2/2     Running   0          82s

$ kubectl describe ingress jenkins -n sandbox
Name:             jenkins
Namespace:        sandbox
Address:          10.97.19.11
Default backend:  default-http-backend:80 (<error: endpoints "default-http-backend" not found>)
TLS:
  jenkins-certificate terminates www.jenkins.internal
Rules:
  Host                  Path  Backends
  ----                  ----  --------
  www.jenkins.internal
                           jenkins:8080 (172.16.86.144:8080)
Annotations:            ingressClassName: nginx
                        kubernetes.io/ingress.class: nginx
                        meta.helm.sh/release-name: jenkins
                        meta.helm.sh/release-namespace: sandbox
Events:
  Type    Reason  Age                  From                      Message
  ----    ------  ----                 ----                      -------
  Normal  Sync    106s (x2 over 114s)  nginx-ingress-controller  Scheduled for sync

アクセス

実際にアクセスして確認してみます。
今回は DNS を利用していないので、hosts ファイルで名前解決します。
URL に対して LB のアドレスを指定して登録しておきます。
※下記のファイル名は「:」「¥」が全角になっているのでコピペ不可

【参考】C:¥Windows¥System32¥drivers¥etc¥hosts
・・・
192.168.10.51 www.jenkins.internal

作成したルート証明書をダウンロードしてクライアントにインストールします。
まずは作成したルート証明書をエクスポートします。

$ kubectl get secrets jenkins-certificate -o jsonpath='{.data.ca\.crt}' | base64 -d > ca.crt

$ cat ca.crt
-----BEGIN CERTIFICATE-----
MIIC/zCCAeegAwIBAgIRAI0d8EpGZX5+1eK7Wo6xOXIwDQYJKoZIhvcNAQELBQAw
・・・
-----END CERTIFICATE-----

作成できた証明書ファイルを SCP などでクライアントまでダウンロードし、証明書をインストールします。以下は Windows での手順です。参考までに。

  1. 証明書ファイルをダブルクリック
  2. 「全般」タブの「証明書のインストール」をクリック
  3. 「現在のユーザー」を選択して「次へ」
  4. 「証明書をすべて次のストアに配置する」を選択し、「参照」をクリック
  5. 表示されたダイアログで「信頼されたルート証明機関」を選択し「OK」
  6. 「次へ」⇒「完了」⇒「セキュリティ警告」が表示されるので「はい」⇒「OK」
  7. certmgr.msc を実行すると証明書マネージャーが起動するので「信頼されたルート証明機関」に「selfsigned-ca」が登録されていることを確認

ルート証明書がインストールできたので、指定した URL(ここではhttps://www.jenkins.internal/)にアクセスすると証明書エラーが表示されずに https でアクセスできることが確認できるかと思います。

まとめ

cert-manager を使うと自己署名証明書(オレオレ証明書)も簡単に作成することができました。Ingress のマニフェストに Annotation を追加で付与することで、Ingress 登録時に証明書が自動的に作成される、ってこともできるそうです。

Securing Ingress Resources
https://cert-manager.io/docs/usage/ingress/

次はクラウドを使って Let's Encrypt にも調整したいですねー。

参考

Ingress Nginx
https://github.com/kubernetes/ingress-nginx

cert-manager
https://github.com/jetstack/cert-manager

cert-manager API-Refference
https://cert-manager.io/docs/reference/api-docs/#cert-manager.io%2Fv1

Discussion