Commit 2ba9dbda authored by Pierre Smeyers's avatar Pierre Smeyers
Browse files

Merge branch 'image-signature' into 'master'

Add support for signing images

Closes #113

See merge request to-be-continuous/docker!144
parents f7a3a235 614838c1
Loading
Loading
Loading
Loading
+24 −0
Original line number Diff line number Diff line
@@ -226,6 +226,30 @@ This file uses:
- template-managed `${docker_snapshot_authent_token}`, `${docker_snapshot_registry_host}`, `${docker_release_authent_token}` and `${docker_release_registry_host}` variables (:warning: mind the double `$$` to prevent GitLab from [trying to evaluate the variable](https://docs.gitlab.com/ci/variables/#use-the--character-in-variables)),
- the user-defined authentication may be inlined as a GitLab project variable is a place safe enough to store secrets.

## Image signing

This template supports signing the Docker image right after it has been built. It also produces a signed attestation including the SBOM of the image. Both elements (image signature and signed attestation) are pushed to the image registry.

By default signatures are only activated on `$INTEG_REF`, `$PROD_REF` and `$RELEASE_REF`.

Cosign is used for signing images and attestations. As of now, we only support signing with a private key defined in `$DOCKER_COSIGN_PRIVATE_KEY`. Certificate and keyless signing may be added in the future.

By default the latest version of cosign is used but you can use a specific version by setting the `$DOCKER_COSIGN_DIST_URL` variable.

When the snapshot image it promoted to release, both the image signature and the signed attestation are promoted as well.

The signing process can be configured with the following variables:

| Input / Variable                                          | Description                                  | Default value         |
| --------------------------------------------------------- | -------------------------------------------- | --------------------- |
| `cosign-strategy` / `DOCKER_COSIGN_STRATEGY`              | Determines when images should be signed with [cosign](https://github.com/sigstore/cosign (`never`: disabled, `onrelease`: only on `$INTEG_REF`, `$PROD_REF` and `$RELEASE_REF` pipelines; `always`: any pipeline).<br/>:warning: `cosign-enabled` / `DOCKER_COSIGN_ENABLED` takes precedence | `never` |
| `cosign-opts` / `DOCKER_COSIGN_OPTS`                      | Options for cosign | `--tlog-upload=false` |
| `cosign-dist-url` / `DOCKER_COSIGN_DIST_URL`              | Url to the `linux-amd64` binary of Cosign to use (ex: `https://github.com/sigstore/cosign/releases/download/v2.5.0/cosign-linux-amd64`)<br/>_When unset, the latest version will be used_ | _none_ |
| :lock: `cosign-private-key` / `DOCKER_COSIGN_PRIVATE_KEY` | Private key used for signing the Docker image and the attestation | _none_ |
| :lock: `cosign-password` / `COSIGN_PASSWORD`              | Password of the private key | _none_ |



## Multi Dockerfile support

This template supports building multiple Docker images from a single Git repository.
+36 −0
Original line number Diff line number Diff line
@@ -216,6 +216,42 @@
          "advanced": true
        }
      ]
    },
    {
      "id": "cosign",
      "name": "Image Signature",
      "description": "Sign the Docker image being built using [cosign](https://github.com/sigstore/cosign)",
      "variables": [
        {
          "name": "DOCKER_COSIGN_STRATEGY",
          "type": "enum",
          "values": ["never", "onrelease", "always"],
          "description": "Determines when images should be signed with [cosign](https://github.com/sigstore/cosign (`never`: disabled, `onrelease`: only on `$INTEG_REF`, `$PROD_REF` and `$RELEASE_REF` pipelines; `always`: any pipeline).",
          "default": "never"
        },
        {
          "name": "DOCKER_COSIGN_OPTS",
          "description": "Options for cosign",
          "default": "--tlog-upload=false",
          "advanced": true
        },
        {
          "name": "DOCKER_COSIGN_DIST_URL",
          "description": "Url to the `linux-amd64` binary of Cosign to use\n\n_When unset, the latest version will be used_",
          "advanced": true
        },
        {
          "name": "DOCKER_COSIGN_PRIVATE_KEY",
          "description": "Private key used for signing the Docker image and the attestation",
          "secret": true
        },
        {
          "name": "COSIGN_PASSWORD",
          "description": "Password of the private key",
          "secret": true
        }

      ]
    }
  ],
  "variants": [
+169 −3
Original line number Diff line number Diff line
@@ -171,6 +171,23 @@ spec:
    sbom-opts:
      description: Options for syft used for SBOM analysis
      default: --override-default-catalogers rpm-db-cataloger,alpm-db-cataloger,apk-db-cataloger,dpkg-db-cataloger,portage-cataloger --select-catalogers -file
    cosign-strategy:
      description: |-
        Determines when images should be signed with [cosign](https://github.com/sigstore/cosign (`never`: disabled, `onrelease`: only on `$INTEG_REF`, `$PROD_REF` and `$RELEASE_REF` pipelines; `always`: any pipeline).
      options:
      - never
      - onrelease
      - always
      default: never
    cosign-opts:
      description: Options for cosign
      default: --tlog-upload=false
    cosign-dist-url:
      description: |-
        Url to the `linux-amd64` binary of Cosign to use (ex: `https://github.com/sigstore/cosign/releases/download/v2.5.0/cosign-linux-amd64`)
        
        _When unset, the latest version will be used_
      default: ''
---
# default workflow rules: Merge Request pipelines
workflow:
@@ -254,6 +271,12 @@ variables:
  DOCKER_SBOM_IMAGE: $[[ inputs.sbom-image ]]
  DOCKER_SBOM_OPTS: $[[ inputs.sbom-opts ]]

  # Cosign configuration
  COSIGN_YES: "true" # skip confirmation prompts for non-destructive operations
  DOCKER_COSIGN_STRATEGY: $[[ inputs.cosign-strategy ]]
  DOCKER_COSIGN_OPTS: $[[ inputs.cosign-opts ]]
  DOCKER_COSIGN_DIST_URL: $[[ inputs.cosign-dist-url ]]

  # default: one-click publish
  DOCKER_PROD_PUBLISH_STRATEGY: $[[ inputs.prod-publish-strategy ]]
  DOCKER_RELEASE_EXTRA_TAGS_PATTERN: $[[ inputs.release-extra-tags-pattern ]]
@@ -380,6 +403,51 @@ stages:
    fi
  }

  function github_get_latest_version() {
    if command -v curl &> /dev/null
    then
      curl -sSf -I "https://github.com/$1/releases/latest" | awk -F '/' -v RS='\r\n' '/location:/ {print $NF}'
    elif command -v wget &> /dev/null
    then
      wget -q -S -O - "https://github.com/$1/releases/latest" 2>&1 | grep -i "location:" | sed 's|.*/||'
    elif command -v python3 &> /dev/null
    then
      python3 -c "import urllib.request;url='https://github.com/$1/releases/latest';opener=urllib.request.build_opener(type('NoRedirection', (urllib.request.HTTPErrorProcessor,), {'http_response': lambda self, req, resp: resp, 'https_response': lambda self, req, resp: resp})());req=urllib.request.Request(url, method='HEAD');print(opener.open(req).headers.get('Location').split('/')[-1])"
    else
      fail "curl or wget or python3 required"
    fi
  }

  function install_cosign() {
    if [[ -z "$DOCKER_COSIGN_DIST_URL" ]]
    then
      log_info "Cosign version unset: retrieve latest version..."
      cosign_version=$(github_get_latest_version sigstore/cosign)
      DOCKER_COSIGN_DIST_URL="https://github.com/sigstore/cosign/releases/download/${cosign_version}/cosign-linux-amd64"
      log_info "... use latest Cosign version: \\e[32m$DOCKER_COSIGN_DIST_URL\\e[0m"
    fi
    docker_cosign="$CI_PROJECT_DIR/.cache/cosign-$(echo "$DOCKER_COSIGN_DIST_URL" | md5sum | cut -d" " -f1)"
    if [[ -f $docker_cosign ]]
    then
      log_info "Cosign found in cache (\\e[32m$DOCKER_COSIGN_DIST_URL\\e[0m): reuse"
    else
      log_info "Cosign not found in cache (\\e[32m$DOCKER_COSIGN_DIST_URL\\e[0m): download"
      if command -v curl > /dev/null
      then
        curl -L -o cosign "$DOCKER_COSIGN_DIST_URL"
      elif command -v wget > /dev/null
      then
        wget -O cosign "$DOCKER_COSIGN_DIST_URL"
      fi
      
      mkdir -p "$CI_PROJECT_DIR/.cache"
      # shellcheck disable=SC2086
      mv ./cosign $docker_cosign
      # shellcheck disable=SC2086
      chmod a+x $docker_cosign
      export docker_cosign
    fi
  }

  function unscope_variables() {
    _scoped_vars=$(env | awk -F '=' "/^scoped__[a-zA-Z0-9_]+=/ {print \$1}" | sort)
@@ -751,6 +819,66 @@ stages:
    echo $DOCKER_METADATA $DOCKER_BUILD_ARGS "$@" | xargs /kaniko/executor ${TRACE+--verbosity debug} --context "$(docker_context_path)" --dockerfile "$DOCKER_FILE" --destination "$docker_image" ${kaniko_cache_args} $kaniko_registry_mirror_option
  }

  configure_cosign_private_key() {
    if [[ -z ${DOCKER_COSIGN_PRIVATE_KEY:-$COSIGN_PRIVATE_KEY} ]]
    then
      fail "Cosign private key is not defined"
    fi
    if [[ -f ${DOCKER_COSIGN_PRIVATE_KEY:-$COSIGN_PRIVATE_KEY} ]]
    then
      docker_cosign_private_key=${DOCKER_COSIGN_PRIVATE_KEY:-$COSIGN_PRIVATE_KEY}
    else
      mkdir -p /tmp
      docker_cosign_private_key=$(mktemp)
      echo "${DOCKER_COSIGN_PRIVATE_KEY:-$COSIGN_PRIVATE_KEY}" > "$docker_cosign_private_key"
    fi
    export docker_cosign_private_key
  }

  function maybe_sign_image() {
    case "${DOCKER_COSIGN_STRATEGY}" in
      always)
        # continue
        ;;
      onrelease)
        if [[ ! "$CI_COMMIT_REF_NAME" =~ ${PROD_REF:1:-1} ]] && [[ ! "$CI_COMMIT_REF_NAME" =~ ${INTEG_REF:1:-1} ]] && [[ ! "$CI_COMMIT_TAG" =~ ${RELEASE_REF:1:-1} ]]
        then
          # not a release: skip
          return
        fi
        ;;
      *)
        # sign disabled: skip
        return
        ;;
    esac

    # shellcheck disable=SC1091
    source ./docker.env
    install_cosign
    configure_cosign_private_key

    # shellcheck disable=SC2154
    log_info "Sign image ${docker_image_digest}"
    # shellcheck disable=SC2154,SC2086
    $docker_cosign sign $DOCKER_COSIGN_OPTS --key ${docker_cosign_private_key} \
      --annotations "gitlab.ci.project.path=https://$CI_SERVER_HOST/$CI_PROJECT_PATH" \
      --annotations "gitlab.ci.user.name=$GITLAB_USER_NAME" \
      --annotations "gitlab.ci.pipeline.name=$CI_PIPELINE_NAME" \
      --annotations "gitlab.ci.pipeline.id=$CI_PIPELINE_ID" \
      --annotations "gitlab.ci.pipeline.url=$CI_PIPELINE_URL" \
      --annotations "gitlab.ci.job.id=$CI_JOB_ID" \
      --annotations "gitlab.ci.job.url=$CI_JOB_URL" \
      --annotations "gitlab.ci.commit.sha=$CI_COMMIT_SHA" \
      --annotations "gitlab.ci.commit.ref.name=$CI_COMMIT_REF_NAME" \
      --annotations "gitlab.ci.runner.id=$CI_RUNNER_ID" \
      --annotations "gitlab.ci.runner.version=$CI_RUNNER_VERSION" \
      --annotations "gitlab.ci.time.startedOn=$CI_JOB_STARTED_AT" \
      --annotations "gitlab.ci.registry.image=$docker_image" \
      --annotations "tag=$docker_tag" \
      ${docker_image_digest}
  }

  # Used by containers tools like buildah, skopeo.
  function configure_containers_registries() {
    if [[ -n "$CONTAINER_REGISTRIES_CONFIG_FILE" ]]
@@ -805,7 +933,7 @@ stages:
  variables:
    BUILDTOOL_HOME: "/kaniko"
  cache:
    key: "$CI_COMMIT_REF_SLUG-docker"
    - key: "$CI_COMMIT_REF_SLUG-docker"
      paths:
        - .cache
  before_script:
@@ -839,6 +967,10 @@ stages:
          if [[ -n "${_CUSTOM_CA_CERTS:-$_DEFAULT_CA_CERTS}" ]]; then echo "${_CUSTOM_CA_CERTS:-$_DEFAULT_CA_CERTS}" | tr -d '\r' >> /etc/ssl/certs/ca-certificates.crt; fi || exit
          if [[ -n "${_TRACE}" ]]; then echo "Here is the list of all CAs that are trusted by the Docker daemon:"; cat /etc/ssl/certs/ca-certificates.crt; fi
          if [[ -n "${DOCKER_REGISTRY_MIRROR}" ]]; then dockerd-entrypoint.sh --registry-mirror ${DOCKER_REGISTRY_MIRROR}; else dockerd-entrypoint.sh; fi || exit
  cache:
    - key: "$CI_COMMIT_REF_SLUG-docker"
      paths:
        - .cache
  before_script:
    - !reference [.docker-scripts]
    - if ! wait_for_docker_daemon; then fail "Docker-in-Docker is not enabled on this runner. Either use a Docker-in-Docker capable runner, or disable this job by setting \$DOCKER_BUILD_TOOL to a different value"; fi
@@ -911,6 +1043,7 @@ docker-kaniko-build:
        echo "docker_tag=$docker_tag"
        echo "docker_digest=$docker_digest"
      } > docker.env
    - maybe_sign_image
  artifacts:
    reports:
      dotenv:
@@ -921,6 +1054,10 @@ docker-kaniko-build:
docker-dind-build:
  extends: .docker-dind-base
  stage: package-build
  cache:
    - key: "$CI_COMMIT_REF_SLUG-docker"
      paths:
        - .cache
  script:
    - docker pull $DOCKER_SNAPSHOT_IMAGE || true
    - |
@@ -948,6 +1085,7 @@ docker-dind-build:
        echo "docker_tag=$docker_tag"
        echo "docker_digest=$docker_digest"
      } > docker.env
    - maybe_sign_image
  artifacts:
    reports:
      dotenv:
@@ -959,6 +1097,10 @@ docker-buildah-build:
  extends: .docker-base
  stage: package-build
  image: "$DOCKER_BUILDAH_IMAGE"
  cache:
    - key: "$CI_COMMIT_REF_SLUG-docker"
      paths:
        - .cache
  script:
    - configure_containers_registries
    # Add build cache related parameters.
@@ -988,6 +1130,7 @@ docker-buildah-build:
        echo "docker_tag=$docker_tag"
        echo "docker_digest=$docker_digest"
      } > docker.env
    - maybe_sign_image
  artifacts:
    reports:
      dotenv:
@@ -1119,6 +1262,10 @@ docker-trivy:
docker-sbom:
  extends: .docker-base
  stage: package-test
  cache:
    - key: "$CI_COMMIT_REF_SLUG-docker"
      paths:
        - .cache
  image:
    name: $DOCKER_SBOM_IMAGE
    entrypoint: [""]
@@ -1133,6 +1280,14 @@ docker-sbom:
    - basename=$(echo "${DOCKER_SNAPSHOT_IMAGE}" | sed 's|[/:]|_|g')
    - /syft scan ${TRACE+-vv} $DOCKER_SNAPSHOT_IMAGE $DOCKER_SBOM_OPTS -o cyclonedx-json=reports/docker-sbom-${basename}.cyclonedx.json
    - chmod a+r reports/docker-sbom-${basename}.cyclonedx.json
    - |
      if [[ ${DOCKER_COSIGN_STRATEGY} == "onrelease" ]] || [[ ${DOCKER_COSIGN_STRATEGY} == "always" ]]
      then
        log_info "Attaching attested SBOM to ${DOCKER_SNAPSHOT_IMAGE}..."
        install_cosign
        configure_cosign_private_key
        $docker_cosign attest --key ${docker_cosign_private_key} ${DOCKER_COSIGN_OPTS} --predicate reports/docker-sbom-${basename}.cyclonedx.json ${docker_image_digest}
      fi
  artifacts:
    name: "SBOM for docker from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    expire_in: 1 week
@@ -1195,6 +1350,17 @@ docker-publish:
    # 1: push main image
    - log_info "Copying ${DOCKER_SNAPSHOT_IMAGE} to ${DOCKER_RELEASE_IMAGE}..."
    - skopeo copy --src-authfile "$BUILDTOOL_HOME/skopeo/.docker/src-config.json" --dest-authfile "$BUILDTOOL_HOME/skopeo/.docker/dest-config.json" ${DOCKER_PUBLISH_ARGS} "docker://$DOCKER_SNAPSHOT_IMAGE" "docker://$DOCKER_RELEASE_IMAGE"
    - |
      if [[ ${DOCKER_COSIGN_STRATEGY} == "onrelease" ]] || [[ ${DOCKER_COSIGN_STRATEGY} == "always" ]]
      then
        snapshot_repository=${DOCKER_SNAPSHOT_IMAGE%:*}
        release_repository=${DOCKER_RELEASE_IMAGE%:*}
        tag=$(echo "${docker_digest}" | tr ':' '-')
        log_info "Copying image signature to ${release_repository}:${tag}.sig..."
        skopeo copy --src-authfile "$BUILDTOOL_HOME/skopeo/.docker/src-config.json" --dest-authfile "$BUILDTOOL_HOME/skopeo/.docker/dest-config.json" ${DOCKER_PUBLISH_ARGS} "docker://${snapshot_repository}:${tag}.sig" "docker://${release_repository}:${tag}.sig"
        log_info "Copying image attestation to ${release_repository}:${tag}.att..."
        skopeo copy --src-authfile "$BUILDTOOL_HOME/skopeo/.docker/src-config.json" --dest-authfile "$BUILDTOOL_HOME/skopeo/.docker/dest-config.json" ${DOCKER_PUBLISH_ARGS} "docker://${snapshot_repository}:${tag}.att" "docker://${release_repository}:${tag}.att"
      fi
    - |
      log_info "Well done your image is pushed and can be pulled with: docker pull $DOCKER_RELEASE_IMAGE"
    # 2: extract info and generate output dotenv