diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ef4ffb8..93c07903 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -141,8 +141,8 @@ jobs: echo "--------" go install github.com/onsi/ginkgo/v2/ginkgo@v2.27.1 go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest - go test -v -json -timeout=30m -race $(go list ./...|grep -v e2e) 2>&1 | tee /tmp/gotest.log | gotestfmt - ginkgo -p --output-interceptor-mode=none -timeout=30m e2e/... + # go test -v -json -timeout=30m -race $(go list ./...|grep -v e2e) 2>&1 | tee /tmp/gotest.log | gotestfmt + ginkgo -v -p --output-interceptor-mode=none -timeout=30m e2e/... # Run Go benchmarks - name: Benchmark run: | diff --git a/docs/content/configuration/_index.md b/docs/content/configuration/_index.md index a05c0046..3dd16928 100644 --- a/docs/content/configuration/_index.md +++ b/docs/content/configuration/_index.md @@ -36,6 +36,7 @@ weight: 2 | \--resources value | list of resources 'uri=/admin*\|methods=GET,PUT\|roles=role1,role2' | | | \--headers value | custom headers to the upstream request, key=value | | | \--preserve-host | preserve the host header of the proxied request in the upstream request | false | PROXY_PRESERVE_HOST +| \--file-root | file root for loading of files | | PROXY_FILE_ROOT | \--request-id-header value | the http header name for request id | X-Request-ID | PROXY_REQUEST_ID_HEADER | \--response-headers value | custom headers to added to the http response key=value | | PROXY_RESPONSE_HEADERS | \--custom-http-methods | list of additional non-standard http methods | | diff --git a/e2e/e2e_root_test.go b/e2e/e2e_root_test.go new file mode 100644 index 00000000..50249757 --- /dev/null +++ b/e2e/e2e_root_test.go @@ -0,0 +1,240 @@ +package e2e_test + +import ( + "context" + "crypto/tls" + "net/http" + "net/url" + "os" + "strings" + "time" + + "github.com/go-jose/go-jose/v4/jwt" + resty "github.com/go-resty/resty/v2" + "github.com/gogatekeeper/gatekeeper/pkg/constant" + "github.com/gogatekeeper/gatekeeper/pkg/encryption" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/session" + . "github.com/onsi/ginkgo/v2" //nolint:revive //we want to use it for ginkgo + . "github.com/onsi/gomega" //nolint:revive //we want to use it for gomega + "golang.org/x/sync/errgroup" +) + +var _ = Describe("Code Flow login/logout compression and encryption Auth Scheme Cookie", func() { + var ( + portNum string + proxyAddress string + server *http.Server + ) + + errGroup, _ := errgroup.WithContext(context.Background()) + + AfterEach(func() { + if server != nil { + err := server.Shutdown(context.Background()) + Expect(err).NotTo(HaveOccurred()) + } + + if errGroup != nil { + err := errGroup.Wait() + Expect(err).NotTo(HaveOccurred()) + } + }) + + BeforeEach(func() { + var ( + err error + upstreamSvcPort string + ) + + server, upstreamSvcPort = startAndWaitTestUpstream(errGroup, false, false, false) + portNum, err = generateRandomPort() + Expect(err).NotTo(HaveOccurred()) + + proxyAddress = localURI + portNum + + tmpDir := os.TempDir() + tmpDirSlash := tmpDir + "/" + tlsCaCertificate := strings.TrimPrefix(tlsCaCertificate, tmpDirSlash) + tlsCertificate := strings.TrimPrefix(tlsCertificate, tmpDirSlash) + tlsPrivateKey := strings.TrimPrefix(tlsPrivateKey, tmpDirSlash) + + proxyArgs := []string{ + "--discovery-url=" + idpRealmURI, + "--openid-provider-timeout=300s", + "--tls-openid-provider-ca-certificate=" + tlsCaCertificate, + "--tls-openid-provider-client-certificate=" + tlsCertificate, + "--tls-openid-provider-client-private-key=" + tlsPrivateKey, + "--listen=" + allInterfaces + portNum, + "--client-id=" + testClient, + "--client-secret=" + testClientSecret, + "--upstream-url=" + localURI + upstreamSvcPort, + "--no-redirects=false", + "--skip-access-token-clientid-check=true", + "--skip-access-token-issuer-check=true", + "--enable-idp-session-check=false", + "--enable-default-deny=false", + "--resources=uri=/*|roles=uma_authorization,offline_access", + "--resources=uri=" + testPath + "|no-redirect=true", + "--openid-provider-retry-count=30", + "--enable-refresh-tokens=true", + "--encryption-key=" + testKey, + "--secure-cookie=false", + "--post-login-redirect-path=" + postLoginRedirectPath, + "--enable-register-handler=true", + "--enable-encrypted-token=true", + "--enable-id-token-cookie=true", + "--enable-user-info-claims=true", + "--add-claims=email_verified", + "--add-claims=email", + "--enable-pkce=false", + "--tls-cert=" + tlsCertificate, + "--tls-private-key=" + tlsPrivateKey, + "--upstream-ca=" + tlsCaCertificate, + "--enable-compress-token=true", + "--compress-token-only-auth-scheme=cookie", + "--enable-logout-redirect=true", + "--post-logout-redirect-uri=https://" + testExternalURI, + "--verbose=true", + "--file-root=" + tmpDir, + } + + osArgs := make([]string, 0, 1+len(proxyArgs)) + osArgs = append(osArgs, os.Args[0]) + osArgs = append(osArgs, proxyArgs...) + startAndWait(portNum, osArgs) + }) + + When("Performing standard login", func() { + It("should login with user/password and logout successfully", + Label("code_flow"), + Label("compression_auth_scheme"), + Label("auth_scheme_cookie"), + func(_ context.Context) { + var err error + + rClient := resty.New() + rClient.SetTLSClientConfig(&tls.Config{RootCAs: caPool, MinVersion: tls.VersionTLS13}) + noCookieClient := rClient.Clone() + resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK, testUser, testPass) + Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true")) + body := resp.Body() + Expect(strings.Contains(string(body), postLoginRedirectPath)).To(BeTrue()) + + jarURI, err := url.Parse(proxyAddress) + Expect(err).NotTo(HaveOccurred()) + + cookiesLogin := rClient.GetClient().Jar.Cookies(jarURI) + + var ( + accessCookieLogin string + refreshCookieLogin string + ) + + for _, cook := range cookiesLogin { + if cook.Name == constant.AccessCookie { + accessCookieLogin = cook.Value + } + + if cook.Name == constant.RefreshCookie { + refreshCookieLogin = cook.Value + } + } + + accessCookieLogin, err = session.DecryptAndDecompressToken(accessCookieLogin, testKey) + Expect(err).NotTo(HaveOccurred()) + _, err = jwt.ParseSigned(accessCookieLogin, constant.SignatureAlgs[:]) + Expect(err).NotTo(HaveOccurred()) + + refreshCookieLogin, err = session.DecryptAndDecompressToken(refreshCookieLogin, testKey) + Expect(err).NotTo(HaveOccurred()) + _, err = jwt.ParseSigned(refreshCookieLogin, constant.SignatureAlgs[:]) + Expect(err).NotTo(HaveOccurred()) + + time.Sleep(testAccessTokenExp) + + resp, err = rClient.R().Get(proxyAddress + anyURI) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true")) + body = resp.Body() + Expect(strings.Contains(string(body), anyURI)).To(BeTrue()) + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + Expect(err).NotTo(HaveOccurred()) + + cookiesAfterRefresh := rClient.GetClient().Jar.Cookies(jarURI) + + var ( + accessCookieAfterRefresh string + refreshCookieAfterRefresh string + testCookie string + ) + + for _, cook := range cookiesAfterRefresh { + if cook.Name == constant.AccessCookie { + accessCookieAfterRefresh = cook.Value + } + + if cook.Name == constant.RefreshCookie { + refreshCookieAfterRefresh = cook.Value + } + + if cook.Name == testCookieValue { + testCookie = cook.Value + } + } + + Expect(testCookie).To(Equal("test_value")) + + accessCookieAfterRefresh, err = session.DecryptAndDecompressToken(accessCookieAfterRefresh, testKey) + Expect(err).NotTo(HaveOccurred()) + _, err = jwt.ParseSigned(accessCookieAfterRefresh, constant.SignatureAlgs[:]) + Expect(err).NotTo(HaveOccurred()) + Expect(accessCookieLogin).NotTo(Equal(accessCookieAfterRefresh)) + + refreshCookieAfterRefresh, err = session.DecryptAndDecompressToken(refreshCookieAfterRefresh, testKey) + Expect(err).NotTo(HaveOccurred()) + _, err = jwt.ParseSigned(refreshCookieAfterRefresh, constant.SignatureAlgs[:]) + Expect(err).NotTo(HaveOccurred()) + + resp, err = rClient.R().Get(proxyAddress + anyURI) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true")) + + body = resp.Body() + + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + Expect(strings.Contains(string(body), anyURI)).To(BeTrue()) + + accessCookieAfterRefresh, err = encryption.EncodeText(accessCookieAfterRefresh, testKey) + Expect(err).NotTo(HaveOccurred()) + + resp, err = noCookieClient.R().SetAuthToken(accessCookieAfterRefresh).Get(proxyAddress + testPath) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true")) + + body = resp.Body() + + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + Expect(strings.Contains(string(body), testPath)).To(BeTrue()) + + resp, err = noCookieClient.R().SetAuthToken(accessCookieAfterRefresh).Get(proxyAddress + anyURI) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true")) + + body = resp.Body() + + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + Expect(strings.Contains(string(body), anyURI)).To(BeTrue()) + + //nolint:gosec + rClient.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) + resp, err = rClient.R().Get(proxyAddress + logoutURI) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + + rClient.SetRedirectPolicy(resty.NoRedirectPolicy()) + resp, _ = rClient.R().Get(proxyAddress) + Expect(resp.StatusCode()).To(Equal(http.StatusSeeOther)) + }, + ) + }) +}) diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index a4bd9e6d..5df29d29 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -924,6 +924,7 @@ var _ = Describe("Code Flow login/logout", func() { "--tls-private-key=" + tlsPrivateKey, "--upstream-ca=" + tlsCaCertificate, "--override-destination-headers=true", + "--verbose=true", } osArgs := make([]string, 0, 1+len(proxyArgs)) @@ -4260,216 +4261,3 @@ var _ = Describe("Code Flow login/logout compression and encrypted Auth Scheme B ) }) }) - -var _ = Describe("Code Flow login/logout compression and encryption Auth Scheme Cookie", func() { - var ( - portNum string - proxyAddress string - server *http.Server - ) - - errGroup, _ := errgroup.WithContext(context.Background()) - - AfterEach(func() { - if server != nil { - err := server.Shutdown(context.Background()) - Expect(err).NotTo(HaveOccurred()) - } - - if errGroup != nil { - err := errGroup.Wait() - Expect(err).NotTo(HaveOccurred()) - } - }) - - BeforeEach(func() { - var ( - err error - upstreamSvcPort string - ) - - server, upstreamSvcPort = startAndWaitTestUpstream(errGroup, false, false, false) - portNum, err = generateRandomPort() - Expect(err).NotTo(HaveOccurred()) - - proxyAddress = localURI + portNum - - proxyArgs := []string{ - "--discovery-url=" + idpRealmURI, - "--openid-provider-timeout=300s", - "--tls-openid-provider-ca-certificate=" + tlsCaCertificate, - "--tls-openid-provider-client-certificate=" + tlsCertificate, - "--tls-openid-provider-client-private-key=" + tlsPrivateKey, - "--listen=" + allInterfaces + portNum, - "--client-id=" + testClient, - "--client-secret=" + testClientSecret, - "--upstream-url=" + localURI + upstreamSvcPort, - "--no-redirects=false", - "--skip-access-token-clientid-check=true", - "--skip-access-token-issuer-check=true", - "--enable-idp-session-check=false", - "--enable-default-deny=false", - "--resources=uri=/*|roles=uma_authorization,offline_access", - "--resources=uri=" + testPath + "|no-redirect=true", - "--openid-provider-retry-count=30", - "--enable-refresh-tokens=true", - "--encryption-key=" + testKey, - "--secure-cookie=false", - "--post-login-redirect-path=" + postLoginRedirectPath, - "--enable-register-handler=true", - "--enable-encrypted-token=true", - "--enable-id-token-cookie=true", - "--enable-user-info-claims=true", - "--add-claims=email_verified", - "--add-claims=email", - "--enable-pkce=false", - "--tls-cert=" + tlsCertificate, - "--tls-private-key=" + tlsPrivateKey, - "--upstream-ca=" + tlsCaCertificate, - "--enable-compress-token=true", - "--compress-token-only-auth-scheme=cookie", - "--enable-logout-redirect=true", - "--post-logout-redirect-uri=https://" + testExternalURI, - "--verbose=true", - } - - osArgs := make([]string, 0, 1+len(proxyArgs)) - osArgs = append(osArgs, os.Args[0]) - osArgs = append(osArgs, proxyArgs...) - startAndWait(portNum, osArgs) - }) - - When("Performing standard login", func() { - It("should login with user/password and logout successfully", - Label("code_flow"), - Label("compression_auth_scheme"), - Label("auth_scheme_cookie"), - func(_ context.Context) { - var err error - - rClient := resty.New() - rClient.SetTLSClientConfig(&tls.Config{RootCAs: caPool, MinVersion: tls.VersionTLS13}) - noCookieClient := rClient.Clone() - resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK, testUser, testPass) - Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true")) - body := resp.Body() - Expect(strings.Contains(string(body), postLoginRedirectPath)).To(BeTrue()) - - jarURI, err := url.Parse(proxyAddress) - Expect(err).NotTo(HaveOccurred()) - - cookiesLogin := rClient.GetClient().Jar.Cookies(jarURI) - - var ( - accessCookieLogin string - refreshCookieLogin string - ) - - for _, cook := range cookiesLogin { - if cook.Name == constant.AccessCookie { - accessCookieLogin = cook.Value - } - - if cook.Name == constant.RefreshCookie { - refreshCookieLogin = cook.Value - } - } - - accessCookieLogin, err = session.DecryptAndDecompressToken(accessCookieLogin, testKey) - Expect(err).NotTo(HaveOccurred()) - _, err = jwt.ParseSigned(accessCookieLogin, constant.SignatureAlgs[:]) - Expect(err).NotTo(HaveOccurred()) - - refreshCookieLogin, err = session.DecryptAndDecompressToken(refreshCookieLogin, testKey) - Expect(err).NotTo(HaveOccurred()) - _, err = jwt.ParseSigned(refreshCookieLogin, constant.SignatureAlgs[:]) - Expect(err).NotTo(HaveOccurred()) - - time.Sleep(testAccessTokenExp) - - resp, err = rClient.R().Get(proxyAddress + anyURI) - Expect(err).NotTo(HaveOccurred()) - Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true")) - body = resp.Body() - Expect(strings.Contains(string(body), anyURI)).To(BeTrue()) - Expect(resp.StatusCode()).To(Equal(http.StatusOK)) - Expect(err).NotTo(HaveOccurred()) - - cookiesAfterRefresh := rClient.GetClient().Jar.Cookies(jarURI) - - var ( - accessCookieAfterRefresh string - refreshCookieAfterRefresh string - testCookie string - ) - - for _, cook := range cookiesAfterRefresh { - if cook.Name == constant.AccessCookie { - accessCookieAfterRefresh = cook.Value - } - - if cook.Name == constant.RefreshCookie { - refreshCookieAfterRefresh = cook.Value - } - - if cook.Name == testCookieValue { - testCookie = cook.Value - } - } - - Expect(testCookie).To(Equal("test_value")) - - accessCookieAfterRefresh, err = session.DecryptAndDecompressToken(accessCookieAfterRefresh, testKey) - Expect(err).NotTo(HaveOccurred()) - _, err = jwt.ParseSigned(accessCookieAfterRefresh, constant.SignatureAlgs[:]) - Expect(err).NotTo(HaveOccurred()) - Expect(accessCookieLogin).NotTo(Equal(accessCookieAfterRefresh)) - - refreshCookieAfterRefresh, err = session.DecryptAndDecompressToken(refreshCookieAfterRefresh, testKey) - Expect(err).NotTo(HaveOccurred()) - _, err = jwt.ParseSigned(refreshCookieAfterRefresh, constant.SignatureAlgs[:]) - Expect(err).NotTo(HaveOccurred()) - - resp, err = rClient.R().Get(proxyAddress + anyURI) - Expect(err).NotTo(HaveOccurred()) - Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true")) - - body = resp.Body() - - Expect(resp.StatusCode()).To(Equal(http.StatusOK)) - Expect(strings.Contains(string(body), anyURI)).To(BeTrue()) - - accessCookieAfterRefresh, err = encryption.EncodeText(accessCookieAfterRefresh, testKey) - Expect(err).NotTo(HaveOccurred()) - - resp, err = noCookieClient.R().SetAuthToken(accessCookieAfterRefresh).Get(proxyAddress + testPath) - Expect(err).NotTo(HaveOccurred()) - Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true")) - - body = resp.Body() - - Expect(resp.StatusCode()).To(Equal(http.StatusOK)) - Expect(strings.Contains(string(body), testPath)).To(BeTrue()) - - resp, err = noCookieClient.R().SetAuthToken(accessCookieAfterRefresh).Get(proxyAddress + anyURI) - Expect(err).NotTo(HaveOccurred()) - Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true")) - - body = resp.Body() - - Expect(resp.StatusCode()).To(Equal(http.StatusOK)) - Expect(strings.Contains(string(body), anyURI)).To(BeTrue()) - - //nolint:gosec - rClient.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) - resp, err = rClient.R().Get(proxyAddress + logoutURI) - Expect(err).NotTo(HaveOccurred()) - Expect(resp.StatusCode()).To(Equal(http.StatusOK)) - - rClient.SetRedirectPolicy(resty.NoRedirectPolicy()) - resp, _ = rClient.R().Get(proxyAddress) - Expect(resp.StatusCode()).To(Equal(http.StatusSeeOther)) - }, - ) - }) -}) diff --git a/pkg/encryption/rotation.go b/pkg/encryption/rotation.go index 6fd75185..b6d4059b 100644 --- a/pkg/encryption/rotation.go +++ b/pkg/encryption/rotation.go @@ -23,6 +23,7 @@ import ( "sync" "github.com/fsnotify/fsnotify" + "github.com/gogatekeeper/gatekeeper/pkg/utils" "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" ) @@ -42,9 +43,10 @@ func NewCertificateRotator( key string, log *zap.Logger, metric *prometheus.Counter, + fileRoot string, ) (*CertificationRotation, error) { // step: attempt to load the certificate - certificate, err := tls.LoadX509KeyPair(cert, key) + certificate, err := utils.LoadX509KeyPairFromRoot(fileRoot, cert, key) if err != nil { return nil, err } diff --git a/pkg/encryption/rotation_test.go b/pkg/encryption/rotation_test.go index 25664fa7..8701d8c2 100644 --- a/pkg/encryption/rotation_test.go +++ b/pkg/encryption/rotation_test.go @@ -42,7 +42,13 @@ func newTestCertificateRotator(t *testing.T) *encryption.CertificationRotation { Help: "The total amount of times the certificate has been rotated", }, ) - rotation, err := encryption.NewCertificateRotator(testCertificateFile, testPrivateKeyFile, zap.NewNop(), &counter) + rotation, err := encryption.NewCertificateRotator( + testCertificateFile, + testPrivateKeyFile, + zap.NewNop(), + &counter, + "", + ) assert.NotNil(t, rotation) require.NoError(t, err, "unable to create the certificate rotator") @@ -56,8 +62,14 @@ func TestNewCeritifacteRotator(t *testing.T) { Help: "The total amount of times the certificate has been rotated", }, ) - c, err := encryption.NewCertificateRotator(testCertificateFile, testPrivateKeyFile, zap.NewNop(), &counter) - assert.NotNil(t, c) + certRot, err := encryption.NewCertificateRotator( + testCertificateFile, + testPrivateKeyFile, + zap.NewNop(), + &counter, + "", + ) + assert.NotNil(t, certRot) require.NoError(t, err) } @@ -68,8 +80,14 @@ func TestNewCeritifacteRotatorFailure(t *testing.T) { Help: "The total amount of times the certificate has been rotated", }, ) - c, err := encryption.NewCertificateRotator("./tests/does_not_exist", testPrivateKeyFile, zap.NewNop(), &counter) - assert.Nil(t, c) + certRot, err := encryption.NewCertificateRotator( + "./tests/does_not_exist", + testPrivateKeyFile, + zap.NewNop(), + &counter, + "", + ) + assert.Nil(t, certRot) require.Error(t, err) } diff --git a/pkg/encryption/self_signed.go b/pkg/encryption/self_signed.go index dc18c62a..e10eccf0 100644 --- a/pkg/encryption/self_signed.go +++ b/pkg/encryption/self_signed.go @@ -26,12 +26,12 @@ import ( "encoding/pem" "math/big" "net" - "os" "sync" "time" "github.com/gogatekeeper/gatekeeper/pkg/apperrors" "github.com/gogatekeeper/gatekeeper/pkg/constant" + "github.com/gogatekeeper/gatekeeper/pkg/utils" "go.uber.org/zap" ) @@ -151,13 +151,13 @@ func CreateCertificate(key *ed25519.PrivateKey, hostnames []string, expire time. } // LoadKeyPair loads the tls key pair. -func LoadKeyPair(certPath, keyPath string) (*tls.Certificate, error) { - cert, err := os.ReadFile(certPath) +func LoadKeyPair(fileRoot, certPath, keyPath string) (*tls.Certificate, error) { + cert, err := utils.ReadFile(fileRoot, certPath) if err != nil { return nil, err } - key, err := os.ReadFile(keyPath) + key, err := utils.ReadFile(fileRoot, keyPath) if err != nil { return nil, err } @@ -172,8 +172,8 @@ func LoadKeyPair(certPath, keyPath string) (*tls.Certificate, error) { return &pair, err } -func LoadCert(certPath string) (*x509.CertPool, error) { - cert, err := os.ReadFile(certPath) +func LoadCert(fileRoot, certPath string) (*x509.CertPool, error) { + cert, err := utils.ReadFile(fileRoot, certPath) if err != nil { return nil, err } diff --git a/pkg/keycloak/config/config.go b/pkg/keycloak/config/config.go index 537ea5bd..3b3ce868 100644 --- a/pkg/keycloak/config/config.go +++ b/pkg/keycloak/config/config.go @@ -111,6 +111,7 @@ type Config struct { ClientSecret string `env:"CLIENT_SECRET" json:"client-secret,omitempty" usage:"client secret used to authenticate to the oauth service" yaml:"client-secret"` RedirectionURL string `env:"REDIRECTION_URL" json:"redirection-url,omitempty" usage:"redirection url for the oauth callback url, defaults to host header if absent" yaml:"redirection-url"` CompressTokenOnlyAuthScheme string `env:"COMPRESS_TOKEN_ONLY_AUTH_SCHEME" json:"compress-token-only-auth-scheme" usage:"compress token only for chosen auth scheme: cookie, bearer, default is empty" yaml:"compress-token-only-auth-scheme"` + FileRoot string `env:"FILE_ROOT" json:"file-root,omitempty" usage:"file root fixes directory for all files specified, prevents traversal attacks" yaml:"file-root"` Hostnames []string `json:"hostnames,omitempty" usage:"list of hostnames the service will respond to" yaml:"hostnames"` ForwardingDomains []string `json:"forwarding-domains,omitempty" usage:"list of domains which should be signed; everything else is relayed unsigned" yaml:"forwarding-domains"` CorsExposedHeaders []string `json:"cors-exposed-headers,omitempty" usage:"expose cors headers access control (Access-Control-Expose-Headers)" yaml:"cors-exposed-headers"` @@ -469,59 +470,59 @@ func (r *Config) isTLSFilesValid(fileCheckEnable bool) error { } if fileCheckEnable { - if r.UpstreamCA != "" && !utils.FileExists(r.UpstreamCA) { + if r.UpstreamCA != "" && !utils.FileExists(r.FileRoot, r.UpstreamCA) { return apperrors.ErrTLSUpstreamCANotExists } - if r.TLSCertificate != "" && !utils.FileExists(r.TLSCertificate) { + if r.TLSCertificate != "" && !utils.FileExists(r.FileRoot, r.TLSCertificate) { return apperrors.ErrTLSCertificateNotExists } - if r.TLSPrivateKey != "" && !utils.FileExists(r.TLSPrivateKey) { + if r.TLSPrivateKey != "" && !utils.FileExists(r.FileRoot, r.TLSPrivateKey) { return apperrors.ErrTLSPrivateKeyNotExists } - if r.TLSClientCertificate != "" && !utils.FileExists(r.TLSClientCertificate) { + if r.TLSClientCertificate != "" && !utils.FileExists(r.FileRoot, r.TLSClientCertificate) { return apperrors.ErrTLSClientCertificateNotExists } - if r.TLSClientPrivateKey != "" && !utils.FileExists(r.TLSClientPrivateKey) { + if r.TLSClientPrivateKey != "" && !utils.FileExists(r.FileRoot, r.TLSClientPrivateKey) { return apperrors.ErrTLSClientPrivateKeyNotExists } - if r.TLSClientCACertificate != "" && !utils.FileExists(r.TLSClientCACertificate) { + if r.TLSClientCACertificate != "" && !utils.FileExists(r.FileRoot, r.TLSClientCACertificate) { return apperrors.ErrTLSClientCACertificateNotExists } - if r.TLSForwardingCACertificate != "" && !utils.FileExists(r.TLSForwardingCACertificate) { + if r.TLSForwardingCACertificate != "" && !utils.FileExists(r.FileRoot, r.TLSForwardingCACertificate) { return apperrors.ErrTLSForwardingCACertificateNotExists } - if r.TLSForwardingCAPrivateKey != "" && !utils.FileExists(r.TLSForwardingCAPrivateKey) { + if r.TLSForwardingCAPrivateKey != "" && !utils.FileExists(r.FileRoot, r.TLSForwardingCAPrivateKey) { return apperrors.ErrTLSForwardingCAPrivateKeyNotExists } - if r.TLSStoreCACertificate != "" && !utils.FileExists(r.TLSStoreCACertificate) { + if r.TLSStoreCACertificate != "" && !utils.FileExists(r.FileRoot, r.TLSStoreCACertificate) { return apperrors.ErrTLSStoreCACertificateNotExists } - if r.TLSStoreClientCertificate != "" && !utils.FileExists(r.TLSStoreClientCertificate) { + if r.TLSStoreClientCertificate != "" && !utils.FileExists(r.FileRoot, r.TLSStoreClientCertificate) { return apperrors.ErrTLSStoreClientCertificateNotExists } - if r.TLSStoreClientPrivateKey != "" && !utils.FileExists(r.TLSStoreClientPrivateKey) { + if r.TLSStoreClientPrivateKey != "" && !utils.FileExists(r.FileRoot, r.TLSStoreClientPrivateKey) { return apperrors.ErrTLSStoreClientPrivateKeyNotExists } - if r.TLSOpenIDProviderCACertificate != "" && !utils.FileExists(r.TLSOpenIDProviderCACertificate) { + if r.TLSOpenIDProviderCACertificate != "" && !utils.FileExists(r.FileRoot, r.TLSOpenIDProviderCACertificate) { return apperrors.ErrTLSOpenIDPCACertificateNotExists } - if r.TLSOpenIDProviderClientCertificate != "" && !utils.FileExists(r.TLSOpenIDProviderClientCertificate) { + if r.TLSOpenIDProviderClientCertificate != "" && !utils.FileExists(r.FileRoot, r.TLSOpenIDProviderClientCertificate) { return apperrors.ErrTLSOpenIDPClientCertificateNotExists } - if r.TLSOpenIDProviderClientPrivateKey != "" && !utils.FileExists(r.TLSOpenIDProviderClientPrivateKey) { + if r.TLSOpenIDProviderClientPrivateKey != "" && !utils.FileExists(r.FileRoot, r.TLSOpenIDProviderClientPrivateKey) { return apperrors.ErrTLSOpenIDPClientPrivateKeyNotExists } } @@ -568,21 +569,21 @@ func (r *Config) isAdminTLSFilesValid(fileCheckEnable bool) error { } if fileCheckEnable { - if r.TLSAdminCertificate != "" && !utils.FileExists(r.TLSAdminCertificate) { + if r.TLSAdminCertificate != "" && !utils.FileExists(r.FileRoot, r.TLSAdminCertificate) { return fmt.Errorf( "the tls certificate %s does not exist for admin endpoint", r.TLSAdminCertificate, ) } - if r.TLSAdminPrivateKey != "" && !utils.FileExists(r.TLSAdminPrivateKey) { + if r.TLSAdminPrivateKey != "" && !utils.FileExists(r.FileRoot, r.TLSAdminPrivateKey) { return fmt.Errorf( "the tls private key %s does not exist for admin endpoint", r.TLSAdminPrivateKey, ) } - if r.TLSAdminClientCACertificate != "" && !utils.FileExists(r.TLSAdminClientCACertificate) { + if r.TLSAdminClientCACertificate != "" && !utils.FileExists(r.FileRoot, r.TLSAdminClientCACertificate) { return fmt.Errorf( "the tls client CA certificate %s does not exist for admin endpoint", r.TLSAdminClientCACertificate, diff --git a/pkg/keycloak/proxy/server.go b/pkg/keycloak/proxy/server.go index 36a78969..63973638 100644 --- a/pkg/keycloak/proxy/server.go +++ b/pkg/keycloak/proxy/server.go @@ -159,6 +159,7 @@ func NewProxy(config *config.Config, log *zap.Logger, upstream core.ReverseProxy config.TLSStoreClientCertificate, config.TLSStoreClientPrivateKey, config.OpenIDProviderTimeout, + config.FileRoot, ) if err != nil { svc.Log.Error("failed to setup store", zap.Error(err)) @@ -218,6 +219,7 @@ func setupStore( tlsStoreClientCertificate string, tlsStoreClientPrivateKey string, timeout time.Duration, + fileRoot string, ) (storage.Storage, error) { var ( certPool *x509.CertPool @@ -226,7 +228,7 @@ func setupStore( ) if tlsStoreCaCertificate != "" { - certPool, err = encryption.LoadCert(tlsStoreCaCertificate) + certPool, err = encryption.LoadCert(fileRoot, tlsStoreCaCertificate) if err != nil { return nil, errors.Join(apperrors.ErrLoadStoreCA, err) } @@ -234,6 +236,7 @@ func setupStore( if tlsStoreClientCertificate != "" && tlsStoreClientPrivateKey != "" { keyPair, err = encryption.LoadKeyPair( + fileRoot, tlsStoreClientCertificate, tlsStoreClientPrivateKey, ) @@ -1098,7 +1101,11 @@ func (r *OauthProxy) createForwardingProxy() error { if r.Config.TLSForwardingCACertificate != "" && r.Config.TLSForwardingCAPrivateKey != "" { r.Log.Info("enabling generating server certificate from CA") - cAuthority, err := encryption.LoadKeyPair(r.Config.TLSForwardingCACertificate, r.Config.TLSForwardingCAPrivateKey) + cAuthority, err := encryption.LoadKeyPair( + r.Config.FileRoot, + r.Config.TLSForwardingCACertificate, + r.Config.TLSForwardingCAPrivateKey, + ) if err != nil { return fmt.Errorf("unable to load certificate/private key pair for CA, error: %w", err) } @@ -1108,7 +1115,7 @@ func (r *OauthProxy) createForwardingProxy() error { if r.Config.TLSClientCACertificate != "" { r.Log.Info("enabling tls client authentication") - clientCA, err = encryption.LoadCert(r.Config.TLSClientCACertificate) + clientCA, err = encryption.LoadCert(r.Config.FileRoot, r.Config.TLSClientCACertificate) if err != nil { return err } @@ -1461,7 +1468,7 @@ func (r *OauthProxy) createHTTPListener(config listenerConfig) (net.Listener, er if strings.HasPrefix(config.listen, "unix://") { socket := config.listen[7:] - if exists := utils.FileExists(socket); exists { + if exists := utils.FileExists(r.Config.FileRoot, socket); exists { err = os.Remove(socket) if err != nil { return nil, err @@ -1563,6 +1570,7 @@ func (r *OauthProxy) createHTTPListener(config listenerConfig) (net.Listener, er config.privateKey, r.Log, &metrics.CertificateRotationMetric, + r.Config.FileRoot, ) if err != nil { return nil, err @@ -1591,7 +1599,7 @@ func (r *OauthProxy) createHTTPListener(config listenerConfig) (net.Listener, er // @check if we doing mutual tls if config.clientCACert != "" { - caCert, err := os.ReadFile(config.clientCACert) + caCert, err := utils.ReadFile(r.Config.FileRoot, config.clientCACert) if err != nil { return nil, err } @@ -1642,7 +1650,7 @@ func (r *OauthProxy) createUpstreamProxy(upstream *url.URL) error { zap.String("path", r.Config.UpstreamCA), ) - cAuthority, err := os.ReadFile(r.Config.UpstreamCA) + cAuthority, err := utils.ReadFile(r.Config.FileRoot, r.Config.UpstreamCA) if err != nil { return err } @@ -1659,7 +1667,11 @@ func (r *OauthProxy) createUpstreamProxy(upstream *url.URL) error { zap.String("client key path", r.Config.TLSClientPrivateKey), ) - clientPair, err := encryption.LoadKeyPair(r.Config.TLSClientCertificate, r.Config.TLSClientPrivateKey) + clientPair, err := encryption.LoadKeyPair( + r.Config.FileRoot, + r.Config.TLSClientCertificate, + r.Config.TLSClientPrivateKey, + ) if err != nil { return fmt.Errorf("unable to load certificate/private client key pair error: %w", err) } @@ -1836,7 +1848,7 @@ func (r *OauthProxy) NewOpenIDProvider() (*oidc3.Provider, *keycloak_client.Clie zap.String("path", r.Config.TLSOpenIDProviderCACertificate), ) - pool, err := encryption.LoadCert(r.Config.TLSOpenIDProviderCACertificate) + pool, err := encryption.LoadCert(r.Config.FileRoot, r.Config.TLSOpenIDProviderCACertificate) if err != nil { return nil, nil, errors.Join(apperrors.ErrLoadIDPCA, err) } @@ -1852,6 +1864,7 @@ func (r *OauthProxy) NewOpenIDProvider() (*oidc3.Provider, *keycloak_client.Clie ) clientKeyPair, err := encryption.LoadKeyPair( + r.Config.FileRoot, r.Config.TLSOpenIDProviderClientCertificate, r.Config.TLSOpenIDProviderClientPrivateKey, ) diff --git a/pkg/testsuite/constant.go b/pkg/testsuite/constant.go index dfd6fde2..eba0b6f1 100644 --- a/pkg/testsuite/constant.go +++ b/pkg/testsuite/constant.go @@ -36,10 +36,12 @@ const ( DefaultIat = 1450372669 OAuthCodeLength = 32 TestBaseURI = "/base-uri" + ForbiddenPagePath = "../../templates/forbidden.html.tmpl" ) var ( ErrCreateFakeProxy = errors.New("failed to create fake proxy service") + ErrRunFakeProxy = errors.New("failed to run fake proxy service") ErrRunHTTPServer = errors.New("failed to run http server") ErrShutHTTPServer = errors.New("failed to shutdown http server") ) diff --git a/pkg/testsuite/fake_proxy.go b/pkg/testsuite/fake_proxy.go index f6b7bf67..99aff685 100644 --- a/pkg/testsuite/fake_proxy.go +++ b/pkg/testsuite/fake_proxy.go @@ -122,7 +122,7 @@ func newFakeProxy(cfg *config.Config, authConfig *fakeAuthConfig) *fakeProxy { // proxy.log = zap.NewNop() _, err = oProxy.Run() if err != nil { - panic("failed to create the proxy service, error: " + err.Error()) + panic(errors.Join(ErrRunFakeProxy, err).Error()) } cfg.RedirectionURL = "http://" + oProxy.Listener.Addr().String() diff --git a/pkg/testsuite/middleware_test.go b/pkg/testsuite/middleware_test.go index 0e05b20a..e6776ea2 100644 --- a/pkg/testsuite/middleware_test.go +++ b/pkg/testsuite/middleware_test.go @@ -3031,8 +3031,7 @@ func TestEnableOpa(t *testing.T) { conf.OpaTimeout = 60 * time.Second conf.ClientID = ValidUsername conf.ClientSecret = ValidPassword - //nolint:goconst - conf.ForbiddenPage = "../../templates/forbidden.html.tmpl" + conf.ForbiddenPage = ForbiddenPagePath }, ExecutionSettings: []fakeRequest{ { diff --git a/pkg/testsuite/server_test.go b/pkg/testsuite/server_test.go index b93cb6f4..a3d36de1 100644 --- a/pkg/testsuite/server_test.go +++ b/pkg/testsuite/server_test.go @@ -759,7 +759,7 @@ func TestEnableHmacForwardingProxy(t *testing.T) { func TestForbiddenTemplate(t *testing.T) { cfg := newFakeKeycloakConfig() - cfg.ForbiddenPage = "../../templates/forbidden.html.tmpl" + cfg.ForbiddenPage = ForbiddenPagePath cfg.Resources = []*core.Resource{ { URL: "/*", @@ -2678,6 +2678,130 @@ func TestMaxBodySize(t *testing.T) { } } +//nolint:cyclop +func TestFileRoot(t *testing.T) { + tmpDir := os.TempDir() + + testCases := []struct { + Name string + ProxySettings func(conf *config.Config) + Panic bool + }{ + { + Name: "TestFileRootMatch", + ProxySettings: func(conf *config.Config) { + conf.FileRoot = tmpDir + conf.EnableDefaultDeny = true + //nolint:gosec + conf.TLSCertificate = strings.TrimPrefix(FakeCertFilePrefix, "/") + strconv.Itoa(rand.Intn(10000)) + //nolint:gosec + conf.TLSPrivateKey = strings.TrimPrefix(FakePrivFilePrefix, "/") + strconv.Itoa(rand.Intn(10000)) + //nolint:gosec + conf.TLSClientCertificate = strings.TrimPrefix(FakeCaFilePrefix, "/") + strconv.Itoa(rand.Intn(10000)) + conf.NoRedirects = true + }, + }, + { + Name: "TestFileRootDiffer", + ProxySettings: func(conf *config.Config) { + conf.FileRoot = "/var/lib" + conf.EnableDefaultDeny = true + //nolint:gosec + conf.TLSCertificate = tmpDir + FakeCertFilePrefix + strconv.Itoa(rand.Intn(10000)) + //nolint:gosec + conf.TLSPrivateKey = tmpDir + FakePrivFilePrefix + strconv.Itoa(rand.Intn(10000)) + //nolint:gosec + conf.TLSClientCertificate = tmpDir + FakeCaFilePrefix + strconv.Itoa(rand.Intn(10000)) + conf.TLSMinVersion = constant.TLS13 + conf.NoRedirects = true + }, + Panic: true, + }, + } + + for idx, testCase := range testCases { + cfg := newFakeKeycloakConfig() + + t.Run( + testCase.Name, + func(t *testing.T) { + testCase.ProxySettings(cfg) + + certFile := "" + privFile := "" + caFile := "" + + if cfg.TLSCertificate != "" { + certFile = cfg.TLSCertificate + } + + if cfg.TLSPrivateKey != "" { + privFile = cfg.TLSPrivateKey + } + + if cfg.TLSClientCertificate != "" { + caFile = cfg.TLSClientCertificate + } + + if certFile != "" { + fakeCertByte := []byte(fakeCert) + + if !testCase.Panic { + certFile = tmpDir + "/" + certFile + } + + err := os.WriteFile(certFile, fakeCertByte, 0o600) + if err != nil { + t.Fatalf("Problem writing certificate %s", err) + } + defer os.Remove(certFile) + } + + if privFile != "" { + fakeKeyByte := []byte(fakePrivateKey) + + if !testCase.Panic { + privFile = tmpDir + "/" + privFile + } + + err := os.WriteFile(privFile, fakeKeyByte, 0o600) + if err != nil { + t.Fatalf("Problem writing privateKey %s", err) + } + defer os.Remove(privFile) + } + + if caFile != "" { + fakeCAByte := []byte(fakeCA) + + if !testCase.Panic { + caFile = tmpDir + "/" + caFile + } + + err := os.WriteFile(caFile, fakeCAByte, 0o600) + if err != nil { + t.Fatalf("Problem writing cacertificate %s", err) + } + defer os.Remove(caFile) + } + + defer func() { + rec := recover() + if testCase.Panic { + assert.NotNil(t, rec, "Expected recover from panic, test: %d", idx) + assert.Contains(t, rec, ErrRunFakeProxy.Error()) + assert.Contains(t, rec, "path escapes from parent") + } else { + assert.Nil(t, rec, "Expected not panic, test: %d", idx) + } + }() + + newFakeProxy(cfg, &fakeAuthConfig{}) + }, + ) + } +} + // commented out because of see https://github.com/golang/go/issues/51416 // func TestUpstreamProxy(t *testing.T) { // errChan := make(chan error) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 9b617577..46d94e7f 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -110,7 +110,25 @@ func DefaultTo(v, d string) string { return d } -func FileExists(filename string) bool { +func FileExists(fileRoot, filename string) bool { + if fileRoot != "" { + root, err := os.OpenRoot(fileRoot) + if err != nil { + return false + } + + defer root.Close() + + _, err = root.Stat(filename) + if err != nil { + if os.IsNotExist(err) { + return false + } + } + + return true + } + _, err := os.Stat(filename) if err != nil { if os.IsNotExist(err) { @@ -752,3 +770,45 @@ func GetRandomString(n int) (string, error) { return string(runes), nil } + +func ReadFile(fileRoot, filename string) ([]byte, error) { + var ( + err error + content []byte + ) + + if fileRoot != "" { + root, err := os.OpenRoot(fileRoot) + if err != nil { + return nil, err + } + + defer root.Close() + + content, err = root.ReadFile(filename) + if err != nil { + return nil, err + } + } else { + content, err = os.ReadFile(filename) + if err != nil { + return nil, err + } + } + + return content, nil +} + +func LoadX509KeyPairFromRoot(fileRoot, certFile, keyFile string) (tls.Certificate, error) { + certPEMBlock, err := ReadFile(fileRoot, certFile) + if err != nil { + return tls.Certificate{}, err + } + + keyPEMBlock, err := ReadFile(fileRoot, keyFile) + if err != nil { + return tls.Certificate{}, err + } + + return tls.X509KeyPair(certPEMBlock, keyPEMBlock) +} diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index 62488fe7..afd69e12 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -368,7 +368,7 @@ func TestIdValidHTTPMethod(t *testing.T) { } func TestFileExists(t *testing.T) { - if utils.FileExists("no_such_file_exsit_32323232") { + if utils.FileExists("", "no_such_file_exsit_32323232") { t.Error("we should have received false") } @@ -382,7 +382,7 @@ func TestFileExists(t *testing.T) { defer os.Remove(tmpfile.Name()) - if !utils.FileExists(tmpfile.Name()) { + if !utils.FileExists("", tmpfile.Name()) { t.Error("we should have received a true") } }