Skip to content

Commit

Permalink
Sign image and attach SBOM attestation
Browse files Browse the repository at this point in the history
Closes #11.
  • Loading branch information
michaelsauter committed Nov 23, 2023
1 parent ca5f75d commit bee9418
Show file tree
Hide file tree
Showing 13 changed files with 154 additions and 24 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.ods/
/.vscode
11 changes: 3 additions & 8 deletions build/docs/package.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,15 @@ nexusUrlWithAuth is equal to nexusUrl.
By default, the image is named after the component and pushed into the image
stream located in the namespace of the pipeline run.

If link:https://www.aquasec.com/products/container-security/[Aqua security scanning]
is enabled in the cluster, images are scanned and registered in Aqua after
they are pushed to the image stream. JSON and HTML report artifacts are
generated. Further, if there is an open pull request on Bitbucket for the
built branch, a code insight report is attached to the Git commit.
An SBOM of the image is created using link:https://aquasecurity.github.io/trivy/v0.47/docs/[Trivy].

If the parameter `cosign-key` is specified, the image is signed with this key using link:https://docs.sigstore.dev/signing/quickstart/[cosign], and an attestation for the generated SBOM will be attached to the image.

Processes tags specified in the `extra-tags` parameter and adds missing tags to
the images stream in the namespace of the pipeline run.

The following artifacts are generated by the task and placed into `.ods/artifacts/`

* `aquasec-scans/`
** `report.html`
** `report.json`
* `image-digests/`
** `<image-name>.json`
** `<image-name>-<tag>.json` for each extra-tag
Expand Down
9 changes: 7 additions & 2 deletions build/images/Dockerfile.package
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
ARG GO_IMG_VERSION=1.21.4
ARG UBI_IMG_VERSION=8.9

FROM golang:${GO_IMG_VERSION} as builder

SHELL ["/bin/bash", "-o", "pipefail", "-c"]
Expand All @@ -16,13 +15,19 @@ RUN cd cmd/package-image && CGO_ENABLED=0 go build -o /usr/local/bin/ods-package
# Final image
# Based on https://catalog.redhat.com/software/containers/detail/5dca3d76dd19c71643b226d5?container-tabs=dockerfile.
FROM registry.access.redhat.com/ubi8:${UBI_IMG_VERSION}
ARG TARGETARCH

ENV BUILDAH_VERSION=1.31.3 \
SKOPEO_VERSION=1.13.3 \
TRIVY_VERSION=0.47.0
TRIVY_VERSION=0.47.0 \
COSIGN_VERSION=2.2.1

COPY --from=builder /usr/local/bin/ods-package-image /usr/local/bin/ods-package-image

RUN curl -fsSLO https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-${TARGETARCH} && \
mv cosign-linux-${TARGETARCH} /usr/local/bin/cosign && \
chmod +x /usr/local/bin/cosign

# Don't include container-selinux and remove
# directories used by yum that are just taking
# up space.
Expand Down
10 changes: 9 additions & 1 deletion build/tasks/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ spec:
description: Extra parameters passed for the trivy command to generate an SBOM.
type: string
default: ''
- name: cosign-key
description: |
Cosign Key. When set, the image will be signed with cosign using the specified key.
To reference a K8s secret, use k8s://<namespace>/<secret>. The secret must have a field
named `cosign.pub` containing the public key.
type: string
default: ''
results:
- description: Digest of the image just built (e.g. `sha256:406cf...f9109`).
name: image-digest
Expand Down Expand Up @@ -92,7 +99,8 @@ spec:
-context-dir=$(params.docker-dir) \
-buildah-build-extra-args=$(params.buildah-build-extra-args) \
-buildah-push-extra-args=$(params.buildah-push-extra-args) \
-trivy-sbom-extra-args=$(params.trivy-sbom-extra-args)
-trivy-sbom-extra-args=$(params.trivy-sbom-extra-args) \
-cosign-key=$(params.cosign-key)
# As this task does not run unter uid 1001, chown created artifacts
# to make them deletable by ods-start's cleanup procedure.
Expand Down
2 changes: 1 addition & 1 deletion cmd/package-image/buildah.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func (p *packageImage) buildahPush(outWriter, errWriter io.Writer) error {
log.Printf("could not parse extra args (%s): %s", opts.buildahPushExtraArgs, err)
}
tlsVerify := opts.tlsVerify
if strings.HasPrefix(opts.registry, "ods-pipeline-registry.kind") {
if strings.HasPrefix(opts.registry, kindRegistry) {
tlsVerify = false
}
args := []string{
Expand Down
46 changes: 46 additions & 0 deletions cmd/package-image/cosign.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"bytes"
"fmt"
"os/exec"
"strings"
)

type CosignClient struct {
exe string
key string
}

func NewCosignClient(key string) *CosignClient {
return &CosignClient{exe: "cosign", key: key}
}

func (c *CosignClient) Sign(imageRef string) error {
args := append([]string{"sign"}, c.commonArgs(imageRef)...)
return c.runCmd(append(args, imageRef)...)
}

func (c *CosignClient) Attest(imageRef, aType, aPredicate string) error {
args := append([]string{"attest"}, c.commonArgs(imageRef)...)
return c.runCmd(append(args, "--type", aType, "--predicate", aPredicate, imageRef)...)
}

func (c *CosignClient) commonArgs(imageRef string) []string {
args := []string{"--tlog-upload=false", "--key", c.key}
if strings.HasPrefix(imageRef, kindRegistry) {
args = append(args, "--allow-insecure-registry=true", "--allow-http-registry=true")
}
return args
}

func (c *CosignClient) runCmd(args ...string) error {
cmd := exec.Command(c.exe, args...)
buf := new(bytes.Buffer)
cmd.Stderr = buf
err := cmd.Run()
if err != nil {
return fmt.Errorf("cosign cmd: %s - %s", err, buf.String())
}
return nil
}
5 changes: 5 additions & 0 deletions cmd/package-image/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const (
kubernetesServiceaccountDir = "/var/run/secrets/kubernetes.io/serviceaccount"
tektonResultsImageDigestFile = "/tekton/results/image-digest"
tektonResultsImageRefFile = "/tekton/results/image-ref"
kindRegistry = "ods-pipeline-registry.kind"
)

type options struct {
Expand All @@ -37,6 +38,7 @@ type options struct {
buildahBuildExtraArgs string
buildahPushExtraArgs string
trivySBOMExtraArgs string
cosignKey string
debug bool
}

Expand Down Expand Up @@ -89,6 +91,7 @@ var defaultOptions = options{
buildahBuildExtraArgs: "",
buildahPushExtraArgs: "",
trivySBOMExtraArgs: "",
cosignKey: "",
debug: (os.Getenv("DEBUG") == "true"),
}

Expand All @@ -111,6 +114,7 @@ func main() {
flag.StringVar(&opts.buildahBuildExtraArgs, "buildah-build-extra-args", defaultOptions.buildahBuildExtraArgs, "extra parameters passed for the build command when building images")
flag.StringVar(&opts.buildahPushExtraArgs, "buildah-push-extra-args", defaultOptions.buildahPushExtraArgs, "extra parameters passed for the push command when pushing images")
flag.StringVar(&opts.trivySBOMExtraArgs, "trivy-sbom-extra-args", defaultOptions.trivySBOMExtraArgs, "extra parameters passed for the trivy command to generate an SBOM")
flag.StringVar(&opts.cosignKey, "cosign-key", defaultOptions.cosignKey, "cosign key to sign the image with")
flag.BoolVar(&opts.debug, "debug", defaultOptions.debug, "debug mode")
flag.Parse()
var logger logging.LeveledLoggerInterface
Expand All @@ -128,6 +132,7 @@ func main() {
buildImageAndGenerateTar(),
generateSBOM(),
pushImage(),
signImage(opts.cosignKey),
storeArtifact(),
storeResults(),
)
Expand Down
2 changes: 1 addition & 1 deletion cmd/package-image/skopeo_tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func (p *packageImage) skopeoTag(idt *image.IdentityWithTag, outWriter, errWrite
tlsVerify := p.opts.tlsVerify
// TLS verification of the KinD registry is not possible at the moment as
// requests error out with "server gave HTTP response to HTTPS client".
if strings.HasPrefix(p.opts.registry, "ods-pipeline-registry.kind") {
if strings.HasPrefix(p.opts.registry, kindRegistry) {
tlsVerify = false
}
args := []string{
Expand Down
27 changes: 25 additions & 2 deletions cmd/package-image/steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package main
import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"

"github.com/google/shlex"
"github.com/opendevstack/ods-pipeline-image/internal/image"
"github.com/opendevstack/ods-pipeline/pkg/artifact"
"github.com/opendevstack/ods-pipeline/pkg/pipelinectxt"
)

Expand Down Expand Up @@ -123,6 +125,24 @@ func pushImage() PackageStep {
}
}

func signImage(cosignKey string) PackageStep {
return func(p *packageImage) (*packageImage, error) {
if cosignKey != "" {
i := imageRef(p.artifactImage())
c := NewCosignClient(cosignKey)
log.Printf("Signing image %s with %s ...\n", p.imageName(), cosignKey)
if err := c.Sign(i); err != nil {
return p, fmt.Errorf("signing: %s", err)
}
log.Println("Generating SBOM attestation ...")
if err := c.Attest(i, pipelinectxt.SBOMsFormat, p.sbomFile); err != nil {
return p, fmt.Errorf("attesting SBOM: %s", err)
}
}
return p, nil
}
}

func storeArtifact() PackageStep {
return func(p *packageImage) (*packageImage, error) {
fmt.Println("Writing image artifact ...")
Expand All @@ -145,8 +165,7 @@ func storeArtifact() PackageStep {
func storeResults() PackageStep {
return func(p *packageImage) (*packageImage, error) {
fmt.Println("Writing image-ref result ...")
i := p.artifactImage()
err := os.WriteFile(tektonResultsImageRefFile, []byte(fmt.Sprintf("%s/%s/%s@%s", i.Registry, i.Repository, i.Name, i.Digest)), 0644)
err := os.WriteFile(tektonResultsImageRefFile, []byte(imageRef(p.artifactImage())), 0644)
return p, err
}
}
Expand Down Expand Up @@ -187,3 +206,7 @@ func imageTagArtifactExists(p *packageImage, tag string) error {
_, err := os.Stat(filepath.Join(imageArtifactsDir, filename))
return err
}

func imageRef(i artifact.Image) string {
return fmt.Sprintf("%s/%s/%s@%s", i.Registry, i.Repository, i.Name, i.Digest)
}
8 changes: 8 additions & 0 deletions dependencies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,11 @@ dependencies:
refPaths:
- path: build/images/Dockerfile.package
match: BUILDAH_VERSION
- name: cosign
version: 2.2.1
upstream:
flavour: github
url: sigstore/cosign
refPaths:
- path: build/images/Dockerfile.package
match: COSIGN_VERSION
19 changes: 11 additions & 8 deletions docs/package.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,15 @@ nexusUrlWithAuth is equal to nexusUrl.
By default, the image is named after the component and pushed into the image
stream located in the namespace of the pipeline run.

If link:https://www.aquasec.com/products/container-security/[Aqua security scanning]
is enabled in the cluster, images are scanned and registered in Aqua after
they are pushed to the image stream. JSON and HTML report artifacts are
generated. Further, if there is an open pull request on Bitbucket for the
built branch, a code insight report is attached to the Git commit.
An SBOM of the image is created using link:https://aquasecurity.github.io/trivy/v0.47/docs/[Trivy].

If the parameter `cosign-key` is specified, the image is signed with this key using link:https://docs.sigstore.dev/signing/quickstart/[cosign], and an attestation for the generated SBOM will be attached to the image.

Processes tags specified in the `extra-tags` parameter and adds missing tags to
the images stream in the namespace of the pipeline run.

The following artifacts are generated by the task and placed into `.ods/artifacts/`

* `aquasec-scans/`
** `report.html`
** `report.json`
* `image-digests/`
** `<image-name>.json`
** `<image-name>-<tag>.json` for each extra-tag
Expand Down Expand Up @@ -101,6 +96,14 @@ The following artifacts are generated by the task and placed into `.ods/artifact
|
| Extra parameters passed for the trivy command to generate an SBOM.


| cosign-key
|
| Cosign Key. When set, the image will be signed with cosign using the specified key.
To reference a K8s secret, use k8s://<namespace>/<secret>. The secret must have a field
named `cosign.pub` containing the public key.


|===

== Results
Expand Down
10 changes: 9 additions & 1 deletion tasks/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ spec:
description: Extra parameters passed for the trivy command to generate an SBOM.
type: string
default: ''
- name: cosign-key
description: |
Cosign Key. When set, the image will be signed with cosign using the specified key.
To reference a K8s secret, use k8s://<namespace>/<secret>. The secret must have a field
named `cosign.pub` containing the public key.
type: string
default: ''
results:
- description: Digest of the image just built (e.g. `sha256:406cf...f9109`).
name: image-digest
Expand Down Expand Up @@ -94,7 +101,8 @@ spec:
-context-dir=$(params.docker-dir) \
-buildah-build-extra-args=$(params.buildah-build-extra-args) \
-buildah-push-extra-args=$(params.buildah-push-extra-args) \
-trivy-sbom-extra-args=$(params.trivy-sbom-extra-args)
-trivy-sbom-extra-args=$(params.trivy-sbom-extra-args) \
-cosign-key=$(params.cosign-key)
# As this task does not run unter uid 1001, chown created artifacts
# to make them deletable by ods-start's cleanup procedure.
Expand Down
28 changes: 28 additions & 0 deletions test/e2e/package_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@ import (
"k8s.io/client-go/util/homedir"
)

const cosignKeySecretName = "cosign-example-key"

func TestPackageImageTask(t *testing.T) {
if err := runTask(
ott.WithGitSourceWorkspace(t, "../testdata/workspaces/hello-world-app", namespaceConfig.Name),
ttr.WithStringParams(map[string]string{
"docker-dir": "docker",
"cosign-key": fmt.Sprintf("k8s://%s/%s", namespaceConfig.Name, cosignKeySecretName),
}),
generateCosignKey(),
ttr.AfterRun(func(config *ttr.TaskRunConfig, run *tekton.TaskRun, logs bytes.Buffer) {
wsDir, ctxt := ott.GetSourceWorkspaceContext(t, config)
checkResultingFiles(t, ctxt, wsDir)
Expand All @@ -54,12 +58,36 @@ func TestPackageImageTask(t *testing.T) {
if resultImageRef != wantImageRef {
t.Fatalf("want image ref %q, got %q", wantImageRef, resultImageRef)
}

// check signature + SBOM attestation
imageRef := fmt.Sprintf("localhost:5000/%s/%s@%s", namespaceConfig.Name, filepath.Base(wsDir), resultImageDigest)
cmd := exec.Command("cosign", "verify-attestation", "--insecure-ignore-tlog=true", "--key", fmt.Sprintf("k8s://%s/%s", namespaceConfig.Name, cosignKeySecretName), "--type=spdx", imageRef)
buf := new(bytes.Buffer)
cmd.Stderr = buf
err := cmd.Run()
if err != nil {
t.Fatalf("verify-attestation: %s - %s", err, buf.String())
}
}),
); err != nil {
t.Fatal(err)
}
}

func generateCosignKey() ttr.TaskRunOpt {
return func(c *ttr.TaskRunConfig) error {
cmd := exec.Command("cosign", "generate-key-pair", fmt.Sprintf("k8s://%s/%s", namespaceConfig.Name, cosignKeySecretName))
buf := new(bytes.Buffer)
cmd.Stderr = buf
cmd.Env = append(os.Environ(), "COSIGN_PASSWORD=s3cr3t")
err := cmd.Run()
if err != nil {
return fmt.Errorf("generate-key-pair: %s - %s", err, buf.String())
}
return os.Remove("cosign.pub")
}
}

func checkResultingFiles(t *testing.T, ctxt *pipelinectxt.ODSContext, wsDir string) {
wantFiles := []string{
fmt.Sprintf(".ods/artifacts/image-digests/%s.json", ctxt.Component),
Expand Down

0 comments on commit bee9418

Please sign in to comment.