2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

OpenShiftの内部レジストリの署名検証機能を使ってOpenShiftクラスター間のイメージ署名&検証を実現する

Last updated at Posted at 2022-06-13

はじめに

今回取り上げるトピックは前後編で記事を分けています。この記事は後編になります。
前編:OpenShiftの内部レジストリのイメージ署名検証機能を使ってみる
後編:OpenShiftの内部レジストリの署名検証機能を使ってOpenShiftクラスター間のイメージ署名&検証を実現する(当記事)

前回の記事では、OpenShiftの公式ドキュメントに従い、Red Hatが提供しているイメージの署名検証を実現しました。
しかしこのユースケースはかなり限定的で、独自のレジストリでこの機能を実行するにはどうすればいいの?というところまで届くものではありません。

そこで今回はある程度実際のユースケースに近しいシナリオでの署名・検証を試してみます。

シナリオの概要

今回は以下のようなシナリオを実現してみます。
スクリーンショット 2022-06-12 18.06.01.png

 ユーザー1がイメージの署名を行い、OpenShift Container Platformクラスター(以下OCPクラスター)#1の内部レジストリにPushします。
そしてユーザー2がOCPクラスター#1のレジストリからイメージを取得し、イメージ署名検証を行った上でOCPクラスター#2にPodをデプロイします。

手順

レジストリの公開

まず前準備として、OCPクラスター#1の内部レジストリに対して、外部アクセスの許可設定を行います。
こちらの公開手順を参考に実施します。

まずはdefaultRoutetrueに設定します。

$ 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
image.png

その前に、まずはイメージ署名に使う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-originProjectの配下に、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-originProject内の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-originProjectにあるイメージのPull権限が欲しいので、image-origin上のdefaultサービスアカウントのトークンを取得します。

#1で実施.
$ 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を作成します。

#2で実施.
$ 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-destProjectにあるサービスアカウントに紐づけます。

#2で実施.
$ 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としました。
  • 前の手順で作成したtest-signing.pubの公開鍵の情報を記載します。
51-worker-rh-registry-trust.bu
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

ではデプロイしましょう。

#2で実施.
$ oc apply -f 51-worker-rh-registry-trust.yaml

しばらく待って、ノードにアクセスしてpolicy.jsonの中身と/etc/pki/test-signing.pubの有無を確認します。

#2で実施.
$ 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

デプロイしてみます。

#2で実施.
$ oc apply -f pod.yaml -n image-dest

Podがrunnnigになっていることを確認します。

#2で実施.
$ 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とダイジェストが違うことも確認できます。

#1で実施.
$ 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イメージをデプロイしてみます。

#2で実施.
$ 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でデプロイしてみます。

#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 Chainsigstoreを使ってビルド・デプロイプロセスで署名・検証を行う方式を採用する方が昨今の主流かと思います。

おわりに

OpenShiftも誕生からかなりの年月がたちました。
新機能が続々と具備されてきた一方で、過去に実装した機能が使えるもののベストプラクティスとは言えなくなってきた機能も確実にあります。
ドキュメントに書いてあるからこの手順でやる、といった考えでなく、実際に触ってみて自分のプロダクトに適しているかどうかを判断するプロセスを検討してみていただければと思います。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?