はじめに
今回取り上げるトピックは前後編で記事を分けています。この記事は後編になります。
前編:OpenShiftの内部レジストリのイメージ署名検証機能を使ってみる
後編:OpenShiftの内部レジストリの署名検証機能を使ってOpenShiftクラスター間のイメージ署名&検証を実現する(当記事)
前回の記事では、OpenShiftの公式ドキュメントに従い、Red Hatが提供しているイメージの署名検証を実現しました。
しかしこのユースケースはかなり限定的で、独自のレジストリでこの機能を実行するにはどうすればいいの?
というところまで届くものではありません。
そこで今回はある程度実際のユースケースに近しいシナリオでの署名・検証を試してみます。
シナリオの概要
ユーザー1がイメージの署名を行い、OpenShift Container Platformクラスター(以下OCPクラスター)#1の内部レジストリにPushします。
そしてユーザー2がOCPクラスター#1のレジストリからイメージを取得し、イメージ署名検証を行った上でOCPクラスター#2にPodをデプロイします。
手順
レジストリの公開
まず前準備として、OCPクラスター#1の内部レジストリに対して、外部アクセスの許可設定を行います。
こちらの公開手順を参考に実施します。
まずはdefaultRoute
をtrue
に設定します。
$ oc patch configs.imageregistry.operator.openshift.io/cluster --patch '{"spec":{"defaultRoute":true}}' --type=merge
次にデフォルトのレジストリールートを取得します。
$ HOST=$(oc get route default-route -n openshift-image-registry --template='{{ .spec.host }}')
Ingress Operator の証明書を取得します。
※ca-trustの場合
$ oc get secret -n openshift-ingress router-certs-default -o go-template='{{index .data "tls.crt"}}' | base64 -d | sudo tee /etc/pki/ca-trust/source/anchors/${HOST}.crt > /dev/null
$ sudo cat << EOT | sudo tee -a /etc/ca-certificates.conf
${HOST}.crt
EOT
※ca-ca-certificatesの場合
$ oc get secret -n openshift-ingress router-certs-default -o go-template='{{index .data "tls.crt"}}' | base64 -d | sudo tee /usr/share/ca-certificates/${HOST}.crt > /dev/null
クラスターのデフォルト証明書がルートを信頼するようにします。
※ca-trustの場合
$ sudo update-ca-trust enable
※ca-ca-certificatesの場合
$ sudo update-ca-certificates
dockerログインします。
$ docker login -u $(oc whoami) -p $(oc whoami -t) $HOST
Login Succeeded
これでローカル端末上からOCPクラスター#1の内部レジストリにアクセスできるようになりました。
イメージの署名と格納
OCPクラスター#1の内部レジストリに、署名したイメージをPushします。
イメージの署名&Pushについては、skopeoを使います。
イメージのPullやPushをより柔軟に行えるツールですが、その中にイメージへの署名機能も具備されているため、今回はその機能を使います。
skopeoを使ったイメージの署名と検証の概要は、以下のサイトが参考になります。
skopeo copy sign-by
コマンドを使うことで、イメージの署名、Push、署名情報の格納を一気に実行できます。
https://luis-javier-arizmendi-alonso.medium.com/container-image-signatures-in-openshift-4-a62b9e1c1b5a
その前に、まずはイメージ署名に使うGnuPGの鍵を作成します。
$ gpg --gen-key
...omit...
Real name: test-signing
Email address: test-signing@sample.com
You selected this USER-ID:
"test-signing <test-signing@sample.com>"
Change (N)ame, (E)mail, or (O)kay/(Q)uit? O
...omit...
pub rsa3072 2022-06-12 [SC] [expires: 2024-06-11]
3C250916719C8A0F7E895C0901674AB2C9AA6080
uid test-signing <test-signing@sample.com>
sub rsa3072 2022-06-12 [E] [expires: 2024-06-11]
次に作成した鍵の公開鍵を取得します。
gpg -ao test-signing.pub --export test-signing
公開鍵ができました。
$ cat test-signing.pub
-----BEGIN PGP PUBLIC KEY BLOCK-----
...omit...
-----END PGP PUBLIC KEY BLOCK-----
次にPushするイメージを作成します。
中身は重要ではないので、以下のコマンドでローカルにnginxのイメージを作成します。
$ mkdir src
$ cat << 'EOF' > Dockerfile
FROM nginx:alpine
COPY src/index.html /usr/share/nginx/html
EOF
$ cat << 'EOF' > src/index.html
<h1>Hello World!</h1>
EOF
$ docker build -t nginx:test-signing ./
次に、skopeoを使って署名とPushを行います。
docker-daemon:<イメージ名>:<タグ名>
がコピー元で、dokcer://<イメージのパス>
が署名したイメージのPush先になります。
今回は、先ほど作成したローカルのnginx:test-signing
イメージを、OCPクラスター上のimage-origin
Projectの配下に、nginx:test-signing
というイメージ名でPushします。
途中にキーのパスワードが聞かれるので、入力してOKします。
$ oc new-project image-origin
$ skopeo copy --sign-by test-signing docker-daemon:nginx:test-signing docker://${HOST}/image-origin/nginx:test-signing
Getting image source signatures
Copying blob 4721bfafc708 done
Copying blob d6dd885da0bb done
Copying blob 4fc242d58285 done
Copying blob c0e7c94aefd8 done
Copying blob 45b275e8a06d done
Copying blob a43749efe4ec done
Copying blob 33034fd4c9f1 done
Copying config 01fcd344e7 done
Writing manifest to image destination
Signing manifest
Storing signatures
実際にイメージをPushできているか確認してみましょう。
$ oc get istag
NAME IMAGE REFERENCE UPDATED
nginx:test-signing image-registry.openshift-image-registry.svc:5000/image-origin/nginx@sha256:038e3702cc578d705fa0c7dd3308eeb29b96822562f509a296bac601589f9e11 11 minutes ago
問題なくPushできているみたいです。
署名情報はどこにある?
さて、今回はOpenShift上のレジストリに署名されたイメージを格納しましたが、署名情報はどこにあるのでしょうか?
実は署名情報はイメージと共にOCPクラスター#1のレジストリに格納されています。
これはOpenshiftのイメージレジストリがDocker Registry HTTP API V2に対応しているため、skopeoがそれを判断してイメージとともに署名情報もレジストリにPushしています。
この機能によってdocker/distribution API endpointのみを使って簡単に署名情報にアクセスすることができます。
そのため、APIを経由してレジストリ内の署名情報にアクセス可能です。
こちらにAPIの情報が記載されています。だいぶ古いドキュメントですが、v4.10でも有効です。
実際にアクセスしてみましょう。
まず内部レジストリへのトークンを取得します。トークンはOCPクラスター内のサービスアカウントのトークンを使うため、以下のコマンドでトークン情報を変数に格納します。
$ TOKEN=$(oc serviceaccounts get-token default -n image-origin)
これでimage-origin
Project内のdefault
サービスアカウントのトークンが取得できました。
このトークンを使って署名情報を取得してみます。
curl "https://<user名>:${TOKEN}@${HOST}/extensions/v2/image-origin/nginx/signatures/sha256:038e3702cc578d705fa0c7dd3308eeb29b96822562f509a296bac601589f9e11" > nginx.sig
取得した署名情報を見てみましょう。
cat nginx.sig | jq .
{
"signatures": [
{
"schemaVersion": 2,
"name": "sha256:038e3702cc578d705fa0c7dd3308eeb29b96822562f509a296bac601589f9e11@c8806c00daee87606f744dde7334c7cf",
"type": "atomic",
"content": "owGbwM<...omit...>UtBwA="
}
]
}
無事に署名情報を取得できました。このように、OpenShiftの内部レジストリは署名情報をイメージと一緒に格納することができます。そのため、前回の記事で設定した公開鍵の取得先の設定(/etc/containers/registries.d/xx.yaml
の作成)が不要になります。
ちなみに上記のcontent
の部分が実際の署名情報になります。これをbase64でデコードしてみると、きちんと暗号化されていることがわかります。
$ cat nginx.sig | jq -r '.signatures[0].content' | base64 -d
�������Ș��䪄���%1$-��Z)�(�$391GɪZ)3%5�$���N�O�N-�-JMK-J�KNU�RJIMK,�)�-�/-I��/H�+��L+���MLO�L�,.)��K,((�+�/Nԭ(1KN�+����+0ԃk+H��Շh�/�L����V%��%�ř�y�y�J�:J`%H��M��L�I�LR@Wg$��Y[��%'��[���%$�����&Y&Y�YU��X&Y�%%&��ZX�Y��,+�,�3�$?73Y!9?�$13/�H�ĒҢT�����<H�%�!���+���:/1�@�����������N��,
�\
�b�,6��b�s���:c8a1���.N��fs��q�g�
4����2$c�K��L�����LM�o�
���if_#��8���Y�k�G�V-4��x��k�1���~���B�sa���'��'��{O�~����
[�I��w��G�]��7�Rj�a�JLJbW�)�ּ
�S ����h^# h��X<笐d�l����g�o� _)�~n�����T�%j��S.μ<o��E?��?+���
�
��g���+����@�\���qkY
���g���z���ק��d��\���tz7��z��0ۘ3s��Գ]��e}+�^_2�O�z�^H�O��W�+^�g~����T���.g�;�u?|ϛ~����.�I�Q��"�b��^�����,�W�Y�Lk�/Y����kV�&��u�-
これでOCPクラスター#1側の準備は完了です。次にOCPクラスター#2側の設定を行います。
OCPクラスター#1上の内部レジストリへのアクセス設定
まず、OCPクラスター#2から#1の内部レジストリへアクセスできるよう設定します。
方法としては、#1側の(内部レジストリからのPull権限を持つ)サービスアカウントのトークン情報を取得し、それを#2側のサービスアカウントに付与してあげます。
参考:https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.10/html/images/images-allow-pods-to-reference-images-from-secure-registries_using-image-pull-secrets
そのため、以下のコマンドで#1のサービスアカウントのトークンを取得します。
今回はimage-origin
ProjectにあるイメージのPull権限が欲しいので、image-origin
上のdefault
サービスアカウントのトークンを取得します。
$ SECRET=$(oc -n image-origin get secret | grep default-docker | awk {'print $1'})
$ SECRET_VALUE=$(oc -n image-origin get secret $SECRET -o jsonpath="{.data['\.dockercfg']}" | base64 --decode | jq -r '.["image-registry.openshift-image-registry.svc:5000"].password')
次に取得したトークンを使って、#2側にSecretを作成します。
$ oc new-project image-dest
$ oc create secret docker-registry pull-secret \
--docker-server=${HOST} \
--docker-username=serviceaccount \
--docker-password=${SECRET_VALUE} \
--docker-email=unused
Secretを#2のimage-dest
Projectにあるサービスアカウントに紐づけます。
$ oc secret link default pull-secret -n image-dest --for=pull
これでOCPクラスター#2から#1のimage-origin
のレジストリにアクセスできるようになりました。
署名検証の設定
では今回の主要ポイントのひとつである、イメージ署名の検証設定を行います。
基本的には前回の記事で説明したように、MachineConfig
に設定を入れて対応する形になります。
まずbutane
の設定ファイルを作成します。
-
policy.json
にOCPクラスター#1のレジストリの宛先default-route-openshift-image-registry.apps.XXX.com
を記載します。- 復号に使う公開鍵のPathを記載します。今回は
/etc/pki/test-signing.pub
としました。
- 復号に使う公開鍵のPathを記載します。今回は
- 前の手順で作成した
test-signing.pub
の公開鍵の情報を記載します。
variant: openshift
version: 4.10.0
metadata:
name: 51-worker-rh-registry-trust
labels:
machineconfiguration.openshift.io/role: worker
storage:
files:
- path: /etc/containers/policy.json
mode: 0644
overwrite: true
contents:
inline: |
{
"default": [
{
"type": "insecureAcceptAnything"
}
],
"transports": {
"docker": {
"registry.access.redhat.com": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release"
}
],
"registry.redhat.io": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release"
}
],
"default-route-openshift-image-registry.apps.XXX.com": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "etc/pki/test-signing.pub"
}
]
},
"docker-daemon": {
"": [
{
"type": "insecureAcceptAnything"
}
]
}
}
}
- path: /etc/pki/test-signing.pub
mode: 0644
overwrite: true
contents:
inline: |
-----BEGIN PGP PUBLIC KEY BLOCK-----
...omit...
-----END PGP PUBLIC KEY BLOCK-----
machineConfig
を作成します。
$ butane 51-worker-rh-registry-trust.bu -o 51-worker-rh-registry-trust.yaml
ではデプロイしましょう。
$ oc apply -f 51-worker-rh-registry-trust.yaml
しばらく待って、ノードにアクセスしてpolicy.json
の中身と/etc/pki/test-signing.pub
の有無を確認します。
$ oc debug node/<ノード名> -- chroot /host cat /etc/containers/policy.json
$ oc debug node/<ノード名> -- chroot /host cat /etc/pki/test-signing.pub
なお、先述した通りOpenShiftの内部レジストリ上に署名情報があるため、今回は/etc/containers/resistries.d/<registry名>.yaml
の作成は不要です。
他のレジストリを使い、署名ストアが別の場所にある場合は作成が必要です。
イメージの検証
これで全ての設定が完了したので、実際にPodをデプロイしてみましょう。
以下のコマンドでyamlファイルを作成します。
$ cat << EOT > pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx-hello-world
spec:
containers:
- name: nginx-hello-world
image: ${HOST}/image-origin/nginx:test-signing
EOT
デプロイしてみます。
$ oc apply -f pod.yaml -n image-dest
Podがrunnnigになっていることを確認します。
$ oc get pods
NAME READY STATUS RESTARTS AGE
nginx-hello-world 1/1 Running 0 118s
これで署名されたイメージを別のクラスターで検証してデプロイすることができました。
では次に署名されていないイメージをデプロイしようとするとどうなるのかをみてみます。
署名されていないイメージを作成する
先ほど作成したDockerfileを流用して、今度は署名されていないイメージを作成します。
src/index.html
だけ書き換えます。
$ ls
Dockerfile src
$ cat << 'EOF' > src/index.html
<h1>Hello World!2</h1>
EOF
$ docker build -t nginx:test-nonsigning ./
skopeo
でイメージをコピーします。今回は署名をしないので、--sign-by
オプションは不要です。
$ sudo docker login -u $(oc whoami) -p $(oc whoami -t) $HOST
$ skopeo copy docker-daemon:nginx:test-nonsigning docker://${HOST}/image-origin/nginx:test-nonsigning
無事にイメージがPushできました。nginx:test-signing
とダイジェストが違うことも確認できます。
$ oc get istag
NAME IMAGE REFERENCE UPDATED
nginx:test-nonsigning image-registry.openshift-image-registry.svc:5000/image-origin/nginx@sha256:c53f557f7243567c0e411401fa1ad92d8ba2277d17a91906e929e91696b067ff 37 seconds ago
nginx:test-signing image-registry.openshift-image-registry.svc:5000/image-origin/nginx@sha256:038e3702cc578d705fa0c7dd3308eeb29b96822562f509a296bac601589f9e11 18 hours ago
ではOCPクラスター#2側でnginx:test-nonsigning
イメージをデプロイしてみます。
$ cat << EOT > pod-nonsigning.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx-hello-world-nonsigning
spec:
containers:
- name: nginx-hello-world-nonsigning
image: ${HOST}/image-origin/nginx:test-nonsigning
EOT
$ oc apply -f pod-nonsigning.yaml
Podの状態をみると、ErrImagePull
になっています。
$ oc get pods
NAME READY STATUS RESTARTS AGE
nginx-hello-world 1/1 Running 0 44m
nginx-hello-world-nonsigning 0/1 ErrImagePull 0 8s
describe
でイベントをみてみましょう。
$ oc describe pods nginx-hello-world-nonsigning
...omit...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 82s default-scheduler Successfully assigned image-dest/nginx-hello-world-nonsigning to ip-10-0-154-156.ap-northeast-1.compute.internal
Normal AddedInterface 81s multus Add eth0 [10.131.2.32/23] from openshift-sdn
Normal Pulling 31s (x3 over 81s) kubelet Pulling image "default-route-openshift-image-registry.apps.XXX.com/image-origin/nginx:test-nonsigning"
Warning Failed 28s (x3 over 77s) kubelet Failed to pull image "default-route-openshift-image-registry.apps.XXX.com/image-origin/nginx:test-nonsigning": rpc error: code = Unknown desc = Source image rejected: A signature was required, but no signature exists
Warning Failed 28s (x3 over 77s) kubelet Error: ErrImagePull
Normal BackOff 4s (x4 over 77s) kubelet Back-off pulling image "default-route-openshift-image-registry.apps.XXX.com/image-origin/nginx:test-nonsigning"
Warning Failed 4s (x4 over 77s) kubelet Error: ImagePullBackOff
Source image rejected: A signature was required, but no signature exists
とあるので、イメージ署名が必要なのに、このイメージには署名が存在しないことがわかります。
別の鍵で署名されたイメージを作成する
念のため別の鍵を使って署名したものをデプロイしてみましょう。
鍵を作成します。
$ gpg --gen-key
...omit...
Real name: another-signing
Email address: another-signing@sample.com
You selected this USER-ID:
"another-signing <another-signing@sample.com>"
...omit...
skopeo
で署名してレジストリにPushします。イメージは先ほどビルドしたnginx:test-nonsigning
を使います。
$ docker tag nginx:test-nonsigning nginx:another-signing
$ skopeo copy --sign-by another-signing docker-daemon:nginx:another-signing docker://${HOST}/image-origin/nginx:another-signing
OCPクラスター#2でデプロイしてみます。
$ cat << EOT > pod-another-signing.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx-hello-world-another-signing
spec:
containers:
- name: nginx-hello-world-another-signing
image: ${HOST}/image-origin/nginx:another-signing
EOT
$ oc apply -f pod-another-signing.yaml
状況をみるとErrImagePull
になっています。
$ oc get pods
NAME READY STATUS RESTARTS AGE
nginx-hello-world 1/1 Running 0 99m
nginx-hello-world-another-signing 0/1 ErrImagePull 0 13s
nginx-hello-world-nonsigning 0/1 ImagePullBackOff 0 54m
詳細を見てみます。
$ oc describe pods nginx-hello-world-another-signing
...omit...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 12s default-scheduler Successfully assigned image-dest/nginx-hello-world-another-signing to ip-10-0-154-156.ap-northeast-1.compute.internal
Normal AddedInterface 10s multus Add eth0 [10.131.2.39/23] from openshift-sdn
Normal Pulling 10s kubelet Pulling image "default-route-openshift-image-registry.apps.XXX.com/image-origin/nginx:another-signing"
Warning Failed 7s kubelet Failed to pull image "default-route-openshift-image-registry.apps.XXX.com/image-origin/nginx:another-signing": rpc error: code = Unknown desc = Source image rejected: Invalid GPG signature: gpgme.Signature{Summary:128, Fingerprint:"C702183D16A8F8473359D8131E96E63F3598E788", Status:gpgme.Error{err:0x9}, Timestamp:time.Date(2022, time.June, 13, 5, 41, 23, 0, time.Local), ExpTimestamp:time.Date(1970, time.January, 1, 0, 0, 0, 0, time.Local), WrongKeyUsage:false, PKATrust:0x0, ChainModel:false, Validity:0, ValidityReason:error(nil), PubkeyAlgo:1, HashAlgo:10}
Warning Failed 7s kubelet Error: ErrImagePull
Normal BackOff 7s kubelet Back-off pulling image "default-route-openshift-image-registry.apps.XXX.com/image-origin/nginx:another-signing"
Warning Failed 7s kubelet Error: ImagePullBackOff
Source image rejected: Invalid GPG signature
とあり、異なった鍵で署名されているためイメージが取得できていないことがわかります。
この機能、使えそう?
今回の記事と前回の記事で、おおむねOpenShiftの内部レジストリのイメージ署名・検証機能の正体が掴めたかと思います。
これを踏まえ、 この機能を使っていくべきか? について、個人的な見解をまとめます。
個人的には、この機能を使って署名・検証機能を実現するのはおすすめしません。
理由としては以下の2点です。
マネージドなOpenShiftではこの機能が利用できない
ROSA(Red Hat OpenShift Service on AWS)やARO(Azure Red Hat OpenShift)では、ノードの管理をRed HatのSREが行う関係上、ユーザー側でMachineConfig
を変更することができません。
そのためマルチクラスター/マルチクラウドを見据えるにあたって、この署名検証機能を使うことが将来の足枷になる可能性があります。
公開鍵の管理やレジストリ設定が煩雑になる
手順を見るとわかる通り、イメージのPull先が増えたり、利用する公開鍵が増える度にMachineConfig
の適用が必要になり、結果としてWorkerノードの一時的な通信断が発生します。
長期的な運用を考えると、この機能を使って署名検証を続けていくのは厳しくなるかと思います。
やはりノードに配置する情報や設定によってK8s上のコンテナのサービス・機能を実現するというのは、コンテナの思想としては微妙かなと思います。
イメージ署名および検証を実施するのであれば、別のレジストリの機能を使ったり、OpenShift PipelineのTekton Chainとsigstoreを使ってビルド・デプロイプロセスで署名・検証を行う方式を採用する方が昨今の主流かと思います。
おわりに
OpenShiftも誕生からかなりの年月がたちました。
新機能が続々と具備されてきた一方で、過去に実装した機能が使えるもののベストプラクティスとは言えなくなってきた機能も確実にあります。
ドキュメントに書いてあるからこの手順でやる、といった考えでなく、実際に触ってみて自分のプロダクトに適しているかどうかを判断するプロセスを検討してみていただければと思います。