From c362e7cd73edf5a4c284c661baf32e4d1f2a47db Mon Sep 17 00:00:00 2001 From: Jude Hung Date: Tue, 5 Nov 2024 11:02:29 +0800 Subject: [PATCH 1/2] feat: Add new go build tag no_openziti to reduce build size closes https://github.com/edgexfoundry/go-mod-bootstrap/issues/789 The usage of OpenZiti packages to support zero trust feature significantly increases the build size. For example, core-metadata increases from 14MB to 21MB, core-command increases from 9.8MB to 17MB, device-virtual increases from 18MB to 31MB, and app-service-configurable increases from 22Mb to 34MB. As many edge user scenarios require to deploy EdgeX services on resource-constrained devices without security, allowing that the services can be built without OpenZiti packages and ZeroTrust features can be helpful to user cases which don't need zero trust feature. This commit refactors the codes importing openziti packages with //go:build !no_openziti directive and creates para codes that don't use openziti packages with //go:build no_openziti directive, so users can simply build services by specifying no_openziti tag. Also remove vestigal zc variables and ZitiContext struct per discussion with https://github.com/dovholuknf in https://github.com/edgexfoundry/go-mod-bootstrap/pull/659#discussion_r1834363537 Signed-off-by: Jude Hung --- bootstrap/handlers/auth_func.go | 55 ++++ bootstrap/handlers/auth_middleware.go | 49 +--- bootstrap/handlers/auth_middleware_no_ziti.go | 86 ++++++ bootstrap/handlers/httpserver.go | 98 +------ bootstrap/zerotrust/constants.go | 21 ++ bootstrap/zerotrust/zerotrust.go | 107 -------- bootstrap/zerotrust/zerotrust_no_ziti.go | 85 ++++++ bootstrap/zerotrust/ziti.go | 244 ++++++++++++++++++ 8 files changed, 505 insertions(+), 240 deletions(-) create mode 100644 bootstrap/handlers/auth_func.go create mode 100644 bootstrap/handlers/auth_middleware_no_ziti.go create mode 100644 bootstrap/zerotrust/constants.go delete mode 100644 bootstrap/zerotrust/zerotrust.go create mode 100644 bootstrap/zerotrust/zerotrust_no_ziti.go create mode 100644 bootstrap/zerotrust/ziti.go diff --git a/bootstrap/handlers/auth_func.go b/bootstrap/handlers/auth_func.go new file mode 100644 index 00000000..61d72d2a --- /dev/null +++ b/bootstrap/handlers/auth_func.go @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright 2024 IOTech Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + *******************************************************************************/ + +package handlers + +import ( + "os" + "strconv" + + "github.com/edgexfoundry/go-mod-core-contracts/v4/clients/logger" + + "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/interfaces" + "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/secret" + + "github.com/labstack/echo/v4" +) + +// NilAuthenticationHandlerFunc just invokes a nested handler +func NilAuthenticationHandlerFunc() echo.MiddlewareFunc { + return func(inner echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + return inner(c) + } + } +} + +// AutoConfigAuthenticationFunc auto-selects between a HandlerFunc +// wrapper that does authentication and a HandlerFunc wrapper that does not. +// By default, JWT validation is enabled in secure mode +// (i.e. when using a real secrets provider instead of a no-op stub) +// +// Set EDGEX_DISABLE_JWT_VALIDATION to 1, t, T, TRUE, true, or True +// to disable JWT validation. This might be wanted for an EdgeX +// adopter that wanted to only validate JWT's at the proxy layer, +// or as an escape hatch for a caller that cannot authenticate. +func AutoConfigAuthenticationFunc(secretProvider interfaces.SecretProviderExt, lc logger.LoggingClient) echo.MiddlewareFunc { + // Golang standard library treats an error as false + disableJWTValidation, _ := strconv.ParseBool(os.Getenv("EDGEX_DISABLE_JWT_VALIDATION")) + authenticationHook := NilAuthenticationHandlerFunc() + if secret.IsSecurityEnabled() && !disableJWTValidation { + authenticationHook = SecretStoreAuthenticationHandlerFunc(secretProvider, lc) + } + return authenticationHook +} diff --git a/bootstrap/handlers/auth_middleware.go b/bootstrap/handlers/auth_middleware.go index bdc80fc3..f277d4c2 100644 --- a/bootstrap/handlers/auth_middleware.go +++ b/bootstrap/handlers/auth_middleware.go @@ -1,6 +1,8 @@ +//go:build !no_openziti + /******************************************************************************* * Copyright 2023 Intel Corporation - * Copyright 2023 IOTech Ltd + * Copyright 2023-2024 IOTech Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -18,14 +20,11 @@ package handlers import ( "fmt" "net/http" - "os" - "strconv" "strings" - "github.com/edgexfoundry/go-mod-core-contracts/v4/clients/logger" - "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/interfaces" - "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/secret" + "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/zerotrust" + "github.com/edgexfoundry/go-mod-core-contracts/v4/clients/logger" "github.com/labstack/echo/v4" "github.com/openziti/sdk-golang/ziti/edge" @@ -57,11 +56,13 @@ func SecretStoreAuthenticationHandlerFunc(secretProvider interfaces.SecretProvid lc.Debugf("Authorizing incoming call to '%s' via JWT (Authorization len=%d), %v", r.URL.Path, len(authHeader), secretProvider.IsZeroTrustEnabled()) if secretProvider.IsZeroTrustEnabled() { - zitiCtx := r.Context().Value(OpenZitiIdentityKey{}) + zitiCtx := r.Context().Value(zerotrust.OpenZitiIdentityKey{}) if zitiCtx != nil { - zitiEdgeConn := zitiCtx.(edge.Conn) - lc.Debugf("Authorizing incoming connection via OpenZiti for %s", zitiEdgeConn.SourceIdentifier()) - return inner(c) + if zitiEdgeConn, ok := zitiCtx.(edge.Conn); ok { + lc.Debugf("Authorizing incoming connection via OpenZiti for %s", zitiEdgeConn.SourceIdentifier()) + return inner(c) + } + lc.Warn("context value for OpenZitiIdentityKey is not an edge.Conn") } lc.Debug("zero trust was enabled, but no marker was found. this is unexpected. falling back to token-based auth") } @@ -92,31 +93,3 @@ func SecretStoreAuthenticationHandlerFunc(secretProvider interfaces.SecretProvid } } } - -// NilAuthenticationHandlerFunc just invokes a nested handler -func NilAuthenticationHandlerFunc() echo.MiddlewareFunc { - return func(inner echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - return inner(c) - } - } -} - -// AutoConfigAuthenticationFunc auto-selects between a HandlerFunc -// wrapper that does authentication and a HandlerFunc wrapper that does not. -// By default, JWT validation is enabled in secure mode -// (i.e. when using a real secrets provider instead of a no-op stub) -// -// Set EDGEX_DISABLE_JWT_VALIDATION to 1, t, T, TRUE, true, or True -// to disable JWT validation. This might be wanted for an EdgeX -// adopter that wanted to only validate JWT's at the proxy layer, -// or as an escape hatch for a caller that cannot authenticate. -func AutoConfigAuthenticationFunc(secretProvider interfaces.SecretProviderExt, lc logger.LoggingClient) echo.MiddlewareFunc { - // Golang standard library treats an error as false - disableJWTValidation, _ := strconv.ParseBool(os.Getenv("EDGEX_DISABLE_JWT_VALIDATION")) - authenticationHook := NilAuthenticationHandlerFunc() - if secret.IsSecurityEnabled() && !disableJWTValidation { - authenticationHook = SecretStoreAuthenticationHandlerFunc(secretProvider, lc) - } - return authenticationHook -} diff --git a/bootstrap/handlers/auth_middleware_no_ziti.go b/bootstrap/handlers/auth_middleware_no_ziti.go new file mode 100644 index 00000000..c5deb293 --- /dev/null +++ b/bootstrap/handlers/auth_middleware_no_ziti.go @@ -0,0 +1,86 @@ +//go:build no_openziti + +/******************************************************************************* + * Copyright 2024 IOTech Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + *******************************************************************************/ + +package handlers + +import ( + "fmt" + "net/http" + "strings" + + "github.com/edgexfoundry/go-mod-core-contracts/v4/clients/logger" + + "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/interfaces" + "github.com/labstack/echo/v4" +) + +// SecretStoreAuthenticationHandlerFunc prefixes an existing HandlerFunc +// with a OpenBao-based JWT authentication check. Usage: +// +// authenticationHook := handlers.NilAuthenticationHandlerFunc() +// if secret.IsSecurityEnabled() { +// lc := container.LoggingClientFrom(dic.Get) +// secretProvider := container.SecretProviderFrom(dic.Get) +// authenticationHook = handlers.SecretStoreAuthenticationHandlerFunc(secretProvider, lc) +// } +// For optionally-authenticated requests +// r.HandleFunc("path", authenticationHook(handlerFunc)).Methods(http.MethodGet) +// +// For unauthenticated requests +// r.HandleFunc("path", handlerFunc).Methods(http.MethodGet) +// +// For typical usage, it is preferred to use AutoConfigAuthenticationFunc which +// will automatically select between a real and a fake JWT validation handler. +func SecretStoreAuthenticationHandlerFunc(secretProvider interfaces.SecretProviderExt, lc logger.LoggingClient) echo.MiddlewareFunc { + return func(inner echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + r := c.Request() + w := c.Response() + authHeader := r.Header.Get("Authorization") + lc.Debugf("Authorizing incoming call to '%s' via JWT (Authorization len=%d), %v", r.URL.Path, len(authHeader), secretProvider.IsZeroTrustEnabled()) + + if secretProvider.IsZeroTrustEnabled() { + // this implementation will be pick up in the build when build tag no_openziti is specified, where + // OpenZiti packages are not included and the Zero Trust feature is not available. + lc.Info("zero trust was enabled, but service is built with no_openziti flag. falling back to token-based auth") + } + + authParts := strings.Split(authHeader, " ") + if len(authParts) >= 2 && strings.EqualFold(authParts[0], "Bearer") { + token := authParts[1] + validToken, err := secretProvider.IsJWTValid(token) + if err != nil { + lc.Errorf("Error checking JWT validity: %v", err) + // set Response.Committed to true in order to rewrite the status code + w.Committed = false + return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + } else if !validToken { + lc.Warnf("Request to '%s' UNAUTHORIZED", r.URL.Path) + // set Response.Committed to true in order to rewrite the status code + w.Committed = false + return echo.NewHTTPError(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized)) + } + lc.Debugf("Request to '%s' authorized", r.URL.Path) + return inner(c) + } + err := fmt.Errorf("unable to parse JWT for call to '%s'; unauthorized", r.URL.Path) + lc.Errorf("%v", err) + // set Response.Committed to true in order to rewrite the status code + w.Committed = false + return echo.NewHTTPError(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized)) + } + } +} diff --git a/bootstrap/handlers/httpserver.go b/bootstrap/handlers/httpserver.go index da5c7e9e..72f6ec91 100644 --- a/bootstrap/handlers/httpserver.go +++ b/bootstrap/handlers/httpserver.go @@ -1,6 +1,6 @@ /******************************************************************************* * Copyright 2019 Dell Inc. - * Copyright 2021-2023 IOTech Ltd + * Copyright 2021-2024 IOTech Ltd * Copyright 2023 Intel Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -19,13 +19,10 @@ package handlers import ( "context" "encoding/json" - "errors" "fmt" - "net" "net/http" "os" "strconv" - "strings" "sync" "time" @@ -33,17 +30,13 @@ import ( "github.com/edgexfoundry/go-mod-core-contracts/v4/common" commonDTO "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/common" - "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/config" "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/zerotrust" "github.com/edgexfoundry/go-mod-bootstrap/v4/di" - "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/zerotrust" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" - edge_apis "github.com/openziti/sdk-golang/edge-apis" - "github.com/openziti/sdk-golang/ziti" - "github.com/openziti/sdk-golang/ziti/edge" ) // HttpServer contains references to dependencies required by the http server implementation. @@ -54,11 +47,6 @@ type HttpServer struct { serverKey string } -type ZitiContext struct { - c *ziti.Context -} -type OpenZitiIdentityKey struct{} - // NewHttpServer is a factory method that returns an initialized HttpServer receiver struct. func NewHttpServer(router *echo.Echo, doListenAndServe bool, serviceKey string) *HttpServer { return &HttpServer{ @@ -139,8 +127,6 @@ func (b *HttpServer) BootstrapHandler( Timeout: timeout, })) - zc := &ZitiContext{} - b.router.Use(RequestLimitMiddleware(bootstrapConfig.Service.MaxRequestSize, lc)) b.router.Use(ProcessCORS(bootstrapConfig.Service.CORSConfiguration)) @@ -153,7 +139,6 @@ func (b *HttpServer) BootstrapHandler( Handler: b.router, ReadHeaderTimeout: 5 * time.Second, // G112: A configured ReadHeaderTimeout in the http.Server averts a potential Slowloris Attack } - server.ConnContext = mutator wg.Add(1) go func() { @@ -174,76 +159,7 @@ func (b *HttpServer) BootstrapHandler( }() b.isRunning = true - listenMode := strings.ToLower(bootstrapConfig.Service.SecurityOptions[config.SecurityModeKey]) - switch listenMode { - case zerotrust.ZeroTrustMode: - secretProvider := container.SecretProviderExtFrom(dic.Get) - if secretProvider == nil { - err = errors.New("secret provider is nil. cannot proceed with zero trust configuration") - break - } - secretProvider.EnableZeroTrust() //mark the secret provider as zero trust enabled - var zitiCtx ziti.Context - var ctxErr error - jwt, jwtErr := secretProvider.GetSelfJWT() - if jwtErr != nil { - lc.Errorf("could not load jwt: %v", jwtErr) - err = jwtErr - break - } - ozUrl := bootstrapConfig.Service.SecurityOptions["OpenZitiController"] - if !strings.Contains(ozUrl, "://") { - ozUrl = "https://" + ozUrl - } - caPool, caErr := ziti.GetControllerWellKnownCaPool(ozUrl) - if caErr != nil { - err = caErr - break - } - - credentials := edge_apis.NewJwtCredentials(jwt) - credentials.CaPool = caPool - - cfg := &ziti.Config{ - ZtAPI: ozUrl + "/edge/client/v1", - Credentials: credentials, - } - cfg.ConfigTypes = append(cfg.ConfigTypes, "all") - - zitiCtx, ctxErr = ziti.NewContext(cfg) - if ctxErr != nil { - err = ctxErr - break - } - - ozServiceName := zerotrust.OpenZitiServicePrefix + b.serverKey - lc.Infof("Using OpenZiti service name: %s", ozServiceName) - for t.HasNotElapsed() { - ln, listenErr := zitiCtx.Listen(ozServiceName) - if listenErr != nil { - err = fmt.Errorf("could not bind service %s: %s", ozServiceName, listenErr.Error()) - t.SleepForInterval() - } else { - zc.c = &zitiCtx - lc.Infof("listening on overlay network. ListenMode '%s' at %s", listenMode, addr) - err = server.Serve(ln) - break - } - } - if !t.HasNotElapsed() { - lc.Error("could not listen on the OpenZiti overlay network. timeout reached") - } - case "http": - fallthrough - default: - lc.Infof("listening on underlay network. ListenMode '%s' at %s", listenMode, addr) - ln, listenErr := net.Listen("tcp", addr) - if listenErr != nil { - err = listenErr - break - } - err = server.Serve(ln) - } + err = zerotrust.ListenOnMode(bootstrapConfig, b.serverKey, addr, t, server, dic) // "Server closed" error occurs when Shutdown above is called in the Done processing, so it can be ignored if err != nil && err != http.ErrServerClosed { @@ -292,11 +208,3 @@ func RequestLimitMiddleware(sizeLimit int64, lc logger.LoggingClient) echo.Middl } } } - -func mutator(srcCtx context.Context, c net.Conn) context.Context { - if zitiConn, ok := c.(edge.Conn); ok { - return context.WithValue(srcCtx, OpenZitiIdentityKey{}, zitiConn) - } - - return srcCtx -} diff --git a/bootstrap/zerotrust/constants.go b/bootstrap/zerotrust/constants.go new file mode 100644 index 00000000..f085fad1 --- /dev/null +++ b/bootstrap/zerotrust/constants.go @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright 2024 IOTech Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + *******************************************************************************/ + +package zerotrust + +const ( + OpenZitiControllerKey = "OpenZitiController" + ZeroTrustMode = "zerotrust" + OpenZitiServicePrefix = "edgex." +) diff --git a/bootstrap/zerotrust/zerotrust.go b/bootstrap/zerotrust/zerotrust.go deleted file mode 100644 index fd1f1a91..00000000 --- a/bootstrap/zerotrust/zerotrust.go +++ /dev/null @@ -1,107 +0,0 @@ -package zerotrust - -import ( - "context" - "fmt" - "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/interfaces" - "github.com/edgexfoundry/go-mod-bootstrap/v4/config" - "github.com/edgexfoundry/go-mod-core-contracts/v4/clients/logger" - edgeapis "github.com/openziti/sdk-golang/edge-apis" - "github.com/openziti/sdk-golang/ziti" - "net" - "net/http" - "strings" -) - -const ( - OpenZitiControllerKey = "OpenZitiController" - ZeroTrustMode = "zerotrust" - OpenZitiServicePrefix = "edgex." -) - -func AuthToOpenZiti(ozController, jwt string) (ziti.Context, error) { - if !strings.Contains(ozController, "://") { - ozController = "https://" + ozController - } - caPool, caErr := ziti.GetControllerWellKnownCaPool(ozController) - if caErr != nil { - return nil, caErr - } - - credentials := edgeapis.NewJwtCredentials(jwt) - credentials.CaPool = caPool - - cfg := &ziti.Config{ - ZtAPI: ozController + "/edge/client/v1", - Credentials: credentials, - } - cfg.ConfigTypes = append(cfg.ConfigTypes, "all") - - ctx, ctxErr := ziti.NewContext(cfg) - if ctxErr != nil { - return nil, ctxErr - } - if authErr := ctx.Authenticate(); authErr != nil { - return nil, authErr - } - - return ctx, nil -} - -func HttpTransportFromService(secretProvider interfaces.SecretProviderExt, serviceInfo config.ServiceInfo, lc logger.LoggingClient) (http.RoundTripper, error) { - roundTripper := http.DefaultTransport - if secretProvider.IsZeroTrustEnabled() { - lc.Debugf("zero trust client detected for service: %s", serviceInfo.Host) - if rt, err := createZitifiedTransport(secretProvider, serviceInfo.SecurityOptions[OpenZitiControllerKey]); err != nil { - return nil, err - } else { - roundTripper = rt - } - } - return roundTripper, nil -} - -func HttpTransportFromClient(secretProvider interfaces.SecretProviderExt, clientInfo *config.ClientInfo, lc logger.LoggingClient) (http.RoundTripper, error) { - roundTripper := http.DefaultTransport - if secretProvider.IsZeroTrustEnabled() { - lc.Debugf("zero trust client detected for client: %s", clientInfo.Host) - if rt, err := createZitifiedTransport(secretProvider, clientInfo.SecurityOptions[OpenZitiControllerKey]); err != nil { - return nil, err - } else { - roundTripper = rt - } - } - return roundTripper, nil -} - -type ZitiDialer struct { - underlayDialer *net.Dialer -} - -func (z ZitiDialer) Dial(network, address string) (net.Conn, error) { - return z.underlayDialer.Dial(network, address) -} - -func createZitifiedTransport(secretProvider interfaces.SecretProviderExt, ozController string) (http.RoundTripper, error) { - jwt, errJwt := secretProvider.GetSelfJWT() - if errJwt != nil { - return nil, fmt.Errorf("could not load jwt: %v", errJwt) - } - ctx, authErr := AuthToOpenZiti(ozController, jwt) - if authErr != nil { - return nil, fmt.Errorf("could not authenticate to OpenZiti: %v", authErr) - } - - zitiContexts := ziti.NewSdkCollection() - zitiContexts.Add(ctx) - - fallback := &ZitiDialer{ - underlayDialer: secretProvider.FallbackDialer(), - } - zitiTransport := http.DefaultTransport.(*http.Transport).Clone() // copy default transport - zitiTransport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { - dialer := zitiContexts.NewDialerWithFallback(ctx, fallback) - return dialer.Dial(network, addr) - } - return zitiTransport, nil -} diff --git a/bootstrap/zerotrust/zerotrust_no_ziti.go b/bootstrap/zerotrust/zerotrust_no_ziti.go new file mode 100644 index 00000000..54248421 --- /dev/null +++ b/bootstrap/zerotrust/zerotrust_no_ziti.go @@ -0,0 +1,85 @@ +//go:build no_openziti + +/******************************************************************************* + * Copyright 2024 IOTech Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + *******************************************************************************/ + +package zerotrust + +import ( + "fmt" + "net" + "net/http" + "strings" + + btConfig "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/config" + "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/interfaces" + "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/v4/config" + "github.com/edgexfoundry/go-mod-bootstrap/v4/di" + "github.com/edgexfoundry/go-mod-core-contracts/v4/clients/logger" +) + +// HttpTransportFromService is the implementation for the case where the service is built with no_openziti flag. +func HttpTransportFromService(secretProvider interfaces.SecretProviderExt, _ config.ServiceInfo, lc logger.LoggingClient) (http.RoundTripper, error) { + return httpDefaultTransport(secretProvider, lc) +} + +// HttpTransportFromClient is the implementation for the case where the service is built with no_openziti flag. +func HttpTransportFromClient(secretProvider interfaces.SecretProviderExt, _ *config.ClientInfo, lc logger.LoggingClient) (http.RoundTripper, error) { + return httpDefaultTransport(secretProvider, lc) +} + +func httpDefaultTransport(secretProvider interfaces.SecretProviderExt, lc logger.LoggingClient) (http.RoundTripper, error) { + if secretProvider.IsZeroTrustEnabled() { + lc.Info("zero trust was enabled, but service is built with no_openziti flag. falling back to default HTTP transport") + } + return http.DefaultTransport, nil +} + +// SetupWebListener is the implementation for the case where the service is built with no_openziti flag. +func SetupWebListener(serviceConfig config.ServiceInfo, serviceName, addr string, dic *di.Container) (net.Listener, error) { + lc := container.LoggingClientFrom(dic.Get) + listenMode, ok := serviceConfig.SecurityOptions[btConfig.SecurityModeKey] + if ok { + lc.Debugf("service security option %s = %s", btConfig.SecurityModeKey, listenMode) + if strings.EqualFold(listenMode, ZeroTrustMode) { + lc.Warnf("service %s is configured with zero trust security mode, but the service is built with no_openziti flag. all zero trust operations will be ignored.", serviceName) + } + } + lc.Debugf("listening on underlay network. ListenMode '%s' at %s", listenMode, addr) + listener, err := net.Listen("tcp", addr) + if err != nil { + return nil, fmt.Errorf("could not listen on %s: %w", addr, err) + } + return listener, nil +} + +// ListenOnMode is the implementation for the case where the service is built with no_openziti flag. +func ListenOnMode(bootstrapConfig config.BootstrapConfiguration, serverKey, addr string, _ startup.Timer, server *http.Server, dic *di.Container) error { + lc := container.LoggingClientFrom(dic.Get) + + listenMode, ok := bootstrapConfig.Service.SecurityOptions[btConfig.SecurityModeKey] + if ok && strings.EqualFold(listenMode, ZeroTrustMode) { + lc.Warnf("service %s is configured with zero trust security mode, but the service is built with no_openziti flag. all zero trust operations will be ignored.", serverKey) + } + + // following codes are executed when SecurityModeKey is not set or not equal to ZeroTrustMode + lc.Infof("listening on underlay network. ListenMode '%s' at %s", listenMode, addr) + ln, listenErr := net.Listen("tcp", addr) + if listenErr != nil { + return listenErr + } + return server.Serve(ln) +} diff --git a/bootstrap/zerotrust/ziti.go b/bootstrap/zerotrust/ziti.go new file mode 100644 index 00000000..e45f305c --- /dev/null +++ b/bootstrap/zerotrust/ziti.go @@ -0,0 +1,244 @@ +//go:build !no_openziti + +/******************************************************************************* + * Copyright 2024 IOTech Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + *******************************************************************************/ + +package zerotrust + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "strings" + + btConfig "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/config" + "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/interfaces" + "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/v4/config" + "github.com/edgexfoundry/go-mod-bootstrap/v4/di" + "github.com/edgexfoundry/go-mod-core-contracts/v4/clients/logger" + edgeapis "github.com/openziti/sdk-golang/edge-apis" + "github.com/openziti/sdk-golang/ziti" + "github.com/openziti/sdk-golang/ziti/edge" +) + +func authToOpenZiti(ozController, jwt string) (ziti.Context, error) { + if !strings.Contains(ozController, "://") { + ozController = "https://" + ozController + } + caPool, caErr := ziti.GetControllerWellKnownCaPool(ozController) + if caErr != nil { + return nil, caErr + } + + credentials := edgeapis.NewJwtCredentials(jwt) + credentials.CaPool = caPool + + cfg := &ziti.Config{ + ZtAPI: ozController + "/edge/client/v1", + Credentials: credentials, + } + cfg.ConfigTypes = append(cfg.ConfigTypes, "all") + + ctx, ctxErr := ziti.NewContext(cfg) + if ctxErr != nil { + return nil, ctxErr + } + if authErr := ctx.Authenticate(); authErr != nil { + return nil, authErr + } + + return ctx, nil +} + +func HttpTransportFromService(secretProvider interfaces.SecretProviderExt, serviceInfo config.ServiceInfo, lc logger.LoggingClient) (http.RoundTripper, error) { + roundTripper := http.DefaultTransport + if secretProvider.IsZeroTrustEnabled() { + lc.Debugf("zero trust client detected for service: %s", serviceInfo.Host) + if rt, err := createZitifiedTransport(secretProvider, serviceInfo.SecurityOptions[OpenZitiControllerKey]); err != nil { + return nil, err + } else { + roundTripper = rt + } + } + return roundTripper, nil +} + +func HttpTransportFromClient(secretProvider interfaces.SecretProviderExt, clientInfo *config.ClientInfo, lc logger.LoggingClient) (http.RoundTripper, error) { + roundTripper := http.DefaultTransport + if secretProvider.IsZeroTrustEnabled() { + lc.Debugf("zero trust client detected for client: %s", clientInfo.Host) + if rt, err := createZitifiedTransport(secretProvider, clientInfo.SecurityOptions[OpenZitiControllerKey]); err != nil { + return nil, err + } else { + roundTripper = rt + } + } + return roundTripper, nil +} + +type ZitiDialer struct { + underlayDialer *net.Dialer +} + +func (z ZitiDialer) Dial(network, address string) (net.Conn, error) { + return z.underlayDialer.Dial(network, address) +} + +func createZitifiedTransport(secretProvider interfaces.SecretProviderExt, ozController string) (http.RoundTripper, error) { + jwt, errJwt := secretProvider.GetSelfJWT() + if errJwt != nil { + return nil, fmt.Errorf("could not load jwt: %v", errJwt) + } + ctx, authErr := authToOpenZiti(ozController, jwt) + if authErr != nil { + return nil, fmt.Errorf("could not authenticate to OpenZiti: %v", authErr) + } + + zitiContexts := ziti.NewSdkCollection() + zitiContexts.Add(ctx) + + fallback := &ZitiDialer{ + underlayDialer: secretProvider.FallbackDialer(), + } + zitiTransport := http.DefaultTransport.(*http.Transport).Clone() // copy default transport + zitiTransport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + dialer := zitiContexts.NewDialerWithFallback(ctx, fallback) + return dialer.Dial(network, addr) + } + return zitiTransport, nil +} + +func validateOpenZitiOptions(serviceConfig config.ServiceInfo, dic *di.Container) (ozToken, ozUrl string, err error) { + secretProvider := container.SecretProviderExtFrom(dic.Get) + if secretProvider == nil { + return ozToken, ozUrl, errors.New("nil secret provider is nil. cannot setup web listener for zero trust overlay network") + } + secretProvider.EnableZeroTrust() //mark the secret provider as zero trust enabled + ozToken, err = secretProvider.GetSelfJWT() + if err != nil { + return ozToken, ozUrl, fmt.Errorf("could not load jwt from secret provider while setting up web listener for zero trust overlay network: %w", err) + } + ozUrl, exists := serviceConfig.SecurityOptions[OpenZitiControllerKey] + if !exists { + return ozToken, ozUrl, fmt.Errorf("%s is not set in the service security options under zero trust mode", OpenZitiControllerKey) + } + return ozToken, ozUrl, nil +} + +func SetupWebListener(serviceConfig config.ServiceInfo, serviceName, addr string, dic *di.Container) (net.Listener, error) { + lc := container.LoggingClientFrom(dic.Get) + listenMode, ok := serviceConfig.SecurityOptions[btConfig.SecurityModeKey] + if ok { + lc.Debugf("service security option %s = %s", btConfig.SecurityModeKey, listenMode) + if strings.EqualFold(listenMode, ZeroTrustMode) { + ozToken, ozUrl, err := validateOpenZitiOptions(serviceConfig, dic) + if err != nil { + return nil, fmt.Errorf("could not setup web listener for zero trust overlay network: %w", err) + } + zitiCtx, err := authToOpenZiti(ozUrl, ozToken) + if err != nil { + return nil, fmt.Errorf("could not authenticate to OpenZiti while preparing web listner: %w", err) + } + ozServiceName := OpenZitiServicePrefix + serviceName + lc.Debugf("Using OpenZiti service name: %s", ozServiceName) + lc.Debugf("listening on overlay network. ListenMode '%s' at %s", listenMode, addr) + listener, err := zitiCtx.Listen(ozServiceName) + if err != nil { + return nil, fmt.Errorf("could not bind service %s: %w", ozServiceName, err) + } + return listener, nil + } + } + lc.Debugf("listening on underlay network. ListenMode '%s' at %s", listenMode, addr) + listener, err := net.Listen("tcp", addr) + if err != nil { + return nil, fmt.Errorf("could not listen on %s: %w", addr, err) + } + return listener, nil +} + +type OpenZitiIdentityKey struct{} + +// ListenOnMode configures and starts an HTTP server based on the provided security mode. +// If the security mode is set to zerotrust, it establishes a zero trust overlay network using OpenZiti. +// Otherwise, it listens on the specified address using the default network. +func ListenOnMode(bootstrapConfig config.BootstrapConfiguration, serverKey, addr string, t startup.Timer, server *http.Server, dic *di.Container) error { + lc := container.LoggingClientFrom(dic.Get) + + server.ConnContext = mutator + + listenMode, ok := bootstrapConfig.Service.SecurityOptions[btConfig.SecurityModeKey] + if ok { + lc.Debugf("service security option %s = %s", btConfig.SecurityModeKey, listenMode) + if strings.EqualFold(listenMode, ZeroTrustMode) { + ozToken, ozUrl, err := validateOpenZitiOptions(*bootstrapConfig.Service, dic) + if err != nil { + return fmt.Errorf("could not setup web listener for zero trust overlay network: %w", err) + } + if !strings.Contains(ozUrl, "://") { + ozUrl = "https://" + ozUrl + } + caPool, caErr := ziti.GetControllerWellKnownCaPool(ozUrl) + if caErr != nil { + return fmt.Errorf("fail to get CA pool while establishing zero trust overlay network: %w", caErr) + } + + credentials := edgeapis.NewJwtCredentials(ozToken) + credentials.CaPool = caPool + + cfg := &ziti.Config{ + ZtAPI: ozUrl + "/edge/client/v1", + Credentials: credentials, + } + cfg.ConfigTypes = append(cfg.ConfigTypes, "all") + + zitiCtx, ctxErr := ziti.NewContext(cfg) + if ctxErr != nil { + return fmt.Errorf("fail to create OpenZiti context while establishing zero trust overlay network: %w", ctxErr) + } + + ozServiceName := OpenZitiServicePrefix + serverKey + lc.Infof("Using OpenZiti service name: %s", ozServiceName) + for t.HasNotElapsed() { + ln, listenErr := zitiCtx.Listen(ozServiceName) + if listenErr != nil { + lc.Errorf("fail to bind OpenZiti service %s: %s. wait for later retry", ozServiceName, listenErr.Error()) + t.SleepForInterval() + } else { + lc.Infof("listening on OpenZiti overlay network at %s", addr) + return server.Serve(ln) + } + } + return errors.New("could not listen on the OpenZiti overlay network. timeout reached") + } + } + // following codes are executed when SecurityModeKey is not set or not equal to ZeroTrustMode + lc.Infof("listening on underlay network. ListenMode '%s' at %s", listenMode, addr) + ln, listenErr := net.Listen("tcp", addr) + if listenErr != nil { + return listenErr + } + return server.Serve(ln) +} + +func mutator(srcCtx context.Context, c net.Conn) context.Context { + if zitiConn, ok := c.(edge.Conn); ok { + return context.WithValue(srcCtx, OpenZitiIdentityKey{}, zitiConn) + } + return srcCtx +} From 64b7558f6f4f2ff9252aef0976e628619711485b Mon Sep 17 00:00:00 2001 From: Jude Hung Date: Tue, 19 Nov 2024 12:34:31 +0800 Subject: [PATCH 2/2] feat: rename the zerotrust_no_ziti.go to no_ziti.go Signed-off-by: Jude Hung --- bootstrap/zerotrust/{zerotrust_no_ziti.go => no_ziti.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename bootstrap/zerotrust/{zerotrust_no_ziti.go => no_ziti.go} (100%) diff --git a/bootstrap/zerotrust/zerotrust_no_ziti.go b/bootstrap/zerotrust/no_ziti.go similarity index 100% rename from bootstrap/zerotrust/zerotrust_no_ziti.go rename to bootstrap/zerotrust/no_ziti.go