Gitlab has built-in support for keyless signing with Sigstore - if you’re using a hosted version of Gitlab. If you’re self-hosting, this requires a bit more effort to setup.
Here’s how I set up a Gitlab CI workflow to build an image with kaniko, push it to a self-hosted Harbor registry, and sign the digest with Cosign and Vault.
Vault setup
Enable transit
secrets engine, create the cosign
key, and create a policy for access:
vault secrets enable transit
vault write -f transit/keys/cosign type=ecdsa-p256
vault policy write cosign - '
path "transit/keys/cosign" {
capabilities = ["read"]
}
path "transit/hmac/cosign/*" {
capabilities = ["update"]
}
path "transit/sign/cosign/*" {
capabilities = ["update"]
}
path "transit/verify/cosign" {
capabilities = ["create"]
}
path "transit/verify/cosign/*" {
capabilities = ["update"]
}
'
Set up JWT auth:
vault auth enable jwt
vault write auth/jwt/role/cosign - '
{
"bound_audiences": [
"https://gitlab.example.com"
],
"bound_claims": {
"iss": [
"gitlab.example.com",
"https:/gitlab.example.com"
],
"project_path": [
"container-images/*"
]
},
"bound_claims_type": "glob",
"policies": [
"cosign"
],
"role_type": "jwt",
"user_claim": "user_email"
}
'
vault write auth/jwt/config \
jwks_url="https://gitlab.example.com/oauth/discovery/keys"
kaniko-cosign image
To be able to handle interacting with Vault, building the image, and signing the digest in a single step, I’m using a customized image which contains all the needed tools. I’ll manually build, sign, and push it to harbor.example.com/library/kaniko-cosign:latest
.
Note: I stash the cosign
and vault
binaries inside of /kaniko/
to make sure they don’t lost during multistage builds.
# Dockerfile
ARG ALPINE_VERSION=3.20
ARG COSIGN_VERSION=2.4.1
ARG VAULT_VERSION=1.18.3
ARG KANIKO_VERSION=1.23.2
FROM bitnami/kaniko:${KANIKO_VERSION} AS kaniko
FROM bitnami/cosign:${COSIGN_VERSION} AS cosign
FROM hashicorp/vault:${VAULT_VERSION} AS vault
FROM alpine:${ALPINE_VERSION}
COPY --from=kaniko /kaniko /kaniko
COPY --from=cosign /opt/bitnami/cosign/bin/cosign /kaniko/cosign
COPY --from=vault /bin/vault /kaniko/vault
ENV PATH=$PATH:/kaniko \
DOCKER_CONFIG=/kaniko/.docker
WORKDIR /workspace
CMD ["/bin/sh"]
Gitlab pipeline
Note: I use --ignore-path
to tell kaniko not to delete /bin/busybox
so I can continue to use those tools.
# .gitlab-ci.yml
stages:
- build
container-build:
stage: build
image: harbor.example.com/library/kaniko-cosign:latest
id_tokens:
VAULT_ID_TOKEN:
aud: https://gitlab.example.com
variables:
VAULT_ADDR: https://vault.example.com
VAULT_JWT_ROLE: cosign
COSIGN_KEY: hashivault://cosign
before_script:
- export VAULT_TOKEN="$(vault write --field=token auth/jwt/login role=$VAULT_JWT_ROLE jwt=$VAULT_ID_TOKEN)"
- echo "{\"auths\":{\"${HARBOR_HOST}\":{\"auth\":\"$(echo -n ${HARBOR_USERNAME}:${HARBOR_PASSWORD} | base64 -w 0)\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor
--context "${CI_PROJECT_DIR}"
--dockerfile "${CI_PROJECT_DIR}/Dockerfile"
--destination "${HARBOR_HOST}/${HARBOR_PROJECT}/${CI_PROJECT_NAME}:${CI_COMMIT_TAG}"
--digest-file "./digest"
--ignore-path=/bin/busybox
- /kaniko/cosign sign "${HARBOR_HOST}/${HARBOR_PROJECT}/${CI_PROJECT_NAME}@$(busybox cat digest)" --tlog-upload=false
See also:
Refs: