From 099f718e6be3b551e52c8b6b236f2f13d2f7aa12 Mon Sep 17 00:00:00 2001 From: Santiago Torres Date: Sun, 20 Oct 2019 22:21:42 -0400 Subject: [PATCH] server: add in-toto support --- Gopkg.lock | 20 +++++++++++++++ cmd/kubesec/http.go | 8 +++++- pkg/ruler/ruleset.go | 34 ++++++++++++++++++++++++++ pkg/ruler/ruleset_test.go | 51 ++++++++++++++++++++++++++++++++++++++- pkg/server/server.go | 45 ++++++++++++++++++++++++++++++---- 5 files changed, 151 insertions(+), 7 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index bd265c375..f4ab4f64b 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -76,6 +76,14 @@ revision = "8cb6e5b959231cc1119e43259c4a608f9c51a241" version = "v1.0.0" +[[projects]] + branch = "master" + digest = "1:4959330ba7cf8c93dbbfdeb7e724e27273f736e6fb77261d3ad14533c8f2c293" + name = "github.com/in-toto/in-toto-golang" + packages = ["in_toto"] + pruneopts = "UT" + revision = "857cd1cfa826f39ecdf9bd98815eb3e5a312cdb9" + [[projects]] digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be" name = "github.com/inconshreveable/mousetrap" @@ -274,6 +282,17 @@ revision = "ff33455a0e382e8a81d14dd7c922020b6b5e7982" version = "v1.9.1" +[[projects]] + branch = "master" + digest = "1:cd7e85fc3687e062714febdee3e8efeb00a413a2a620d28908fd0258261d2353" + name = "golang.org/x/crypto" + packages = [ + "ed25519", + "ed25519/internal/edwards25519", + ] + pruneopts = "UT" + revision = "bd318be0434a57050ed475e0f45c3dbb16c09c2e" + [[projects]] branch = "master" digest = "1:8b466798e96432c23185ca32826702885299f94eb644c9a8dedb79771dff383a" @@ -311,6 +330,7 @@ input-imports = [ "github.com/garethr/kubeval/kubeval", "github.com/ghodss/yaml", + "github.com/in-toto/in-toto-golang/in_toto", "github.com/prometheus/client_golang/prometheus/promhttp", "github.com/spf13/cobra", "github.com/thedevsaddam/gojsonq", diff --git a/cmd/kubesec/http.go b/cmd/kubesec/http.go index c2d5c06a6..bc89d1350 100644 --- a/cmd/kubesec/http.go +++ b/cmd/kubesec/http.go @@ -10,6 +10,10 @@ import ( ) func init() { + // FIXME: I don't understand why I need a reference to keypath here, + // and the cobra docs don't make it exactly clear. + var keypath string + httpCmd.Flags().StringVarP(&keypath, "keypath", "k", "", "Path to in-toto link signing key") rootCmd.AddCommand(httpCmd) } @@ -33,7 +37,9 @@ var httpCmd = &cobra.Command{ stopCh := server.SetupSignalHandler() jsonLogger, _ := NewLogger("info", "json") - server.ListenAndServe(port, time.Minute, jsonLogger, stopCh) + keypath := cmd.Flag("keypath").Value.String() + + server.ListenAndServe(port, time.Minute, jsonLogger, stopCh, keypath) return nil }, } diff --git a/pkg/ruler/ruleset.go b/pkg/ruler/ruleset.go index 6c324e520..fd3853042 100644 --- a/pkg/ruler/ruleset.go +++ b/pkg/ruler/ruleset.go @@ -2,11 +2,13 @@ package ruler import ( "bytes" + "crypto/sha256" "encoding/json" "fmt" "github.com/controlplaneio/kubesec/pkg/rules" "github.com/garethr/kubeval/kubeval" "github.com/ghodss/yaml" + "github.com/in-toto/in-toto-golang/in_toto" "github.com/thedevsaddam/gojsonq" "go.uber.org/zap" "runtime" @@ -270,6 +272,38 @@ func (rs *Ruleset) Run(fileBytes []byte) ([]Report, error) { return reports, nil } +func GenerateInTotoLink(reports []Report, fileBytes []byte) in_toto.Metablock { + + var linkMb in_toto.Metablock + + materials := make(map[string]interface{}) + request := make(map[string]interface{}) + request["sha256"] = fmt.Sprintf("%x", sha256.Sum256([]uint8(fileBytes))) + materials["request"] = request + + products := make(map[string]interface{}) + for _, report := range reports { + reportArtifact := make(map[string]interface{}) + // FIXME: encoding as json now for integrity check, this is the wrong way + // to compute the hash over the result. Also, some error checking would be + // more than ideal. + reportValue, _ := json.Marshal(report) + reportArtifact["sha256"] = + fmt.Sprintf("%x", sha256.Sum256([]uint8(reportValue))) + products[report.Object] = reportArtifact + } + + linkMb.Signatures = []in_toto.Signature{} + linkMb.Signed = in_toto.Link{ + Type: "link", + Name: "kubesec", + Materials: materials, + Products: products, + } + + return linkMb +} + func (rs *Ruleset) generateReport(json []byte) Report { report := Report{ Object: "Unknown", diff --git a/pkg/ruler/ruleset_test.go b/pkg/ruler/ruleset_test.go index 9bdbfa138..733865ab2 100644 --- a/pkg/ruler/ruleset_test.go +++ b/pkg/ruler/ruleset_test.go @@ -2,6 +2,7 @@ package ruler import ( "github.com/ghodss/yaml" + "github.com/in-toto/in-toto-golang/in_toto" "go.uber.org/zap" "strings" "testing" @@ -66,7 +67,7 @@ kind: Deployment spec: template: spec: - hostNetwork: + hostNetwork: initContainers: - name: init1 containers: @@ -167,3 +168,51 @@ data: t.Errorf("Got error %v ", report.Message) } } + +func TestRuleset_Get_intoto(t *testing.T) { + var data = ` +--- +apiVersion: apps/v1 +kind: Deployment +spec: + template: + spec: + hostNetwork: true + initContainers: + - name: init1 + securityContext: + readOnlyRootFilesystem: true + - name: init2 + securityContext: + readOnlyRootFilesystem: false + - name: init3 + containers: + - name: c1 + - name: c2 + securityContext: + readOnlyRootFilesystem: false + runAsNonRoot: true + runAsUser: 1001 + - name: c3 + securityContext: + readOnlyRootFilesystem: true + +` + + json, err := yaml.YAMLToJSON([]byte(data)) + if err != nil { + t.Fatal(err.Error()) + } + + var reports []Report + + report := NewRuleset(zap.NewNop().Sugar()).generateReport(json) + reports = append(reports, report) + + link := GenerateInTotoLink(reports, []byte(data)).Signed.(in_toto.Link) + + if len(link.Materials) < 1 || len(link.Products) < 1 { + t.Errorf("Should have generated a report with at least one material and a product %+v", + link) + } +} diff --git a/pkg/server/server.go b/pkg/server/server.go index 5cd51555a..fae1a0b49 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "github.com/controlplaneio/kubesec/pkg/ruler" + "github.com/in-toto/in-toto-golang/in_toto" "go.uber.org/zap" "io/ioutil" "net/http" @@ -17,10 +18,10 @@ import ( ) // ListenAndServe starts a web server and waits for SIGTERM -func ListenAndServe(port string, timeout time.Duration, logger *zap.SugaredLogger, stopCh <-chan struct{}) { +func ListenAndServe(port string, timeout time.Duration, logger *zap.SugaredLogger, stopCh <-chan struct{}, keypath string) { mux := http.DefaultServeMux - mux.Handle("/", scanHandler(logger)) - mux.Handle("/scan", scanHandler(logger)) + mux.Handle("/", scanHandler(logger, keypath)) + mux.Handle("/scan", scanHandler(logger, keypath)) mux.Handle("/metrics", promhttp.Handler()) mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) @@ -80,13 +81,20 @@ func PrettyJSON(b []byte) string { return string(out.Bytes()) } -func scanHandler(logger *zap.SugaredLogger) http.Handler { +func scanHandler(logger *zap.SugaredLogger, keypath string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { http.Redirect(w, r, "https://kubesec.io", http.StatusSeeOther) return } + // fail early if no in-toto signing key is configured for this server + if r.URL.Query().Get("in-toto") != "" && keypath == "" { + logger.Errorf("Attempted to serve an in-toto payload but no key is available") + w.WriteHeader(http.StatusInternalServerError) + return + } + body, err := ioutil.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusBadRequest) @@ -95,6 +103,7 @@ func scanHandler(logger *zap.SugaredLogger) http.Handler { } defer r.Body.Close() + var payload interface{} reports, err := ruler.NewRuleset(logger).Run(body) if err != nil { w.WriteHeader(http.StatusBadRequest) @@ -102,7 +111,33 @@ func scanHandler(logger *zap.SugaredLogger) http.Handler { return } - res, err := json.Marshal(reports) + if r.URL.Query().Get("in-toto") != "" { + json_key, err := ioutil.ReadFile(keypath) + if err != nil { + logger.Errorf("Attempted to serve an in-toto payload but the key is unavailable: %v", + err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + key, err := in_toto.ParseEd25519FromPrivateJSON(string(json_key)) + if err != nil { + logger.Errorf("Attempted to serve an in-toto payload but the key is unavailable: %v", + err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + link := ruler.GenerateInTotoLink(reports, body) + link.Sign(key) + payload = map[string]interface{}{ + "reports": reports, + "link": link, + } + } else { + payload = reports + } + + res, err := json.Marshal(payload) if err != nil { w.WriteHeader(http.StatusInternalServerError) return