fix: Strict-Transport-Security via proxy

This commit is contained in:
Michal Klos
2025-12-19 18:36:01 +01:00
parent 56037d2ebd
commit 1d0559866e
6 changed files with 41 additions and 9 deletions

View File

@@ -0,0 +1,5 @@
Enhancement: Force Strict-Transport-Security
Added `PROXY_FORCE_STRICT_TRANSPORT_SECURITY` environment variable to force emission of `Strict-Transport-Security` header on all responses, including plain HTTP requests when TLS is terminated upstream. Useful when oCIS is deployed behind a proxy.
https://github.com/owncloud/ocis/pull/11880

View File

@@ -385,6 +385,7 @@ When using the ocis IDP service instead of an external IDP:
- Use the environment variable `OCIS_URL` to define how ocis can be accessed, mandatory use `https` as protocol for the URL.
- If no reverse proxy is set up, the `PROXY_TLS` environment variable **must** be set to `true` because the embedded `libreConnect` shipped with the IDP service has a hard check if the connection is on TLS and uses the HTTPS protocol. If this mismatches, an error will be logged and no connection from the client can be established.
- `PROXY_TLS` **can** be set to `false` if a reverse proxy is used and the https connection is terminated at the reverse proxy. When setting to `false`, the communication between the reverse proxy and ocis is not secured. If set to `true`, you must provide certificates.
- `PROXY_FORCE_STRICT_TRANSPORT_SECURITY`: Set to `true` to force emission of the `Strict-Transport-Security` header on all responses, including plain HTTP requests. Required when `PROXY_TLS=false` (TLS terminated upstream) to ensure the header is emitted despite oCIS receiving plain HTTP from the reverse proxy.
## Metrics

View File

@@ -335,7 +335,7 @@ func loadMiddlewares(logger log.Logger, cfg *config.Config,
middleware.AccessLog(logger),
middleware.ContextLogger(logger),
middleware.HTTPSRedirect(cfg.Commons.OcisURL),
middleware.Security(cspConfig),
middleware.Security(cfg, cspConfig),
router.Middleware(serviceSelector, cfg.PolicySelector, cfg.Policies, logger),
middleware.Authentication(
authenticators,

View File

@@ -2,10 +2,11 @@ package config
// HTTP defines the available http configuration.
type HTTP struct {
Addr string `yaml:"addr" env:"PROXY_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"pre5.0"`
Root string `yaml:"root" env:"PROXY_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"pre5.0"`
Namespace string `yaml:"-"`
TLSCert string `yaml:"tls_cert" env:"PROXY_TRANSPORT_TLS_CERT" desc:"Path/File name of the TLS server certificate (in PEM format) for the external http services. If not defined, the root directory derives from $OCIS_BASE_DATA_PATH/proxy." introductionVersion:"pre5.0"`
TLSKey string `yaml:"tls_key" env:"PROXY_TRANSPORT_TLS_KEY" desc:"Path/File name for the TLS certificate key (in PEM format) for the server certificate to use for the external http services. If not defined, the root directory derives from $OCIS_BASE_DATA_PATH/proxy." introductionVersion:"pre5.0"`
TLS bool `yaml:"tls" env:"PROXY_TLS" desc:"Enable/Disable HTTPS for external HTTP services. Must be set to 'true' if the built-in IDP service an no reverse proxy is used. See the text description for details." introductionVersion:"pre5.0"`
Addr string `yaml:"addr" env:"PROXY_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"pre5.0"`
Root string `yaml:"root" env:"PROXY_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"pre5.0"`
Namespace string `yaml:"-"`
TLSCert string `yaml:"tls_cert" env:"PROXY_TRANSPORT_TLS_CERT" desc:"Path/File name of the TLS server certificate (in PEM format) for the external http services. If not defined, the root directory derives from $OCIS_BASE_DATA_PATH/proxy." introductionVersion:"pre5.0"`
TLSKey string `yaml:"tls_key" env:"PROXY_TRANSPORT_TLS_KEY" desc:"Path/File name for the TLS certificate key (in PEM format) for the server certificate to use for the external http services. If not defined, the root directory derives from $OCIS_BASE_DATA_PATH/proxy." introductionVersion:"pre5.0"`
TLS bool `yaml:"tls" env:"PROXY_TLS" desc:"Enable/Disable HTTPS for external HTTP services. Must be set to 'true' if the built-in IDP service an no reverse proxy is used. See the text description for details." introductionVersion:"pre5.0"`
ForceStrictTransportSecurity bool `yaml:"force_strict_transport_security" env:"PROXY_FORCE_STRICT_TRANSPORT_SECURITY" desc:"Force emission of the Strict-Transport-Security header on all responses, including plain HTTP requests. See also: PROXY_TLS" introductionVersion:"Curie"`
}

View File

@@ -49,7 +49,7 @@ func loadCSPYaml(proxyCfg *config.Config) ([]byte, error) {
}
// Security is a middleware to apply security relevant http headers like CSP.
func Security(cspConfig *config.CSP) func(h http.Handler) http.Handler {
func Security(cfg *config.Config, cspConfig *config.CSP) func(h http.Handler) http.Handler {
cspBuilder := cspbuilder.Builder{
Directives: cspConfig.Directives,
}
@@ -61,6 +61,7 @@ func Security(cspConfig *config.CSP) func(h http.Handler) http.Handler {
FrameDeny: true,
ReferrerPolicy: "no-referrer",
STSSeconds: 315360000,
ForceSTSHeader: cfg.HTTP.ForceStrictTransportSecurity,
STSIncludeSubdomains: true,
STSPreload: true,
PermittedCrossDomainPolicies: "none",

View File

@@ -42,7 +42,8 @@ func TestStrictTransportSecurity(t *testing.T) {
"default-src": {"'none'"},
},
}
securityMiddleware := Security(cspConfig)
cfg := &config.Config{HTTP: config.HTTP{ForceStrictTransportSecurity: false}}
securityMiddleware := Security(cfg, cspConfig)
// Test HTTPS request, url not important, only headers will be checked
req, err := http.NewRequest("GET", "https://example.com", nil)
@@ -60,3 +61,26 @@ func TestStrictTransportSecurity(t *testing.T) {
expected := "max-age=315360000; includeSubDomains; preload"
assert.Equal(t, hstsHeader, expected, "HSTS header missing includeSubDomains directive - subdomains not protected")
}
func TestStrictTransportSecurity_ForceOnHTTP(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
cspConfig := &config.CSP{
Directives: map[string][]string{
"default-src": {"'none'"},
},
}
cfg := &config.Config{HTTP: config.HTTP{ForceStrictTransportSecurity: true}}
securityMiddleware := Security(cfg, cspConfig)
// Plain HTTP request (no TLS); should still emit Strict-Transport-Security when forced.
req := httptest.NewRequest("GET", "http://example.com/", nil)
rr := httptest.NewRecorder()
securityMiddleware(handler).ServeHTTP(rr, req)
stsHeader := rr.Header().Get("Strict-Transport-Security")
expected := "max-age=315360000; includeSubDomains; preload"
assert.Equal(t, stsHeader, expected)
}