Skip to content
This repository was archived by the owner on Sep 23, 2025. It is now read-only.

Commit 214c2dd

Browse files
committed
Feature: Create an interface for downstream CIP integrations.
🎁 This change factors a new small library `./pkg/policy` which is intended to streamline incorporating CIP validation into downstream tooling. For a (much) more verbose explanation see [here](ko-build/ko#356 (comment)), but the general idea behind this is to allow CIP's to gate consumption of images in other contexts, for example the base images in build tools such as `ko` or `kaniko`. The idea is to enable the tool providers to bake-in default policies for default base images, and optionally expose configuration to let users write policies to authorize base images prior to consumption. For example, I might write the following `.ko.yaml`: ```yaml verification: noMatchPolicy: deny policies: - data: | # inline policy - url: https://github.com/foo/bar/blobs/main/POLICY.yaml ``` With this library, it is likely <100 LoC to add base image policy verification to `ko`, and significantly simplifies our own `policy-tester` which has spaghetti code replicating some of this functionality. /kind feature Signed-off-by: Matt Moore <mattmoor@chainguard.dev>
1 parent d6ef1f3 commit 214c2dd

10 files changed

Lines changed: 1620 additions & 102 deletions

File tree

cmd/tester/main.go

Lines changed: 36 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -18,51 +18,35 @@ package main
1818
import (
1919
"context"
2020
"encoding/json"
21-
"errors"
2221
"flag"
2322
"fmt"
24-
"io"
2523
"log"
26-
"net/http"
2724
"os"
2825
"strings"
2926

3027
"github.com/google/go-containerregistry/pkg/authn"
3128
"github.com/google/go-containerregistry/pkg/name"
32-
"github.com/google/go-containerregistry/pkg/v1/remote"
33-
ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
3429
"go.uber.org/zap"
3530
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3631
"knative.dev/pkg/apis"
3732
"knative.dev/pkg/logging"
3833
"sigs.k8s.io/release-utils/version"
3934
"sigs.k8s.io/yaml"
4035

41-
"github.com/sigstore/policy-controller/pkg/apis/glob"
42-
"github.com/sigstore/policy-controller/pkg/apis/policy/v1alpha1"
36+
"github.com/sigstore/policy-controller/pkg/policy"
4337
"github.com/sigstore/policy-controller/pkg/webhook"
44-
webhookcip "github.com/sigstore/policy-controller/pkg/webhook/clusterimagepolicy"
4538
)
4639

4740
var (
48-
ns = "unused"
49-
50-
remoteOpts = []ociremote.Option{
51-
ociremote.WithRemoteOptions(
52-
remote.WithAuthFromKeychain(authn.DefaultKeychain),
53-
),
54-
}
55-
5641
ctx = logging.WithLogger(context.Background(), func() *zap.SugaredLogger {
5742
x, _ := zap.NewDevelopmentConfig().Build()
5843
return x.Sugar()
5944
}())
6045
)
6146

6247
type output struct {
63-
Errors []string `json:"errors,omitempty"`
64-
Warnings []string `json:"warnings,omitempty"`
65-
Result *webhook.PolicyResult `json:"result"`
48+
Errors []string `json:"errors,omitempty"`
49+
Warnings []string `json:"warnings,omitempty"`
6650
}
6751

6852
func main() {
@@ -83,42 +67,46 @@ func main() {
8367
os.Exit(1)
8468
}
8569

86-
var cipRaw []byte
87-
var err error
70+
pols := make([]policy.Source, 0, 1)
71+
8872
if strings.HasPrefix(*cipFilePath, "https://") || strings.HasPrefix(*cipFilePath, "http://") {
89-
log.Printf("Fetching CIP from: %s", *cipFilePath)
90-
resp, err := http.Get(*cipFilePath)
91-
if err != nil {
92-
log.Fatal(err)
93-
}
94-
cipRaw, err = io.ReadAll(resp.Body)
95-
resp.Body.Close()
96-
if err != nil {
97-
log.Fatal(err)
98-
}
73+
pols = append(pols, policy.Source{
74+
URL: *cipFilePath,
75+
})
9976
} else {
100-
cipRaw, err = os.ReadFile(*cipFilePath)
101-
if err != nil {
102-
log.Fatal(err)
77+
pols = append(pols, policy.Source{
78+
Path: *cipFilePath,
79+
})
80+
}
81+
82+
v := policy.Verification{
83+
NoMatchPolicy: "deny",
84+
Policies: &pols,
85+
}
86+
if err := v.Validate(ctx); err != nil {
87+
// CIP validation can return Warnings so let's just go through them
88+
// and only exit if there are Errors.
89+
if warnFE := err.Filter(apis.WarningLevel); warnFE != nil {
90+
log.Printf("CIP has warnings:\n%s\n", warnFE.Error())
91+
}
92+
if errorFE := err.Filter(apis.ErrorLevel); errorFE != nil {
93+
log.Fatalf("CIP is invalid: %s", errorFE.Error())
10394
}
10495
}
10596

106-
// TODO(jdolitsky): This should use v1beta1 once there exists a
107-
// webhookcip.ConvertClusterImagePolicyV1beta1ToWebhook() method
108-
var v1alpha1cip v1alpha1.ClusterImagePolicy
109-
if err := yaml.UnmarshalStrict(cipRaw, &v1alpha1cip); err != nil {
97+
ref, err := name.ParseReference(*image)
98+
if err != nil {
11099
log.Fatal(err)
111100
}
112-
v1alpha1cip.SetDefaults(ctx)
113101

114-
// Show what the defaults look like
115-
defaulted, err := yaml.Marshal(v1alpha1cip)
102+
warningStrings := []string{}
103+
vfy, err := policy.Compile(ctx, v, func(s string, i ...interface{}) {
104+
warningStrings = append(warningStrings, fmt.Sprintf(s, i...))
105+
})
116106
if err != nil {
117-
log.Fatalf("Failed to marshal the defaulted cip: %s", err)
107+
log.Fatal(err)
118108
}
119109

120-
log.Printf("Using the following cip:\n%s", defaulted)
121-
122110
if *resourceFilePath != "" {
123111
raw, err := os.ReadFile(*resourceFilePath)
124112
if err != nil {
@@ -152,76 +140,22 @@ func main() {
152140
ctx = webhook.IncludeTypeMeta(ctx, typeMeta)
153141
}
154142

155-
validateErrs := v1alpha1cip.Validate(ctx)
156-
if validateErrs != nil {
157-
// CIP validation can return Warnings so let's just go through them
158-
// and only exit if there are Errors.
159-
if warnFE := validateErrs.Filter(apis.WarningLevel); warnFE != nil {
160-
log.Printf("CIP has warnings:\n%s\n", warnFE.Error())
161-
}
162-
if errorFE := validateErrs.Filter(apis.ErrorLevel); errorFE != nil {
163-
log.Fatalf("CIP is invalid: %s", errorFE.Error())
164-
}
165-
}
166-
cip := webhookcip.ConvertClusterImagePolicyV1alpha1ToWebhook(&v1alpha1cip)
167-
168-
// We have to marshal/unmarshal the CIP since that handles converting
169-
// inlined Data into PublicKey objects that validator uses.
170-
webhookCip, err := json.Marshal(cip)
171-
if err != nil {
172-
log.Fatalf("Failed to marshal the webhook cip: %s", err)
173-
}
174-
if err := json.Unmarshal(webhookCip, &cip); err != nil {
175-
log.Fatalf("Failed to unmarshal the webhook CIP: %s", err)
176-
}
177-
ref, err := name.ParseReference(*image)
178-
if err != nil {
179-
log.Fatal(err)
180-
}
181-
182-
matches := false
183-
for _, pattern := range cip.Images {
184-
if pattern.Glob != "" {
185-
if matched, err := glob.Match(pattern.Glob, *image); err != nil {
186-
log.Fatalf("Failed to match glob: %s", err)
187-
} else if matched {
188-
log.Printf("image matches glob %q", pattern.Glob)
189-
matches = true
190-
}
191-
}
192-
}
193-
if !matches {
194-
log.Fatalf("Image does not match any of the provided globs")
195-
}
196-
197-
result, errs := webhook.ValidatePolicy(ctx, ns, ref, *cip, authn.DefaultKeychain, remoteOpts...)
198143
errStrings := []string{}
199-
warningStrings := []string{}
200-
for _, err := range errs {
201-
var fe *apis.FieldError
202-
if errors.As(err, &fe) {
203-
if warnFE := fe.Filter(apis.WarningLevel); warnFE != nil {
204-
warningStrings = append(warningStrings, strings.Trim(warnFE.Error(), "\n"))
205-
}
206-
if errorFE := fe.Filter(apis.ErrorLevel); errorFE != nil {
207-
errStrings = append(errStrings, strings.Trim(errorFE.Error(), "\n"))
208-
}
209-
} else {
210-
errStrings = append(errStrings, strings.Trim(err.Error(), "\n"))
211-
}
144+
if err := vfy.Verify(ctx, ref, authn.DefaultKeychain); err != nil {
145+
errStrings = append(errStrings, strings.Trim(err.Error(), "\n"))
212146
}
147+
213148
var o []byte
214149
o, err = json.Marshal(&output{
215150
Errors: errStrings,
216151
Warnings: warningStrings,
217-
Result: result,
218152
})
219153
if err != nil {
220154
log.Fatal(err)
221155
}
222156

223157
fmt.Println(string(o))
224-
if len(errs) > 0 {
158+
if len(errStrings) > 0 {
225159
os.Exit(1)
226160
}
227161
}

pkg/policy/README.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Integrating Policy Verification
2+
3+
The goal of this package is to make it easy for downstream tools to incorporate
4+
the verification capabilities of `ClusterImagePolicy` in other contexts where
5+
OCI artifacts are consumed.
6+
7+
The most straightforward example of this is to enable OCI build tooling to
8+
incorporate policies over the base images on top of which an application image
9+
is built (e.g. `ko`, `kaniko`). However, this can be used by other tooling
10+
that stores artifacts in OCI registries to verify those as well, examples of
11+
this could include the way Buildpacks v3 and Crossplane store elements in OCI
12+
registries.
13+
14+
## Configuration
15+
16+
Verification is configured via `policy.Verification`:
17+
18+
```golang
19+
type Verification struct {
20+
// NoMatchPolicy specifies the behavior when a base image doesn't match any
21+
// of the listed policies. It allows the values: allow, deny, and warn.
22+
NoMatchPolicy string `yaml:"no-match-policy,omitempty"`
23+
24+
// Policies specifies a collection of policies to use to cover the base
25+
// images used as part of evaluation. See "policy" below for usage.
26+
// Policies can be nil so that we can distinguish between an explicitly
27+
// specified empty list and when policies is unspecified.
28+
Policies *[]Source `yaml:"policies,omitempty"`
29+
}
30+
```
31+
32+
`NoMatchPolicy` controls the behavior when an image reference is passed that
33+
does not match any of the configured policies.
34+
35+
`Policies` can be specified via three possible sources:
36+
37+
```golang
38+
// Source contains a set of options for specifying policies. Exactly
39+
// one of the fields may be specified for each Source entry.
40+
type Source struct {
41+
// Data is a collection of one or more ClusterImagePolicy resources.
42+
Data string `yaml:"data,omitempty"`
43+
44+
// Path is a path to a file containing one or more ClusterImagePolicy
45+
// resources.
46+
// TODO(mattmoor): Make this support taking a directory similar to kubectl.
47+
// TODO(mattmoor): How do we want to handle something like -R? Perhaps we
48+
// don't and encourage folks to list each directory individually?
49+
Path string `yaml:"path,omitempty"`
50+
51+
// URL links to a file containing one or more ClusterImagePolicy resources.
52+
URL string `yaml:"url,omitempty"`
53+
}
54+
```
55+
56+
### With `spf13/viper`
57+
58+
Many tools leverage `spf13/viper` for configuration, and `policy.Verification`
59+
may be used in conjunction with viper via:
60+
61+
```golang
62+
vfy := policy.Verification{}
63+
if err := v.UnmarshalKey("verification", &vfy); err != nil { ... }
64+
```
65+
66+
This allows a section of the viper config:
67+
68+
```yaml
69+
verification:
70+
noMatchPolicy: deny
71+
policies:
72+
- data: ... # Inline policies
73+
- url: ... # URL to policies
74+
...
75+
```
76+
77+
## Compilation
78+
79+
The `policy.Verification` can be compiled into a `policy.Verifier` using
80+
`policy.Compile`, which also takes a `context.Context` and a function that
81+
controls how warnings are surfaced:
82+
83+
```golang
84+
verifier, err := policy.Compile(ctx, verification,
85+
func(s string, i ...interface{}) {
86+
// Handle warnings your own way!
87+
})
88+
if err != nil { ... }
89+
```
90+
91+
The compilation process will surface compilation warnings via the supplied
92+
function and return any errors resolving or compiling the policies immediately.
93+
94+
## Verification
95+
96+
With a compiled `policy.Verifier` many image references can be verified against
97+
the compiled policies by invoking `Verify`:
98+
```golang
99+
// Verifier is the interface for checking that a given image digest satisfies
100+
// the policies backing this interface.
101+
type Verifier interface {
102+
// Verify checks that the provided reference satisfies the backing policies.
103+
Verify(context.Context, name.Reference, authn.Keychain) error
104+
}
105+
```

0 commit comments

Comments
 (0)