Skip to content

Commit

Permalink
Feature: Create an interface for downstream CIP integrations.
Browse files Browse the repository at this point in the history
🎁 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 <[email protected]>
  • Loading branch information
mattmoor committed Jan 3, 2023
1 parent d6ef1f3 commit 214c2dd
Show file tree
Hide file tree
Showing 10 changed files with 1,620 additions and 102 deletions.
138 changes: 36 additions & 102 deletions cmd/tester/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,51 +18,35 @@ package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"

"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
"go.uber.org/zap"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"knative.dev/pkg/apis"
"knative.dev/pkg/logging"
"sigs.k8s.io/release-utils/version"
"sigs.k8s.io/yaml"

"github.com/sigstore/policy-controller/pkg/apis/glob"
"github.com/sigstore/policy-controller/pkg/apis/policy/v1alpha1"
"github.com/sigstore/policy-controller/pkg/policy"
"github.com/sigstore/policy-controller/pkg/webhook"
webhookcip "github.com/sigstore/policy-controller/pkg/webhook/clusterimagepolicy"
)

var (
ns = "unused"

remoteOpts = []ociremote.Option{
ociremote.WithRemoteOptions(
remote.WithAuthFromKeychain(authn.DefaultKeychain),
),
}

ctx = logging.WithLogger(context.Background(), func() *zap.SugaredLogger {
x, _ := zap.NewDevelopmentConfig().Build()
return x.Sugar()
}())
)

type output struct {
Errors []string `json:"errors,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Result *webhook.PolicyResult `json:"result"`
Errors []string `json:"errors,omitempty"`
Warnings []string `json:"warnings,omitempty"`
}

func main() {
Expand All @@ -83,42 +67,46 @@ func main() {
os.Exit(1)
}

var cipRaw []byte
var err error
pols := make([]policy.Source, 0, 1)

if strings.HasPrefix(*cipFilePath, "https://") || strings.HasPrefix(*cipFilePath, "http://") {
log.Printf("Fetching CIP from: %s", *cipFilePath)
resp, err := http.Get(*cipFilePath)
if err != nil {
log.Fatal(err)
}
cipRaw, err = io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
log.Fatal(err)
}
pols = append(pols, policy.Source{
URL: *cipFilePath,
})
} else {
cipRaw, err = os.ReadFile(*cipFilePath)
if err != nil {
log.Fatal(err)
pols = append(pols, policy.Source{
Path: *cipFilePath,
})
}

v := policy.Verification{
NoMatchPolicy: "deny",
Policies: &pols,
}
if err := v.Validate(ctx); err != nil {
// CIP validation can return Warnings so let's just go through them
// and only exit if there are Errors.
if warnFE := err.Filter(apis.WarningLevel); warnFE != nil {
log.Printf("CIP has warnings:\n%s\n", warnFE.Error())
}
if errorFE := err.Filter(apis.ErrorLevel); errorFE != nil {
log.Fatalf("CIP is invalid: %s", errorFE.Error())
}
}

// TODO(jdolitsky): This should use v1beta1 once there exists a
// webhookcip.ConvertClusterImagePolicyV1beta1ToWebhook() method
var v1alpha1cip v1alpha1.ClusterImagePolicy
if err := yaml.UnmarshalStrict(cipRaw, &v1alpha1cip); err != nil {
ref, err := name.ParseReference(*image)
if err != nil {
log.Fatal(err)
}
v1alpha1cip.SetDefaults(ctx)

// Show what the defaults look like
defaulted, err := yaml.Marshal(v1alpha1cip)
warningStrings := []string{}
vfy, err := policy.Compile(ctx, v, func(s string, i ...interface{}) {
warningStrings = append(warningStrings, fmt.Sprintf(s, i...))
})
if err != nil {
log.Fatalf("Failed to marshal the defaulted cip: %s", err)
log.Fatal(err)
}

log.Printf("Using the following cip:\n%s", defaulted)

if *resourceFilePath != "" {
raw, err := os.ReadFile(*resourceFilePath)
if err != nil {
Expand Down Expand Up @@ -152,76 +140,22 @@ func main() {
ctx = webhook.IncludeTypeMeta(ctx, typeMeta)
}

validateErrs := v1alpha1cip.Validate(ctx)
if validateErrs != nil {
// CIP validation can return Warnings so let's just go through them
// and only exit if there are Errors.
if warnFE := validateErrs.Filter(apis.WarningLevel); warnFE != nil {
log.Printf("CIP has warnings:\n%s\n", warnFE.Error())
}
if errorFE := validateErrs.Filter(apis.ErrorLevel); errorFE != nil {
log.Fatalf("CIP is invalid: %s", errorFE.Error())
}
}
cip := webhookcip.ConvertClusterImagePolicyV1alpha1ToWebhook(&v1alpha1cip)

// We have to marshal/unmarshal the CIP since that handles converting
// inlined Data into PublicKey objects that validator uses.
webhookCip, err := json.Marshal(cip)
if err != nil {
log.Fatalf("Failed to marshal the webhook cip: %s", err)
}
if err := json.Unmarshal(webhookCip, &cip); err != nil {
log.Fatalf("Failed to unmarshal the webhook CIP: %s", err)
}
ref, err := name.ParseReference(*image)
if err != nil {
log.Fatal(err)
}

matches := false
for _, pattern := range cip.Images {
if pattern.Glob != "" {
if matched, err := glob.Match(pattern.Glob, *image); err != nil {
log.Fatalf("Failed to match glob: %s", err)
} else if matched {
log.Printf("image matches glob %q", pattern.Glob)
matches = true
}
}
}
if !matches {
log.Fatalf("Image does not match any of the provided globs")
}

result, errs := webhook.ValidatePolicy(ctx, ns, ref, *cip, authn.DefaultKeychain, remoteOpts...)
errStrings := []string{}
warningStrings := []string{}
for _, err := range errs {
var fe *apis.FieldError
if errors.As(err, &fe) {
if warnFE := fe.Filter(apis.WarningLevel); warnFE != nil {
warningStrings = append(warningStrings, strings.Trim(warnFE.Error(), "\n"))
}
if errorFE := fe.Filter(apis.ErrorLevel); errorFE != nil {
errStrings = append(errStrings, strings.Trim(errorFE.Error(), "\n"))
}
} else {
errStrings = append(errStrings, strings.Trim(err.Error(), "\n"))
}
if err := vfy.Verify(ctx, ref, authn.DefaultKeychain); err != nil {
errStrings = append(errStrings, strings.Trim(err.Error(), "\n"))
}

var o []byte
o, err = json.Marshal(&output{
Errors: errStrings,
Warnings: warningStrings,
Result: result,
})
if err != nil {
log.Fatal(err)
}

fmt.Println(string(o))
if len(errs) > 0 {
if len(errStrings) > 0 {
os.Exit(1)
}
}
105 changes: 105 additions & 0 deletions pkg/policy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Integrating Policy Verification

The goal of this package is to make it easy for downstream tools to incorporate
the verification capabilities of `ClusterImagePolicy` in other contexts where
OCI artifacts are consumed.

The most straightforward example of this is to enable OCI build tooling to
incorporate policies over the base images on top of which an application image
is built (e.g. `ko`, `kaniko`). However, this can be used by other tooling
that stores artifacts in OCI registries to verify those as well, examples of
this could include the way Buildpacks v3 and Crossplane store elements in OCI
registries.

## Configuration

Verification is configured via `policy.Verification`:

```golang
type Verification struct {
// NoMatchPolicy specifies the behavior when a base image doesn't match any
// of the listed policies. It allows the values: allow, deny, and warn.
NoMatchPolicy string `yaml:"no-match-policy,omitempty"`

// Policies specifies a collection of policies to use to cover the base
// images used as part of evaluation. See "policy" below for usage.
// Policies can be nil so that we can distinguish between an explicitly
// specified empty list and when policies is unspecified.
Policies *[]Source `yaml:"policies,omitempty"`
}
```

`NoMatchPolicy` controls the behavior when an image reference is passed that
does not match any of the configured policies.

`Policies` can be specified via three possible sources:

```golang
// Source contains a set of options for specifying policies. Exactly
// one of the fields may be specified for each Source entry.
type Source struct {
// Data is a collection of one or more ClusterImagePolicy resources.
Data string `yaml:"data,omitempty"`

// Path is a path to a file containing one or more ClusterImagePolicy
// resources.
// TODO(mattmoor): Make this support taking a directory similar to kubectl.
// TODO(mattmoor): How do we want to handle something like -R? Perhaps we
// don't and encourage folks to list each directory individually?
Path string `yaml:"path,omitempty"`

// URL links to a file containing one or more ClusterImagePolicy resources.
URL string `yaml:"url,omitempty"`
}
```

### With `spf13/viper`

Many tools leverage `spf13/viper` for configuration, and `policy.Verification`
may be used in conjunction with viper via:

```golang
vfy := policy.Verification{}
if err := v.UnmarshalKey("verification", &vfy); err != nil { ... }
```

This allows a section of the viper config:

```yaml
verification:
noMatchPolicy: deny
policies:
- data: ... # Inline policies
- url: ... # URL to policies
...
```

## Compilation

The `policy.Verification` can be compiled into a `policy.Verifier` using
`policy.Compile`, which also takes a `context.Context` and a function that
controls how warnings are surfaced:

```golang
verifier, err := policy.Compile(ctx, verification,
func(s string, i ...interface{}) {
// Handle warnings your own way!
})
if err != nil { ... }
```

The compilation process will surface compilation warnings via the supplied
function and return any errors resolving or compiling the policies immediately.

## Verification

With a compiled `policy.Verifier` many image references can be verified against
the compiled policies by invoking `Verify`:
```golang
// Verifier is the interface for checking that a given image digest satisfies
// the policies backing this interface.
type Verifier interface {
// Verify checks that the provided reference satisfies the backing policies.
Verify(context.Context, name.Reference, authn.Keychain) error
}
```
Loading

0 comments on commit 214c2dd

Please sign in to comment.