diff --git a/charts/external-data-provider/templates/external-data-provider-deployment.yaml b/charts/external-data-provider/templates/external-data-provider-deployment.yaml index 21d13cd..f521ec3 100644 --- a/charts/external-data-provider/templates/external-data-provider-deployment.yaml +++ b/charts/external-data-provider/templates/external-data-provider-deployment.yaml @@ -25,6 +25,22 @@ spec: - --client-ca-file={{ .Values.clientCAFile }} {{- end }} - --port={{ .Values.port }} + {{- if .Values.tufRoot }} + - --tuf-root={{ .Values.tufRoot }} + {{- end }} + {{- if .Values.tufMetadataSource }} + - --tuf-metadata-source={{ .Values.tufMetadataSource }} + {{- end }} + {{- if .Values.tufTargetsSource }} + - --tuf-targets-source={{ .Values.tufTargetsSource }} + {{- end }} + {{- if .Values.localPolicyDir }} + - --local-policy-dir={{ .Values.localPolicyDir }} + {{- end }} + {{- if .Values.policyCacheDir }} + - --policy-cache-dir={{ .Values.policyCacheDir }} + {{- end }} + ports: - containerPort: {{ .Values.port }} protocol: TCP diff --git a/charts/external-data-provider/values.yaml b/charts/external-data-provider/values.yaml index 786f835..6abe884 100644 --- a/charts/external-data-provider/values.yaml +++ b/charts/external-data-provider/values.yaml @@ -1,6 +1,12 @@ certDir: /certs clientCAFile: /tmp/gatekeeper/ca.crt port: 8090 + +# uncomment these lines to use the dev TUF root +# tufRoot: dev +# tufMetadataSource: https://docker.github.io/tuf-dev/metadata +# tufTargetsSource: https://docker.github.io/tuf-dev/targets + provider: timeout: 30 tls: diff --git a/internal/embed/embedded-roots/1.root-dev.json b/internal/embed/embedded-roots/1.root-dev.json index 5e0adaa..7ab8187 100644 --- a/internal/embed/embedded-roots/1.root-dev.json +++ b/internal/embed/embedded-roots/1.root-dev.json @@ -1,42 +1,42 @@ { "signatures": [ { - "keyid": "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09", - "sig": "3064023037bbb03c3472b140572a7d5a2895bd80e74435bbcb7053949731f81b104c6d05a0876590cd6a2e94d7ed619426a2f6fa02303adc8c9006fa5506fdd7ea87d2960074a537ad8bf2459f2863e806b47682cbb2f9b01b7502eaf5437a1a68fdaaeac114" + "keyid": "76d0a7e1ff8617ce99627d0fa5c9809f2c0f0d52e0bf65c7b84c031608d25221", + "sig": "3065023000f7d0a866576e94eaabc173b9233d4c8fcfa495527088f9022dff5a553f7a457da1015a6d0fc714f84848ec627387360231009fa70b2eebbe15241a2ec9b96a094ebd28661e30b8c3d1eab8d694df2b340bda511c489393630c9a9dacde42c99e9fa1" } ], "signed": { "_type": "root", "consistent_snapshot": true, - "expires": "2034-04-02T17:00:22Z", + "expires": "2034-05-29T20:14:11Z", "keys": { - "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3": { - "keytype": "ecdsa", - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgDpP6O0sEt2R+l84WlfmqPBsFSby\nxJsJ6YmeUVgDk/wk9++8IAR6YBYewaKye56gMnIYjTFbyOI8WomA2NQFBw==\n-----END PUBLIC KEY-----\n" - }, - "scheme": "ecdsa-sha2-nistp256", - "x-tuf-on-ci-online-uri": "awskms:arn:aws:kms:us-east-1:175142243308:key/fbd8dab6-5677-4b57-87e6-8369c45b3b61" - }, - "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09": { + "76d0a7e1ff8617ce99627d0fa5c9809f2c0f0d52e0bf65c7b84c031608d25221": { "keytype": "ecdsa", "keyval": { "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3+asmp2GD6UijwWvMezwVG/BwFLuQa3o\nT6eRxFvkILGpVDbZ92ZYWidHl9LZ/eJUjhIjuVEkNVKoenw5KjKl8veP3MthZrQA\nSkYytOIwkidZo9Rk2dczbDcFSJvLGsmd\n-----END PUBLIC KEY-----\n" }, "scheme": "ecdsa-sha2-nistp384", "x-tuf-on-ci-keyowner": "@mrjoelkamp" + }, + "bdd1703ecbde8812614b112a6551d58de3ad681048fd90fca2a3e491edd8afe5": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgDpP6O0sEt2R+l84WlfmqPBsFSby\nxJsJ6YmeUVgDk/wk9++8IAR6YBYewaKye56gMnIYjTFbyOI8WomA2NQFBw==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-online-uri": "awskms:arn:aws:kms:us-east-1:175142243308:key/fbd8dab6-5677-4b57-87e6-8369c45b3b61" } }, "roles": { "root": { "keyids": [ - "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09" + "76d0a7e1ff8617ce99627d0fa5c9809f2c0f0d52e0bf65c7b84c031608d25221" ], "threshold": 1 }, "snapshot": { "keyids": [ - "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3" + "bdd1703ecbde8812614b112a6551d58de3ad681048fd90fca2a3e491edd8afe5" ], "threshold": 1, "x-tuf-on-ci-expiry-period": 3650, @@ -44,13 +44,13 @@ }, "targets": { "keyids": [ - "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09" + "76d0a7e1ff8617ce99627d0fa5c9809f2c0f0d52e0bf65c7b84c031608d25221" ], "threshold": 1 }, "timestamp": { "keyids": [ - "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3" + "bdd1703ecbde8812614b112a6551d58de3ad681048fd90fca2a3e491edd8afe5" ], "threshold": 1, "x-tuf-on-ci-expiry-period": 3650, diff --git a/main.go b/main.go index 67b2555..22d4484 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,24 @@ var ( certDir string clientCAFile string port int + + tufRoot string + tufoutputPath string + metadataURL string + targetsURL string + + policyDir string + policyCacheDir string +) + +const ( + defaultMetadataURL = "registry-1.docker.io/docker/tuf-metadata:latest" + defaultTargetsURL = "registry-1.docker.io/docker/tuf-targets" +) + +var ( + defaultTUFOutputPath = filepath.Join("/tuf_temp", ".docker", "tuf") + defaultPolicyCacheDir = filepath.Join("/tuf_temp", ".docker", "policy") ) var timeoutError = string(utils.GatekeeperError("operation timed out")) @@ -41,13 +59,47 @@ func init() { flag.StringVar(&certDir, "cert-dir", "", "path to directory containing TLS certificates") flag.StringVar(&clientCAFile, "client-ca-file", "", "path to client CA certificate") flag.IntVar(&port, "port", defaultPort, "Port for the server to listen on") + flag.StringVar(&tufRoot, "tuf-root", "staging", "specify embedded tuf root [dev, staging], default [staging]") + + if tufRoot != "dev" && tufRoot != "staging" { + klog.Errorf("invalid tuf root: %s", tufRoot) + os.Exit(1) + } + + flag.StringVar(&metadataURL, "tuf-metadata-source", defaultMetadataURL, "source (URL or repo) for TUF metadata") + flag.StringVar(&targetsURL, "tuf-targets-source", defaultTargetsURL, "source (URL or repo) for TUF targets") + flag.StringVar(&tufoutputPath, "tuf-output-path", defaultTUFOutputPath, "local dir to store TUF repo metadata") + + flag.StringVar(&policyDir, "local-policy-dir", "", "path to local policy directory (overrides TUF policy)") + flag.StringVar(&policyCacheDir, "policy-cache-dir", defaultPolicyCacheDir, "path to store policy downloaded from TUF") + flag.Parse() } func main() { mux := http.NewServeMux() - mux.Handle("POST /validate", http.TimeoutHandler(handler.Validate(), handlerTimeout, timeoutError)) - mux.Handle("POST /mutate", http.TimeoutHandler(handler.Mutate(), handlerTimeout, timeoutError)) + + validateHandler, err := handler.NewValidateHandler(&handler.ValidateHandlerOptions{ + TUFRoot: tufRoot, + TUFOutputPath: tufoutputPath, + TUFMetadataURL: metadataURL, + TUFTargetsURL: targetsURL, + PolicyDir: policyDir, + PolicyCacheDir: policyCacheDir, + }) + if err != nil { + klog.ErrorS(err, "unable to create validate handler") + os.Exit(1) + } + + mutateHandler, err := handler.NewMutateHandler() + if err != nil { + klog.ErrorS(err, "unable to create validate handler") + os.Exit(1) + } + + mux.Handle("POST /validate", http.TimeoutHandler(validateHandler, handlerTimeout, timeoutError)) + mux.Handle("POST /mutate", http.TimeoutHandler(mutateHandler, handlerTimeout, timeoutError)) server := &http.Server{ Addr: fmt.Sprintf(":%d", port), diff --git a/pkg/handler/mutate.go b/pkg/handler/mutate.go index eaf10a6..cfdcaa0 100644 --- a/pkg/handler/mutate.go +++ b/pkg/handler/mutate.go @@ -15,11 +15,13 @@ import ( "k8s.io/klog/v2" ) -func Mutate() http.Handler { - return http.HandlerFunc(mutate) +type mutateHandler struct{} + +func NewMutateHandler() (http.Handler, error) { + return &mutateHandler{}, nil } -func mutate(w http.ResponseWriter, req *http.Request) { +func (h *mutateHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { defer func() { if r := recover(); r != nil { klog.Error(string(debug.Stack())) diff --git a/pkg/handler/tuf.go b/pkg/handler/tuf.go deleted file mode 100644 index 5a91566..0000000 --- a/pkg/handler/tuf.go +++ /dev/null @@ -1,17 +0,0 @@ -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/validate.go b/pkg/handler/validate.go index 2df85d0..d6ef15a 100644 --- a/pkg/handler/validate.go +++ b/pkg/handler/validate.go @@ -5,14 +5,15 @@ import ( "fmt" "io" "net/http" - "path/filepath" "runtime/debug" "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" ) @@ -24,11 +25,50 @@ type ValidationResult struct { Violations []policy.Violation `json:"violations"` } -func Validate() http.Handler { - return http.HandlerFunc(validate) +type ValidateHandlerOptions struct { + TUFRoot string + TUFOutputPath string + TUFMetadataURL string + TUFTargetsURL string + + PolicyDir string + PolicyCacheDir string } -func validate(w http.ResponseWriter, req *http.Request) { +type validateHandler struct { + opts *ValidateHandlerOptions +} + +func NewValidateHandler(opts *ValidateHandlerOptions) (http.Handler, error) { + handler := &validateHandler{opts: opts} + + // a TUF client can only be used once, so we need to create a new one for each request. + // we create this one up front to ensure that the TUF root is valid and to pre-load the metadata. + // TODO: this pre-loading works for the root, targets, snapshot, and timestamp roles, but not for delegated roles. + _, err := handler.createTUFClient() + if err != nil { + return nil, err + } + + klog.Infof("validate handler initialized with %s TUF root", opts.TUFRoot) + + return handler, nil +} + +func (h *validateHandler) createTUFClient() (*tuf.TufClient, error) { + var rootBytes []byte + switch h.opts.TUFRoot { + case "dev": + rootBytes = embed.DevRoot + case "staging": + rootBytes = embed.StagingRoot + default: + return nil, fmt.Errorf("invalid tuf root: %s", h.opts.TUFRoot) + } + return tuf.NewTufClient(rootBytes, h.opts.TUFOutputPath, h.opts.TUFMetadataURL, h.opts.TUFTargetsURL, tuf.NewVersionChecker()) +} + +func (h *validateHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { defer func() { if r := recover(); r != nil { klog.Error(string(debug.Stack())) @@ -36,6 +76,10 @@ func validate(w http.ResponseWriter, req *http.Request) { } }() + ctx := req.Context() + debug := true + ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(debug)) + // read request body requestBody, err := io.ReadAll(req.Body) if err != nil { @@ -53,16 +97,19 @@ func validate(w http.ResponseWriter, req *http.Request) { return } - results := make([]externaldata.Item, 0) - - // create a tuf client - tufOutputPath := filepath.Join("/tuf_temp", ".docker", "tuf") - tufClient, err := createTufClient(tufOutputPath) + tufClient, err := h.createTUFClient() if err != nil { - utils.SendResponse(nil, err.Error(), w) + utils.SendResponse(nil, fmt.Sprintf("unable to create TUF client: %v", err), w) return } + policyOpts := &policy.PolicyOptions{ + TufClient: tufClient, + LocalTargetsDir: h.opts.PolicyCacheDir, + LocalPolicyDir: h.opts.PolicyDir, + } + + results := make([]externaldata.Item, 0) for _, key := range providerRequest.Request.Keys { platform := "linux/amd64" src, err := oci.ParseImageSpec(key, oci.WithPlatform(platform)) @@ -71,16 +118,7 @@ func validate(w http.ResponseWriter, req *http.Request) { return } - opts := &policy.PolicyOptions{ - TufClient: tufClient, - LocalTargetsDir: filepath.Join("/tuf_temp", ".docker", "policy"), // location to store policy files downloaded from TUF - LocalPolicyDir: "", // overrides TUF policy for local policy files if set - } - - ctx := req.Context() - debug := true - ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(debug)) - result, err := attest.Verify(ctx, src, opts) + result, err := attest.Verify(ctx, src, policyOpts) if err != nil { utils.SendResponse(nil, err.Error(), w) return