From 9972b9b9464135ad9f18ee17002c57180a3e8b43 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Wed, 5 Feb 2025 14:51:43 +0000 Subject: [PATCH 1/9] feat: add opa allowlisting support Signed-off-by: Arjun Raja Yogidas --- api/router/router.go | 65 ++++++++++++++++++++- api/router/router_test.go | 71 ++++++++++++++++++++++- cmd/finch-daemon/main.go | 99 +++++++++++++++++++++++++++++++- cmd/finch-daemon/router_utils.go | 2 + go.mod | 28 ++++++++- go.sum | 69 ++++++++++++++++++++-- sample.rego | 15 +++++ 7 files changed, 337 insertions(+), 12 deletions(-) create mode 100644 sample.rego diff --git a/api/router/router.go b/api/router/router.go index 65066929..02217d0d 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -5,6 +5,7 @@ package router import ( "context" + "errors" "fmt" "net/http" "os" @@ -15,6 +16,7 @@ import ( "github.com/moby/moby/api/server/httputils" "github.com/moby/moby/api/types/versions" + "github.com/open-policy-agent/opa/v1/rego" "github.com/runfinch/finch-daemon/api/handlers/builder" "github.com/runfinch/finch-daemon/api/handlers/container" "github.com/runfinch/finch-daemon/api/handlers/distribution" @@ -30,6 +32,14 @@ import ( "github.com/runfinch/finch-daemon/version" ) +var errRego = errors.New("error in rego policy file") +var errInput = errors.New("error in HTTP request") + +type inputRegoRequest struct { + Method string + Path string +} + // Options defines the router options to be passed into the handlers. type Options struct { Config *config.Config @@ -41,6 +51,7 @@ type Options struct { VolumeService volume.Service ExecService exec.Service DistributionService distribution.Service + RegoFilePath string // NerdctlWrapper wraps the interactions with nerdctl to build NerdctlWrapper *backend.NerdctlWrapper @@ -48,9 +59,16 @@ type Options struct { // New creates a new router and registers the handlers to it. Returns a handler object // The struct definitions of the HTTP responses come from https://github.com/moby/moby/tree/master/api/types. -func New(opts *Options) http.Handler { +func New(opts *Options) (http.Handler, error) { r := mux.NewRouter() r.Use(VersionMiddleware) + if opts.RegoFilePath != "" { + regoMiddleware, err := CreateRegoMiddleware(opts.RegoFilePath) + if err != nil { + return nil, err + } + r.Use(regoMiddleware) + } vr := types.VersionedRouter{Router: r} logger := flog.NewLogrus() @@ -62,7 +80,7 @@ func New(opts *Options) http.Handler { volume.RegisterHandlers(vr, opts.VolumeService, opts.Config, logger) exec.RegisterHandlers(vr, opts.ExecService, opts.Config, logger) distribution.RegisterHandlers(vr, opts.DistributionService, opts.Config, logger) - return ghandlers.LoggingHandler(os.Stderr, r) + return ghandlers.LoggingHandler(os.Stderr, r), nil } // VersionMiddleware checks for the requested version of the api and makes sure it falls within the bounds @@ -90,3 +108,46 @@ func VersionMiddleware(next http.Handler) http.Handler { next.ServeHTTP(w, newReq) }) } + +// CreateRegoMiddleware dynamically parses the rego file at the path specified in options +// and allows or denies the request based on the policy. +// Will return a nil function and an error if the given file path is blank or invalid. +func CreateRegoMiddleware(regoFilePath string) (func(next http.Handler) http.Handler, error) { + if regoFilePath == "" { + return nil, errRego + } + + query := "data.finch.authz.allow" + nr := rego.New( + rego.Load([]string{regoFilePath}, nil), + rego.Query(query), + ) + + preppedQuery, err := nr.PrepareForEval(context.Background()) + if err != nil { + return nil, err + } + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + input := inputRegoRequest{ + Method: r.Method, + Path: r.URL.Path, + } + + rs, err := preppedQuery.Eval(r.Context(), rego.EvalInput(input)) + if err != nil { + response.SendErrorResponse(w, http.StatusInternalServerError, errInput) + return + } + + if !rs.Allowed() { + response.SendErrorResponse(w, http.StatusForbidden, + fmt.Errorf("method %s not allowed for path %s", r.Method, r.URL.Path)) + return + } + newReq := r.WithContext(r.Context()) + next.ServeHTTP(w, newReq) + }) + }, nil +} diff --git a/api/router/router_test.go b/api/router/router_test.go index 4b9a0115..2b69ed2b 100644 --- a/api/router/router_test.go +++ b/api/router/router_test.go @@ -8,6 +8,8 @@ import ( "fmt" "net/http" "net/http/httptest" + "os" + "path/filepath" "testing" "github.com/containerd/nerdctl/v2/pkg/config" @@ -51,8 +53,9 @@ var _ = Describe("version middleware test", func() { BuilderService: nil, VolumeService: nil, NerdctlWrapper: nil, + RegoFilePath: "", } - h = New(opts) + h, _ = New(opts) rr = httptest.NewRecorder() expected = types.VersionInfo{ Platform: struct { @@ -126,3 +129,69 @@ var _ = Describe("version middleware test", func() { Expect(v).Should(Equal(expected)) }) }) + +// Unit tests for the rego handler. +var _ = Describe("rego middleware test", func() { + var ( + opts *Options + rr *httptest.ResponseRecorder + expected types.VersionInfo + sysSvc *mocks_system.MockService + regoFilePath string + ) + + BeforeEach(func() { + mockCtrl := gomock.NewController(GinkgoT()) + defer mockCtrl.Finish() + + tempDirPath := GinkgoT().TempDir() + regoFilePath = filepath.Join(tempDirPath, "authz.rego") + os.Create(regoFilePath) + + c := config.Config{} + sysSvc = mocks_system.NewMockService(mockCtrl) + opts = &Options{ + Config: &c, + SystemService: sysSvc, + } + rr = httptest.NewRecorder() + expected = types.VersionInfo{} + sysSvc.EXPECT().GetVersion(gomock.Any()).Return(&expected, nil).AnyTimes() + }) + It("should return a 200 error for calls by default", func() { + h, err := New(opts) + Expect(err).Should(BeNil()) + + req, _ := http.NewRequest(http.MethodGet, "/version", nil) + h.ServeHTTP(rr, req) + + Expect(rr).Should(HaveHTTPStatus(http.StatusOK)) + }) + + It("should return a 400 error for disallowed calls", func() { + regoPolicy := `package finch.authz +import rego.v1 + +default allow = false` + + os.WriteFile(regoFilePath, []byte(regoPolicy), 0644) + opts.RegoFilePath = regoFilePath + h, err := New(opts) + Expect(err).Should(BeNil()) + + req, _ := http.NewRequest(http.MethodGet, "/version", nil) + h.ServeHTTP(rr, req) + + Expect(rr).Should(HaveHTTPStatus(http.StatusForbidden)) + }) + + It("should return an error for poorly formed rego files", func() { + regoPolicy := `poorly formed rego file` + + os.WriteFile(regoFilePath, []byte(regoPolicy), 0644) + opts.RegoFilePath = regoFilePath + _, err := New(opts) + + Expect(err).Should(Not(BeNil())) + }) +}) diff --git a/cmd/finch-daemon/main.go b/cmd/finch-daemon/main.go index 314a83f8..55c1a9c2 100644 --- a/cmd/finch-daemon/main.go +++ b/cmd/finch-daemon/main.go @@ -49,6 +49,9 @@ type DaemonOptions struct { debugAddress string configPath string pidFile string + regoFilePath string + enableOpa bool + regoFileLock *flock.Flock } var options = new(DaemonOptions) @@ -67,6 +70,8 @@ func main() { rootCmd.Flags().StringVar(&options.debugAddress, "debug-addr", "", "") rootCmd.Flags().StringVar(&options.configPath, "config-file", defaultConfigPath, "Daemon Config Path") rootCmd.Flags().StringVar(&options.pidFile, "pidfile", defaultPidFile, "pid file location") + rootCmd.Flags().StringVar(&options.regoFilePath, "rego-file", "", "Rego Policy Path") + rootCmd.Flags().BoolVar(&options.enableOpa, "enable-opa", false, "turn on opa allowlisting") if err := rootCmd.Execute(); err != nil { log.Printf("got error: %v", err) log.Fatal(err) @@ -193,6 +198,16 @@ func run(options *DaemonOptions) error { } }() + defer func() { + if options.regoFileLock != nil { + if err := options.regoFileLock.Unlock(); err != nil { + logrus.Errorf("failed to unlock Rego file: %v", err) + } + logger.Infof("rego file unlocked") + // todo : chmod to read-write permissions + } + }() + sdNotify(daemon.SdNotifyReady, logger) serverWg.Wait() logger.Debugln("Server stopped. Exiting...") @@ -215,8 +230,20 @@ func newRouter(options *DaemonOptions, logger *flog.Logrus) (http.Handler, error return nil, err } - opts := createRouterOptions(conf, clientWrapper, ncWrapper, logger) - return router.New(opts), nil + var regoFilePath string + if options.enableOpa { + regoFilePath, err = sanitizeRegoFile(options) + if err != nil { + return nil, err + } + } + + opts := createRouterOptions(conf, clientWrapper, ncWrapper, logger, regoFilePath) + newRouter, err := router.New(opts) + if err != nil { + return nil, err + } + return newRouter, nil } func handleSignal(socket string, server *http.Server, logger *flog.Logrus) { @@ -265,3 +292,71 @@ func defineDockerConfig(uid int) error { return true }) } + +func checkRegoFileValidity(filePath string) error { + fmt.Println("checking file validity.....") + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return fmt.Errorf("provided Rego file path does not exist: %s", filePath) + } + + // Check if the file has a valid extension (.rego, .yaml, or .json) + // validExtensions := []string{".rego", ".yaml", ".yml", ".json"} + fileExt := strings.ToLower(filepath.Ext(options.regoFilePath)) + + if fileExt != ".rego" { + return fmt.Errorf("invalid file extension for Rego file. Only .rego files are supported") + } + + // isValidExtension := false + // for _, ext := range validExtensions { + // if fileExt == ext { + // isValidExtension = true + // break + // } + // } + + // if !isValidExtension { + // return fmt.Errorf("Invalid file extension for Rego file. Allowed extensions are: %v", validExtensions) + // } + + fmt.Println(" file valid!") + return nil +} + +// todo : rename this function to be more descriptve +func sanitizeRegoFile(options *DaemonOptions) (string, error) { + fmt.Println("sanitizeRegoFile called.....") + if options.regoFilePath != "" { + if !options.enableOpa { + return "", fmt.Errorf("rego file path was provided without the --enable-opa flag, please provide the --enable-opa flag") // todo, can we default to setting this flag ourselves is this better UX? + } + + if err := checkRegoFileValidity(options.regoFilePath); err != nil { + return "", err + } + } + + if options.enableOpa && options.regoFilePath == "" { + return "", fmt.Errorf("rego file path not provided, please provide the policy file path using the --rego-file flag") + } + + fileLock := flock.New(options.regoFilePath) + + locked, err := fileLock.TryLock() + if err != nil { + return "", fmt.Errorf("error acquiring lock on rego file: %v", err) + } + if !locked { + return "", fmt.Errorf("unable to acquire lock on rego file, it may be in use by another process") + } + + // Change file permissions to read-only + err = os.Chmod(options.regoFilePath, 0444) // read-only for all users + if err != nil { + fileLock.Unlock() + return "", fmt.Errorf("error changing rego file permissions: %v", err) + } + options.regoFileLock = fileLock + + return options.regoFilePath, nil +} diff --git a/cmd/finch-daemon/router_utils.go b/cmd/finch-daemon/router_utils.go index 0261f4c4..3aa4175e 100644 --- a/cmd/finch-daemon/router_utils.go +++ b/cmd/finch-daemon/router_utils.go @@ -96,6 +96,7 @@ func createRouterOptions( clientWrapper *backend.ContainerdClientWrapper, ncWrapper *backend.NerdctlWrapper, logger *flog.Logrus, + regoFilePath string, ) *router.Options { fs := afero.NewOsFs() tarCreator := archive.NewTarCreator(ecc.NewExecCmdCreator(), logger) @@ -112,5 +113,6 @@ func createRouterOptions( ExecService: exec.NewService(clientWrapper, logger), DistributionService: distribution.NewService(clientWrapper, ncWrapper, logger), NerdctlWrapper: ncWrapper, + RegoFilePath: regoFilePath, } } diff --git a/go.mod b/go.mod index 8c19f2e9..70108905 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,29 @@ require ( google.golang.org/protobuf v1.36.6 ) +require ( + github.com/OneOfOne/xxhash v1.2.8 // indirect + github.com/agnivade/levenshtein v1.2.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect + github.com/tchap/go-patricia/v2 v2.3.2 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/yashtewari/glob-intersection v0.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/sdk v1.35.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) + require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect @@ -115,6 +138,7 @@ require ( github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.0.7 // indirect + github.com/open-policy-agent/opa v1.1.0 github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 // indirect github.com/opencontainers/selinux v1.12.0 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect @@ -132,7 +156,6 @@ require ( github.com/vbatts/tar-split v0.11.6 // indirect github.com/yuchanns/srslog v1.1.0 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect @@ -144,13 +167,12 @@ require ( golang.org/x/sync v0.14.0 // indirect golang.org/x/term v0.32.0 // indirect golang.org/x/text v0.25.0 // indirect - golang.org/x/time v0.8.0 // indirect + golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.31.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/grpc v1.72.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect tags.cncf.io/container-device-interface v1.0.1 // indirect tags.cncf.io/container-device-interface/specs-go v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index 1c38955f..61e6d23c 100644 --- a/go.sum +++ b/go.sum @@ -12,9 +12,23 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA= github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok= +github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= +github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY= +github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= +github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -84,6 +98,12 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger/v4 v4.5.1 h1:7DCIXrQjo1LKmM96YD+hLVJ2EEsyyoWxJfpdd56HLps= +github.com/dgraph-io/badger/v4 v4.5.1/go.mod h1:qn3Be0j3TfV4kPbVoK0arXCD1/nr1ftth6sbL5jxdoA= +github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I= +github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= @@ -98,6 +118,8 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -108,12 +130,18 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fluent/fluent-logger-golang v1.10.0 h1:JcLj8u3WclQv2juHGKTSzBRM5vIZjEqbrmvn/n+m1W0= github.com/fluent/fluent-logger-golang v1.10.0/go.mod h1:UNyv8FAGmQcYJRtk+yfxhWqWUwsabTipgjXvBDR8kTs= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/getlantern/httptest v0.0.0-20161025015934-4b40f4c7e590 h1:OhyiFx+yBN30O3IHrIq+9LAEhy6o7fin21wUQxF8NiE= github.com/getlantern/httptest v0.0.0-20161025015934-4b40f4c7e590/go.mod h1:rE/jidqqHHG9sjSxC24Gd5YCfZ1AT91C2wjJ28TAOfA= github.com/getlantern/mockconn v0.0.0-20200818071412-cb30d065a848 h1:2MhMMVBTnaHrst6HyWFDhwQCaJ05PZuOv1bE2gN8WFY= github.com/getlantern/mockconn v0.0.0-20200818071412-cb30d065a848/go.mod h1:+F5GJ7qGpQ03DBtcOEyQpM30ix4BLswdaojecFtsdy8= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -127,6 +155,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -150,6 +180,8 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8= +github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -170,6 +202,8 @@ github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyE github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -199,6 +233,8 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/ github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= @@ -253,10 +289,14 @@ github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7B github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/open-policy-agent/opa v1.1.0 h1:HMz2evdEMTyNqtdLjmu3Vyx06BmhNYAx67Yz3Ll9q2s= +github.com/open-policy-agent/opa v1.1.0/go.mod h1:T1pASQ1/vwfTa+e2fYcfpLCvWgYtqtiUv+IuA/dLPQs= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -282,9 +322,17 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rootless-containers/bypass4netns v0.4.2 h1:JUZcpX7VLRfDkLxBPC6fyNalJGv9MjnjECOilZIvKRc= @@ -326,6 +374,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= +github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/urfave/cli v1.19.1/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -344,6 +394,8 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= +github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/yuchanns/srslog v1.1.0 h1:CEm97Xxxd8XpJThE0gc/XsqUGgPufh5u5MUjC27/KOk= github.com/yuchanns/srslog v1.1.0/go.mod h1:HsLjdv3XV02C3kgBW2bTyW6i88OQE+VYJZIxrPKPPak= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -357,6 +409,10 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRND go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= @@ -365,10 +421,12 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= -go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= -go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -474,8 +532,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= -golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -499,6 +557,9 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= diff --git a/sample.rego b/sample.rego new file mode 100644 index 00000000..750c9783 --- /dev/null +++ b/sample.rego @@ -0,0 +1,15 @@ +package finch.authz + +import future.keywords.if +import rego.v1 + +default allow = false + +allow if { + not is_container_create +} + +is_container_create if { + input.Method == "POST" + input.Path == "/v1.43/containers/create" +} \ No newline at end of file From 96fa3533da638b105d0c400ab7fc06fdc9a96ffb Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Thu, 6 Feb 2025 22:33:15 +0000 Subject: [PATCH 2/9] chore: add e2e tests Signed-off-by: Arjun Raja Yogidas --- .github/workflows/ci.yaml | 10 ++++ Makefile | 8 +++ cmd/finch-daemon/main.go | 32 +++++------ e2e/e2e_test.go | 37 ++++++++++++- e2e/tests/opa_middleware.go | 102 ++++++++++++++++++++++++++++++++++++ 5 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 e2e/tests/opa_middleware.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b35014a4..60466fce 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -109,3 +109,13 @@ jobs: run: sudo bin/finch-daemon --debug --socket-owner $UID & - name: Run e2e test run: sudo make test-e2e + - name: Clean up Daemon socket + run: sudo rm /var/run/finch.sock && sudo rm /run/finch.pid + - name: Verify Rego file presence + run: ls -l ${{ github.workspace }}/sample.rego + - name: Set Rego file path + run: echo "REGO_FILE_PATH=${{ github.workspace }}/sample.rego" >> $GITHUB_ENV + - name: Start finch-daemon with opa Authz + run: sudo bin/finch-daemon --debug --enable-opa --rego-file ${{ github.workspace }}/sample.rego --socket-owner $UID & + - name: Run opa e2e tests + run: sudo -E make test-e2e-opa diff --git a/Makefile b/Makefile index 1622edeb..db415ca2 100644 --- a/Makefile +++ b/Makefile @@ -114,6 +114,14 @@ test-e2e: linux TEST_E2E=1 \ $(GINKGO) $(GFLAGS) ./e2e/... +.PHONY: test-e2e-opa +test-e2e-opa: linux + DOCKER_HOST="unix:///run/finch.sock" \ + DOCKER_API_VERSION="v1.43" \ + OPA_E2E=1 \ + TEST_E2E=1 \ + $(GINKGO) $(GFLAGS) ./e2e/... + .PHONY: licenses licenses: PATH=$(BIN):$(PATH) go-licenses report --template="scripts/third-party-license.tpl" --ignore github.com/runfinch ./... > THIRD_PARTY_LICENSES diff --git a/cmd/finch-daemon/main.go b/cmd/finch-daemon/main.go index 55c1a9c2..f201fef5 100644 --- a/cmd/finch-daemon/main.go +++ b/cmd/finch-daemon/main.go @@ -200,11 +200,16 @@ func run(options *DaemonOptions) error { defer func() { if options.regoFileLock != nil { + // unlock the rego file upon daemon exit if err := options.regoFileLock.Unlock(); err != nil { logrus.Errorf("failed to unlock Rego file: %v", err) } logger.Infof("rego file unlocked") - // todo : chmod to read-write permissions + + // make rego file editable upon daemon exit + if err := os.Chmod(options.regoFilePath, 0600); err != nil { + logrus.Errorf("failed to change file permissions of rego file: %v", err) + } } }() @@ -293,39 +298,26 @@ func defineDockerConfig(uid int) error { }) } +// checkRegoFileValidity verifies that the given rego file exists and has the right file extension func checkRegoFileValidity(filePath string) error { - fmt.Println("checking file validity.....") if _, err := os.Stat(filePath); os.IsNotExist(err) { return fmt.Errorf("provided Rego file path does not exist: %s", filePath) } - // Check if the file has a valid extension (.rego, .yaml, or .json) - // validExtensions := []string{".rego", ".yaml", ".yml", ".json"} + // Check if the file has a valid extension (.rego) fileExt := strings.ToLower(filepath.Ext(options.regoFilePath)) if fileExt != ".rego" { return fmt.Errorf("invalid file extension for Rego file. Only .rego files are supported") } - // isValidExtension := false - // for _, ext := range validExtensions { - // if fileExt == ext { - // isValidExtension = true - // break - // } - // } - - // if !isValidExtension { - // return fmt.Errorf("Invalid file extension for Rego file. Allowed extensions are: %v", validExtensions) - // } - - fmt.Println(" file valid!") return nil } -// todo : rename this function to be more descriptve +// sanitizeRegoFile validates and prepares the Rego policy file for use. +// It checks validates the file, acquires a file lock, +// and sets rego file to be read-only. func sanitizeRegoFile(options *DaemonOptions) (string, error) { - fmt.Println("sanitizeRegoFile called.....") if options.regoFilePath != "" { if !options.enableOpa { return "", fmt.Errorf("rego file path was provided without the --enable-opa flag, please provide the --enable-opa flag") // todo, can we default to setting this flag ourselves is this better UX? @@ -351,7 +343,7 @@ func sanitizeRegoFile(options *DaemonOptions) (string, error) { } // Change file permissions to read-only - err = os.Chmod(options.regoFilePath, 0444) // read-only for all users + err = os.Chmod(options.regoFilePath, 0400) if err != nil { fileLock.Unlock() return "", fmt.Errorf("error changing rego file permissions: %v", err) diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index 0d82caec..f1b5516e 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -5,6 +5,7 @@ package e2e import ( "flag" + "fmt" "log" "os" "strings" @@ -25,8 +26,12 @@ var SubjectPrefix = flag.String("daemon-context-subject-prefix", "", `A string w var PrefixedSubjectEnv = flag.String("daemon-context-subject-env", "", `Environment to add when running a prefixed subject, in the form of a string like "EXAMPLE=foo EXAMPLE2=bar"`) func TestRun(t *testing.T) { - if os.Getenv("TEST_E2E") != "1" { - t.Skip("E2E tests skipped. Set TEST_E2E=1 to run these tests") + if os.Getenv("OPA_E2E") == "1" { + runOPATests(t) + } else if os.Getenv("TEST_E2E") == "1" { + runE2ETests(t) + } else { + t.Skip("E2E tests skipped. Set TEST_E2E=1 to run regular E2E tests or OPA_E2E=1 to run OPA middleware tests") } if err := parseTestFlags(); err != nil { @@ -35,6 +40,34 @@ func TestRun(t *testing.T) { } opt, _ := option.New([]string{*Subject, "--namespace", "finch"}) +} + +func runOPATests(t *testing.T) { + opt, _ := option.New([]string{*Subject, "--namespace", "finch"}) + + ginkgo.SynchronizedBeforeSuite(func() []byte { + tests.SetupLocalRegistry(opt) + return nil + }, func(bytes []byte) {}) + + ginkgo.SynchronizedAfterSuite(func() { + tests.CleanupLocalRegistry(opt) + // clean up everything after the local registry is cleaned up + command.RemoveAll(opt) + }, func() {}) + + const description = "Finch Daemon OPA E2E Tests" + ginkgo.Describe(description, func() { + tests.OpaMiddlewareTest(opt) + fmt.Print(opt) + }) + + gomega.RegisterFailHandler(ginkgo.Fail) + ginkgo.RunSpecs(t, description) +} + +func runE2ETests(t *testing.T) { + opt, _ := option.New([]string{*Subject, "--namespace", "finch"}) ginkgo.SynchronizedBeforeSuite(func() []byte { tests.SetupLocalRegistry(opt) diff --git a/e2e/tests/opa_middleware.go b/e2e/tests/opa_middleware.go new file mode 100644 index 00000000..db9cca7d --- /dev/null +++ b/e2e/tests/opa_middleware.go @@ -0,0 +1,102 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package tests + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "os" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/runfinch/common-tests/command" + "github.com/runfinch/common-tests/option" + + "github.com/runfinch/finch-daemon/api/types" + "github.com/runfinch/finch-daemon/e2e/client" +) + +// OpaMiddlewareTest tests the OPA functionality. +func OpaMiddlewareTest(opt *option.Option) { + Describe("test opa middleware functionality", func() { + var ( + uClient *http.Client + version string + wantContainerName string + options types.ContainerCreateRequest + createUrl string + ) + BeforeEach(func() { + // create a custom client to use http over unix sockets + uClient = client.NewClient(GetDockerHostUrl()) + // get the docker api version that will be tested + version = GetDockerApiVersion() + wantContainerName = fmt.Sprintf("/%s", testContainerName) + // set default container options + options = types.ContainerCreateRequest{} + options.Image = defaultImage + createUrl = client.ConvertToFinchUrl(version, "/containers/create") + }) + AfterEach(func() { + command.RemoveAll(opt) + }) + It("should allow GET version API request", func() { + res, err := uClient.Get(client.ConvertToFinchUrl("", "/version")) + Expect(err).ShouldNot(HaveOccurred()) + jd := json.NewDecoder(res.Body) + var v types.VersionInfo + err = jd.Decode(&v) + Expect(err).ShouldNot(HaveOccurred()) + Expect(v.Version).ShouldNot(BeNil()) + Expect(v.ApiVersion).Should(Equal("1.43")) + fmt.Println(version) + }) + + It("shold allow GET containers API request", func() { + id := command.StdoutStr(opt, "run", "-d", "--name", testContainerName, defaultImage, "sleep", "infinity") + want := []types.ContainerListItem{ + { + Id: id[:12], + Names: []string{wantContainerName}, + }, + } + + res, err := uClient.Get(client.ConvertToFinchUrl(version, "/containers/json")) + Expect(err).Should(BeNil()) + Expect(res.StatusCode).Should(Equal(http.StatusOK)) + var got []types.ContainerListItem + err = json.NewDecoder(res.Body).Decode(&got) + Expect(err).Should(BeNil()) + Expect(len(got)).Should(Equal(2)) + got = filterContainerList(got) + Expect(got).Should(ContainElements(want)) + }) + + It("shold disallow POST containers/create API request", func() { + options.Cmd = []string{"echo", "hello world"} + + reqBody, err := json.Marshal(options) + Expect(err).Should(BeNil()) + + fmt.Println("createUrl = ", createUrl) + res, _ := uClient.Post(createUrl, "application/json", bytes.NewReader(reqBody)) + + Expect(res.StatusCode).Should(Equal(http.StatusForbidden)) + }) + + It("should not allow updates to the rego file", func() { + regoFilePath := os.Getenv("REGO_FILE_PATH") + Expect(regoFilePath).NotTo(BeEmpty(), "REGO_FILE_PATH environment variable should be set") + + fileInfo, err := os.Stat(regoFilePath) + Expect(err).NotTo(HaveOccurred(), "Failed to get Rego file info") + + // Check file permissions + mode := fileInfo.Mode() + Expect(mode.Perm()).To(Equal(os.FileMode(0400)), "Rego file should be read-only (0400)") + }) + }) +} From 2943796d0400467d3ba59e2667a7d08684b47ec1 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Fri, 7 Feb 2025 21:48:56 +0000 Subject: [PATCH 3/9] docs: update opa middleware docs Signed-off-by: Arjun Raja Yogidas --- cmd/finch-daemon/main.go | 2 +- docs/opa-middleware.md | 42 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 docs/opa-middleware.md diff --git a/cmd/finch-daemon/main.go b/cmd/finch-daemon/main.go index f201fef5..ceaecbd1 100644 --- a/cmd/finch-daemon/main.go +++ b/cmd/finch-daemon/main.go @@ -298,7 +298,7 @@ func defineDockerConfig(uid int) error { }) } -// checkRegoFileValidity verifies that the given rego file exists and has the right file extension +// checkRegoFileValidity verifies that the given rego file exists and has the right file extension. func checkRegoFileValidity(filePath string) error { if _, err := os.Stat(filePath); os.IsNotExist(err) { return fmt.Errorf("provided Rego file path does not exist: %s", filePath) diff --git a/docs/opa-middleware.md b/docs/opa-middleware.md new file mode 100644 index 00000000..3c3d237d --- /dev/null +++ b/docs/opa-middleware.md @@ -0,0 +1,42 @@ +# Applying OPA authz policies + +This guide provides instructions for setting up [OPA](https://github.com/open-policy-agent/opa) authz policies with the finch-daemon. Authz policies allow users to allowlist or deny certain resources based on policy rules. + +## What Is OPA Authz implementation +Open Policy Agent (OPA) is an open-source, general-purpose policy engine that enables unified, context-aware policy enforcement across the entire stack. OPA provides a high-level declarative language, Rego, for specifying policy as code and simple APIs to offload policy decision-making from your software. + +In the current implementation, users can use OPA Rego policies to filter API requests at the Daemon level. It's important to note that the current implementation only supports allowlisting of requests. This means you can specify which requests should be allowed, and all others will be denied by default. + +## Setting up a policy + +Use the [sample rego](../sample.rego) policy template to build your policy rules. + +The package name must be `finch.authz`, the daemon middleware will look for the result of the `allow` key on each API call to determine wether to allow/deny the request. +An approved request will go through without any events, a rejected request will fail with status code 403 + +Example: + +The following policy blocks all API requests made to the daemon. +``` +package finch.authz + +default allow = false + +``` +`allow` can be modified based on the business requirements for example we can prevent users from creating new containers by preventing them from accessing the create API + +``` +allow if { + not (input.Method == "POST" and input.Path == "/v1.43/containers/create") +} +``` +Use the [Rego playground](https://play.openpolicyagent.org/) to fine tune your rego policies + +## Enable OPA Middleware + +Once you are ready with your policy document, use the `--enable-opa` flag to tell the finch-daemon to enable the OPA middleware. The daemon will then look for the policy document provided by the `--rego-file` flag. + +Note: The `--rego-file` flag is required when `--enable-opa` is set. + +Example: +`sudo bin/finch-daemon --debug --socket-owner $UID --socket-addr /run/finch-test.sock --pidfile /run/finch-test.pid --enable-opa --rego-file //finch-daemon/sample.rego &` \ No newline at end of file From fbb21c0bedeb41818e15bc2bab4984b83318931b Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Fri, 7 Feb 2025 23:28:24 +0000 Subject: [PATCH 4/9] chore: update variable names Signed-off-by: Arjun Raja Yogidas --- .github/workflows/ci.yaml | 2 +- Makefile | 2 +- cmd/finch-daemon/main.go | 28 ++++++++++++++-------------- docs/opa-middleware.md | 6 +++--- e2e/e2e_test.go | 4 ++-- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 60466fce..ee944aed 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -116,6 +116,6 @@ jobs: - name: Set Rego file path run: echo "REGO_FILE_PATH=${{ github.workspace }}/sample.rego" >> $GITHUB_ENV - name: Start finch-daemon with opa Authz - run: sudo bin/finch-daemon --debug --enable-opa --rego-file ${{ github.workspace }}/sample.rego --socket-owner $UID & + run: sudo bin/finch-daemon --debug --enable-middleware --rego-file ${{ github.workspace }}/sample.rego --socket-owner $UID & - name: Run opa e2e tests run: sudo -E make test-e2e-opa diff --git a/Makefile b/Makefile index db415ca2..0f515ea9 100644 --- a/Makefile +++ b/Makefile @@ -118,7 +118,7 @@ test-e2e: linux test-e2e-opa: linux DOCKER_HOST="unix:///run/finch.sock" \ DOCKER_API_VERSION="v1.43" \ - OPA_E2E=1 \ + MIDDLEWARE_E2E=1 \ TEST_E2E=1 \ $(GINKGO) $(GFLAGS) ./e2e/... diff --git a/cmd/finch-daemon/main.go b/cmd/finch-daemon/main.go index ceaecbd1..c048a15f 100644 --- a/cmd/finch-daemon/main.go +++ b/cmd/finch-daemon/main.go @@ -43,15 +43,15 @@ const ( ) type DaemonOptions struct { - debug bool - socketAddr string - socketOwner int - debugAddress string - configPath string - pidFile string - regoFilePath string - enableOpa bool - regoFileLock *flock.Flock + debug bool + socketAddr string + socketOwner int + debugAddress string + configPath string + pidFile string + regoFilePath string + enableMiddleware bool + regoFileLock *flock.Flock } var options = new(DaemonOptions) @@ -71,7 +71,7 @@ func main() { rootCmd.Flags().StringVar(&options.configPath, "config-file", defaultConfigPath, "Daemon Config Path") rootCmd.Flags().StringVar(&options.pidFile, "pidfile", defaultPidFile, "pid file location") rootCmd.Flags().StringVar(&options.regoFilePath, "rego-file", "", "Rego Policy Path") - rootCmd.Flags().BoolVar(&options.enableOpa, "enable-opa", false, "turn on opa allowlisting") + rootCmd.Flags().BoolVar(&options.enableMiddleware, "enable-middleware", false, "turn on middleware for allowlisting") if err := rootCmd.Execute(); err != nil { log.Printf("got error: %v", err) log.Fatal(err) @@ -236,7 +236,7 @@ func newRouter(options *DaemonOptions, logger *flog.Logrus) (http.Handler, error } var regoFilePath string - if options.enableOpa { + if options.enableMiddleware { regoFilePath, err = sanitizeRegoFile(options) if err != nil { return nil, err @@ -319,8 +319,8 @@ func checkRegoFileValidity(filePath string) error { // and sets rego file to be read-only. func sanitizeRegoFile(options *DaemonOptions) (string, error) { if options.regoFilePath != "" { - if !options.enableOpa { - return "", fmt.Errorf("rego file path was provided without the --enable-opa flag, please provide the --enable-opa flag") // todo, can we default to setting this flag ourselves is this better UX? + if !options.enableMiddleware { + return "", fmt.Errorf("rego file path was provided without the --enable-middleware flag, please provide the --enable-middleware flag") // todo, can we default to setting this flag ourselves is this better UX? } if err := checkRegoFileValidity(options.regoFilePath); err != nil { @@ -328,7 +328,7 @@ func sanitizeRegoFile(options *DaemonOptions) (string, error) { } } - if options.enableOpa && options.regoFilePath == "" { + if options.enableMiddleware && options.regoFilePath == "" { return "", fmt.Errorf("rego file path not provided, please provide the policy file path using the --rego-file flag") } diff --git a/docs/opa-middleware.md b/docs/opa-middleware.md index 3c3d237d..77bfec78 100644 --- a/docs/opa-middleware.md +++ b/docs/opa-middleware.md @@ -34,9 +34,9 @@ Use the [Rego playground](https://play.openpolicyagent.org/) to fine tune your r ## Enable OPA Middleware -Once you are ready with your policy document, use the `--enable-opa` flag to tell the finch-daemon to enable the OPA middleware. The daemon will then look for the policy document provided by the `--rego-file` flag. +Once you are ready with your policy document, use the `--enable-middleware` flag to tell the finch-daemon to enable the OPA middleware. The daemon will then look for the policy document provided by the `--rego-file` flag. -Note: The `--rego-file` flag is required when `--enable-opa` is set. +Note: The `--rego-file` flag is required when `--enable-middleware` is set. Example: -`sudo bin/finch-daemon --debug --socket-owner $UID --socket-addr /run/finch-test.sock --pidfile /run/finch-test.pid --enable-opa --rego-file //finch-daemon/sample.rego &` \ No newline at end of file +`sudo bin/finch-daemon --debug --socket-owner $UID --socket-addr /run/finch-test.sock --pidfile /run/finch-test.pid --enable-middleware --rego-file //finch-daemon/sample.rego &` \ No newline at end of file diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index f1b5516e..ff049493 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -26,12 +26,12 @@ var SubjectPrefix = flag.String("daemon-context-subject-prefix", "", `A string w var PrefixedSubjectEnv = flag.String("daemon-context-subject-env", "", `Environment to add when running a prefixed subject, in the form of a string like "EXAMPLE=foo EXAMPLE2=bar"`) func TestRun(t *testing.T) { - if os.Getenv("OPA_E2E") == "1" { + if os.Getenv("MIDDLEWARE_E2E") == "1" { runOPATests(t) } else if os.Getenv("TEST_E2E") == "1" { runE2ETests(t) } else { - t.Skip("E2E tests skipped. Set TEST_E2E=1 to run regular E2E tests or OPA_E2E=1 to run OPA middleware tests") + t.Skip("E2E tests skipped. Set TEST_E2E=1 to run regular E2E tests or MIDDLEWARE_E2E=1 to run OPA middleware tests") } if err := parseTestFlags(); err != nil { From 220bb4c7ddecd112e970826e2fef0568f94cb3e0 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Fri, 21 Mar 2025 02:26:06 +0000 Subject: [PATCH 5/9] fix: update tests and add logging Signed-off-by: Arjun Raja Yogidas --- .github/workflows/ci.yaml | 6 +- api/router/router.go | 3 + cmd/finch-daemon/main.go | 74 +--------- cmd/finch-daemon/router_utils.go | 80 +++++++++++ cmd/finch-daemon/router_utils_test.go | 130 ++++++++++++++++++ docs/opa-middleware.md | 75 +++++++++- .../case1-incompatible_API.rego | 9 +- docs/sample-rego-policies/default.rego | 38 +++++ docs/sample-rego-policies/test.rego | 39 ++++++ e2e/tests/opa_middleware.go | 38 +++-- 10 files changed, 406 insertions(+), 86 deletions(-) rename sample.rego => docs/sample-rego-policies/case1-incompatible_API.rego (53%) create mode 100644 docs/sample-rego-policies/default.rego create mode 100644 docs/sample-rego-policies/test.rego diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ee944aed..574a6b87 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -112,10 +112,10 @@ jobs: - name: Clean up Daemon socket run: sudo rm /var/run/finch.sock && sudo rm /run/finch.pid - name: Verify Rego file presence - run: ls -l ${{ github.workspace }}/sample.rego + run: ls -l ${{ github.workspace }}/docs/sample-rego-policies/default.rego - name: Set Rego file path - run: echo "REGO_FILE_PATH=${{ github.workspace }}/sample.rego" >> $GITHUB_ENV + run: echo "REGO_FILE_PATH=${{ github.workspace }}/docs/sample-rego-policies/default.rego" >> $GITHUB_ENV - name: Start finch-daemon with opa Authz - run: sudo bin/finch-daemon --debug --enable-middleware --rego-file ${{ github.workspace }}/sample.rego --socket-owner $UID & + run: sudo bin/finch-daemon --debug --enable-middleware --rego-file ${{ github.workspace }}/docs/sample-rego-policies/default.rego --socket-owner $UID & - name: Run opa e2e tests run: sudo -E make test-e2e-opa diff --git a/api/router/router.go b/api/router/router.go index 02217d0d..9c1b4728 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -135,6 +135,7 @@ func CreateRegoMiddleware(regoFilePath string) (func(next http.Handler) http.Han Path: r.URL.Path, } + fmt.Printf("Evaluating policy rules for API request with Method = %s and Path = %s \n", input.Method, input.Path) rs, err := preppedQuery.Eval(r.Context(), rego.EvalInput(input)) if err != nil { response.SendErrorResponse(w, http.StatusInternalServerError, errInput) @@ -142,6 +143,8 @@ func CreateRegoMiddleware(regoFilePath string) (func(next http.Handler) http.Han } if !rs.Allowed() { + // need to log evaluation result in order to mitigate Repudiation threat + fmt.Printf("Evaluation result: failed, method %s not allowed for path %s \n", r.Method, r.URL.Path) response.SendErrorResponse(w, http.StatusForbidden, fmt.Errorf("method %s not allowed for path %s", r.Method, r.URL.Path)) return diff --git a/cmd/finch-daemon/main.go b/cmd/finch-daemon/main.go index c048a15f..17148049 100644 --- a/cmd/finch-daemon/main.go +++ b/cmd/finch-daemon/main.go @@ -149,6 +149,10 @@ func run(options *DaemonOptions) error { logger := flog.NewLogrus() r, err := newRouter(options, logger) if err != nil { + // call regoFile cleanup function here to unlock previously locked file + if options.regoFilePath != "" { + cleanupRegoFile(options, logger) + } return fmt.Errorf("failed to create a router: %w", err) } @@ -198,20 +202,7 @@ func run(options *DaemonOptions) error { } }() - defer func() { - if options.regoFileLock != nil { - // unlock the rego file upon daemon exit - if err := options.regoFileLock.Unlock(); err != nil { - logrus.Errorf("failed to unlock Rego file: %v", err) - } - logger.Infof("rego file unlocked") - - // make rego file editable upon daemon exit - if err := os.Chmod(options.regoFilePath, 0600); err != nil { - logrus.Errorf("failed to change file permissions of rego file: %v", err) - } - } - }() + defer cleanupRegoFile(options, logger) sdNotify(daemon.SdNotifyReady, logger) serverWg.Wait() @@ -297,58 +288,3 @@ func defineDockerConfig(uid int) error { return true }) } - -// checkRegoFileValidity verifies that the given rego file exists and has the right file extension. -func checkRegoFileValidity(filePath string) error { - if _, err := os.Stat(filePath); os.IsNotExist(err) { - return fmt.Errorf("provided Rego file path does not exist: %s", filePath) - } - - // Check if the file has a valid extension (.rego) - fileExt := strings.ToLower(filepath.Ext(options.regoFilePath)) - - if fileExt != ".rego" { - return fmt.Errorf("invalid file extension for Rego file. Only .rego files are supported") - } - - return nil -} - -// sanitizeRegoFile validates and prepares the Rego policy file for use. -// It checks validates the file, acquires a file lock, -// and sets rego file to be read-only. -func sanitizeRegoFile(options *DaemonOptions) (string, error) { - if options.regoFilePath != "" { - if !options.enableMiddleware { - return "", fmt.Errorf("rego file path was provided without the --enable-middleware flag, please provide the --enable-middleware flag") // todo, can we default to setting this flag ourselves is this better UX? - } - - if err := checkRegoFileValidity(options.regoFilePath); err != nil { - return "", err - } - } - - if options.enableMiddleware && options.regoFilePath == "" { - return "", fmt.Errorf("rego file path not provided, please provide the policy file path using the --rego-file flag") - } - - fileLock := flock.New(options.regoFilePath) - - locked, err := fileLock.TryLock() - if err != nil { - return "", fmt.Errorf("error acquiring lock on rego file: %v", err) - } - if !locked { - return "", fmt.Errorf("unable to acquire lock on rego file, it may be in use by another process") - } - - // Change file permissions to read-only - err = os.Chmod(options.regoFilePath, 0400) - if err != nil { - fileLock.Unlock() - return "", fmt.Errorf("error changing rego file permissions: %v", err) - } - options.regoFileLock = fileLock - - return options.regoFilePath, nil -} diff --git a/cmd/finch-daemon/router_utils.go b/cmd/finch-daemon/router_utils.go index 3aa4175e..b14756b4 100644 --- a/cmd/finch-daemon/router_utils.go +++ b/cmd/finch-daemon/router_utils.go @@ -7,11 +7,14 @@ import ( "errors" "fmt" "os" + "path/filepath" + "strings" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/pkg/namespaces" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/config" + "github.com/gofrs/flock" toml "github.com/pelletier/go-toml/v2" "github.com/runfinch/finch-daemon/api/router" "github.com/runfinch/finch-daemon/internal/backend" @@ -26,6 +29,7 @@ import ( "github.com/runfinch/finch-daemon/pkg/archive" "github.com/runfinch/finch-daemon/pkg/ecc" "github.com/runfinch/finch-daemon/pkg/flog" + "github.com/sirupsen/logrus" "github.com/spf13/afero" ) @@ -90,6 +94,45 @@ func createContainerdClient(conf *config.Config) (*backend.ContainerdClientWrapp return backend.NewContainerdClientWrapper(client), nil } +// sanitizeRegoFile validates and prepares the Rego policy file for use. +// It checks validates the file, acquires a file lock, +// and sets rego file to be read-only. +func sanitizeRegoFile(options *DaemonOptions) (string, error) { + if options.regoFilePath != "" { + if !options.enableMiddleware { + return "", fmt.Errorf("rego file path was provided without the --enable-middleware flag, please provide the --enable-middleware flag") // todo, can we default to setting this flag ourselves is this better UX? + } + + if err := checkRegoFileValidity(options.regoFilePath); err != nil { + return "", err + } + } + + if options.enableMiddleware && options.regoFilePath == "" { + return "", fmt.Errorf("rego file path not provided, please provide the policy file path using the --rego-file flag") + } + + fileLock := flock.New(options.regoFilePath) + + locked, err := fileLock.TryLock() + if err != nil { + return "", fmt.Errorf("error acquiring lock on rego file: %v", err) + } + if !locked { + return "", fmt.Errorf("unable to acquire lock on rego file, it may be in use by another process") + } + + // Change file permissions to read-only + err = os.Chmod(options.regoFilePath, 0400) + if err != nil { + fileLock.Unlock() + return "", fmt.Errorf("error changing rego file permissions: %v", err) + } + options.regoFileLock = fileLock + + return options.regoFilePath, nil +} + // createRouterOptions creates router options by initializing all required services. func createRouterOptions( conf *config.Config, @@ -116,3 +159,40 @@ func createRouterOptions( RegoFilePath: regoFilePath, } } + +// checkRegoFileValidity verifies that the given rego file exists and has the right file extension. +func checkRegoFileValidity(regoFilePath string) error { + fmt.Println("filepath in checkRegoFileValidity = ", regoFilePath) + if _, err := os.Stat(regoFilePath); os.IsNotExist(err) { + return fmt.Errorf("provided Rego file path does not exist: %s", regoFilePath) + } + + // Check if the file has a valid extension (.rego) + fileExt := strings.ToLower(filepath.Ext(regoFilePath)) + + fmt.Println("fileExt = ", fileExt) + if fileExt != ".rego" { + return fmt.Errorf("invalid file extension for Rego file. Only .rego files are supported") + } + + return nil +} + +func cleanupRegoFile(options *DaemonOptions, logger *flog.Logrus) { + if options.regoFileLock == nil { + return // Already cleaned up or nothing to clean + } + + // unlock the rego file + if err := options.regoFileLock.Unlock(); err != nil { + logrus.Errorf("failed to unlock Rego file: %v", err) + } + logger.Infof("rego file unlocked") + + // make rego file editable + if err := os.Chmod(options.regoFilePath, 0600); err != nil { + logrus.Errorf("failed to change file permissions of rego file: %v", err) + } + + options.regoFileLock = nil +} diff --git a/cmd/finch-daemon/router_utils_test.go b/cmd/finch-daemon/router_utils_test.go index c5ee9537..ddd585cd 100644 --- a/cmd/finch-daemon/router_utils_test.go +++ b/cmd/finch-daemon/router_utils_test.go @@ -4,10 +4,14 @@ package main import ( + "fmt" "os" + "path/filepath" "testing" "github.com/containerd/nerdctl/v2/pkg/config" + "github.com/gofrs/flock" + "github.com/runfinch/finch-daemon/pkg/flog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -71,3 +75,129 @@ namespace = "test_namespace" assert.Equal(t, "test_address", cfg.Address) assert.Equal(t, "test_namespace", cfg.Namespace) } + +func TestCleanupRegoFile(t *testing.T) { + tests := []struct { + name string + setupFunc func() (*DaemonOptions, *flog.Logrus, func()) + }{ + { + name: "successful cleanup", + setupFunc: func() (*DaemonOptions, *flog.Logrus, func()) { + tmpFile, err := os.CreateTemp("", "test.rego") + require.NoError(t, err) + + fileLock := flock.New(tmpFile.Name()) + _, err = fileLock.TryLock() + require.NoError(t, err) + + err = os.Chmod(tmpFile.Name(), 0400) + require.NoError(t, err) + + logger := flog.NewLogrus() + + cleanup := func() { + os.Remove(tmpFile.Name()) + } + + return &DaemonOptions{ + regoFilePath: tmpFile.Name(), + regoFileLock: fileLock, + }, logger, cleanup + }, + }, + { + name: "nil lock handle", + setupFunc: func() (*DaemonOptions, *flog.Logrus, func()) { + return &DaemonOptions{ + regoFileLock: nil, + }, flog.NewLogrus(), func() {} + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + options, logger, cleanup := tt.setupFunc() + defer cleanup() + + cleanupRegoFile(options, logger) + + if options.regoFilePath != "" { + // Verify file permissions are restored + info, err := os.Stat(options.regoFilePath) + require.NoError(t, err) + assert.Equal(t, os.FileMode(0600), info.Mode().Perm()) + } + + // Verify lock is released + assert.Nil(t, options.regoFileLock) + }) + } +} + +func TestCheckRegoFileValidity(t *testing.T) { + tests := []struct { + name string + setupFunc func() (string, func()) + expectedError string + }{ + { + name: "valid rego file", + setupFunc: func() (string, func()) { + // Create a temporary directory + tmpDir, err := os.MkdirTemp("", "rego_test") + require.NoError(t, err) + + // Create a file with .rego extension and proper content + regoPath := filepath.Join(tmpDir, "test.rego") + regoContent := `package finch.authz + +import future.keywords.if +import rego.v1 + +default allow = false +` + fmt.Println("regopath = ", regoPath) + err = os.WriteFile(regoPath, []byte(regoContent), 0600) + require.NoError(t, err) + + return regoPath, func() { + os.RemoveAll(tmpDir) + } + }, + expectedError: "", + }, + { + name: "non-existent file", + setupFunc: func() (string, func()) { + return filepath.Join(os.TempDir(), "nonexistent.rego"), func() {} + }, + expectedError: "provided Rego file path does not exist", + }, + { + name: "wrong extension", + setupFunc: func() (string, func()) { + tmpFile, err := os.CreateTemp("", "test.txt") + require.NoError(t, err) + return tmpFile.Name(), func() { os.Remove(tmpFile.Name()) } + }, + expectedError: "invalid file extension", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filePath, cleanup := tt.setupFunc() + defer cleanup() + + err := checkRegoFileValidity(filePath) + + if tt.expectedError != "" { + assert.ErrorContains(t, err, tt.expectedError) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/docs/opa-middleware.md b/docs/opa-middleware.md index 77bfec78..dfee49c8 100644 --- a/docs/opa-middleware.md +++ b/docs/opa-middleware.md @@ -9,7 +9,7 @@ In the current implementation, users can use OPA Rego policies to filter API req ## Setting up a policy -Use the [sample rego](../sample.rego) policy template to build your policy rules. +Use the [sample rego](../docs/sample-rego-policies/default.rego) policy template to build your policy rules. The package name must be `finch.authz`, the daemon middleware will look for the result of the `allow` key on each API call to determine wether to allow/deny the request. An approved request will go through without any events, a rejected request will fail with status code 403 @@ -39,4 +39,75 @@ Once you are ready with your policy document, use the `--enable-middleware` flag Note: The `--rego-file` flag is required when `--enable-middleware` is set. Example: -`sudo bin/finch-daemon --debug --socket-owner $UID --socket-addr /run/finch-test.sock --pidfile /run/finch-test.pid --enable-middleware --rego-file //finch-daemon/sample.rego &` \ No newline at end of file +`sudo bin/finch-daemon --debug --socket-owner $UID --socket-addr /run/finch-test.sock --pidfile /run/finch-test.pid --enable-middleware --rego-file //finch-daemon/docs/sample-rego-policies/default.rego &` + + +# Best practices for secure rego policies + +## Comprehensive API Path Protection + +When writing Rego policies, it's crucial to implement thorough path matching to prevent unintended access to APIs. The daemon processes API paths without strict prefix validation, which could lead to security bypasses. + +## Path Matching Best Practices + +``` +package finch.authz + +import future.keywords.if +import rego.v1 + +# Use pattern matching for comprehensive path protection +is_container_api if { + glob.match("/*/containers/*", [], input.Path) +} + +is_container_create if { + input.Method == "POST" + glob.match("/*/containers/create", [], input.Path) +} + +# Protect against path variations +allow if { + not is_container_api # Blocks all container-related paths + not is_container_create # Specifically blocks container creation +} +``` +Use these [example policies](https://github.com/open-policy-agent/opa-docker-authz/blob/2c7eb5c729fca70a3e5cda6f15c2d9cc121b9481/example.rego) to build your opa policy + +Remember that only `Method` and `Path` is the only values that +gets passed to the opa middleware. + + +### Common Security Pitfalls + +- **Incomplete Path Matching**: Always use pattern matching functions like glob.match() instead of exact string matching to catch path variations. +- **Missing HTTP Methods**: Consider all HTTP methods that could access a resource (GET, POST, PUT, DELETE). +- **Alternative API Endpoints**: Be aware that some operations can be performed through multiple endpoints. + +### Monitoring and Alerting +The finch-daemon's inability to start due to policy issues could impact system operations. Implement System Service Monitoring in order to be on top of any such failures. + +### Security Recommendations +- Policy Testing + - Test policies in a non-production environment + - Use the [rego playground](https://play.openpolicyagent.org/) to test policies +- Logging and Audit + - Enable comprehensive logging of policy decisions + - Monitor for unexpected denials + + +### Critical Security Considerations : Rego Policy File Protection +The Rego policy file is a critical security control. +Any user with sudo privileges can: + +- Modify the policy file to weaken security controls +- Replace the policy with a more permissive version +- Disable policy enforcement entirely + +#### Recomended Security Controls + +- Access Controls + - Restrict sudo access to specific commands +- Monitoring + - Monitor policy file changes + - Monitor daemon service status \ No newline at end of file diff --git a/sample.rego b/docs/sample-rego-policies/case1-incompatible_API.rego similarity index 53% rename from sample.rego rename to docs/sample-rego-policies/case1-incompatible_API.rego index 750c9783..ae6fe9b9 100644 --- a/sample.rego +++ b/docs/sample-rego-policies/case1-incompatible_API.rego @@ -11,5 +11,10 @@ allow if { is_container_create if { input.Method == "POST" - input.Path == "/v1.43/containers/create" -} \ No newline at end of file + glob.match("/**/containers/create", ["/"], input.Path) +} + +is_swarm_api if { + input.Method == "GET" + glob.match("/**/swarm", ["/"], input.Path) +} diff --git a/docs/sample-rego-policies/default.rego b/docs/sample-rego-policies/default.rego new file mode 100644 index 00000000..efa0223e --- /dev/null +++ b/docs/sample-rego-policies/default.rego @@ -0,0 +1,38 @@ +package finch.authz + +import future.keywords.if +import rego.v1 + +default allow = false + +allow if { + not is_container_create + not is_networs_api + not is_swarm_api + not is_plugins +} + +is_container_create if { + input.Method == "POST" + glob.match("/**/containers/create", ["/"], input.Path) +} + +is_networs_api if { + input.Method == "GET" + glob.match("/**/networks", ["/"], input.Path) +} + +is_swarm_api if { + input.Method == "GET" + glob.match("/**/swarm", ["/"], input.Path) +} + +is_plugins if { + input.Method == "GET" + glob.match("/**/plugins", ["/"], input.Path) +} + +is_forbidden_container if { + intpu.Method == "GET" + glob.match("/**/container/1f576a797a486438548377124f6cb7770a5cb7c8ff6a11c069cb4128d3f59462/json", ["/"], input.Path) +} \ No newline at end of file diff --git a/docs/sample-rego-policies/test.rego b/docs/sample-rego-policies/test.rego new file mode 100644 index 00000000..906e2abc --- /dev/null +++ b/docs/sample-rego-policies/test.rego @@ -0,0 +1,39 @@ +package finch.authz + +import future.keywords.if +import rego.v1 + +default allow = false + +allow if { + not is_container_create + not is_malformed_api + not is_containers +} + +# Helper rule to ensure path starts with API version +is_api_path if { + startswith(input.Path, "/v1.43/") +} + +# Helper rule to check if path contains containers endpoint regardless of prefix +contains_containers if { + contains(input.Path, "/containers/") +} + +is_container_create if { + input.Method == "POST" + contains_containers + endswith(input.Path, "/create") +} + +is_malformed_api if { + input.Method == "GET" + contains(input.Path, "/plugins") +} + +is_containers if { + input.Method == "GET" + contains_containers + endswith(input.Path, "/json") +} \ No newline at end of file diff --git a/e2e/tests/opa_middleware.go b/e2e/tests/opa_middleware.go index db9cca7d..c20c027a 100644 --- a/e2e/tests/opa_middleware.go +++ b/e2e/tests/opa_middleware.go @@ -23,11 +23,13 @@ import ( func OpaMiddlewareTest(opt *option.Option) { Describe("test opa middleware functionality", func() { var ( - uClient *http.Client - version string - wantContainerName string - options types.ContainerCreateRequest - createUrl string + uClient *http.Client + version string + wantContainerName string + containerCreateOptions types.ContainerCreateRequest + createUrl string + unimplementedUnspecifiedUrl string + unimplementedSpecifiedUrl string ) BeforeEach(func() { // create a custom client to use http over unix sockets @@ -35,10 +37,12 @@ func OpaMiddlewareTest(opt *option.Option) { // get the docker api version that will be tested version = GetDockerApiVersion() wantContainerName = fmt.Sprintf("/%s", testContainerName) - // set default container options - options = types.ContainerCreateRequest{} - options.Image = defaultImage + // set default container containerCreateOptions + containerCreateOptions = types.ContainerCreateRequest{} + containerCreateOptions.Image = defaultImage createUrl = client.ConvertToFinchUrl(version, "/containers/create") + unimplementedUnspecifiedUrl = client.ConvertToFinchUrl(version, "/secrets") + unimplementedSpecifiedUrl = client.ConvertToFinchUrl(version, "/swarm") }) AfterEach(func() { command.RemoveAll(opt) @@ -76,9 +80,9 @@ func OpaMiddlewareTest(opt *option.Option) { }) It("shold disallow POST containers/create API request", func() { - options.Cmd = []string{"echo", "hello world"} + containerCreateOptions.Cmd = []string{"echo", "hello world"} - reqBody, err := json.Marshal(options) + reqBody, err := json.Marshal(containerCreateOptions) Expect(err).Should(BeNil()) fmt.Println("createUrl = ", createUrl) @@ -98,5 +102,19 @@ func OpaMiddlewareTest(opt *option.Option) { mode := fileInfo.Mode() Expect(mode.Perm()).To(Equal(os.FileMode(0400)), "Rego file should be read-only (0400)") }) + + It("should fail unimplemented API calls, fail via daemon", func() { + fmt.Println("incompatibleUrl = ", unimplementedUnspecifiedUrl) + res, _ := uClient.Get(unimplementedUnspecifiedUrl) + + Expect(res.StatusCode).Should(Equal(http.StatusNotFound)) + }) + + It("should fail non implemented API calls,even if specified in the rego file", func() { + fmt.Println("incompatibleUrl = ", unimplementedSpecifiedUrl) + res, _ := uClient.Get(unimplementedSpecifiedUrl) + + Expect(res.StatusCode).Should(Equal(http.StatusNotFound)) + }) }) } From f70fc9c42bb304f016b8f8ebaac7cb1636e59d3c Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Wed, 2 Apr 2025 17:27:56 +0000 Subject: [PATCH 6/9] chore: add tests Signed-off-by: Arjun Raja Yogidas --- docs/opa-middleware.md | 15 ++++++- .../case1-incompatible_API.rego | 20 ---------- docs/sample-rego-policies/default.rego | 2 +- docs/sample-rego-policies/test.rego | 39 ------------------- e2e/e2e_test.go | 11 ++++-- 5 files changed, 22 insertions(+), 65 deletions(-) delete mode 100644 docs/sample-rego-policies/case1-incompatible_API.rego delete mode 100644 docs/sample-rego-policies/test.rego diff --git a/docs/opa-middleware.md b/docs/opa-middleware.md index dfee49c8..ffbf1f44 100644 --- a/docs/opa-middleware.md +++ b/docs/opa-middleware.md @@ -46,7 +46,20 @@ Example: ## Comprehensive API Path Protection -When writing Rego policies, it's crucial to implement thorough path matching to prevent unintended access to APIs. The daemon processes API paths without strict prefix validation, which could lead to security bypasses. +When writing Rego policies, use pattern matching for API paths to prevent unauthorized access. Simple string matching can be bypassed by adding prefixes to API paths. + +Consider this potentially vulnerable policy that tries to restrict access to a specific container: +``` +# INCORRECT: Can be bypassed +allow if { + not (input.Path == "/v1.43/containers/sensitive-container/json") +} +``` +This policy can be bypassed in multiple ways: +1. Using container ID instead of name: `/v1.43/containers/abc123.../json` +2. Adding path prefixes: `/custom/v1.43/containers/sensitive-container/json` + +Follow the path matching best practices below to properly secure your resources. ## Path Matching Best Practices diff --git a/docs/sample-rego-policies/case1-incompatible_API.rego b/docs/sample-rego-policies/case1-incompatible_API.rego deleted file mode 100644 index ae6fe9b9..00000000 --- a/docs/sample-rego-policies/case1-incompatible_API.rego +++ /dev/null @@ -1,20 +0,0 @@ -package finch.authz - -import future.keywords.if -import rego.v1 - -default allow = false - -allow if { - not is_container_create -} - -is_container_create if { - input.Method == "POST" - glob.match("/**/containers/create", ["/"], input.Path) -} - -is_swarm_api if { - input.Method == "GET" - glob.match("/**/swarm", ["/"], input.Path) -} diff --git a/docs/sample-rego-policies/default.rego b/docs/sample-rego-policies/default.rego index efa0223e..e1fa7737 100644 --- a/docs/sample-rego-policies/default.rego +++ b/docs/sample-rego-policies/default.rego @@ -33,6 +33,6 @@ is_plugins if { } is_forbidden_container if { - intpu.Method == "GET" + input.Method == "GET" glob.match("/**/container/1f576a797a486438548377124f6cb7770a5cb7c8ff6a11c069cb4128d3f59462/json", ["/"], input.Path) } \ No newline at end of file diff --git a/docs/sample-rego-policies/test.rego b/docs/sample-rego-policies/test.rego deleted file mode 100644 index 906e2abc..00000000 --- a/docs/sample-rego-policies/test.rego +++ /dev/null @@ -1,39 +0,0 @@ -package finch.authz - -import future.keywords.if -import rego.v1 - -default allow = false - -allow if { - not is_container_create - not is_malformed_api - not is_containers -} - -# Helper rule to ensure path starts with API version -is_api_path if { - startswith(input.Path, "/v1.43/") -} - -# Helper rule to check if path contains containers endpoint regardless of prefix -contains_containers if { - contains(input.Path, "/containers/") -} - -is_container_create if { - input.Method == "POST" - contains_containers - endswith(input.Path, "/create") -} - -is_malformed_api if { - input.Method == "GET" - contains(input.Path, "/plugins") -} - -is_containers if { - input.Method == "GET" - contains_containers - endswith(input.Path, "/json") -} \ No newline at end of file diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index ff049493..7a7db9a0 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -33,17 +33,15 @@ func TestRun(t *testing.T) { } else { t.Skip("E2E tests skipped. Set TEST_E2E=1 to run regular E2E tests or MIDDLEWARE_E2E=1 to run OPA middleware tests") } +} +func runOPATests(t *testing.T) { if err := parseTestFlags(); err != nil { log.Println("failed to parse go test flags", err) os.Exit(1) } opt, _ := option.New([]string{*Subject, "--namespace", "finch"}) -} - -func runOPATests(t *testing.T) { - opt, _ := option.New([]string{*Subject, "--namespace", "finch"}) ginkgo.SynchronizedBeforeSuite(func() []byte { tests.SetupLocalRegistry(opt) @@ -67,6 +65,11 @@ func runOPATests(t *testing.T) { } func runE2ETests(t *testing.T) { + if err := parseTestFlags(); err != nil { + log.Println("failed to parse go test flags", err) + os.Exit(1) + } + opt, _ := option.New([]string{*Subject, "--namespace", "finch"}) ginkgo.SynchronizedBeforeSuite(func() []byte { From 9167b13654b12b1d65a827071bd015e69bb7ea6c Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Wed, 9 Apr 2025 15:27:38 +0000 Subject: [PATCH 7/9] chore: update e2e_test.go Signed-off-by: Arjun Raja Yogidas --- e2e/e2e_test.go | 198 ++++++++++++++++++++++++++++-------------------- 1 file changed, 116 insertions(+), 82 deletions(-) diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index 7a7db9a0..fde29726 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -5,7 +5,6 @@ package e2e import ( "flag" - "fmt" "log" "os" "strings" @@ -20,29 +19,36 @@ import ( "github.com/runfinch/finch-daemon/e2e/util" ) -// Subject defines which CLI the tests are run against, defaults to \"nerdctl\" in the user's PATH. -var Subject = flag.String("subject", "nerdctl", `which CLI the tests are run against, defaults to "nerdctl" in the user's PATH.`) -var SubjectPrefix = flag.String("daemon-context-subject-prefix", "", `A string which prefixes the command the tests are run against, defaults to "". This string will be split by spaces.`) -var PrefixedSubjectEnv = flag.String("daemon-context-subject-env", "", `Environment to add when running a prefixed subject, in the form of a string like "EXAMPLE=foo EXAMPLE2=bar"`) +const ( + defaultNamespace = "finch" + testE2EEnv = "TEST_E2E" + middlewareE2EEnv = "MIDDLEWARE_E2E" + opaTestDescription = "Finch Daemon OPA E2E Tests" + e2eTestDescription = "Finch Daemon Functional test" +) + +var ( + Subject = flag.String("subject", "nerdctl", `which CLI the tests are run against, defaults to "nerdctl" in the user's PATH.`) + SubjectPrefix = flag.String("daemon-context-subject-prefix", "", `A string which prefixes the command the tests are run against, defaults to "". This string will be split by spaces.`) + PrefixedSubjectEnv = flag.String("daemon-context-subject-env", "", `Environment to add when running a prefixed subject, in the form of a string like "EXAMPLE=foo EXAMPLE2=bar"`) +) func TestRun(t *testing.T) { - if os.Getenv("MIDDLEWARE_E2E") == "1" { + switch { + case os.Getenv(middlewareE2EEnv) == "1": runOPATests(t) - } else if os.Getenv("TEST_E2E") == "1" { + case os.Getenv(testE2EEnv) == "1": runE2ETests(t) - } else { + default: t.Skip("E2E tests skipped. Set TEST_E2E=1 to run regular E2E tests or MIDDLEWARE_E2E=1 to run OPA middleware tests") } } -func runOPATests(t *testing.T) { - if err := parseTestFlags(); err != nil { - log.Println("failed to parse go test flags", err) - os.Exit(1) - } - - opt, _ := option.New([]string{*Subject, "--namespace", "finch"}) +func createTestOption() (*option.Option, error) { + return option.New([]string{*Subject, "--namespace", defaultNamespace}) +} +func setupTestSuite(opt *option.Option) { ginkgo.SynchronizedBeforeSuite(func() []byte { tests.SetupLocalRegistry(opt) return nil @@ -50,95 +56,123 @@ func runOPATests(t *testing.T) { ginkgo.SynchronizedAfterSuite(func() { tests.CleanupLocalRegistry(opt) - // clean up everything after the local registry is cleaned up command.RemoveAll(opt) }, func() {}) +} + +func runOPATests(t *testing.T) { + if err := parseTestFlags(); err != nil { + log.Fatal("failed to parse go test flags:", err) + } + + opt, err := createTestOption() + if err != nil { + log.Fatal("failed to create test option:", err) + } - const description = "Finch Daemon OPA E2E Tests" - ginkgo.Describe(description, func() { + setupTestSuite(opt) + + ginkgo.Describe(opaTestDescription, func() { tests.OpaMiddlewareTest(opt) - fmt.Print(opt) }) - gomega.RegisterFailHandler(ginkgo.Fail) - ginkgo.RunSpecs(t, description) + runTests(t, opaTestDescription) } func runE2ETests(t *testing.T) { if err := parseTestFlags(); err != nil { - log.Println("failed to parse go test flags", err) - os.Exit(1) + log.Fatal("failed to parse go test flags:", err) } - opt, _ := option.New([]string{*Subject, "--namespace", "finch"}) + opt, err := createTestOption() + if err != nil { + log.Fatal("failed to create test option:", err) + } - ginkgo.SynchronizedBeforeSuite(func() []byte { - tests.SetupLocalRegistry(opt) - return nil - }, func(bytes []byte) {}) + setupTestSuite(opt) - ginkgo.SynchronizedAfterSuite(func() { - tests.CleanupLocalRegistry(opt) - // clean up everything after the local registry is cleaned up - command.RemoveAll(opt) - }, func() {}) + pOpt := createPrefixedOption() - var pOpt = option.New - if *SubjectPrefix != "" { - var modifiers []option.Modifier - if *PrefixedSubjectEnv != "" { - modifiers = append(modifiers, option.Env(strings.Split(*PrefixedSubjectEnv, " "))) - } - pOpt = util.WrappedOption(strings.Split(*SubjectPrefix, " "), modifiers...) + ginkgo.Describe(e2eTestDescription, func() { + runContainerTests(opt) + runVolumeTests(opt) + runNetworkTests(opt, pOpt) + runImageTests(opt) + runSystemTests(opt) + runDistributionTests(opt) + }) + + runTests(t, e2eTestDescription) +} + +func createPrefixedOption() func([]string, ...option.Modifier) (*option.Option, error) { + if *SubjectPrefix == "" { + return option.New } - const description = "Finch Daemon Functional test" - ginkgo.Describe(description, func() { - // functional test for container APIs - tests.ContainerCreate(opt, pOpt) - tests.ContainerStart(opt) - tests.ContainerStop(opt) - tests.ContainerRestart(opt) - tests.ContainerRemove(opt) - tests.ContainerList(opt) - tests.ContainerRename(opt) - tests.ContainerStats(opt) - tests.ContainerAttach(opt) - tests.ContainerLogs(opt) - tests.ContainerKill(opt) - tests.ContainerInspect(opt) - tests.ContainerWait(opt) - tests.ContainerPause(opt) - tests.ContainerUnpause(opt) - - // functional test for volume APIs - tests.VolumeList(opt) - tests.VolumeInspect(opt) - tests.VolumeRemove(opt) - - // functional test for network APIs - tests.NetworkCreate(opt, pOpt) - tests.NetworkRemove(opt) - tests.NetworkList(opt) - tests.NetworkInspect(opt) - - // functional test for image APIs - tests.ImageRemove(opt) - tests.ImagePush(opt) - tests.ImagePull(opt) - - // functional test for system api - tests.SystemVersion(opt) - tests.SystemEvents(opt) - - // functional test for distribution api - tests.DistributionInspect(opt) - }) + var modifiers []option.Modifier + if *PrefixedSubjectEnv != "" { + modifiers = append(modifiers, option.Env(strings.Split(*PrefixedSubjectEnv, " "))) + } + return util.WrappedOption(strings.Split(*SubjectPrefix, " "), modifiers...) +} +func runTests(t *testing.T, description string) { gomega.RegisterFailHandler(ginkgo.Fail) ginkgo.RunSpecs(t, description) } +// functional test for container APIs. +func runContainerTests(opt *option.Option) { + tests.ContainerCreate(opt) + tests.ContainerStart(opt) + tests.ContainerStop(opt) + tests.ContainerRestart(opt) + tests.ContainerRemove(opt) + tests.ContainerList(opt) + tests.ContainerRename(opt) + tests.ContainerStats(opt) + tests.ContainerAttach(opt) + tests.ContainerLogs(opt) + tests.ContainerKill(opt) + tests.ContainerInspect(opt) + tests.ContainerWait(opt) + tests.ContainerPause(opt) +} + +// functional test for volume APIs. +func runVolumeTests(opt *option.Option) { + tests.VolumeList(opt) + tests.VolumeInspect(opt) + tests.VolumeRemove(opt) +} + +// functional test for network APIs. +func runNetworkTests(opt *option.Option, pOpt func([]string, ...option.Modifier) (*option.Option, error)) { + tests.NetworkCreate(opt, pOpt) + tests.NetworkRemove(opt) + tests.NetworkList(opt) + tests.NetworkInspect(opt) +} + +// functional test for image APIs. +func runImageTests(opt *option.Option) { + tests.ImageRemove(opt) + tests.ImagePush(opt) + tests.ImagePull(opt) +} + +// . +func runSystemTests(opt *option.Option) { + tests.SystemVersion(opt) + tests.SystemEvents(opt) +} + +// functional test for distribution api. +func runDistributionTests(opt *option.Option) { + tests.DistributionInspect(opt) +} + // parseTestFlags parses go test flags because pflag package ignores flags with '-test.' prefix // Related issues: // https://github.com/spf13/pflag/issues/63 From c806d8ca522cb97fe993d657051efa5f7a20ffa0 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Fri, 16 May 2025 07:06:17 +0000 Subject: [PATCH 8/9] chore: add file permission check Signed-off-by: Arjun Raja Yogidas --- .github/workflows/ci.yaml | 18 ++-- Makefile | 7 +- api/router/router.go | 19 ++-- cmd/finch-daemon/main.go | 32 +++---- cmd/finch-daemon/router_utils.go | 89 +++++------------- cmd/finch-daemon/router_utils_test.go | 93 ++++--------------- docs/opa-middleware.md | 39 ++++++-- .../{default.rego => example.rego} | 6 +- e2e/e2e_test.go | 6 +- e2e/tests/opa_middleware.go | 59 +++++++++--- e2e/tests/tests.go | 8 ++ 11 files changed, 173 insertions(+), 203 deletions(-) rename docs/sample-rego-policies/{default.rego => example.rego} (93%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 574a6b87..5c25c512 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -105,17 +105,19 @@ jobs: run: | sudo ls /etc/cni/net.d sudo rm /etc/cni/net.d/87-podman-bridge.conflist + - name: Verify Rego file presence + run: ls -l ${{ github.workspace }}/docs/sample-rego-policies/example.rego + - name: Set Rego file path + run: echo "REGO_FILE_PATH=${{ github.workspace }}/docs/sample-rego-policies/example.rego" >> $GITHUB_ENV + - name: Start finch-daemon with opa Authz + run: sudo bin/finch-daemon --debug --enable-opa-middleware --rego-file ${{ github.workspace }}/docs/sample-rego-policies/example.rego --skip-rego-perm-check --socket-owner $UID --socket-addr /run/finch.sock --pidfile /run/finch.pid & + - name: Run opa e2e tests + run: sudo -E make test-e2e-opa + - name: Clean up Daemon socket + run: sudo rm /run/finch.sock && sudo rm /run/finch.pid - name: Start finch-daemon run: sudo bin/finch-daemon --debug --socket-owner $UID & - name: Run e2e test run: sudo make test-e2e - name: Clean up Daemon socket run: sudo rm /var/run/finch.sock && sudo rm /run/finch.pid - - name: Verify Rego file presence - run: ls -l ${{ github.workspace }}/docs/sample-rego-policies/default.rego - - name: Set Rego file path - run: echo "REGO_FILE_PATH=${{ github.workspace }}/docs/sample-rego-policies/default.rego" >> $GITHUB_ENV - - name: Start finch-daemon with opa Authz - run: sudo bin/finch-daemon --debug --enable-middleware --rego-file ${{ github.workspace }}/docs/sample-rego-policies/default.rego --socket-owner $UID & - - name: Run opa e2e tests - run: sudo -E make test-e2e-opa diff --git a/Makefile b/Makefile index 0f515ea9..a567ebed 100644 --- a/Makefile +++ b/Makefile @@ -117,9 +117,10 @@ test-e2e: linux .PHONY: test-e2e-opa test-e2e-opa: linux DOCKER_HOST="unix:///run/finch.sock" \ - DOCKER_API_VERSION="v1.43" \ + DOCKER_API_VERSION="v1.41" \ MIDDLEWARE_E2E=1 \ - TEST_E2E=1 \ + TEST_E2E=0 \ + DAEMON_ROOT="$(BIN)/finch-daemon" \ $(GINKGO) $(GFLAGS) ./e2e/... .PHONY: licenses @@ -134,4 +135,4 @@ coverage: linux .PHONY: release release: linux @echo "$@" - @$(FINCH_DAEMON_PROJECT_ROOT)/scripts/create-releases.sh $(RELEASE_TAG) + @$(FINCH_DAEMON_PROJECT_ROOT)/scripts/create-releases.sh $(RELEASE_TAG) \ No newline at end of file diff --git a/api/router/router.go b/api/router/router.go index 9c1b4728..fc6a6559 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -62,16 +62,17 @@ type Options struct { func New(opts *Options) (http.Handler, error) { r := mux.NewRouter() r.Use(VersionMiddleware) + + logger := flog.NewLogrus() + if opts.RegoFilePath != "" { - regoMiddleware, err := CreateRegoMiddleware(opts.RegoFilePath) + regoMiddleware, err := CreateRegoMiddleware(opts.RegoFilePath, logger) if err != nil { return nil, err } r.Use(regoMiddleware) } vr := types.VersionedRouter{Router: r} - - logger := flog.NewLogrus() system.RegisterHandlers(vr, opts.SystemService, opts.Config, opts.NerdctlWrapper, logger) image.RegisterHandlers(vr, opts.ImageService, opts.Config, logger) container.RegisterHandlers(vr, opts.ContainerService, opts.Config, logger) @@ -112,7 +113,7 @@ func VersionMiddleware(next http.Handler) http.Handler { // CreateRegoMiddleware dynamically parses the rego file at the path specified in options // and allows or denies the request based on the policy. // Will return a nil function and an error if the given file path is blank or invalid. -func CreateRegoMiddleware(regoFilePath string) (func(next http.Handler) http.Handler, error) { +func CreateRegoMiddleware(regoFilePath string, logger *flog.Logrus) (func(next http.Handler) http.Handler, error) { if regoFilePath == "" { return nil, errRego } @@ -135,20 +136,24 @@ func CreateRegoMiddleware(regoFilePath string) (func(next http.Handler) http.Han Path: r.URL.Path, } - fmt.Printf("Evaluating policy rules for API request with Method = %s and Path = %s \n", input.Method, input.Path) + logger.Debugf("OPA input being evaluated: Method=%s, Path=%s", input.Method, input.Path) + rs, err := preppedQuery.Eval(r.Context(), rego.EvalInput(input)) if err != nil { + logger.Errorf("OPA policy evaluation failed: %v", err) response.SendErrorResponse(w, http.StatusInternalServerError, errInput) return } + logger.Debugf("OPA evaluation results: %+v", rs) + if !rs.Allowed() { - // need to log evaluation result in order to mitigate Repudiation threat - fmt.Printf("Evaluation result: failed, method %s not allowed for path %s \n", r.Method, r.URL.Path) + logger.Infof("OPA request denied: Method=%s, Path=%s", r.Method, r.URL.Path) response.SendErrorResponse(w, http.StatusForbidden, fmt.Errorf("method %s not allowed for path %s", r.Method, r.URL.Path)) return } + logger.Debugf("OPA request allowed: Method=%s, Path=%s", r.Method, r.URL.Path) newReq := r.WithContext(r.Context()) next.ServeHTTP(w, newReq) }) diff --git a/cmd/finch-daemon/main.go b/cmd/finch-daemon/main.go index 17148049..2951972b 100644 --- a/cmd/finch-daemon/main.go +++ b/cmd/finch-daemon/main.go @@ -43,15 +43,15 @@ const ( ) type DaemonOptions struct { - debug bool - socketAddr string - socketOwner int - debugAddress string - configPath string - pidFile string - regoFilePath string - enableMiddleware bool - regoFileLock *flock.Flock + debug bool + socketAddr string + socketOwner int + debugAddress string + configPath string + pidFile string + regoFilePath string + enableOPAMiddleware bool + skipRegoPermCheck bool } var options = new(DaemonOptions) @@ -71,7 +71,9 @@ func main() { rootCmd.Flags().StringVar(&options.configPath, "config-file", defaultConfigPath, "Daemon Config Path") rootCmd.Flags().StringVar(&options.pidFile, "pidfile", defaultPidFile, "pid file location") rootCmd.Flags().StringVar(&options.regoFilePath, "rego-file", "", "Rego Policy Path") - rootCmd.Flags().BoolVar(&options.enableMiddleware, "enable-middleware", false, "turn on middleware for allowlisting") + rootCmd.Flags().BoolVar(&options.enableOPAMiddleware, "enable-opa-middleware", false, "turn on OPA middleware for allowlisting") + rootCmd.Flags().BoolVar(&options.skipRegoPermCheck, "skip-rego-perm-check", false, "skip the rego file permission check (allows permissions more permissive than 0600)") + if err := rootCmd.Execute(); err != nil { log.Printf("got error: %v", err) log.Fatal(err) @@ -149,10 +151,6 @@ func run(options *DaemonOptions) error { logger := flog.NewLogrus() r, err := newRouter(options, logger) if err != nil { - // call regoFile cleanup function here to unlock previously locked file - if options.regoFilePath != "" { - cleanupRegoFile(options, logger) - } return fmt.Errorf("failed to create a router: %w", err) } @@ -202,8 +200,6 @@ func run(options *DaemonOptions) error { } }() - defer cleanupRegoFile(options, logger) - sdNotify(daemon.SdNotifyReady, logger) serverWg.Wait() logger.Debugln("Server stopped. Exiting...") @@ -227,8 +223,8 @@ func newRouter(options *DaemonOptions, logger *flog.Logrus) (http.Handler, error } var regoFilePath string - if options.enableMiddleware { - regoFilePath, err = sanitizeRegoFile(options) + if options.enableOPAMiddleware { + regoFilePath, err = checkRegoFileValidity(options, logger) if err != nil { return nil, err } diff --git a/cmd/finch-daemon/router_utils.go b/cmd/finch-daemon/router_utils.go index b14756b4..839eccef 100644 --- a/cmd/finch-daemon/router_utils.go +++ b/cmd/finch-daemon/router_utils.go @@ -14,7 +14,6 @@ import ( "github.com/containerd/containerd/v2/pkg/namespaces" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/config" - "github.com/gofrs/flock" toml "github.com/pelletier/go-toml/v2" "github.com/runfinch/finch-daemon/api/router" "github.com/runfinch/finch-daemon/internal/backend" @@ -29,7 +28,6 @@ import ( "github.com/runfinch/finch-daemon/pkg/archive" "github.com/runfinch/finch-daemon/pkg/ecc" "github.com/runfinch/finch-daemon/pkg/flog" - "github.com/sirupsen/logrus" "github.com/spf13/afero" ) @@ -94,45 +92,6 @@ func createContainerdClient(conf *config.Config) (*backend.ContainerdClientWrapp return backend.NewContainerdClientWrapper(client), nil } -// sanitizeRegoFile validates and prepares the Rego policy file for use. -// It checks validates the file, acquires a file lock, -// and sets rego file to be read-only. -func sanitizeRegoFile(options *DaemonOptions) (string, error) { - if options.regoFilePath != "" { - if !options.enableMiddleware { - return "", fmt.Errorf("rego file path was provided without the --enable-middleware flag, please provide the --enable-middleware flag") // todo, can we default to setting this flag ourselves is this better UX? - } - - if err := checkRegoFileValidity(options.regoFilePath); err != nil { - return "", err - } - } - - if options.enableMiddleware && options.regoFilePath == "" { - return "", fmt.Errorf("rego file path not provided, please provide the policy file path using the --rego-file flag") - } - - fileLock := flock.New(options.regoFilePath) - - locked, err := fileLock.TryLock() - if err != nil { - return "", fmt.Errorf("error acquiring lock on rego file: %v", err) - } - if !locked { - return "", fmt.Errorf("unable to acquire lock on rego file, it may be in use by another process") - } - - // Change file permissions to read-only - err = os.Chmod(options.regoFilePath, 0400) - if err != nil { - fileLock.Unlock() - return "", fmt.Errorf("error changing rego file permissions: %v", err) - } - options.regoFileLock = fileLock - - return options.regoFilePath, nil -} - // createRouterOptions creates router options by initializing all required services. func createRouterOptions( conf *config.Config, @@ -160,39 +119,37 @@ func createRouterOptions( } } -// checkRegoFileValidity verifies that the given rego file exists and has the right file extension. -func checkRegoFileValidity(regoFilePath string) error { - fmt.Println("filepath in checkRegoFileValidity = ", regoFilePath) - if _, err := os.Stat(regoFilePath); os.IsNotExist(err) { - return fmt.Errorf("provided Rego file path does not exist: %s", regoFilePath) +// checkRegoFileValidity validates and prepares the Rego policy file for use. +// It verifies that the file exists, has the right extension (.rego), and has appropriate permissions. +func checkRegoFileValidity(options *DaemonOptions, logger *flog.Logrus) (string, error) { + if options.regoFilePath == "" { + return "", fmt.Errorf("rego file path not provided, please provide the policy file path using the --rego-file flag") } - // Check if the file has a valid extension (.rego) - fileExt := strings.ToLower(filepath.Ext(regoFilePath)) - - fmt.Println("fileExt = ", fileExt) - if fileExt != ".rego" { - return fmt.Errorf("invalid file extension for Rego file. Only .rego files are supported") + if _, err := os.Stat(options.regoFilePath); os.IsNotExist(err) { + return "", fmt.Errorf("provided Rego file path does not exist: %s", options.regoFilePath) } - return nil -} + // Check if the file has a valid extension (.rego) + fileExt := strings.ToLower(filepath.Ext(options.regoFilePath)) -func cleanupRegoFile(options *DaemonOptions, logger *flog.Logrus) { - if options.regoFileLock == nil { - return // Already cleaned up or nothing to clean + if fileExt != ".rego" { + return "", fmt.Errorf("invalid file extension for Rego file. Only .rego files are supported") } - // unlock the rego file - if err := options.regoFileLock.Unlock(); err != nil { - logrus.Errorf("failed to unlock Rego file: %v", err) - } - logger.Infof("rego file unlocked") + if !options.skipRegoPermCheck { + fileInfo, err := os.Stat(options.regoFilePath) + if err != nil { + return "", fmt.Errorf("error checking rego file permissions: %v", err) + } - // make rego file editable - if err := os.Chmod(options.regoFilePath, 0600); err != nil { - logrus.Errorf("failed to change file permissions of rego file: %v", err) + if fileInfo.Mode().Perm()&0177 != 0 { + return "", fmt.Errorf("rego file permissions %o are too permissive (maximum allowable permissions: 0600)", fileInfo.Mode().Perm()) + } + logger.Debugf("rego file permissions check passed: %o", fileInfo.Mode().Perm()) + } else { + logger.Warnf("skipping rego file permission check - file may have permissions more permissive than 0600") } - options.regoFileLock = nil + return options.regoFilePath, nil } diff --git a/cmd/finch-daemon/router_utils_test.go b/cmd/finch-daemon/router_utils_test.go index ddd585cd..b52f9069 100644 --- a/cmd/finch-daemon/router_utils_test.go +++ b/cmd/finch-daemon/router_utils_test.go @@ -4,13 +4,11 @@ package main import ( - "fmt" "os" "path/filepath" "testing" "github.com/containerd/nerdctl/v2/pkg/config" - "github.com/gofrs/flock" "github.com/runfinch/finch-daemon/pkg/flog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -76,78 +74,19 @@ namespace = "test_namespace" assert.Equal(t, "test_namespace", cfg.Namespace) } -func TestCleanupRegoFile(t *testing.T) { - tests := []struct { - name string - setupFunc func() (*DaemonOptions, *flog.Logrus, func()) - }{ - { - name: "successful cleanup", - setupFunc: func() (*DaemonOptions, *flog.Logrus, func()) { - tmpFile, err := os.CreateTemp("", "test.rego") - require.NoError(t, err) - - fileLock := flock.New(tmpFile.Name()) - _, err = fileLock.TryLock() - require.NoError(t, err) - - err = os.Chmod(tmpFile.Name(), 0400) - require.NoError(t, err) - - logger := flog.NewLogrus() - - cleanup := func() { - os.Remove(tmpFile.Name()) - } - - return &DaemonOptions{ - regoFilePath: tmpFile.Name(), - regoFileLock: fileLock, - }, logger, cleanup - }, - }, - { - name: "nil lock handle", - setupFunc: func() (*DaemonOptions, *flog.Logrus, func()) { - return &DaemonOptions{ - regoFileLock: nil, - }, flog.NewLogrus(), func() {} - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - options, logger, cleanup := tt.setupFunc() - defer cleanup() - - cleanupRegoFile(options, logger) - - if options.regoFilePath != "" { - // Verify file permissions are restored - info, err := os.Stat(options.regoFilePath) - require.NoError(t, err) - assert.Equal(t, os.FileMode(0600), info.Mode().Perm()) - } - - // Verify lock is released - assert.Nil(t, options.regoFileLock) - }) - } -} - func TestCheckRegoFileValidity(t *testing.T) { + logger := flog.NewLogrus() tests := []struct { name string - setupFunc func() (string, func()) + setupFunc func(t *testing.T) (string, func()) + skipPermCheck bool expectedError string }{ { name: "valid rego file", - setupFunc: func() (string, func()) { - // Create a temporary directory - tmpDir, err := os.MkdirTemp("", "rego_test") - require.NoError(t, err) + setupFunc: func(t *testing.T) (string, func()) { + // Create a temporary directory that will be automatically cleaned up + tmpDir := t.TempDir() // Create a file with .rego extension and proper content regoPath := filepath.Join(tmpDir, "test.rego") @@ -158,26 +97,23 @@ import rego.v1 default allow = false ` - fmt.Println("regopath = ", regoPath) - err = os.WriteFile(regoPath, []byte(regoContent), 0600) + err := os.WriteFile(regoPath, []byte(regoContent), 0600) require.NoError(t, err) - return regoPath, func() { - os.RemoveAll(tmpDir) - } + return regoPath, func() {} }, expectedError: "", }, { name: "non-existent file", - setupFunc: func() (string, func()) { + setupFunc: func(t *testing.T) (string, func()) { return filepath.Join(os.TempDir(), "nonexistent.rego"), func() {} }, expectedError: "provided Rego file path does not exist", }, { name: "wrong extension", - setupFunc: func() (string, func()) { + setupFunc: func(t *testing.T) (string, func()) { tmpFile, err := os.CreateTemp("", "test.txt") require.NoError(t, err) return tmpFile.Name(), func() { os.Remove(tmpFile.Name()) } @@ -188,15 +124,20 @@ default allow = false for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - filePath, cleanup := tt.setupFunc() + filePath, cleanup := tt.setupFunc(t) defer cleanup() - err := checkRegoFileValidity(filePath) + options := &DaemonOptions{ + regoFilePath: filePath, + } + path, err := checkRegoFileValidity(options, logger) if tt.expectedError != "" { assert.ErrorContains(t, err, tt.expectedError) + assert.Empty(t, path) } else { assert.NoError(t, err) + assert.Equal(t, filePath, path) } }) } diff --git a/docs/opa-middleware.md b/docs/opa-middleware.md index ffbf1f44..7e563369 100644 --- a/docs/opa-middleware.md +++ b/docs/opa-middleware.md @@ -9,7 +9,7 @@ In the current implementation, users can use OPA Rego policies to filter API req ## Setting up a policy -Use the [sample rego](../docs/sample-rego-policies/default.rego) policy template to build your policy rules. +Use the [sample rego](../docs/sample-rego-policies/example.rego) policy template to build your policy rules. The package name must be `finch.authz`, the daemon middleware will look for the result of the `allow` key on each API call to determine wether to allow/deny the request. An approved request will go through without any events, a rejected request will fail with status code 403 @@ -34,12 +34,23 @@ Use the [Rego playground](https://play.openpolicyagent.org/) to fine tune your r ## Enable OPA Middleware -Once you are ready with your policy document, use the `--enable-middleware` flag to tell the finch-daemon to enable the OPA middleware. The daemon will then look for the policy document provided by the `--rego-file` flag. +Once you are ready with your policy document, use the `--enable-opa-middleware` flag to tell the finch-daemon to enable the OPA middleware. The daemon will then look for the policy document provided by the `--rego-file` flag. -Note: The `--rego-file` flag is required when `--enable-middleware` is set. +Note: The `--rego-file` flag is required when `--enable-opa-middleware` is set. -Example: -`sudo bin/finch-daemon --debug --socket-owner $UID --socket-addr /run/finch-test.sock --pidfile /run/finch-test.pid --enable-middleware --rego-file //finch-daemon/docs/sample-rego-policies/default.rego &` +The daemon enforces strict permissions (0600 or more restrictive) on the Rego policy file to prevent unauthorized modifications. You can bypass this check using the `--skip-rego-perm-check` flag. + +Examples: + +Standard secure usage: +```bash +sudo bin/finch-daemon --debug --socket-owner $UID --socket-addr /run/finch-test.sock --pidfile /run/finch-test.pid --enable-opa-middleware --rego-file /path/to/policy.rego +``` + +With permission check bypassed: +```bash +sudo bin/finch-daemon --debug --socket-owner $UID --socket-addr /run/finch-test.sock --pidfile /run/finch-test.pid --enable-opa-middleware --rego-file /path/to/policy.rego --skip-rego-perm-check +``` # Best practices for secure rego policies @@ -109,7 +120,21 @@ The finch-daemon's inability to start due to policy issues could impact system o - Monitor for unexpected denials -### Critical Security Considerations : Rego Policy File Protection +### Critical Security Considerations: Rego Policy File Protection + +### Rego File Permissions +By default, the daemon requires the Rego policy file to have permissions no more permissive than 0600 (readable and writable only by the owner). This restriction helps prevent unauthorized modifications to the policy file. + +The `--skip-rego-perm-check` flag can be used to bypass this permission check. However, using this flag comes with significant security risks: +- More permissive file permissions could allow unauthorized users to modify the policy +- Changes to the policy file could go unnoticed +- Security controls could be weakened without proper oversight + +It is strongly recommended to: +- Avoid using `--skip-rego-perm-check` in production environments +- Always use proper file permissions (0600 or more restrictive) +- Implement additional monitoring if the flag must be used + The Rego policy file is a critical security control. Any user with sudo privileges can: @@ -123,4 +148,4 @@ Any user with sudo privileges can: - Restrict sudo access to specific commands - Monitoring - Monitor policy file changes - - Monitor daemon service status \ No newline at end of file + - Monitor daemon service status diff --git a/docs/sample-rego-policies/default.rego b/docs/sample-rego-policies/example.rego similarity index 93% rename from docs/sample-rego-policies/default.rego rename to docs/sample-rego-policies/example.rego index e1fa7737..3d52a8c1 100644 --- a/docs/sample-rego-policies/default.rego +++ b/docs/sample-rego-policies/example.rego @@ -7,7 +7,7 @@ default allow = false allow if { not is_container_create - not is_networs_api + not is_networks_api not is_swarm_api not is_plugins } @@ -17,7 +17,7 @@ is_container_create if { glob.match("/**/containers/create", ["/"], input.Path) } -is_networs_api if { +is_networks_api if { input.Method == "GET" glob.match("/**/networks", ["/"], input.Path) } @@ -35,4 +35,4 @@ is_plugins if { is_forbidden_container if { input.Method == "GET" glob.match("/**/container/1f576a797a486438548377124f6cb7770a5cb7c8ff6a11c069cb4128d3f59462/json", ["/"], input.Path) -} \ No newline at end of file +} diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index fde29726..b3918bed 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -94,7 +94,7 @@ func runE2ETests(t *testing.T) { pOpt := createPrefixedOption() ginkgo.Describe(e2eTestDescription, func() { - runContainerTests(opt) + runContainerTests(opt, pOpt) runVolumeTests(opt) runNetworkTests(opt, pOpt) runImageTests(opt) @@ -123,8 +123,8 @@ func runTests(t *testing.T, description string) { } // functional test for container APIs. -func runContainerTests(opt *option.Option) { - tests.ContainerCreate(opt) +func runContainerTests(opt *option.Option, pOpt func([]string, ...option.Modifier) (*option.Option, error)) { + tests.ContainerCreate(opt, pOpt) tests.ContainerStart(opt) tests.ContainerStop(opt) tests.ContainerRestart(opt) diff --git a/e2e/tests/opa_middleware.go b/e2e/tests/opa_middleware.go index c20c027a..0b0c7cc9 100644 --- a/e2e/tests/opa_middleware.go +++ b/e2e/tests/opa_middleware.go @@ -9,6 +9,9 @@ import ( "fmt" "net/http" "os" + "os/exec" + "path/filepath" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -91,18 +94,6 @@ func OpaMiddlewareTest(opt *option.Option) { Expect(res.StatusCode).Should(Equal(http.StatusForbidden)) }) - It("should not allow updates to the rego file", func() { - regoFilePath := os.Getenv("REGO_FILE_PATH") - Expect(regoFilePath).NotTo(BeEmpty(), "REGO_FILE_PATH environment variable should be set") - - fileInfo, err := os.Stat(regoFilePath) - Expect(err).NotTo(HaveOccurred(), "Failed to get Rego file info") - - // Check file permissions - mode := fileInfo.Mode() - Expect(mode.Perm()).To(Equal(os.FileMode(0400)), "Rego file should be read-only (0400)") - }) - It("should fail unimplemented API calls, fail via daemon", func() { fmt.Println("incompatibleUrl = ", unimplementedUnspecifiedUrl) res, _ := uClient.Get(unimplementedUnspecifiedUrl) @@ -116,5 +107,49 @@ func OpaMiddlewareTest(opt *option.Option) { Expect(res.StatusCode).Should(Equal(http.StatusNotFound)) }) + + // Add this test to OpaMiddlewareTest function + It("should handle rego file permissions correctly", func() { + // Create a temporary rego file with overly permissive permissions + tmpDir := GinkgoT().TempDir() + + regoPath := filepath.Join(tmpDir, "test.rego") + regoContent := []byte(`package finch.authz + default allow = false`) + + var err error + err = os.WriteFile(regoPath, regoContent, 0644) + Expect(err).NotTo(HaveOccurred()) + + // Try to start daemon with overly permissive file + cmd := exec.Command(GetFinchDaemonExe(), //nolint:gosec // G204: This is a test file with controlled inputs + "--socket-addr", "/run/test.sock", + "--pidfile", "/run/test.pid", + "--rego-file", regoPath, + "--enable-opa-middleware") + err = cmd.Run() + + // Should fail due to permissions + Expect(err).To(HaveOccurred()) + + // For the second test with skip-check: + cmd = exec.Command(GetFinchDaemonExe(), //nolint:gosec // G204: This is a test file with controlled inputs + "--socket-addr", "/run/test.sock", + "--pidfile", "/run/test.pid", + "--rego-file", regoPath, + "--enable-opa-middleware", + "--skip-rego-perm-check") + + // Start the process in background + err = cmd.Start() + Expect(err).NotTo(HaveOccurred()) + + // Give it a moment to initialize + time.Sleep(1 * time.Second) + + // Kill the process + err = cmd.Process.Kill() + Expect(err).NotTo(HaveOccurred()) + }) }) } diff --git a/e2e/tests/tests.go b/e2e/tests/tests.go index 932827a0..c26f0d67 100644 --- a/e2e/tests/tests.go +++ b/e2e/tests/tests.go @@ -226,3 +226,11 @@ func GetFinchExe() string { } return finchexe } + +func GetFinchDaemonExe() string { + daemonPath := os.Getenv("DAEMON_ROOT") + if daemonPath == "" { + daemonPath = "./bin/finch-daemon" // fallback + } + return daemonPath +} From 19067910569d0754c9045735dba96880436a4391 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Wed, 11 Jun 2025 16:42:11 +0000 Subject: [PATCH 9/9] chore: add experimental documentation Signed-off-by: Arjun Raja Yogidas --- .github/workflows/ci.yaml | 2 +- README.md | 26 +++++++++++++++++++++ api/router/router.go | 2 +- cmd/finch-daemon/main.go | 31 ++++++++++++++++---------- docs/opa-middleware.md | 28 ++++++++++++++++++----- docs/sample-rego-policies/example.rego | 5 +++++ e2e/tests/opa_middleware.go | 4 ++-- 7 files changed, 76 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5c25c512..383ae8e3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -110,7 +110,7 @@ jobs: - name: Set Rego file path run: echo "REGO_FILE_PATH=${{ github.workspace }}/docs/sample-rego-policies/example.rego" >> $GITHUB_ENV - name: Start finch-daemon with opa Authz - run: sudo bin/finch-daemon --debug --enable-opa-middleware --rego-file ${{ github.workspace }}/docs/sample-rego-policies/example.rego --skip-rego-perm-check --socket-owner $UID --socket-addr /run/finch.sock --pidfile /run/finch.pid & + run: sudo bin/finch-daemon --debug --experimental --rego-file ${{ github.workspace }}/docs/sample-rego-policies/example.rego --skip-rego-perm-check --socket-owner $UID --socket-addr /run/finch.sock --pidfile /run/finch.pid & - name: Run opa e2e tests run: sudo -E make test-e2e-opa - name: Clean up Daemon socket diff --git a/README.md b/README.md index deb95ecb..93612850 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,32 @@ Getting started with Finch Daemon on Linux only requires a few steps: 5. Test any changes with `make test-unit` and `sudo make test-e2e` +## Experimental Features + +Finch Daemon includes experimental features that can be enabled using the `--experimental` flag. These features are under development and may change in future releases. + +### Using Experimental Features + +To enable experimental features, use the `--experimental` flag when starting the daemon: + +```bash +sudo bin/finch-daemon --debug --socket-owner $UID --experimental +``` + +### Current Experimental Features + +#### OPA Authorization Middleware + +The OPA (Open Policy Agent) middleware allows you to define authorization policies for API requests using Rego policy language. This feature requires both the `--experimental` flag and the `--rego-file` flag to be set. + +Example usage: +```bash +sudo bin/finch-daemon --debug --socket-owner $UID --experimental --rego-file /path/to/policy.rego +``` + +For detailed documentation on the OPA middleware, see [opa-middleware.md](docs/opa-middleware.md). + + ## Creating a systemd service If you want finch-daemon to be managed as a systemd service, for benefits like automatic restart if it gets killed, you can configure it as a systemd service on Linux by diff --git a/api/router/router.go b/api/router/router.go index fc6a6559..b5e703e1 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -111,7 +111,7 @@ func VersionMiddleware(next http.Handler) http.Handler { } // CreateRegoMiddleware dynamically parses the rego file at the path specified in options -// and allows or denies the request based on the policy. +// and return a function that allows or denies the request based on the policy. // Will return a nil function and an error if the given file path is blank or invalid. func CreateRegoMiddleware(regoFilePath string, logger *flog.Logrus) (func(next http.Handler) http.Handler, error) { if regoFilePath == "" { diff --git a/cmd/finch-daemon/main.go b/cmd/finch-daemon/main.go index 2951972b..ed2f8129 100644 --- a/cmd/finch-daemon/main.go +++ b/cmd/finch-daemon/main.go @@ -43,15 +43,15 @@ const ( ) type DaemonOptions struct { - debug bool - socketAddr string - socketOwner int - debugAddress string - configPath string - pidFile string - regoFilePath string - enableOPAMiddleware bool - skipRegoPermCheck bool + debug bool + socketAddr string + socketOwner int + debugAddress string + configPath string + pidFile string + regoFilePath string + enableExperimental bool + skipRegoPermCheck bool } var options = new(DaemonOptions) @@ -70,9 +70,9 @@ func main() { rootCmd.Flags().StringVar(&options.debugAddress, "debug-addr", "", "") rootCmd.Flags().StringVar(&options.configPath, "config-file", defaultConfigPath, "Daemon Config Path") rootCmd.Flags().StringVar(&options.pidFile, "pidfile", defaultPidFile, "pid file location") - rootCmd.Flags().StringVar(&options.regoFilePath, "rego-file", "", "Rego Policy Path") - rootCmd.Flags().BoolVar(&options.enableOPAMiddleware, "enable-opa-middleware", false, "turn on OPA middleware for allowlisting") + rootCmd.Flags().StringVar(&options.regoFilePath, "rego-file", "", "Rego Policy Path (requires --experimental flag)") rootCmd.Flags().BoolVar(&options.skipRegoPermCheck, "skip-rego-perm-check", false, "skip the rego file permission check (allows permissions more permissive than 0600)") + rootCmd.Flags().BoolVar(&options.enableExperimental, "experimental", false, "enable experimental features") if err := rootCmd.Execute(); err != nil { log.Printf("got error: %v", err) @@ -223,11 +223,18 @@ func newRouter(options *DaemonOptions, logger *flog.Logrus) (http.Handler, error } var regoFilePath string - if options.enableOPAMiddleware { + + if options.regoFilePath != "" { + if !options.enableExperimental { + return nil, fmt.Errorf("rego file provided without experimental flag - OPA middleware is an experimental feature, please enable it with '--experimental' flag") + } regoFilePath, err = checkRegoFileValidity(options, logger) if err != nil { return nil, err } + } else if options.enableExperimental { + // Only experimental flag set + logger.Info("experimental flag passed, but no experimental features enabled") } opts := createRouterOptions(conf, clientWrapper, ncWrapper, logger, regoFilePath) diff --git a/docs/opa-middleware.md b/docs/opa-middleware.md index 7e563369..f6caf6ee 100644 --- a/docs/opa-middleware.md +++ b/docs/opa-middleware.md @@ -1,6 +1,20 @@ -# Applying OPA authz policies +# OPA Authorization Middleware (Experimental) -This guide provides instructions for setting up [OPA](https://github.com/open-policy-agent/opa) authz policies with the finch-daemon. Authz policies allow users to allowlist or deny certain resources based on policy rules. +> ⚠️ **Experimental Feature**: The OPA authorization middleware is being introduced as an experimental feature. + +This guide provides instructions for setting up [OPA](https://github.com/open-policy-agent/opa) authorization policies with the finch-daemon. These policies allow users to allowlist or deny certain resources based on policy rules. + +## Experimental Status + +This feature is being released as experimental because: +- Integration patterns and best practices are still being established +- Performance characteristics are being evaluated + +As an experimental feature: +- Breaking changes may occur in any release +- Long-term backward compatibility is not guaranteed +- Documentation and examples may evolve substantially +- Production use is not recommended at this stage ## What Is OPA Authz implementation Open Policy Agent (OPA) is an open-source, general-purpose policy engine that enables unified, context-aware policy enforcement across the entire stack. OPA provides a high-level declarative language, Rego, for specifying policy as code and simple APIs to offload policy decision-making from your software. @@ -34,9 +48,9 @@ Use the [Rego playground](https://play.openpolicyagent.org/) to fine tune your r ## Enable OPA Middleware -Once you are ready with your policy document, use the `--enable-opa-middleware` flag to tell the finch-daemon to enable the OPA middleware. The daemon will then look for the policy document provided by the `--rego-file` flag. +Once you are ready with your policy document, use the `--experimental` flag to enable experimental features including OPA middleware. The daemon will then look for the policy document provided by the `--rego-file` flag. -Note: The `--rego-file` flag is required when `--enable-opa-middleware` is set. +Note: Since OPA middleware is an experimental feature, the `--experimental` flag is required when using `--rego-file`. The daemon enforces strict permissions (0600 or more restrictive) on the Rego policy file to prevent unauthorized modifications. You can bypass this check using the `--skip-rego-perm-check` flag. @@ -44,14 +58,16 @@ Examples: Standard secure usage: ```bash -sudo bin/finch-daemon --debug --socket-owner $UID --socket-addr /run/finch-test.sock --pidfile /run/finch-test.pid --enable-opa-middleware --rego-file /path/to/policy.rego +sudo bin/finch-daemon --debug --socket-owner $UID --socket-addr /run/finch-test.sock --pidfile /run/finch-test.pid --experimental --rego-file /path/to/policy.rego ``` With permission check bypassed: ```bash -sudo bin/finch-daemon --debug --socket-owner $UID --socket-addr /run/finch-test.sock --pidfile /run/finch-test.pid --enable-opa-middleware --rego-file /path/to/policy.rego --skip-rego-perm-check +sudo bin/finch-daemon --debug --socket-owner $UID --socket-addr /run/finch-test.sock --pidfile /run/finch-test.pid --experimental --rego-file /path/to/policy.rego --skip-rego-perm-check ``` +Note: If you enable experimental features with `--experimental` but don't provide a `--rego-file`, the daemon will run without OPA policy evaluation. + # Best practices for secure rego policies diff --git a/docs/sample-rego-policies/example.rego b/docs/sample-rego-policies/example.rego index 3d52a8c1..37f48f9e 100644 --- a/docs/sample-rego-policies/example.rego +++ b/docs/sample-rego-policies/example.rego @@ -1,3 +1,8 @@ +# This is an experimental preview policy example. +# As this feature is under active development: +# - Breaking changes may occur without notice +# - Production use is not recommended + package finch.authz import future.keywords.if diff --git a/e2e/tests/opa_middleware.go b/e2e/tests/opa_middleware.go index 0b0c7cc9..24f5001c 100644 --- a/e2e/tests/opa_middleware.go +++ b/e2e/tests/opa_middleware.go @@ -126,7 +126,7 @@ func OpaMiddlewareTest(opt *option.Option) { "--socket-addr", "/run/test.sock", "--pidfile", "/run/test.pid", "--rego-file", regoPath, - "--enable-opa-middleware") + "--experimental") err = cmd.Run() // Should fail due to permissions @@ -137,7 +137,7 @@ func OpaMiddlewareTest(opt *option.Option) { "--socket-addr", "/run/test.sock", "--pidfile", "/run/test.pid", "--rego-file", regoPath, - "--enable-opa-middleware", + "--experimental", "--skip-rego-perm-check") // Start the process in background