Cosign и Jenkins

Cosign и Jenkins

Понимаю, что это мало кому надо (или наоборот кому-то надо, но найти никак).

Я тут поднимал свой Registry для отгрузки docker images. Ничего суперсекретного, но есть просто некоторые вещи, которые мне давно было стыдно лить в паблик.

Я установил Harbor, настроил там зеркало кое-куда, собрал свой ansible-runner для быстрых заливок ключей на VPS. Заглянул в Harbor и увидел красный кружок рядом с образом и внутри красный крестик.

Наверно кому-то будет смешно, но я не знал, что images можно и иногда нужно подписывать. Я себя осознал как перфекциониста и потратил полдня на то, чтобы красные кружок с крестиком превратились в зеленый кружок с галочкой.

Идеи нет, только путь. К тому же собираю я все это дело у себя в K8s кластере.

Получился вот такой Pipeline для сборки образа внутри кубера, подписывания его с помощью cosign и отправки в свой registry:


#!/usr/bin/env groovy

def label = "k8s-${UUID.randomUUID().toString()}"
def home = "/home/jenkins"
def workspace = "${home}/workspace/ansible_image_build"
def workdir = "${workspace}/src/ansible_image_build"
def dockerImage = "ansible-runner"
def registryDomain = "registry.example.com"
def dockerRegistry = "registry.example.com/runners"
def version = "0.0.${env.BUILD_NUMBER}"
def imageDigest = ""
podTemplate(
        label: label,
        volumes: [
          emptyDirVolume(mountPath: "/var/lib/containers/storage", memory: false),
          emptyDirVolume(mountPath: "/.docker", memory: false)
        ],
        containers: [
        containerTemplate(
            name: 'podman',
            image: 'quay.io/containers/podman:v4.8.1',
            ttyEnabled: true,
            command: 'cat',
            privileged: true,
            envVars: [
              secretEnvVar(key: 'DOCKERHUB_USER', secretName: 'registry-local', secretKey: 'access_username'),
              secretEnvVar(key: 'DOCKERHUB_PASS', secretName: 'registry-local', secretKey: 'access_token')
            ],
        ),
        containerTemplate(
            name: 'cosign',
            image: 'ghcr.io/sigstore/cosign/cosign:v2.1.1',
            ttyEnabled: true,
            privileged: true,
            runAsUser: "1000",
            runAsGroup: "1000",
            command: 'cat',
            envVars: [
              secretEnvVar(key: 'COSIGN_KEY', secretName: 'cosign', secretKey: 'cosign.key'),
              secretEnvVar(key: 'COSIGN_PASSWORD', secretName: 'cosign', secretKey: 'cosign.password'),
              secretEnvVar(key: 'DOCKERHUB_USER', secretName: 'registry-local', secretKey: 'access_username'),
              secretEnvVar(key: 'DOCKERHUB_PASS', secretName: 'registry-local', secretKey: 'access_token')
            ]
        )
]) {
    node(label) {
        stage('Get Dockerfile') {
            checkout scm: [$class: 'GitSCM',
                           branches: [[name: '*/main']],
                           userRemoteConfigs: [[url: 'http://gitea-http.gitea:3000/aladex/ansible-docker.git']]]
        }

        stage('Build and Push Image') {
            container('podman') {
                script {
                    sh "podman login -u \${DOCKERHUB_USER} -p \${DOCKERHUB_PASS} ${dockerRegistry}"
                    sh "podman build -t ${dockerImage} -f Dockerfile ."
                    sh "podman tag ${dockerImage} ${dockerRegistry}/${dockerImage}:latest"
                    sh "podman tag ${dockerImage} ${dockerRegistry}/${dockerImage}:${version}"
                }
            }
        }
        stage('Push Image') {
            container('podman') {
                script {
                    sh "podman push ${dockerRegistry}/${dockerImage}:latest"
                    sh "podman push ${dockerRegistry}/${dockerImage}:${version}"
                    imageDigest = sh(script: "podman inspect --format='{{.Digest}}' ${dockerRegistry}/${dockerImage}:${version}", returnStdout: true).trim()
                }
            }
        }
        stage('Sign Image') {
            container('cosign') {
                sh "set -x" // Включить режим отладки
                sh "echo 'Running cosign version'"
                sh "cosign version || echo 'Failed to run cosign version'"

                sh "echo 'Logging in to ${registryDomain}'"
                sh "cosign login ${registryDomain} --username=\$DOCKERHUB_USER --password=\$DOCKERHUB_PASS || echo 'Login failed'"
                sh "echo 'Signning the hashed image'"
                sh "cosign sign -y --key env://COSIGN_KEY ${dockerRegistry}/${dockerImage}:latest || echo 'Failed to sign hashed image'"
                sh "cosign sign -y --key env://COSIGN_KEY ${dockerRegistry}/${dockerImage}:${version} || echo 'Failed to sign hashed image'"
                sh "cosign sign -y --key env://COSIGN_KEY ${dockerRegistry}/${dockerImage}@${imageDigest} || echo 'Failed to sign hashed image'"
            }
        }
    }
}

Не идеально, но вот основные моменты:

  1. Podman делает login в наш docker registry
  2. Далее берем хэш-сумму для подписи в cosign
  3. Логинимся в наш registry уже с помощью cosign
  4. Получаем на image и подписываем его

Неправильно, что мы подписываем конкретные версии нашего image. На это ругается cosign и best practice тут - использовать только хэш:

sh "cosign sign -y --key env://COSIGN_KEY ${dockerRegistry}/${dockerImage}@${imageDigest} || echo 'Failed to sign hashed image'"

И при заливке этого дела в OpenSource проектах или в Dockerhub я бы так и сделал. Но Harbor просит подпись для всех версий, поэтому я проигнорировал предупреждения и подписал latest, номер версии и непосредственно хэш.

И еще один важный момент. Я монтирую при создании пода папку /.docker. Туда записывает креды cosign. Без монтирования прав нет. Точнее права пропадают, если не запускать его под UID=1000. А если не запустить его под этим пользователем, то Jenkins не сможет записать лог и билд повиснет на использовании контейнера с cosign.

С таким набором можно спокойно билдить все у себя в кластере, не отдавая на откуп в Github или Gitlab. Особенно, если у вас поднят какой-то простой репозиторий в Gogs или Gitea, у которых есть вебхуки, но нет собственного CI.