diff --git a/cmd/zitadel/startup.yaml b/cmd/zitadel/startup.yaml index 46f14d489..ca3090f87 100644 --- a/cmd/zitadel/startup.yaml +++ b/cmd/zitadel/startup.yaml @@ -205,6 +205,10 @@ Admin: Console: Port: 50050 EnvOverwriteDir: $ZITADEL_CONSOLE_ENV_DIR + Cache: + MaxAge: $ZITADEL_CACHE_MAXAGE + SharedMaxAge: $ZITADEL_CACHE_SHARED_MAXAGE + CSPDomain: $ZITADEL_DEFAULT_DOMAIN Notification: diff --git a/internal/api/http/middleware/csp.go b/internal/api/http/middleware/csp.go index c276cb850..70658edd8 100644 --- a/internal/api/http/middleware/csp.go +++ b/internal/api/http/middleware/csp.go @@ -6,29 +6,31 @@ import ( ) type CSP struct { - DefaultSrc CSPSourceOptions - ScriptSrc CSPSourceOptions - ObjectSrc CSPSourceOptions - StyleSrc CSPSourceOptions - ImgSrc CSPSourceOptions - MediaSrc CSPSourceOptions - FrameSrc CSPSourceOptions - FontSrc CSPSourceOptions - ConnectSrc CSPSourceOptions - FormAction CSPSourceOptions + DefaultSrc CSPSourceOptions + ScriptSrc CSPSourceOptions + ObjectSrc CSPSourceOptions + StyleSrc CSPSourceOptions + ImgSrc CSPSourceOptions + MediaSrc CSPSourceOptions + FrameSrc CSPSourceOptions + FontSrc CSPSourceOptions + ManifestSrc CSPSourceOptions + ConnectSrc CSPSourceOptions + FormAction CSPSourceOptions } var ( DefaultSCP = CSP{ - DefaultSrc: CSPSourceOptsNone(), - ScriptSrc: CSPSourceOptsSelf(), - ObjectSrc: CSPSourceOptsNone(), - StyleSrc: CSPSourceOptsSelf(), - ImgSrc: CSPSourceOptsSelf(), - MediaSrc: CSPSourceOptsNone(), - FrameSrc: CSPSourceOptsNone(), - FontSrc: CSPSourceOptsSelf(), - ConnectSrc: CSPSourceOptsSelf(), + DefaultSrc: CSPSourceOptsNone(), + ScriptSrc: CSPSourceOptsSelf(), + ObjectSrc: CSPSourceOptsNone(), + StyleSrc: CSPSourceOptsSelf(), + ImgSrc: CSPSourceOptsSelf(), + MediaSrc: CSPSourceOptsNone(), + FrameSrc: CSPSourceOptsNone(), + FontSrc: CSPSourceOptsSelf(), + ManifestSrc: CSPSourceOptsSelf(), + ConnectSrc: CSPSourceOptsSelf(), } ) @@ -49,16 +51,17 @@ func (csp *CSP) Value(nonce string) string { func (csp *CSP) asMap() map[string]CSPSourceOptions { return map[string]CSPSourceOptions{ - "default-src": csp.DefaultSrc, - "script-src": csp.ScriptSrc, - "object-src": csp.ObjectSrc, - "style-src": csp.StyleSrc, - "img-src": csp.ImgSrc, - "media-src": csp.MediaSrc, - "frame-src": csp.FrameSrc, - "font-src": csp.FontSrc, - "connect-src": csp.ConnectSrc, - "form-action": csp.FormAction, + "default-src": csp.DefaultSrc, + "script-src": csp.ScriptSrc, + "object-src": csp.ObjectSrc, + "style-src": csp.StyleSrc, + "img-src": csp.ImgSrc, + "media-src": csp.MediaSrc, + "frame-src": csp.FrameSrc, + "font-src": csp.FontSrc, + "manifest-src": csp.ManifestSrc, + "connect-src": csp.ConnectSrc, + "form-action": csp.FormAction, } } diff --git a/internal/api/http/middleware/security_headers.go b/internal/api/http/middleware/security_headers.go index 6f7b566c2..98bd4ac70 100644 --- a/internal/api/http/middleware/security_headers.go +++ b/internal/api/http/middleware/security_headers.go @@ -48,7 +48,16 @@ func (h *headers) ServeHTTP(w http.ResponseWriter, r *http.Request) { var err error nonce, err = generateNonce(h.nonceLength) if err != nil { - h.errorHandler(err).ServeHTTP(w, r) + errorHandler := h.errorHandler + if errorHandler == nil { + errorHandler = func(err error) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + }) + } + } + errorHandler(err).ServeHTTP(w, r) return } r = saveContext(r, nonceKey, nonce) diff --git a/pkg/console/console.go b/pkg/console/console.go index 3b5eaac8e..e7de09fb3 100644 --- a/pkg/console/console.go +++ b/pkg/console/console.go @@ -5,15 +5,20 @@ import ( "net/http" "os" "path" + "strings" + "time" "github.com/rakyll/statik/fs" + "github.com/caos/zitadel/internal/api/http/middleware" _ "github.com/caos/zitadel/pkg/console/statik" ) type Config struct { Port string EnvOverwriteDir string + Cache middleware.CacheConfig + CSPDomain string } type spaHandler struct { @@ -23,6 +28,8 @@ type spaHandler struct { const ( envRequestPath = "/assets/environment.json" envDefaultDir = "/console/" + + manifestFile = "/manifest.webmanifest" ) func (i *spaHandler) Open(name string) (http.File, error) { @@ -43,7 +50,33 @@ func Start(ctx context.Context, config Config) error { if config.EnvOverwriteDir != "" { envDir = config.EnvOverwriteDir } - http.Handle("/", http.FileServer(&spaHandler{statikFS})) - http.Handle(envRequestPath, http.StripPrefix("/assets", http.FileServer(http.Dir(envDir)))) + cache := AssetsCacheInterceptorIgnoreManifest(config.Cache.MaxAge.Duration, config.Cache.SharedMaxAge.Duration) + security := middleware.SecurityHeaders(csp(config.CSPDomain), nil) + http.Handle("/", cache(security(http.FileServer(&spaHandler{statikFS})))) + http.Handle(envRequestPath, cache(security(http.StripPrefix("/assets", http.FileServer(http.Dir(envDir)))))) return http.ListenAndServe(":"+config.Port, nil) } + +func csp(zitadelDomain string) *middleware.CSP { + if !strings.HasPrefix(zitadelDomain, "*.") { + zitadelDomain = "*." + zitadelDomain + } + csp := middleware.DefaultSCP + csp.StyleSrc = csp.StyleSrc.AddInline().AddHost("fonts.googleapis.com").AddHost("maxst.icons8.com") //TODO: host it + csp.FontSrc = csp.FontSrc.AddHost("fonts.gstatic.com").AddHost("maxst.icons8.com") //TODO: host it + csp.ScriptSrc = csp.ScriptSrc.AddEval() + csp.ConnectSrc = csp.ConnectSrc.AddHost(zitadelDomain) + return &csp +} + +func AssetsCacheInterceptorIgnoreManifest(maxAge, sharedMaxAge time.Duration) func(http.Handler) http.Handler { + return func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == manifestFile { + middleware.NoCacheInterceptor(handler).ServeHTTP(w, r) + return + } + middleware.AssetsCacheInterceptor(maxAge, sharedMaxAge, handler).ServeHTTP(w, r) + }) + } +}