2023/03/15 に OpenShift 4.12 上で Red Hat 版の Cert Manager が正式に GA されました。
この記事では、Community 版の Cert Manager を使用していますが、Red Hat からの正式なサポートがある Red Hat 版の Cert Manager の御使用をお勧めします。
Red Hat 版の Cert-Manager も正式にリリースされたので、Red Hat版を使用した新しい記事を作成しました。
Cert-Manager は、証明書を自動更新するためのソフトウェアです。こちらのコミニュティで開発されています。
OpenShift の AWS 上の Managed サービスである ROSA(Red Hat OpenShift on AWS) 上で、Operator 版の Cert-Managr を使用してみいます。OpenShift 用に Operator のパッケージが提供されているので、それを使用してみます。
今回の環境
今回の手順は以下の環境で作成しました。
・Kubernetes のディストリビューションは、OpenShift の Managed Service である ROSA を使用
・Cert-Manger Operator は、Community 版と、Red Hat版があるが Community 版を使用。
・証明書を発行する対象ドメインは、ocp4.work
・証明書を発行する対象ドメイン ocp4.workは、Route53 を権威 DNSサーバーにする
・証明書の CA は Let's Encrypt とする。
AWS の Public 向けの証明書を発行する ACM (AWS Certificate Manager)は、AWSのサービスに対してだけ証明書を発行する特殊なサービスなので、Cert-Manager と組み合わせて使用する事はできません。Cert-Manager は複数のCAをサポートしていますが、今回は Let's Encrypt を使用します。Cert-Manager がサポートする CAは、ドキュメントのこの辺りから書かれています。
環境の準備
以下の環境を使用します
・Amazon Linux 2
・oc コマンド が実行できる環境 (インストールするための gist はこちら )
・AWS CLIがセットアップ済み (Amazon Linux 2 に aws コマンドは入って居るので aws configure で構成)
・jq コマンドがインストール済み ( sudo yum install -y jq)
必要な情報の収集
今回は、Cert Manager が、Let's Encrypt の Challenge に対して自動的に Route53 のレコードを書き替えて対応するようにします。そのために、AWS の IAMRole/Policy を作成したり、対象ドメイン(この例では ocp4.work)をホストしている Route53の情報が必要になります。
AWS CLI の構成をしておきます。
$ aws configure
AWS Access Key ID [None]: ASBDERFFI4B4VVUXXYYZZ
AWS Secret Access Key [None]: xXv7/X4FsXlLaTEteaabbccddjwV+6M7SHm02
Default region name [None]: ap-northeast-1
Default output format [None]:
$
作業ディレクトリの作成
export WORK_DIR=~/work
mkdir -pv $WORK_DIR
rosa list cluster で表示される作成済みの ROSA クラスターの名前
CLUSTER_NAME="rosa-cluster"
rh-oidc.s3.us-east-1.amazonaws.com/20s0h5421tff8a6tqsoh2hk78m1ocp0o
のような値
export OIDC_PROVIDER=$(oc get authentication.config.openshift.io cluster -o json \
| jq -r .spec.serviceAccountIssuer| sed -e "s/^https:\/\///")
Service Account の情報 201068234712
のような値
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
Let's Encrypt に対して証明書を申請する際に使う有効なメールアドレス
export LETSENCRYPT_EMAIL=test@test.com
今回の証明書発行をリクエストするドメイン名は ocp4.work
です。自分で取得するドメイン名に読み替えて下さい。
このドメインを Route53 でホストします。
export HOSTED_ZONE_REGION=ap-northeast-1
export DOMAIN=ocp4.work
HOSTED_ZONE_ID は、証明書発行ドメインをホストしている Route53 の Zone IDです。Z01429121KPW1GKICNEO2
のような値です。
export HOSTED_ZONE_ID=$(aws route53 list-hosted-zones --query "HostedZones[].[Name,Id,Config.PrivateZone,ResourceRecordSetCount]" --output text | grep $DOMAIN | awk '{print $2}' | sed -e "s/.*\///")
HOSTED_ZONE_ID は、上記の例のように AWS CLIで取得するか、Route53 の画面に表示されています。
AWS Role と Policy の作成
route53 用 許可ポリシーの json ファイルの作成します。
cat <<EOF > $WORK_DIR/cert-manager-r53-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "route53:GetChange",
"Resource": "arn:aws:route53:::change/*"
},
{
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets",
"route53:ListResourceRecordSets"
],
"Resource": "arn:aws:route53:::hostedzone/*"
},
{
"Effect": "Allow",
"Action": "route53:ListHostedZonesByName",
"Resource": "*"
}
]
}
EOF
作成したjson ファイルを使用して、AWS上に許可ポリシーを作成します。
POLICY=$(aws iam create-policy --policy-name "${CLUSTER_NAME}-cert-manager-r53-policy" \
--policy-document file://$WORK_DIR/cert-manager-r53-policy.json \
--query 'Policy.Arn' --output text) || echo $POLICY
変数にきちんと値が返っているか確認します。
$ echo $POLICY
arn:aws:iam::452752386616:policy/rosa-cluster-cert-manager-r53-policy
$
次に、信頼ポリシー用のjson ファイルを作成します。
この Policyは、openshift-operators という project 名の cert-manager という Service Account を条件にしている事に注意して下さい。
現状で Community 版の Cert-Manager は、GUIからは openshift-operators にしか導入できませんが、将来、導入場所が変更になったり、ここでは触れませんが CLIを使って導入先 project を変更した場合は、内容を変更する必要があります。
cat <<EOF > $WORK_DIR/TrustPolicy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/${OIDC_PROVIDER}"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"${OIDC_PROVIDER}:sub": [
"system:serviceaccount:openshift-operators:cert-manager"
]
}
}
}
]
}
EOF
作成した 信頼ポリシー(Trust Policy) のファイルを指定して、AWS上にIAM Role を作成します。
ROLE=$(aws iam create-role \
--role-name "${CLUSTER_NAME}-cert-manager-operator" \
--assume-role-policy-document file://$WORK_DIR/TrustPolicy.json \
--query "Role.Arn" --output text)
上手くできているか変数の値を確認します。
$ echo $ROLE
arn:aws:iam::452752386616:role/rosa-cluster-cert-manager-operator
$
作成したIAM Role に、一番はじめに作成した 許可ポリシーも Attach します。
aws iam attach-role-policy \
--role-name "${CLUSTER_NAME}-cert-manager-operator" \
--policy-arn $POLICY
これで、IAM Role (${CLUSTER_NAME}-cert-manager-operator
) に、許可ポリシーと信頼ポリシーがアタッチされました。
Certificaton Manager Operator のインストール
Operator Hub から、Community 提供の cert-manager
を選択してインストールします。
(この記事作成時点で Community 版のリリースが Nov4,2022 に対して、Red Hat版が Sep 21, 2021でかなり古いため)
openshift-cert-manager project に移動
oc project openshift-operators
以下のような service account が作られているはずです。
$ oc get sa | grep cert
cert-manager 1 5m22s
cert-manager-cainjector 1 5m20s
cert-manager-webhook 1 5m18s
$
cert-manager という Service Account に、前のステップで作成した IAM Role 名を入れた annotation を付けます。
$ echo $ROLE
arn:aws:iam::452752386616:role/rosa-cluster-cert-manager-operator
$
oc annotate serviceaccount cert-manager eks.amazonaws.com/role-arn=$ROLE
$ oc describe sa cert-manager | grep Annotation
Annotations: eks.amazonaws.com/role-arn: arn:aws:iam::452752386616:role/rosa-cluster-cert-manager-operator
$
Service Account に annotation を付けたので、新しい権限で Pod を再起動します。
$ oc get pods
NAME READY STATUS RESTARTS AGE
cert-manager-6d98b9cd6f-wn46t 1/1 Running 0 7m
cert-manager-cainjector-f7c545bdf-zklld 1/1 Running 0 7m
cert-manager-webhook-7775d9699d-dbgrf 1/1 Running 0 7m
cert-manager-xxxxxx-xxxx を delete します。delete すると自動で再起動されます。
$ od delete pods cert-manager-6d98b9cd6f-wn46t
ClusterIssure リソースの作成
ClusterIssure リソースを作成します。
envsubst <<EOF | oc apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencryptissuer
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: $LETSENCRYPT_EMAIL
# This key doesn't exist, cert-manager creates it
privateKeySecretRef:
name: prod-letsencrypt-issuer-account-key
solvers:
- dns01:
route53:
hostedZoneID: $HOSTED_ZONE_ID
region: $HOSTED_ZONE_REGION
secretAccessKeySecretRef:
name: ''
EOF
Read になるまで待ちます。
$ oc get clusterissuer letsencryptissuer
NAME READY AGE
letsencryptissuer 90s
$
$ oc get clusterissuer letsencryptissuer
NAME READY AGE
letsencryptissuer True 5m34s
$
この時、Route 53には、自動的に Let's Encrypt からの Challenge が書き込まれ、自動で消えます。(ずっと観察していないと見逃してしまうので、運が良ければ見る事ができます)
Certificate リソースの作成
Certficate リソースを作成します。Certificate リソースを作成する事で、証明書の発行をリクエストします。
前段で作成した ClusterIssure 名 letsencryptissuer
は、証明書のリクエスト先 CA (Certificate Auhtority) としてこのリソースの中に指定されています。
envsubst <<EOF | oc apply -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: customdomain-cert
spec:
secretName: custom-domain-certificate-tls
issuerRef:
name: letsencryptissuer
kind: ClusterIssuer
commonName: "*.$DOMAIN"
dnsNames:
- "*.$DOMAIN"
EOF
状態確認をします。True になるまで待ちます。
$ oc get certificate customdomain-cert
NAME READY SECRET AGE
customdomain-cert False custom-domain-certificate-tls 21s
$
$ oc get certificate customdomain-cert
NAME READY SECRET AGE
customdomain-cert True custom-domain-certificate-tls 21s
$
ここで作成された証明書は上記の表示にあるように custom-domain-certificate-tls
という Secret になります。
debugの方法
数分で作成される場合もありますが、場合によっては Certificate の発行が待たされている場合があります。
状況は Pod のログを見る事で確認できます。
$ oc get pods | grep cert-manager
NAME READY STATUS RESTARTS AGE
cert-manager-5cb58c6b55-r4hcj 1/1 Running 0 56m
cert-manager-cainjector-7889dc796c-mjqqm 1/1 Running 0 60m
cert-manager-webhook-85f99cb964-2qk6n 1/1 Running 0 60m
$
一番上の Pod のログを確認します。
$ oc logs -f cert-manager-5cb58c6b55-r4hcj
<省略>
I0102 03:10:10.041269 1 conditions.go:192] Found status change for Certificate "customdomain-cert" condition "Issuing": "True" -> "False"; setting lastTransitionTime to 2023-01-02 03:10:10.041262527 +0000 UTC m=+54.359300594
I0102 03:10:10.055415 1 trigger_controller.go:179] cert-manager/certificates-trigger "msg"="Backing off from issuance due to previously failed issuance(s). Issuance will next be attempted at 2023-01-02 04:10:10.000000666 +0000 UTC m=+3654.318038737" "key"="cert-manager/customdomain-cert
I0102 03:10:10.088248 1 trigger_controller.go:179] cert-manager/certificates-trigger "msg"="Backing off from issuance due to previously failed issuance(s). Issuance will next be attempted at 2023-01-02 04:10:10.000000521 +0000 UTC m=+3654.318038589" "key"="cert-manager/customdomain-cert"
[実践編] CDO の証明書を Cert-Manager Operator が管理する証明書に置きかえる
ここでは実践編としてROSA の CDO(Custom Domain Operator) が使用している証明書の Secret を、Cert-Manager Operator が使用している Secert と置き換えてみます。
前提条件:CDO が全てデプロイされており、*.ocp4.work
という証明書(Cert-Managerを使わないで発行した)ものを使用している。
別記事のROSA の CDO(Custom Domain Operator) を使用するが終わった状態を想定しています。
現在の証明遺書の確認
まず、現在 CDO が使っている現在の証明書の期限を確認しておきます。
[ec2-user@ip-10-11-145-199 ~]$ echo | openssl s_client -connect test1.ocp4.work:443 2>&1 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | openssl x509 -text 2>/dev/null | grep -e "Public-Key" -e "Not"
Not Before: Dec 27 09:14:09 2022 GMT
Not After : Mar 27 09:14:08 2023 GMT
Public-Key: (2048 bit)
[ec2-user@ip-10-11-145-199 ~]$
行う事は、CustomeDomain リソースの Secret の名前を置き換えるだけです。
ここでは、CustomDomain リソースの名前は acme
になっています。置き換え前の証明書のSecretは、namespace my-custom-domain
に存在し、my-tls
という名前です。
[ec2-user@ip-10-11-145-199 ~]$ oc edit customdomain acme
# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: managed.openshift.io/v1alpha1
kind: CustomDomain
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"managed.openshift.io/v1alpha1","kind":"CustomDomain","metadata":{"annotations":{},"name":"acme"},"spec":{"certificate":{"name":"my-tls","namespace":"my-custom-domain"},"domain":"ocp4.work"}}
creationTimestamp: "2022-12-27T10:25:41Z"
finalizers:
- finalizer.customdomain.managed.openshift.io
generation: 1
name: acme
resourceVersion: "280910"
uid: 05d86204-312f-4444-a288-e5c555456bd2
spec:
certificate:
name: my-tls # ここを Cert-Manager が作成したものに置き換えます。
namespace: my-custom-domain # ここを Cert-Manager が作成したものに置き換えます。
domain: ocp4.work
status:
conditions:
- lastProbeTime: "2022-12-27T10:25:41Z"
lastTransitionTime: "2022-12-27T10:25:41Z"
message: Creating Apps Custom Domain (ocp4.work)
reason: Creating
status: "True"
type: Creating
- lastProbeTime: "2022-12-27T10:26:41Z"
lastTransitionTime: "2022-12-27T10:26:41Z"
message: Custom Apps Domain (ocp4.work) Is Ready
reason: Ready
status: "True"
type: Ready
dnsRecord: '*.acme.singleaz.dzfa.p1.openshiftapps.com.'
endpoint: qgeenq.acme.singleaz.dzfa.p1.openshiftapps.com
scope: ""
state: Ready
sed で Secret の情報を置き換えて適用します。
oc get customdomain acme -o yaml > cdo.yaml
sed -i -e 's/name: my-tls/name: custom-domain-certificate-tls/' cdo.yaml
sed -i -e 's/namespace: my-custom-domain/namespace: openshift-operators/' cdo.yaml
oc apply -f cdo.yaml
更新の確認
証明書が置き換えられているかどうかを確認します。
[ec2-user@ip-10-11-145-199 ~]$ echo | openssl s_client -connect test1.ocp4.work:443 2>&1 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | openssl x509 -text 2>/dev/null | grep -e "Public-Key" -e "Not"
Not Before: Dec 30 13:06:56 2022 GMT
Not After : Mar 30 13:06:55 2023 GMT
Public-Key: (2048 bit)
[ec2-user@ip-10-11-145-199 ~]$
有効期限の日付が、以前と変わっているので無事置き換わった事がわかります。
[回避策] Red Hat Cert-Manager Operator を使った場合
As of 2022年12月
Red Hat の Cert Manager はこの記事作成時の最新が1年前のものでした。そのため、以下の回避策が必要になりました。
Operator のアップグレード等で回避策が消えてしまう可能性もあるので、回避策を使った場合はその後のメンテナンスンに注意しましょう。
(この記事の初稿時点(2022年12月)で、Red Hat Cert Manager は、Tech Preview の状態なので Red Hat からのサポートは受けられません)
また、Red Hat Cert Manager Operator を使用した場合は、AWS IAM Role に付けている信頼ポリシーに含まれる Service Account の project 名がこの記事とは異なるものになるので注意して下さい。
デフォルトのインストールでは、Certificate リソースの作成後 False のままステータスが変わらないはずです。
[ec2-user@ip-10-11-145-199 ~]$ oc get certificate
NAME READY SECRET AGE
customdomain-cert False custom-domain-certificate-tls 4m14s
[ec2-user@ip-10-11-145-199 ~]$
以下からの作業は、openshift-cert-manager
namespace で行います。project に移動します。
[ec2-user@ip-10-11-145-199 ~]$ oc project openshift-cert-manager
namespace の pod をリストし、cert-manager-xxxxx-xxxx
のログを確認します。
[ec2-user@ip-10-11-145-199 ~]$ oc get pods
NAME READY STATUS RESTARTS AGE
cert-manager-79bb458c89-dgg8p 1/1 Running 0 9m57s
cert-manager-cainjector-84db9f4996-fpj7l 1/1 Running 0 10m
cert-manager-webhook-78ffc5bb76-thfm6 1/1 Running 0 10m
[ec2-user@ip-10-11-145-199 ~]$
以下の error ログが記録されているはずです。
[ec2-user@ip-10-11-145-199 ~]$ oc logs -f cert-manager-79bb458c89-dgg8p
....
E1231 01:01:00.628862 1 controller.go:163] cert-manager/challenges "msg"="re-queuing item due to error processing" "error"="failed to change Route 53 record set: NoCredentialProviders: no valid providers in chain. Deprecated.\n\tFor verbose messaging see aws.Config.CredentialsChainVerboseErrors" "key"="openshift-cert-manager/customdomain-cert-r22v7-1185219526-588719510"
修復するために、以下のコマンドを実行します。
$ export WORK_DIR=work
$ mkdir -p work
$ openssl s_client -showcerts -verify 5 -connect sts.amazonaws.com:443 < /dev/null 2> /dev/null | awk '/BEGIN/,/END/{ if(/BEGIN/){a++}; print}' > $WORK_DIR/extra-ca.pem
$ openssl s_client -showcerts -verify 5 -connect acme-v02.api.letsencrypt.org:443 < /dev/null 2> /dev/null | awk '/BEGIN/,/END/{ if(/BEGIN/){a++}; print}' >> $WORK_DIR/extra-ca.pem
$ oc create configmap extra-ca --from-file=$WORK_DIR/extra-ca.pem
configmap/extra-ca created
$ CERT_MANAGER_CSV_NAME=$(oc get csv | grep 'cert-manager' | awk '{print $1}')
$ echo $CERT_MANAGER_CSV_NAME
openshift-cert-manager.v1.7.1
$ oc patch csv $CERT_MANAGER_CSV_NAME --type='json' -p '[{"op": "add", "path": "/spec/install/spec/deployments/0/spec/template/spec/volumes", "value": [{"name": "extra-ca"}]}, {"op": "add", "path": "/spec/install/spec/deployments/0/spec/template/spec/volumes/0/configMap", "value": {"name": "extra-ca", "defaultMode": 420}}, {"op": "add", "path": "/spec/install/spec/deployments/0/spec/template/spec/containers/0/volumeMounts", "value": [{"name": "extra-ca", "mountPath": "/etc/ssl/certs/extra-ca-bundle.pem", "readOnly": true, "subPath": "extra-ca-bundle.pem"}]}]'
eos.com/cert-manager.v1.10.1 patchedclusterserviceversion.operators.coreos.com/openshift-cert-manager.v1.7.1 patched
[ec2-user@ip-10-11-145-199 ~]$
pod cert-manager-xxxxx-xxxx
を再起動します。delete すれば contrller が再作成してくれます。
[ec2-user@ip-10-11-145-199 ~]$ oc get pods
NAME READY STATUS RESTARTS AGE
cert-manager-79bb458c89-dgg8p 1/1 Running 0 16m
cert-manager-cainjector-84db9f4996-fpj7l 1/1 Running 0 16m
cert-manager-webhook-78ffc5bb76-thfm6 1/1 Running 0 16m
[ec2-user@ip-10-11-145-199 ~]$ oc delete pod cert-manager-79bb458c89-dgg8p
pod "cert-manager-79bb458c89-dgg8p" deleted
[ec2-user@ip-10-11-145-199 ~]$ oc get pods
NAME READY STATUS RESTARTS AGE
cert-manager-79bb458c89-rc524 1/1 Running 0 4s
cert-manager-cainjector-84db9f4996-fpj7l 1/1 Running 0 16m
cert-manager-webhook-78ffc5bb76-thfm6 1/1 Running 0 16m
[ec2-user@ip-10-11-145-199 ~]$
pod のログを再確認して、同じエラーができてないか確認します。
[ec2-user@ip-10-11-145-199 ~]$ oc logs -f cert-manager-79bb458c89-rc524
I1231 01:08:18.575936 1 controller.go:220] cert-manager/controller "msg"="starting controller" "controller"="challenges"
I1231 01:08:18.575983 1 controller.go:220] cert-manager/controller "msg"="starting controller" "controller"="certificaterequests-approver"
I1231 01:08:18.575993 1 controller.go:220] cert-manager/controller "msg"="starting controller" "controller"="clusterissuers"
I1231 01:08:18.578966 1 util.go:84] cert-manager/controller/orders/handleOwnedResource "msg"="owning resource not found in cache" "related_resource_kind"="Order" "related_resource_name"="customdomain-cert-r22v7-1185219526" "related_resource_namespace"="openshift-cert-manager" "resource_kind"="Challenge" "resource_name"="customdomain-cert-r22v7-1185219526-588719510" "resource_namespace"="openshift-cert-manager" "resource_version"="v1"
E1231 01:08:19.576786 1 controller.go:163] cert-manager/challenges "msg"="re-queuing item due to error processing" "error"="ACME client for issuer not initialised/available" "key"="openshift-cert-manager/customdomain-cert-r22v7-1185219526-588719510"
I1231 01:08:19.576860 1 setup.go:202] cert-manager/clusterissuers "msg"="skipping re-verifying ACME account as cached registration details look sufficient" "related_resource_kind"="Secret" "related_resource_name"="prod-letsencrypt-issuer-account-key" "related_resource_namespace"="openshift-cert-manager" "resource_kind"="ClusterIssuer" "resource_name"="letsencryptissuer" "resource_namespace"="" "resource_version"="v1"
I1231 01:08:24.574930 1 setup.go:202] cert-manager/clusterissuers "msg"="skipping re-verifying ACME account as cached registration details look sufficient" "related_resource_kind"="Secret" "related_resource_name"="prod-letsencrypt-issuer-account-key" "related_resource_namespace"="openshift-cert-manager" "resource_kind"="ClusterIssuer" "resource_name"="letsencryptissuer" "resource_namespace"="" "resource_version"="v1"
Certificate リソースが、True になっているか確認します。
[ec2-user@ip-10-11-145-199 ~]$ oc get certificate
NAME READY SECRET AGE
customdomain-cert True custom-domain-certificate-tls 11m
[ec2-user@ip-10-11-145-199 ~]$
無事証明書の発行が確認できました。