Skip to content

Commit

Permalink
Use ClusterImagePolicy with Keyless + e2e tests for CIP with kind (#1650
Browse files Browse the repository at this point in the history
)

* Just setting up the cluster tests.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* Sign/verify a simple container.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* Plumb Keyless with Fulcio / Rekor through.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* Start wiring tests together. Not complete.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* Ok, now the test should work, last run should've failed, it did. woot!

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* Return policies instead of all the authorities in one fell swoop since
we need to make sure that all policies have at least one validating
authority.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* Cleanup, add keyless unit tests. For now just failure cases, need to
understand how to test fully with both rekor/fulcio the good case.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* Removed the API path portion of the test CIP as no longer necessary.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>
  • Loading branch information
vaikas authored Mar 25, 2022
1 parent bdef009 commit 340b6c6
Show file tree
Hide file tree
Showing 12 changed files with 746 additions and 145 deletions.
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
- 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}
- 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 -
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
- 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::'
- 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) {
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

0 comments on commit 340b6c6

Please sign in to comment.