OpenShiftでTech PreviewになったTekton Chainsを試してみよう!

こんにちはRed Hatでソリューションアーキテクトをしている石川です。 過去にもOpenShiftでCI/CDの機能を提供するOpenShift Pipelinesについて何度かご紹介してきましたが、今回はOpenShift Pipelinesのv1.7からTech Previewとして利用可能となったTekton Chainsというコンポーネントについて試してみたいと思います。

ATTENTION! Tech Preview機能の位置付けについてはこちらのページを参照して下さい。

そもそもOpenShift Pipelinesとは何か?について知りたい方は過去記事をご参照下さい。

rheb.hatenablog.com rheb.hatenablog.com

Tekton Chainsの概要

まずTekton Chainsが何者なのかを把握するために、GithubのTekton Chainsページを見てみましょう。 github.com

Tekton Chains is a Kubernetes Custom Resource Definition (CRD) controller 
that allows you to manage your supply chain security in Tekton.

In its default mode of operation, Chains works by observing all TaskRuns
executions in your cluster. When TaskRuns complete, Chains takes a snapshot of
them. Chains then converts this snapshot to one or more standard payload
formats, signs them and stores them somewhere.

最初の文を読むと、Tekton ChainsはKubernetesのCRDコントローラーであり、Tektonにおけるサプライチェーンセキュリティを管理する、とあります。 また以降の文では、TektonにおけるTaskRunの実行結果のスナップショットを標準的なペイロードフォーマットに変換し、署名をした上で保管する、とあります。

上記だけでTekton Chainsの全容を掴むことは中々難しいですが、要約すると、Tektonで実行したパイプラインの成果物に署名を行って、適切に管理するためのツールであると言えるでしょう。

ここで書かれているパイプラインの成果物は、大きく二つに分類することができます。 一つ目はTaskRunの実行結果、二つ目はパイプラインの中で作成したコンテナイメージです。

一つ目はともかく、二つ目のコンテナイメージについては、これまでもイメージの発行元を確認するために、署名や検証を実施するということはあったため、比較的理解し易いのではないでしょうか。

本記事ではまずTekton Chainsによるコンテナイメージの署名について考えてみたいと思います。

コンテナイメージに署名するメリット

CIパイプラインの中でビルドしたコンテナイメージに対し署名を行うと、どんなメリットが得られるでしょうか?

分かりやすいメリットとしてはセキュリティの向上です。 例として、コンテナレジストリに不正なアクセスを受け、悪意のあるイメージを外部からプッシュされるパターンを考えてみましょう。 コンテナイメージに対する署名や、その検証というプロセスが無い場合、外部からプッシュされたイメージをクラスタ上で稼働させるリスクがあります。

一方でCIパイプラインの中で署名し、その検証を行っていると、外部から勝手にプッシュされたイメージには署名が存在しないため、実際にクラスタ上でコンテナが稼働することを防ぐことが可能となります。

そもそもコンテナレジストリに簡単にアクセスさせない事は非常に重要ですが、上記のようなプロセスを確立しておくことで、万が一の場合もリスクを低減させることができます。

セキュリティ以外にも、例えばISVベンダーが自社のアプリケーションをコンテナとしてユーザーに提供する際にコンテナイメージに署名を行うことで、ユーザー側でイメージの提供元が正しいことを確認することができます。

Tekton Chainsの設定方法

ここからはOpenShift上でTekton Chainsを使用する方法について見ていきたいと思います。 基本的に以下のドキュメントに沿って設定を行っていくためこちらもご参照下さい。 docs.openshift.com

今回使用する環境はOpenShift 4.10で、OpenShift Pipelinesのv1.7をあらかじめインストールしています。 自身でも試してみたいという場合は、OpenShiftクラスタのバージョンとOpenShift Pipelinesのバージョンに注意しましょう。

まず以下のCustom Resourceオブジェクトを作成し、openshift-pipelinesProjectでTekton Chainsを有効化します。

apiVersion: operator.tekton.dev/v1alpha1
kind: TektonChain
metadata:
  name: chain
spec:
  targetNamespace: openshift-pipelines

その上で設定ファイルに必要な値を入れます。

oc patch configmap chains-config -n openshift-pipelines -p='{"data":{"artifacts.taskrun.format": "in-toto", "artifacts.taskrun.storage": "tekton",  "artifacts.taskrun.signer": "x509", "artifacts.oci.format": "simplesigning", "artifacts.oci.storage": "oci", "artifacts.oci.signer": "x509"}}'

設定値を見ると、artifacts.xxxxxx部分でTaskRunを対象とする設定か、OCI(コンテナイメージ)に対する設定かを表しており、それ以下の箇所でフォーマットや、保管場所、署名時のキーとして何を使用するかを決めています。

コミュニティ版のTekton Chainsでは、署名時の鍵として各種クラウドサービスが提供するKMSを使用したり、実験的な機能として、自分自身で鍵管理を不要とするKeyless Signingを設定することも可能です。 設定可能な値についてより詳しく知りたい方は以下を参照してみて下さい。

docs.openshift.com

tekton.dev

署名用の鍵作成

ここからは署名に必要な鍵を作成していきます。 鍵の作成にはcosignというツールを使用します。 cosignはsigstoreという、ソフトウェア署名の利用の簡素化を目指すプロジェクトのコンポーネントの一つとなっています。 Tekton Chainsはsigstoreのその他のコンポーネントとも深く関連しているため興味がある方は是非調べてみて下さい。

まずはcosignを作業環境にインストールします。 Githubのリリースページから最新版を取得します。

curl -L https://github.com/sigstore/cosign/releases/download/v1.9.0/cosign-linux-amd64 > cosign-linux-amd64
chmod 755 cosign-linux-amd64
mv cosign-linux-amd64 /usr/local/bin/cosign
# バージョン確認
cosign version
---
  ______   ______        _______. __    _______ .__   __.
 /      | /  __  \      /       ||  |  /  _____||  \ |  |
|  ,----'|  |  |  |    |   (----`|  | |  |  __  |   \|  |
|  |     |  |  |  |     \   \    |  | |  | |_ | |  . `  |
|  `----.|  `--'  | .----)   |   |  | |  |__| | |  |\   |
 \______| \______/  |_______/    |__|  \______| |__| \__|
cosign: A tool for Container Signing, Verification and Storage in an OCI registry.

GitVersion:    v1.9.0
GitCommit:     a4cb262dc3d45a283a6a7513bb767a38a2d3f448
GitTreeState:  clean
BuildDate:     2022-06-03T13:47:07Z
GoVersion:     go1.17.11
Compiler:      gc
Platform:      linux/amd64

cosignを使って鍵を作成します。作成時に暗号鍵に対するパスワードを求められるので適宜設定します。

cosign generate-key-pair k8s://openshift-pipelines/signing-secrets
---
Enter password for private key: 
Enter password for private key again: 
Successfully created secret signing-secrets in namespace openshift-pipelines
Public key written to cosign.pub

上記のコマンド実行結果により、ローカルにはcosign.pub、クラスタ上にはsigning-secretsというSecretが作成されました。 cosign.pubには公開鍵の情報、signing-secretsには公開鍵 + 暗号鍵 + パスワードの情報が含まれています。 間違えてパブリックGitレポジトリ等にこれらの情報をアップロードしたりしないよう注意しましょう。

コンテナビルド用のTaskの確認

CIパイプラインの中でコンテナビルドを行うためには、当然ビルドのためのTaskが必要となるのですが、Tekton Chainsによる署名を行う場合、ビルド用のTaskの実行結果としてIMAGE_DIGESTIMAGE_URLという二つのパラメーターを返却する必要があります。

github.com

そのため、これらのパラメーターが返却されるよう使用するTaskに適宜変更を加えます。 以下はs2iのTaskにパラメーター設定を行った例です。

chains-s2i-nodejs.yaml (クリックで展開)

apiVersion: tekton.dev/v1beta1
kind: Task
metadata:    
  name: s2i-nodejs
  labels:
    app.kubernetes.io/version: '0.1'
    operator.tekton.dev/provider-type: redhat
spec:
  description: >-
    s2i-nodejs task clones a Git repository and builds and pushes a container
    image using S2I and a nodejs builder image.
  params:
    - default: 14-ubi8
      description: The tag of nodejs imagestream for nodejs version
      name: VERSION
      type: string
    - default: .
      description: The location of the path to run s2i from.
      name: PATH_CONTEXT
      type: string
    - default: 'true'
      description: >-
        Verify the TLS on the registry endpoint (for push/pull to a non-TLS
        registry)
      name: TLSVERIFY
      type: string
    - description: Location of the repo where image has to be pushed
      name: IMAGE
      type: string
    - default: >-
        registry.redhat.io/rhel8/buildah@sha256:e19cf23d5f1e0608f5a897f0a50448beb9f8387031cca49c7487ec71bd91c4d3
      description: The location of the buildah builder image.
      name: BUILDER_IMAGE
      type: string
  results:
    - description: Digest of the image just built.
      name: IMAGE_DIGEST
    - description: ''
      name: IMAGE_URL
  steps:
    - command:
        - s2i
        - build
        - $(params.PATH_CONTEXT)
        - >-
          image-registry.openshift-image-registry.svc:5000/openshift/nodejs:$(params.VERSION)
        - '--as-dockerfile'
        - /gen-source/Dockerfile.gen
      env:
        - name: HOME
          value: /tekton/home
      image: >-
        registry.redhat.io/ocp-tools-4-tech-preview/source-to-image-rhel8@sha256:e518e05a730ae066e371a4bd36a5af9cedc8686fd04bd59648d20ea0a486d7e5
      name: generate
      resources: {}
      volumeMounts:
        - mountPath: /gen-source
          name: gen-source
      workingDir: $(workspaces.source.path)
    - command:
        - buildah
        - bud
        - '--storage-driver=vfs'
        - '--tls-verify=$(params.TLSVERIFY)'
        - '--layers'
        - '-f'
        - /gen-source/Dockerfile.gen
        - '-t'
        - $(params.IMAGE)
        - .
      image: $(params.BUILDER_IMAGE)
      name: build
      resources: {}
      volumeMounts:
        - mountPath: /var/lib/containers
          name: varlibcontainers
        - mountPath: /gen-source
          name: gen-source
      workingDir: /gen-source
    - command:
        - buildah
        - push
        - '--storage-driver=vfs'
        - '--tls-verify=$(params.TLSVERIFY)'
        - '--digestfile=$(workspaces.source.path)/image-digest'
        - $(params.IMAGE)
        - 'docker://$(params.IMAGE)'
      image: $(params.BUILDER_IMAGE)
      name: push
      resources: {}
      volumeMounts:
        - mountPath: /var/lib/containers
          name: varlibcontainers
      workingDir: $(workspaces.source.path)
    - image: $(params.BUILDER_IMAGE)
      name: digest-to-results
      resources: {}
      script: >-
        cat $(workspaces.source.path)/image-digest | tee
        /tekton/results/IMAGE_DIGEST 
    - image: $(params.BUILDER_IMAGE)
      name: url-to-results
      resources: {}
      script: 'echo $(params.IMAGE) | tee /tekton/results/IMAGE_URL '
  volumes:
    - emptyDir: {}
      name: varlibcontainers
    - emptyDir: {}
      name: gen-source
  workspaces:
    - mountPath: /workspace/source
      name: source

Tekton HubにあるKanikoのTaskでは必要なパラメーターが設定されているのでそちらを使用してもよいでしょう。

hub.tekton.dev

パイプラインの実行と署名

それではいよいよパイプラインを実行してみたいと思います。 今回はソースコードをGitからクローンし、その後s2iによるコンテナビルドを行うシンプルなパイプラインを構成します。

chains-pipeline.yaml (クリックで展開)

apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
  name: chains-pipeline
spec:
  workspaces:
  - name: shared-workspace
    description: Git Repo for Patient Health Records Apps
  params:
  - name: target-path
    type: string 
  - name: git-url
    type: string
  - name: git-revision  
    type: string
  - name: image-name
    type: string
  tasks:
  - name: git-clone-health
    taskRef:
      name: git-clone
    params:
    - name: url
      value: $(params.git-url)
    - name: revision
      value: $(params.git-revision)
    workspaces:
    - name: output
      workspace: shared-workspace
      
  - name: build-container
    taskRef:
      name: s2i-nodejs
      kind: Task
    runAfter:
    - git-clone-health
    workspaces:
    - name: source
      workspace: shared-workspace
    params:
    - name: PATH_CONTEXT
      value: $(workspaces.source.path)/$(params.target-path)
    - name: IMAGE
      value: ${IMAGE_REGISTRY}/$(params.image-name):$(params.git-revision)

クラスタにPipelineをデプロイします。

export IMAGE_REGISTRY=$(oc get route -n openshift-image-registry default-route -o "jsonpath={.spec.host}")
cat rhf-pipeline.yaml | envsubst |oc apply -f -

対応するPipelineRunを作成し、定義したPipelineを実行しましょう。

chains-pipelinerun.yaml (クリックで展開)

apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
  generateName: chains-pipeline-run-
spec:
  pipelineRef:
    name: chains-pipeline
  params:
    - name: target-path
      value: 'site'
    - name: git-url
      value: 'https://gitlab.com/jpishikawa/rhf2021-cicd-app'
    - name: git-revision
      value: 'master'
    - name: image-name
      value: 'tekton-chains/sample-image'
  workspaces:
    - name: shared-workspace
      volumeClaimTemplate:
        spec:
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 1Gi

oc create -f chains-pipelinerun.yaml

実行してしばらく待つとPipelineRunが成功します。

さて、無事パイプラインが完了しましたが、今回の目的であるコンテナイメージへの署名はどこから確認できるでしょうか。
実は署名は、ビルドしたコンテナイメージと同じくレジストリに格納されています。(今回の場合OpenShiftの内部レジストリ)

上図の、sample-image:masterがコンテナイメージで、sample-image:sha256-xxx.sigが署名となっています。

一般的なOCIレジストリでは、コンテナのイメージレイヤー以外にもMedia typeを指定することでBlobを保存することが可能になっています。 コンテナイメージの場合、各レイヤーのMedia typeにapplication/vnd.oci.image.layer.v1.tar+gzipが設定されていますが、 署名ではapplication/vnd.dev.cosign.simplesigning.v1+jsonが設定されています。 (これらはcraneなどのツールを使うことで確認が可能です。)

格納された署名を検証してみましょう。 鍵を生成する際に使ったcosignで署名の検証についても実施することができます。

# 認証
cosign login -u cluster-admin -p $(oc whoami -t) $IMAGE_REGISTRY

# ローカルに保存したcosign.pubにより検証を実施
cosign verify --key cosign.pub $IMAGE_REGISTRY/tekton-chains/sample-image:master
---
Verification for default-route-openshift-image-registry.apps.mycluster2.t7cy.p1.openshiftapps.com/tekton-chains/sample-image:master --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key

[{"critical":{"identity":{"docker-reference":"default-route-openshift-image-registry.apps.mycluster2.t7cy.p1.openshiftapps.com/tekton-chains/sample-image"},"image":{"docker-manifest-digest":"sha256:bc3daf4033d41be7434a72615ddbf3b68f54f65e394071a81b9c81bcbdca90c2"},"type":"cosign container image signature"},"optional":null}]

検証の結果から、署名に用いられた鍵が検証時の公開鍵と対になっていることが確認できました。

上記のようにcosignを使用し手動で検証を行う以外に、OPA GatekeeperやKyvernoといったAdmission Controllerを使うことで、コンテナをデプロイする際に署名の検証を自動化する方法が知られています。デプロイ時に署名検証を厳格化するにはこうしたツールを導入することが望ましいと言えるでしょう。

まとめ

今回はTekton Chainsについてご紹介しました。現在はTech Previewであるため、機能としての成熟はこれからというところですが、sigstoreなどの関連プロジェクトの進化も相まって、今後更なる発展が見込めるのではないでしょうか。 昨今ホットなテーマであるSupply Chain Security対策の一つとしてこれからも注目していきたいと思います。

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。