diff --git a/charts/external-data-provider/templates/external-data-provider.yaml b/charts/external-data-provider/templates/external-data-provider.yaml index c59f04d..fbaee49 100644 --- a/charts/external-data-provider/templates/external-data-provider.yaml +++ b/charts/external-data-provider/templates/external-data-provider.yaml @@ -1,8 +1,17 @@ apiVersion: externaldata.gatekeeper.sh/v1beta1 kind: Provider metadata: - name: attest-provider + name: attest-provider-validate spec: - url: https://attest-provider.{{ .Release.Namespace }}:{{ .Values.port }} + url: https://attest-provider.{{ .Release.Namespace }}:{{ .Values.port }}/validate + timeout: {{ .Values.provider.timeout }} + caBundle: {{ required "You must provide .Values.provider.tls.caBundle" .Values.provider.tls.caBundle }} +--- +apiVersion: externaldata.gatekeeper.sh/v1beta1 +kind: Provider +metadata: + name: attest-provider-mutate +spec: + url: https://attest-provider.{{ .Release.Namespace }}:{{ .Values.port }}/mutate timeout: {{ .Values.provider.timeout }} caBundle: {{ required "You must provide .Values.provider.tls.caBundle" .Values.provider.tls.caBundle }} diff --git a/go.mod b/go.mod index be6e068..8999708 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.22.1 require ( github.com/docker/attest v0.1.5-0.20240618150600-86878482c3d9 + github.com/google/go-containerregistry v0.19.2 + github.com/in-toto/in-toto-golang v0.9.0 github.com/open-policy-agent/frameworks/constraint v0.0.0-20221214024800-b745745c4118 k8s.io/klog/v2 v2.120.1 ) @@ -64,14 +66,12 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/certificate-transparency-go v1.1.8 // indirect - github.com/google/go-containerregistry v0.19.2 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.6 // indirect github.com/hashicorp/hcl v1.0.1-vault-5 // indirect - github.com/in-toto/in-toto-golang v0.9.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 // indirect github.com/jellydator/ttlcache/v3 v3.2.0 // indirect diff --git a/main.go b/main.go index d25a703..67b2555 100644 --- a/main.go +++ b/main.go @@ -46,7 +46,8 @@ func init() { func main() { mux := http.NewServeMux() - mux.Handle("POST /", http.TimeoutHandler(handler.Handler(), handlerTimeout, timeoutError)) + mux.Handle("POST /validate", http.TimeoutHandler(handler.Validate(), handlerTimeout, timeoutError)) + mux.Handle("POST /mutate", http.TimeoutHandler(handler.Mutate(), handlerTimeout, timeoutError)) server := &http.Server{ Addr: fmt.Sprintf(":%d", port), diff --git a/mutation/external-data-provider-mutation.yaml b/mutation/pin-to-digest.yaml similarity index 57% rename from mutation/external-data-provider-mutation.yaml rename to mutation/pin-to-digest.yaml index 7667a6a..a919d35 100644 --- a/mutation/external-data-provider-mutation.yaml +++ b/mutation/pin-to-digest.yaml @@ -1,20 +1,22 @@ apiVersion: mutations.gatekeeper.sh/v1beta1 kind: Assign metadata: - name: append-valid-suffix-to-image + name: pin-to-digest spec: applyTo: - - groups: [""] - kinds: ["Pod"] - versions: ["v1"] + - groups: [""] + kinds: ["Pod"] + versions: ["v1"] match: scope: Namespaced kinds: - - apiGroups: ["*"] - kinds: ["Pod"] + - apiGroups: ["*"] + kinds: ["Pod"] + namespaces: ["test"] location: "spec.containers[name: *].image" parameters: assign: externalData: - provider: external-data-provider + provider: attest-provider-mutate dataSource: ValueAtLocation + failurePolicy: Fail diff --git a/pkg/handler/mutate.go b/pkg/handler/mutate.go new file mode 100644 index 0000000..eaf10a6 --- /dev/null +++ b/pkg/handler/mutate.go @@ -0,0 +1,85 @@ +package handler + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "runtime/debug" + + "github.com/docker/attest/pkg/oci" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/open-policy-agent/frameworks/constraint/pkg/externaldata" + "github.com/open-policy-agent/gatekeeper-external-data-provider/pkg/utils" + "k8s.io/klog/v2" +) + +func Mutate() http.Handler { + return http.HandlerFunc(mutate) +} + +func mutate(w http.ResponseWriter, req *http.Request) { + defer func() { + if r := recover(); r != nil { + klog.Error(string(debug.Stack())) + klog.ErrorS(fmt.Errorf("%v", r), "panic occurred") + } + }() + + // read request body + requestBody, err := io.ReadAll(req.Body) + if err != nil { + utils.SendResponse(nil, fmt.Sprintf("unable to read request body: %v", err), w) + return + } + + klog.InfoS("received request", "body", requestBody) + + // parse request body + var providerRequest externaldata.ProviderRequest + err = json.Unmarshal(requestBody, &providerRequest) + if err != nil { + utils.SendResponse(nil, fmt.Sprintf("unable to unmarshal request body: %v", err), w) + return + } + + results := make([]externaldata.Item, 0) + + ctx := req.Context() + opts := oci.WithOptions(ctx, nil) + + for _, key := range providerRequest.Request.Keys { + output, err := getReferenceWithDigest(key, opts) + if err != nil { + utils.SendResponse(nil, err.Error(), w) + return + } + + results = append(results, externaldata.Item{ + Key: key, + Value: output, + }) + } + utils.SendResponse(&results, "", w) +} + +func getReferenceWithDigest(imageRef string, opts []remote.Option) (string, error) { + ref, err := name.ParseReference(imageRef) + if err != nil { + return "", fmt.Errorf("unable to parse reference %s: %v", imageRef, err) + } + + // if it already contains a digest, just return it as is + if _, ok := ref.(name.Digest); ok { + return ref.String(), nil + } + + // we need to make a request to the registry to get the digest + desc, err := remote.Head(ref, opts...) + if err != nil { + return "", fmt.Errorf("unable to get digest for reference %s: %v", imageRef, err) + } + + return ref.Name() + "@" + desc.Digest.String(), nil +} diff --git a/pkg/handler/tuf.go b/pkg/handler/tuf.go new file mode 100644 index 0000000..20680af --- /dev/null +++ b/pkg/handler/tuf.go @@ -0,0 +1,17 @@ +package handler + +import ( + "github.com/docker/attest/pkg/tuf" + "github.com/open-policy-agent/gatekeeper-external-data-provider/internal/embed" +) + +func createTufClient(outputPath string) (*tuf.TufClient, error) { + // using oci tuf metadata and targets + // metadataURI := "registry-1.docker.io/docker/tuf-metadata:latest" + // targetsURI := "registry-1.docker.io/docker/tuf-targets" + // example using http tuf metadata and targets + metadataURI := "https://docker.github.io/tuf-staging/metadata" + targetsURI := "https://docker.github.io/tuf-staging/targets" + + return tuf.NewTufClient(embed.StagingRoot, outputPath, metadataURI, targetsURI, tuf.NewVersionChecker()) +} diff --git a/pkg/handler/handler.go b/pkg/handler/validate.go similarity index 77% rename from pkg/handler/handler.go rename to pkg/handler/validate.go index e69fa26..b6f5eca 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/validate.go @@ -11,18 +11,24 @@ import ( "github.com/docker/attest/pkg/attest" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/policy" - "github.com/docker/attest/pkg/tuf" + intoto "github.com/in-toto/in-toto-golang/in_toto" "github.com/open-policy-agent/frameworks/constraint/pkg/externaldata" - "github.com/open-policy-agent/gatekeeper-external-data-provider/internal/embed" "github.com/open-policy-agent/gatekeeper-external-data-provider/pkg/utils" "k8s.io/klog/v2" ) -func Handler() http.Handler { - return http.HandlerFunc(handler) +type ValidationResult struct { + Outcome attest.Outcome `json:"outcome"` + Input *policy.PolicyInput `json:"input"` + VSA *intoto.Statement `json:"vsa"` + Violations []policy.Violation `json:"violations"` } -func handler(w http.ResponseWriter, req *http.Request) { +func Validate() http.Handler { + return http.HandlerFunc(validate) +} + +func validate(w http.ResponseWriter, req *http.Request) { defer func() { if r := recover(); r != nil { klog.Error(string(debug.Stack())) @@ -56,7 +62,6 @@ func handler(w http.ResponseWriter, req *http.Request) { utils.SendResponse(nil, err.Error(), w) } - // iterate over all keys for _, key := range providerRequest.Request.Keys { // create a resolver for remote attestations platform := "linux/amd64" @@ -103,14 +108,3 @@ func handler(w http.ResponseWriter, req *http.Request) { } utils.SendResponse(&results, "", w) } - -func createTufClient(outputPath string) (*tuf.TufClient, error) { - // using oci tuf metadata and targets - metadataURI := "registry-1.docker.io/docker/tuf-metadata:latest" - targetsURI := "registry-1.docker.io/docker/tuf-targets" - // example using http tuf metadata and targets - // metadataURI := "https://docker.github.io/tuf-staging/metadata" - // targetsURI := "https://docker.github.io/tuf-staging/targets" - - return tuf.NewTufClient(embed.StagingRoot, outputPath, metadataURI, targetsURI, tuf.NewVersionChecker()) -} diff --git a/validation/attest-constraint-template.yaml b/validation/attest-constraint-template.yaml index 3b34ae3..dda1823 100644 --- a/validation/attest-constraint-template.yaml +++ b/validation/attest-constraint-template.yaml @@ -17,7 +17,7 @@ spec: images := [img | img = input.review.object.spec.containers[_].image] # send external data request - response := external_data({"provider": "attest-provider", "keys": images}) + response := external_data({"provider": "attest-provider-validate", "keys": images}) response_with_error(response)