Cert-Manager Operator
を使った証明書の更新方法は、ドキュメントに載っているのですが、ccoctl
という OpenShift
の独自ツールを使用して IAM Role
や IAM Policy
を作成するようになっています。
できるだけシンプルに導入したいので、そこは標準の AWS CLI
を利用する事にして、それ以外は基本、ドキュメントに従って導入して行きます。
ここでは、GUI から Cert-Manager Operator
をインストールしていますが、CLIからも導入できます。(ちょっと手数が多くなります)
今回の環境
今回の手順は以下の環境で作成しました。
・Kubernetes
のディストリビューションは、OpenShift の AWS 上の Managed Service である ROSA
のバージョン 4.14.27
を使用 (ROSA
の最新アーキテクチャ版である ROSA
HCP(Hosted Control Plane)
を使用)
・Cert-Manger Operator
は、Community
版とRed Hat
版があるが Red Hat
版を使用。
・証明書を発行する対象ドメインは、ocp4.work
・証明書を発行する対象ドメイン ocp4.work
は、Route53
を権威 DNSサーバーにする
・ROSA
とocp4.work
の権威DNSである Route53
は、同じ AWS Account 内に存在
・証明書の 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 コマンド が実行できる環境 (インストールするための こちらにスクリプトを作りました )
・AWS CLIがセットアップ済み (Amazon Linux 2 に aws コマンドは入って居るので aws configure で構成)
・jq コマンドがインストール済み ( sudo yum install -y jq
)
必要な情報の収集
今回は、Cert-Manager
が、Let's Encrypt
の Challenge に対して自動的に Route53
のレコードを書き替えて対応するようにします。そのために、AWS の IAM Role
/IAM 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:\/\///")
AWS Account ID の値。 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 の作成
Cert-Manager Operator は、証明書の発行や更新のために AWS の Route53 にアクセスして編集などの作業するので、AWS の STS(Secure Token Service) を使用して必要な権限を一時的に取得します。その時にどの対象からのリクエストを信頼して、どのような許可を与えるかを AWS の IAM Policy や IAM Role を使って事前に定義しておく必要があります。
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
上記の Policy を良く見ると、"Action" として Route53 への
幾つかの権限を与えている事がわかります。
作成した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
は、cert-manager
という project
名の cert-manager
という Service Account
を条件(Condition
) に設定している事に注意して下さい。
導入する 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:cert-manager:cert-manager"
]
}
}
}
]
}
EOF
上記の Policy
を良く見ると AWS_ACCOUNT_ID
が Principal
として指定されていたり、さらに条件 (Condition
) として、OIDC_RPOVIDER
の値や、Service Account
名や project
名 等も指定されている事がわかります。これらの条件に一致しない相手でないと信頼されない(=Tokenが発行されない)事になります。
作成した 信頼ポリシー(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::081920418460:role/myhcpcluster-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
) に、許可ポリシーと信頼ポリシーがアタッチされました。
Cert-Manager Operator のインストール
Cert-Manager は CLI からもインストールする事ができますが、ここでは OpenShift Console の GUI 上の OperatorHub からインストールします。
Cert-Manager は、Community 版と、Red Hat 版があるので、Red Hat 版を選択します。
role ARN
では、前のステップで作成した AWS の IAM Role
の ARN arn:aws:iam::081920418460:role/myhcpcluster-cert-manager-operator
を入力します。(ここに IAM Role の ARN を入力すると Service Account に annotation が付くのかと思ったのですが、そういう訳ではありませんでした。用途がちょっと謎です)
また、ここでは Update approval
は、Automatic
を選択し、承認なしで自動でアップデートされるようにしました。
CLI に戻り、以下で導入を確認します。
oc get pods -n cert-manager
こんな風に3つのPodが動いてればOKです。
$ oc get pods -n cert-manager
NAME READY STATUS RESTARTS AGE
cert-manager-85dfdd4f8b-zgvc8 1/1 Running 0 65m
cert-manager-cainjector-56b774cdf8-bt4tm 1/1 Running 0 66m
cert-manager-webhook-8f694d645-tfcsn 1/1 Running 0 66m
$
以下のような service account が作られているはずです。
$ oc get sa -n cert-manager | grep cert
cert-manager 1 3h38m
cert-manager-cainjector 1 3h38m
cert-manager-webhook 1 3h38m
$
cert-manager という Service Account に、前のステップで作成した IAM Role 名を入れた annotation を付けます。
$ echo $ROLE
arn:aws:iam::081920418460:role/myhcpcluster-cert-manager-operator
$
oc annotate -n cert-manager serviceaccount cert-manager eks.amazonaws.com/role-arn=$ROLE
$ oc describe -n cert-manager sa cert-manager | grep Annotation
Annotations: eks.amazonaws.com/role-arn: arn:aws:iam::081920418460:role/myhcpcluster-cert-manager-operator
$
この annotation を付ける事で、この Service Account (cert-manager) は、作業に必要な AWS の IAMRole を STSを使って手に入れる(AssumeRole)事ができるようになります。
Service Account
に annotation
を付けたので、新しい権限で Pod を再起動します。以下のコマンドで delete すると再起動します。
oc delete pods -l app.kubernetes.io/name=cert-manager -n cert-manager
CertMnager リソースの編集
証明書を保管するための project
を作成します。
CERT_PROJECT=letencrypt
oc new-project $CERT_PROJECT
この手順は、ドキュメントのこちら を参考にしています。
CertManager
リソースを編集して --issuer-ambient-credentials
引数をつけます。
oc patch certmanager/cluster --type=merge -p='{"spec":{"controllerConfig":{"overrideArgs":["--issuer-ambient-credentials"]}}}'
ClusterIssure リソースの作成
ClusterIssure
リソースを作成します。
envsubst <<EOF | oc apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencryptissuer
namespace: $CERT_PROJECT
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
EOF
公式ドキュメントのspec.acme.server
field の値は、 https://acme-staging-v02.api.letsencrypt.org/directory
となっており staging
用の Let's Encrypt
証明書を発行するようになっています。staging
用証明書の CA が普通の環境の Trusted Store には登録されてないため、curl
等では -k
オプションを使ってアプリケーションのテストする必要があります。
一方、ここで作成する 非staging
環境用 (本場用:spec.acme.server
field の値が https://acme-v02.api.letsencrypt.org/directory
) の証明書は、一週間に5回以上、同じドメインの証明書をリクエストをすると Rate Limit
にひっかかり新規の発行ができなくなります。
そのため、実験で繰り返す事が予想される場合は、本番用の証明書を発行するClusterIssure
リソースの他に、staging
用の証明書を作成するClusterIssure
リソース (spec.acme.server
の値は、 https://acme-staging-v02.api.letsencrypt.org/directory
)も作成しておく事をおすすめします。
Read が True になるまで待ちます。
$ oc get clusterissuer letsencryptissuer
NAME READY AGE
letsencryptissuer 90s
$
$ oc get clusterissuer letsencryptissuer
NAME READY AGE
letsencryptissuer True 5m34s
$
Certificate リソースの作成
Certficate
リソースを作成します。Certificate
リソースを作成する事で、証明書の発行をリクエストします。
前段で作成した ClusterIssure
名 letsencryptissuer
は、証明書のリクエスト先 CA (Certificate Auhtority) としてこのリソースの中に指定されています。
envsubst <<EOF | oc apply -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: customdomain-cert
namespace: $CERT_PROJECT
spec:
isCA: false
secretName: custom-domain-certificate-tls
issuerRef:
name: letsencryptissuer
kind: ClusterIssuer
commonName: "*.$DOMAIN"
dnsNames:
- "*.$DOMAIN"
EOF
状態確認をします。True
になるまで待ちます。
$ oc get -n $CERT_PROJECT certificate customdomain-cert
NAME READY SECRET AGE
customdomain-cert False custom-domain-certificate-tls 21s
$
$ oc get -n $CERT_PROJECT 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 -n cert-manager 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
$
cert-manager-<ランダム>
な Pod
のログを確認します。ここでは、一番上の 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"
[実践編] IngressController の証明書を Cert-Manager Operator が管理する証明書に置きかえる
ここでは実践編として、ROSA に追加で作成した Ingresscontroller
(name: additional) が使用している証明書の Secret を、Cert-Manager Operator が使用している Secert と置き換えてみます。こちらの記事の環境を前提にしています。
前提条件
以下のように既に Secondary の IngressController
である additional
という IngressController
がデプロイされています。
$ oc get ingresscontroller -n openshift-ingress-operator
NAME AGE
additional 46m
default 8h
$
この IngressController
の使用している defaultCertifidcate
は、my-tls
という名前になっています。これを Cert-Manager
が作成したものに置き換えてみます。
$ oc get ingresscontroller additional -n openshift-ingress-operator -o yaml | grep defaultCertificate: -A1
defaultCertificate:
name: my-tls
$
現在の証明遺書の確認
まず、現在の IngressController
を使っているアプリの現在の証明書の期限を確認しておきます。
$ echo | openssl s_client -connect hello.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: Jun 10 10:59:15 2024 GMT
Not After : Sep 8 10:59:14 2024 GMT
Public-Key: (2048 bit)
$
まず、openshift-ingress
project
内に 新しい証明書の Secret
を作成します。
証明書の Secret
を生成する project
は、IngressController
が Router pod を生成する openshift-ingress
project
である必要があります。
Certificate
リソースによってリクエストされた 証明書(Secret
) は、Certificate
リソースと同じproject
に作成されます。
envsubst <<EOF | oc apply -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: my-tls2
namespace: openshift-ingress
spec:
isCA: false
secretName: my-tls2
issuerRef:
name: letsencryptissuer
kind: ClusterIssuer
commonName: "*.$DOMAIN"
dnsNames:
- "*.$DOMAIN"
EOF
作成を確認します。
$ oc get -n openshift-ingress certificate my-tls2
NAME READY SECRET AGE
my-tls2 True my-tls2 14s
$
Secret の作成を確認します。
$ oc get secret my-tls2 -n openshift-ingress
NAME TYPE DATA AGE
my-tls2 kubernetes.io/tls 2 36s
$
次にこの Secret を IngressController に指定します。
証明書の Secret
を作成したのは、openshift-ingress
project
ですが、Secret
名を指定する IngressController
リソースは openshift-ingress-operator
に存在します。
oc edit ingresscontroller additional -n openshift-ingress-operator
spec.defaultCertificate.name
の部分に、先ほど作成した Secret
の名前を指定します。project
名は指定する field は無いので指定できません。Router Pod が作成される openshif-ingress
project
内の Secret
である必要があります。
apiVersion: operator.openshift.io/v1
kind: IngressController
metadata:
creationTimestamp: "2024-06-07T00:37:53Z"
finalizers:
- ingresscontroller.operator.openshift.io/finalizer-ingresscontroller
generation: 5
name: acme
namespace: openshift-ingress-operator
resourceVersion: "363927"
uid: 3851a50e-66b8-4218-9266-9811aae54055
spec:
clientTLS:
clientCA:
name: ""
clientCertificatePolicy: ""
defaultCertificate:
name: my-tls2 # ここを作成した Secret 名 (my-tls2)に
domain: acme.rosa-1w8dvp.18to.p1.openshiftapps.com
endpointPublishingStrategy:
loadBalancer:
dnsManagementPolicy: Manage
...
更新の確認
証明書が置き換えられているかどうかを確認します。
openshift-ingress
project
の Pod が証明書の更新のために再起動するはずなので、少し待ちます。
$ oc get pods -n openshift-ingress
NAME READY STATUS RESTARTS AGE
router-additional-6667c9f8d6-5gr5l 1/1 Running 1 104m
router-additional-6667c9f8d6-bmtrl 1/1 Terminating 1 104m
router-additional-6b6b6464d6-f4m9k 1/1 Running 0 15s
router-additional-6b6b6464d6-n8mqq 1/1 Running 0 13s
router-default-578b69d8f4-k99j7 1/1 Running 1 9h
router-default-578b69d8f4-tq9m8 1/1 Running 1 9h
$
再起動が完了したら、証明書の日付を確認してみます。
$ echo | openssl s_client -connect hello.$DOMAIN:443 2>&1 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | openssl x509 -text 2>/dev/null | grep -e "Public-Key" -e "Not"
Not Before: Jun 10 12:56:20 2024 GMT
Not After : Sep 8 12:56:19 2024 GMT
Public-Key: (2048 bit)
$
有効期限の日付は同じですが、時間が以前と変わっているので無事置き換わった事がわかります。