Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use ClusterImagePolicy with Keyless + e2e tests for CIP with kind #1650

Merged
merged 8 commits into from
Mar 25, 2022
215 changes: 215 additions & 0 deletions .github/workflows/kind-cluster-image-policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# Copyright 2022 The Sigstore Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: Test cosigned with ClusterImagePolicy

on:
pull_request:
branches: [ 'main', 'release-*' ]

defaults:
run:
shell: bash

permissions: read-all

jobs:
cip-test:
name: ClusterImagePolicy e2e tests
runs-on: ubuntu-latest

strategy:
matrix:
k8s-version:
- v1.21.x

env:
KNATIVE_VERSION: "1.1.0"
KO_DOCKER_REPO: "registry.local:5000/knative"
SCAFFOLDING_RELEASE_VERSION: "v0.2.2"
GO111MODULE: on
GOFLAGS: -ldflags=-s -ldflags=-w
KOCACHE: ~/ko
COSIGN_EXPERIMENTAL: true

steps:
- name: Configure DockerHub mirror
run: |
tmp=$(mktemp)
jq '."registry-mirrors" = ["https://mirror.gcr.io"]' /etc/docker/daemon.json > "$tmp"
sudo mv "$tmp" /etc/docker/daemon.json
sudo service docker restart
vaikas marked this conversation as resolved.
Show resolved Hide resolved

- uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0
- uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0
with:
go-version: '1.17.x'

# will use the latest release available for ko
- uses: imjasonh/setup-ko@2c3450ca27f6e6f2b02e72a40f2163c281a1f675 # v0.4

- name: Setup Cluster
run: |
curl -Lo ./setup-kind.sh https://github.com/sigstore/scaffolding/releases/download/${{ env.SCAFFOLDING_RELEASE_VERSION }}/setup-kind.sh
chmod u+x ./setup-kind.sh
./setup-kind.sh \
--registry-url $(echo ${KO_DOCKER_REPO} | cut -d'/' -f 1) \
--cluster-suffix cluster.local \
--k8s-version ${{ matrix.k8s-version }} \
--knative-version ${KNATIVE_VERSION}
vaikas marked this conversation as resolved.
Show resolved Hide resolved

- name: Install all the everythings
timeout-minutes: 10
run: |
curl -L https://github.com/sigstore/scaffolding/releases/download/${{ env.SCAFFOLDING_RELEASE_VERSION }}/release.yaml | kubectl apply -f -

# Wait for all the ksvc to be up.
kubectl wait --timeout 10m -A --for=condition=Ready ksvc --all

- name: Run Scaffolding Tests
run: |
# Grab the secret from the ctlog-system namespace and make a copy
# in our namespace so we can get access to the CT Log public key
# so we can verify the SCT coming from there.
kubectl -n ctlog-system get secrets ctlog-public-key -oyaml | sed 's/namespace: .*/namespace: default/' | kubectl apply -f -

# Also grab the secret from the fulcio-system namespace and make a copy
# in our namespace so we can get access to the Fulcio public key
# so we can verify against it.
kubectl -n fulcio-system get secrets fulcio-secret -oyaml | sed 's/namespace: .*/namespace: default/' | kubectl apply -f -

curl -L https://github.com/sigstore/scaffolding/releases/download/${{ env.SCAFFOLDING_RELEASE_VERSION }}/testrelease.yaml | kubectl create -f -
vaikas marked this conversation as resolved.
Show resolved Hide resolved

kubectl wait --for=condition=Complete --timeout=180s job/sign-job job/checktree job/verify-job

- name: Install cosigned
env:
GIT_HASH: $GITHUB_SHA
GIT_VERSION: ci
LDFLAGS: ""
run: |
ko apply -Bf config/

# Wait for the webhook to come up and become Ready
kubectl rollout status --timeout 5m --namespace cosign-system deployments/webhook

- name: Create sample image - demoimage
run: |
pushd $(mktemp -d)
go mod init example.com/demo
cat <<EOF > main.go
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
EOF
demoimage=`ko publish -B example.com/demo`
echo "demoimage=$demoimage" >> $GITHUB_ENV
echo Created image $demoimage
popd

- name: Create sample image2 - demoimage2
run: |
pushd $(mktemp -d)
go mod init example.com/demo2
cat <<EOF > main.go
package main
import "fmt"
func main() {
fmt.Println("hello world 2")
}
EOF
demoimage2=`ko publish -B example.com/demo2`
echo "demoimage2=$demoimage2" >> $GITHUB_ENV
echo Created image $demoimage2
popd

# TODO(vaikas): There should be a fake issuer on the cluster. This one
# fetches a k8s auth token from the kind cluster that we spin up above. We
# do not want to use the Github OIDC token, but do want PRs to run this
# flow.
- name: Install a Knative service for fetch tokens off the cluster
run: |
ko apply -f ./test/config/gettoken
sleep 2
kubectl wait --for=condition=Ready --timeout=15s ksvc gettoken

# These set up the env variables so that
- name: Set the endpoints on the cluster and grab secrets
run: |
REKOR_URL=`kubectl -n rekor-system get --no-headers ksvc rekor | cut -d ' ' -f 4`
echo "REKOR_URL=$REKOR_URL" >> $GITHUB_ENV
curl -s $REKOR_URL/api/v1/log/publicKey > ./rekor-public.pem

FULCIO_URL=`kubectl -n fulcio-system get --no-headers ksvc fulcio | cut -d ' ' -f 4`
echo "FULCIO_URL=$FULCIO_URL" >> $GITHUB_ENV
CTLOG_URL=`kubectl -n ctlog-system get --no-headers ksvc ctlog | cut -d ' ' -f 4`
echo "CTLOG_URL=$CTLOG_URL" >> $GITHUB_ENV

ISSUER_URL=`kubectl get --no-headers ksvc gettoken | cut -d ' ' -f 4`
echo "ISSUER_URL=$ISSUER_URL" >> $GITHUB_ENV
OIDC_TOKEN=`curl -s $ISSUER_URL`
echo "OIDC_TOKEN=$OIDC_TOKEN" >> $GITHUB_ENV

kubectl -n ctlog-system get secrets ctlog-public-key -o=jsonpath='{.data.public}' | base64 -d > ./ctlog-public.pem
echo "SIGSTORE_CT_LOG_PUBLIC_KEY_FILE=./ctlog-public.pem" >> $GITHUB_ENV

kubectl -n fulcio-system get secrets fulcio-secret -ojsonpath='{.data.cert}' | base64 -d > ./fulcio-root.pem
echo "SIGSTORE_ROOT_FILE=./fulcio-root.pem" >> $GITHUB_ENV

- name: Deploy ClusterImagePolicy
run: |
kubectl apply -f ./test/testdata/cosigned/e2e/cip.yaml

- name: build cosign
run: |
make cosign
vaikas marked this conversation as resolved.
Show resolved Hide resolved

- name: Sign demoimage with cosign
run: |
./cosign sign --rekor-url ${{ env.REKOR_URL }} --fulcio-url ${{ env.FULCIO_URL }} --force --allow-insecure-registry ${{ env.demoimage }} --identity-token ${{ env.OIDC_TOKEN }}

- name: Verify with cosign
run: |
SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 COSIGN_EXPERIMENTAL=1 ./cosign verify --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }}

- name: Deploy jobs and verify signed works, unsigned fails
run: |
kubectl create namespace demo
kubectl label namespace demo cosigned.sigstore.dev/include=true

echo '::group:: test job success'
# We signed this above, this should work
if ! kubectl create -n demo job demo --image=${{ env.demoimage }} ; then
echo Failed to create Job in namespace without label!
exit 1
else
echo Succcessfully created Job with signed image
fi
echo '::endgroup:: test job success'

echo '::group:: test job rejection'
# We did not sign this, should fail
if kubectl create -n demo job demo2 --image=${{ env.demoimage2 }} ; then
echo Failed to block unsigned Job creation!
exit 1
else
echo Successfully blocked Job creation with unsigned image
fi
echo '::endgroup::'
Comment on lines +149 to +211
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this into scripts like these?

- name: Run Insecure Registry Tests
run: |
go install github.com/google/go-containerregistry/cmd/crane
./test/e2e_test_insecure_registry.sh
- name: Run Image Policy Tests
run: |
./test/e2e_test_policy_crd.sh
- name: Run Cosigned Tests
run: |
./test/e2e_test_cosigned.sh

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then you can avoid some of the GITHUB_ENV bits for passing env vars across steps, and folks can more easily run this locally.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah! I was planning on doing that as a follow on. Part of the original reasoning for the test/testadata/cosigned was to dump things into there so that we can then easier pick and choose what we run and automate that. I wasn't quite sure how to structure that :)

There's some stuff that I'd like to pass from the cluster (like the oidc_token, or a way to fetch it, or fulcio URL), etc. but I reckon when I call from actions to shell, env persists?


- name: Collect diagnostics
if: ${{ failure() }}
uses: chainguard-dev/actions/kind-diag@84c993eaf02da1c325854fb272a4df9184bd80fc # main
6 changes: 5 additions & 1 deletion config/webhook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ spec:
value: sigstore.dev/cosigned
- name: WEBHOOK_NAME
value: webhook
# Since we need to validate against different Rekor clients based on
# differing policies, we fetch the Rekor public key during validation.
- name: SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY
value: "true"

securityContext:
allowPrivilegeEscalation: false
Expand Down Expand Up @@ -103,4 +107,4 @@ metadata:
namespace: cosign-system
# stringData:
# cosign.pub: |
# <PEM encoded public key>
# <PEM encoded public key>
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ require (
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.3
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87
github.com/in-toto/in-toto-golang v0.3.4-0.20211211042327-af1f9fb822bf
github.com/kelseyhightower/envconfig v1.4.0
github.com/manifoldco/promptui v0.9.0
github.com/miekg/pkcs11 v1.1.1
github.com/mitchellh/go-homedir v1.1.0
Expand Down Expand Up @@ -218,7 +219,6 @@ require (
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kelseyhightower/envconfig v1.4.0 // indirect
github.com/klauspost/compress v1.14.2 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/magiconair/properties v1.8.5 // indirect
Expand Down
16 changes: 9 additions & 7 deletions pkg/apis/config/image_policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,30 +81,32 @@ func parseEntry(entry string, out interface{}) error {
return json.Unmarshal(j, &out)
}

// GetAuthorities returns all matching Authorities that need to be matched for
// the given Image.
func (p *ImagePolicyConfig) GetAuthorities(image string) ([]v1alpha1.Authority, error) {
// GetMatchingPolicies returns all matching Policies and their Authorities that
// need to be matched for the given Image.
// Returned map contains the name of the CIP as the key, and an array of
// authorities from that Policy that must be validated against.
func (p *ImagePolicyConfig) GetMatchingPolicies(image string) (map[string][]v1alpha1.Authority, error) {
if p == nil {
return nil, errors.New("config is nil")
}

var lastError error
ret := []v1alpha1.Authority{}
ret := map[string][]v1alpha1.Authority{}

// TODO(vaikas): this is very inefficient, we should have a better
// way to go from image to Authorities, but just seeing if this is even
// workable so fine for now.
for _, v := range p.Policies {
for k, v := range p.Policies {
for _, pattern := range v.Images {
if pattern.Glob != "" {
if GlobMatch(image, pattern.Glob) {
ret = append(ret, v.Authorities...)
ret[k] = append(ret[k], v.Authorities...)
}
} else if pattern.Regex != "" {
if regex, err := regexp.Compile(pattern.Regex); err != nil {
lastError = err
} else if regex.MatchString(image) {
ret = append(ret, v.Authorities...)
ret[k] = append(ret[k], v.Authorities...)
}
}
}
Expand Down
64 changes: 46 additions & 18 deletions pkg/apis/config/image_policies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,62 +43,90 @@ func TestGetAuthorities(t *testing.T) {
if err != nil {
t.Error("NewImagePoliciesConfigFromConfigMap(example) =", err)
}
c, err := defaults.GetAuthorities("rando")
c, err := defaults.GetMatchingPolicies("rando")
checkGetMatches(t, c, err)
matchedPolicy := "cluster-image-policy-0"
want := "inlinedata here"
if got := c[0].Key.Data; got != want {
if got := c[matchedPolicy][0].Key.Data; got != want {
t.Errorf("Did not get what I wanted %q, got %+v", want, got)
}
// Make sure glob matches 'randomstuff*'
c, err = defaults.GetAuthorities("randomstuffhere")
c, err = defaults.GetMatchingPolicies("randomstuffhere")
checkGetMatches(t, c, err)
matchedPolicy = "cluster-image-policy-1"
want = "otherinline here"
if got := c[0].Key.Data; got != want {
if got := c[matchedPolicy][0].Key.Data; got != want {
t.Errorf("Did not get what I wanted %q, got %+v", want, got)
}
c, err = defaults.GetAuthorities("rando3")
c, err = defaults.GetMatchingPolicies("rando3")
checkGetMatches(t, c, err)
matchedPolicy = "cluster-image-policy-2"
want = "cacert chilling here"
if got := c[0].Keyless.CACert.Data; got != want {
t.Errorf("Did not get what I wanted %q, got %+v", want, c[0].Keyless.CACert.Data)
if got := c[matchedPolicy][0].Keyless.CACert.Data; got != want {
t.Errorf("Did not get what I wanted %q, got %+v", want, got)
}
want = "issuer"
if got := c[0].Keyless.Identities[0].Issuer; got != want {
t.Errorf("Did not get what I wanted %q, got %+v", want, c[0].Keyless.Identities[0].Issuer)
if got := c[matchedPolicy][0].Keyless.Identities[0].Issuer; got != want {
t.Errorf("Did not get what I wanted %q, got %+v", want, got)
}
want = "subject"
if got := c[0].Keyless.Identities[0].Subject; got != want {
t.Errorf("Did not get what I wanted %q, got %+v", want, c[0].Keyless.Identities[0].Subject)
if got := c[matchedPolicy][0].Keyless.Identities[0].Subject; got != want {
t.Errorf("Did not get what I wanted %q, got %+v", want, got)
}
// Make sure regex matches ".*regexstring.*"
c, err = defaults.GetAuthorities("randomregexstringstuff")
c, err = defaults.GetMatchingPolicies("randomregexstringstuff")
checkGetMatches(t, c, err)
matchedPolicy = "cluster-image-policy-4"
want = inlineKeyData
if got := c[0].Key.Data; got != want {
if got := c[matchedPolicy][0].Key.Data; got != want {
t.Errorf("Did not get what I wanted %q, got %+v", want, got)
}

// Test multiline yaml cert
c, err = defaults.GetAuthorities("inlinecert")
c, err = defaults.GetMatchingPolicies("inlinecert")
checkGetMatches(t, c, err)
matchedPolicy = "cluster-image-policy-3"
want = inlineKeyData
if got := c[0].Key.Data; got != want {
if got := c[matchedPolicy][0].Key.Data; got != want {
t.Errorf("Did not get what I wanted %q, got %+v", want, got)
}
// Test multiline cert but json encoded
c, err = defaults.GetAuthorities("ghcr.io/example/*")
c, err = defaults.GetMatchingPolicies("ghcr.io/example/*")
checkGetMatches(t, c, err)
matchedPolicy = "cluster-image-policy-json"
want = inlineKeyData
if got := c[0].Key.Data; got != want {
if got := c[matchedPolicy][0].Key.Data; got != want {
t.Errorf("Did not get what I wanted %q, got %+v", want, got)
}
// Test multiple matches
c, err = defaults.GetMatchingPolicies("regexstringtoo")
checkGetMatches(t, c, err)
if len(c) != 2 {
t.Errorf("Wanted two matches, got %d", len(c))
}
matchedPolicy = "cluster-image-policy-4"
want = inlineKeyData
if got := c[matchedPolicy][0].Key.Data; got != want {
t.Errorf("Did not get what I wanted %q, got %+v", want, got)
}
matchedPolicy = "cluster-image-policy-5"
want = "inlinedata here"
if got := c[matchedPolicy][0].Key.Data; got != want {
t.Errorf("Did not get what I wanted %q, got %+v", want, got)
}
}

func checkGetMatches(t *testing.T, c []v1alpha1.Authority, err error) {
func checkGetMatches(t *testing.T, c map[string][]v1alpha1.Authority, err error) {
vaikas marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
t.Error("GetMatches Failed =", err)
}
if len(c) == 0 {
t.Error("Wanted a config, got none.")
}
for _, v := range c {
if v != nil || len(v) > 0 {
return
}
}
t.Error("Wanted a config and non-zero authorities, got no authorities")
}
Loading