From 1833540ff3c8aecafd93d356cb30ed7f4f30ae9f 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 | 120 ++++++++++++++++++++++++++++ test/extended/util/framework.go | 32 ++++++++ 2 files changed, 152 insertions(+) create mode 100644 test/extended/registry/signature.go diff --git a/test/extended/registry/signature.go b/test/extended/registry/signature.go new file mode 100644 index 000000000000..e1813a9874eb --- /dev/null +++ b/test/extended/registry/signature.go @@ -0,0 +1,120 @@ +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] image signature workflow", func() { + defer g.GinkgoRecover() + var oc = exutil.NewCLI("registry-signing", exutil.KubeConfigPath()) + + 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("new-build").Args("--docker-image", "openshift/origin:latest", "--to", "signer:latest", "-D", "-").InputString(`FROM openshift/origin:latest +RUN yum install -y skopeo && yum clean all && mkdir -p gnupg && chmod -R 0777 /var/lib/origin && \ +echo $'%echo Generating a basic 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`).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 targed 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")) + + _, 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/util/framework.go b/test/extended/util/framework.go index 610b0a913700..8655f3895fed 100644 --- a/test/extended/util/framework.go +++ b/test/extended/util/framework.go @@ -1439,3 +1439,35 @@ 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 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() + framework.Logf("output:\n%s\n", out) + return true, err + }) + return out, waitErr +}