From 1abda051155f8237356328567e481072a85cc8b5 Mon Sep 17 00:00:00 2001 From: Michal Fojtik Date: Wed, 10 May 2017 10:18:41 +0200 Subject: [PATCH] Image signature verification worflow test --- test/extended/registry/signature.go | 110 ++++++++++++++++++ test/extended/testdata/bindata.go | 62 ++++++++++ .../extended/testdata/signer-buildconfig.yaml | 42 +++++++ test/extended/util/framework.go | 31 +++++ 4 files changed, 245 insertions(+) create mode 100644 test/extended/registry/signature.go create mode 100644 test/extended/testdata/signer-buildconfig.yaml diff --git a/test/extended/registry/signature.go b/test/extended/registry/signature.go new file mode 100644 index 000000000000..12d34432c6c6 --- /dev/null +++ b/test/extended/registry/signature.go @@ -0,0 +1,110 @@ +package registry + +import ( + "fmt" + + g "github.com/onsi/ginkgo" + o "github.com/onsi/gomega" + + imagesutil "github.com/openshift/origin/test/extended/images" + exutil "github.com/openshift/origin/test/extended/util" + + e2e "k8s.io/kubernetes/test/e2e/framework" +) + +var _ = g.Describe("[imageapis][registry] image signature workflow", func() { + defer g.GinkgoRecover() + + var ( + oc = exutil.NewCLI("registry-signing", exutil.KubeConfigPath()) + signerBuildFixture = exutil.FixturePath("testdata", "signer-buildconfig.yaml") + ) + + g.It("can push a signed image to openshift registry and verify it", func() { + g.By("building an signer image that know how to sign images") + _, err := oc.Run("create").Args("-f", signerBuildFixture).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + err = exutil.WaitForAnImageStreamTag(oc, oc.Namespace(), "signer", "latest") + containerLog, _ := oc.Run("logs").Args("builds/signer-1").Output() + e2e.Logf("signer build logs:\n%s\n", containerLog) + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("looking up the openshift registry URL") + registryURL, err := imagesutil.GetDockerRegistryURL(oc) + signerImage := fmt.Sprintf("%s/%s/signer:latest", registryURL, oc.Namespace()) + signedImage := fmt.Sprintf("%s/%s/signed:latest", registryURL, oc.Namespace()) + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("obtaining bearer token for the test user") + user := oc.Username() + token, err := oc.Run("whoami").Args("-t").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("granting the image-signer role to test user") + _, err = oc.AsAdmin().Run("adm").Args("policy", "add-cluster-role-to-user", "system:image-signer", oc.Username()).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + + // TODO: The test user needs to be able to unlink the /dev/random which is owned by a + // root. This cannot be done during image build time because the /dev is plugged into + // container after it starts. This SCC could be avoided in the future when /dev/random + // issue is fixed in Docker. + g.By("granting the anyuid scc to test user") + _, err = oc.AsAdmin().Run("adm").Args("policy", "add-scc-to-user", "anyuid", oc.Username()).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("preparing the image stream where the signed image will be pushed") + _, err = oc.Run("create").Args("imagestream", "signed").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("granting the image-auditor role to test user") + _, err = oc.AsAdmin().Run("adm").Args("policy", "add-cluster-role-to-user", "system:image-auditor", oc.Username()).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + + pod, err := exutil.NewPodExecutor(oc, "sign-and-push", signerImage) + o.Expect(err).NotTo(o.HaveOccurred()) + + // Generate GPG key + // Note that we need to replace the /dev/random with /dev/urandom to get more entropy + // into container so we can successfully generate the GPG keypair. + g.By("creating dummy GPG key") + out, err := pod.Exec("rm -f /dev/random; ln -sf /dev/urandom /dev/random && " + + "GNUPGHOME=/var/lib/origin/gnupg gpg2 --batch --gen-key dummy_key.conf") + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(out).To(o.ContainSubstring("keyring `/var/lib/origin/gnupg/secring.gpg' created")) + + // Create kubeconfig for skopeo + g.By("logging as a test user") + out, err = pod.Exec("oc login https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT --token=" + token + " --certificate-authority=/run/secrets/kubernetes.io/serviceaccount/ca.crt") + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(out).To(o.ContainSubstring("Logged in")) + + // Sign and copy the origin-pod image into target image stream tag + // TODO: Fix skopeo to pickup the Kubernetes environment variables (remove the $KUBERNETES_MASTER) + g.By("signing the origin-pod:latest image and pushing it into openshift registry") + _, err = pod.Exec("KUBERNETES_MASTER=https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT GNUPGHOME=/var/lib/origin/gnupg " + + "skopeo --debug --tls-verify=false copy --sign-by joe@foo.bar --dest-creds " + user + ":" + token + " --dest-tls-verify=false docker://docker.io/openshift/origin-pod:latest atomic:" + signedImage) + o.Expect(err).NotTo(o.HaveOccurred()) + + err = exutil.WaitForAnImageStreamTag(oc, oc.Namespace(), "signed", "latest") + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("obtaining the signed:latest image name") + imageName, err := oc.Run("get").Args("istag", "signed:latest", "-o", "jsonpath='{.image.metadata.name}'").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("expecting the image to have unverified signature") + out, err = oc.Run("describe").Args("istag", "signed:latest").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(out).To(o.ContainSubstring("Unverified")) + + out, err = pod.Exec("GNUPGHOME=/var/lib/origin/gnupg " + + "oc adm verify-image-signature " + imageName + " --expected-identity=" + signedImage + " --save") + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(out).To(o.ContainSubstring("identity is now confirmed")) + + g.By("checking the signature is present on the image and it is now verified") + out, err = oc.Run("describe").Args("istag", "signed:latest").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(out).To(o.ContainSubstring("Verified")) + }) +}) diff --git a/test/extended/testdata/bindata.go b/test/extended/testdata/bindata.go index 8b198d621f37..995b9b126872 100644 --- a/test/extended/testdata/bindata.go +++ b/test/extended/testdata/bindata.go @@ -115,6 +115,7 @@ // test/extended/testdata/samplepipeline-withenvs.yaml // test/extended/testdata/scoped-router.yaml // test/extended/testdata/service-serving-cert/nginx-serving-cert.conf +// test/extended/testdata/signer-buildconfig.yaml // test/extended/testdata/statusfail-assemble/.s2i/bin/assemble // test/extended/testdata/statusfail-failedassemble.yaml // test/extended/testdata/statusfail-fetchbuilderimage.yaml @@ -6501,6 +6502,65 @@ func testExtendedTestdataServiceServingCertNginxServingCertConf() (*asset, error return a, nil } +var _testExtendedTestdataSignerBuildconfigYaml = []byte(`kind: List +apiVersion: v1 +items: + +- kind: ImageStream + apiVersion: v1 + metadata: + name: signer + +- kind: BuildConfig + apiVersion: v1 + metadata: + name: signer-build + spec: + triggers: + - type: ConfigChange + source: + dockerfile: | + FROM openshift/origin:latest + RUN yum install -y skopeo && yum clean all && mkdir -p gnupg && chmod -R 0777 /var/lib/origin + RUN echo $'%echo Generating openpgp key ...\n\ + Key-Type: DSA \n\ + Key-Length: 1024 \n\ + Subkey-Type: ELG-E \n\ + Subkey-Length: 1024 \n\ + Name-Real: Joe Tester \n\ + Name-Comment: with stupid passphrase \n\ + Name-Email: joe@foo.bar \n\ + Expire-Date: 0 \n\ + Creation-Date: 2017-01-01 \n\ + %commit \n\ + %echo done \n' >> dummy_key.conf + strategy: + type: Docker + dockerStrategy: + from: + kind: DockerImage + name: openshift/origin:latest + output: + to: + kind: ImageStreamTag + name: signer:latest +`) + +func testExtendedTestdataSignerBuildconfigYamlBytes() ([]byte, error) { + return _testExtendedTestdataSignerBuildconfigYaml, nil +} + +func testExtendedTestdataSignerBuildconfigYaml() (*asset, error) { + bytes, err := testExtendedTestdataSignerBuildconfigYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "test/extended/testdata/signer-buildconfig.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _testExtendedTestdataStatusfailAssembleS2iBinAssemble = []byte(`#!/usr/bin/env bash exit 123123 @@ -19244,6 +19304,7 @@ var _bindata = map[string]func() (*asset, error){ "test/extended/testdata/samplepipeline-withenvs.yaml": testExtendedTestdataSamplepipelineWithenvsYaml, "test/extended/testdata/scoped-router.yaml": testExtendedTestdataScopedRouterYaml, "test/extended/testdata/service-serving-cert/nginx-serving-cert.conf": testExtendedTestdataServiceServingCertNginxServingCertConf, + "test/extended/testdata/signer-buildconfig.yaml": testExtendedTestdataSignerBuildconfigYaml, "test/extended/testdata/statusfail-assemble/.s2i/bin/assemble": testExtendedTestdataStatusfailAssembleS2iBinAssemble, "test/extended/testdata/statusfail-failedassemble.yaml": testExtendedTestdataStatusfailFailedassembleYaml, "test/extended/testdata/statusfail-fetchbuilderimage.yaml": testExtendedTestdataStatusfailFetchbuilderimageYaml, @@ -19609,6 +19670,7 @@ var _bintree = &bintree{nil, map[string]*bintree{ "service-serving-cert": &bintree{nil, map[string]*bintree{ "nginx-serving-cert.conf": &bintree{testExtendedTestdataServiceServingCertNginxServingCertConf, map[string]*bintree{}}, }}, + "signer-buildconfig.yaml": &bintree{testExtendedTestdataSignerBuildconfigYaml, map[string]*bintree{}}, "statusfail-assemble": &bintree{nil, map[string]*bintree{ ".s2i": &bintree{nil, map[string]*bintree{ "bin": &bintree{nil, map[string]*bintree{ diff --git a/test/extended/testdata/signer-buildconfig.yaml b/test/extended/testdata/signer-buildconfig.yaml new file mode 100644 index 000000000000..59c626a5c790 --- /dev/null +++ b/test/extended/testdata/signer-buildconfig.yaml @@ -0,0 +1,42 @@ +kind: List +apiVersion: v1 +items: + +- kind: ImageStream + apiVersion: v1 + metadata: + name: signer + +- kind: BuildConfig + apiVersion: v1 + metadata: + name: signer-build + spec: + triggers: + - type: ConfigChange + source: + dockerfile: | + FROM openshift/origin:latest + RUN yum install -y skopeo && yum clean all && mkdir -p gnupg && chmod -R 0777 /var/lib/origin + RUN echo $'%echo Generating openpgp key ...\n\ + Key-Type: DSA \n\ + Key-Length: 1024 \n\ + Subkey-Type: ELG-E \n\ + Subkey-Length: 1024 \n\ + Name-Real: Joe Tester \n\ + Name-Comment: with stupid passphrase \n\ + Name-Email: joe@foo.bar \n\ + Expire-Date: 0 \n\ + Creation-Date: 2017-01-01 \n\ + %commit \n\ + %echo done \n' >> dummy_key.conf + strategy: + type: Docker + dockerStrategy: + from: + kind: DockerImage + name: openshift/origin:latest + output: + to: + kind: ImageStreamTag + name: signer:latest diff --git a/test/extended/util/framework.go b/test/extended/util/framework.go index 610b0a913700..6fa045edf2c4 100644 --- a/test/extended/util/framework.go +++ b/test/extended/util/framework.go @@ -1439,3 +1439,34 @@ func CheckForBuildEvent(client kcoreclient.CoreV1Interface, build *buildapi.Buil o.ExpectWithOffset(1, expectedEvent).NotTo(o.BeNil(), "Did not find a %q event on build %s/%s", reason, build.Namespace, build.Name) o.ExpectWithOffset(1, expectedEvent.Message).To(o.Equal(fmt.Sprintf(message, build.Namespace, build.Name))) } + +type podExecutor struct { + client *CLI + podName string +} + +// NewPodExecutor returns an executor capable of running commands in a Pod. +func NewPodExecutor(oc *CLI, name, image string) (*podExecutor, error) { + out, err := oc.Run("run").Args(name, "--labels", "name="+name, "--image", image, "--restart", "Never", "--command", "--", "/bin/bash", "-c", "sleep infinity").Output() + if err != nil { + return nil, fmt.Errorf("error: %v\n(%s)", err, out) + } + _, err = WaitForPods(oc.KubeClient().CoreV1().Pods(oc.Namespace()), ParseLabelsOrDie("name="+name), CheckPodIsReadyFn, 1, 3*time.Minute) + if err != nil { + return nil, err + } + return &podExecutor{client: oc, podName: name}, nil +} + +// Exec executes a single command or a bash script in the running pod. It returns the +// command output and error if the command finished with non-zero status code or the +// command took longer then 3 minutes to run. +func (r *podExecutor) Exec(script string) (string, error) { + var out string + waitErr := wait.PollImmediate(1*time.Second, 3*time.Minute, func() (bool, error) { + var err error + out, err = r.client.Run("exec").Args(r.podName, "--", "/bin/bash", "-c", script).Output() + return true, err + }) + return out, waitErr +}