Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions services/proxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion services/proxy/pkg/command/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
13 changes: 7 additions & 6 deletions services/proxy/pkg/config/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
3 changes: 2 additions & 1 deletion services/proxy/pkg/middleware/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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",
Expand Down
26 changes: 25 additions & 1 deletion services/proxy/pkg/middleware/security_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}