diff --git a/api/handler/api.go b/api/handler/api.go index 559977aa..6ef460ba 100644 --- a/api/handler/api.go +++ b/api/handler/api.go @@ -41,6 +41,7 @@ type ( RetryMaxAttempts() int RetryMaxBackoff() time.Duration RetryStrategy() RetryStrategy + TLSTerminationHeader() string } FrostFSID interface { diff --git a/api/handler/attributes.go b/api/handler/attributes.go index 22ceffb4..4e4a9132 100644 --- a/api/handler/attributes.go +++ b/api/handler/attributes.go @@ -98,7 +98,7 @@ func (h *handler) GetObjectAttributesHandler(w http.ResponseWriter, r *http.Requ } info := extendedInfo.ObjectInfo - encryptionParams, err := formEncryptionParams(r) + encryptionParams, err := h.formEncryptionParams(r) if err != nil { h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) return diff --git a/api/handler/copy.go b/api/handler/copy.go index 00473fa9..f4c8c783 100644 --- a/api/handler/copy.go +++ b/api/handler/copy.go @@ -103,12 +103,12 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) { } srcObjInfo := extendedSrcObjInfo.ObjectInfo - srcEncryptionParams, err := formCopySourceEncryptionParams(r) + srcEncryptionParams, err := h.formCopySourceEncryptionParams(r) if err != nil { h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) return } - dstEncryptionParams, err := formEncryptionParams(r) + dstEncryptionParams, err := h.formEncryptionParams(r) if err != nil { h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) return diff --git a/api/handler/get.go b/api/handler/get.go index 11b87703..3403ac7d 100644 --- a/api/handler/get.go +++ b/api/handler/get.go @@ -202,7 +202,7 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) { return } - encryptionParams, err := formEncryptionParams(r) + encryptionParams, err := h.formEncryptionParams(r) if err != nil { h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) return diff --git a/api/handler/handlers_test.go b/api/handler/handlers_test.go index e5a224b9..42db0eb0 100644 --- a/api/handler/handlers_test.go +++ b/api/handler/handlers_test.go @@ -141,6 +141,10 @@ func (c *configMock) RetryStrategy() RetryStrategy { return RetryStrategyConstant } +func (c *configMock) TLSTerminationHeader() string { + return "X-Frostfs-TLS-Termination" +} + func prepareHandlerContext(t *testing.T) *handlerContext { hc, err := prepareHandlerContextBase(layer.DefaultCachesConfigs(zap.NewExample())) require.NoError(t, err) diff --git a/api/handler/head.go b/api/handler/head.go index 314adfe2..8556e4b7 100644 --- a/api/handler/head.go +++ b/api/handler/head.go @@ -51,7 +51,7 @@ func (h *handler) HeadObjectHandler(w http.ResponseWriter, r *http.Request) { } info := extendedInfo.ObjectInfo - encryptionParams, err := formEncryptionParams(r) + encryptionParams, err := h.formEncryptionParams(r) if err != nil { h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) return diff --git a/api/handler/multipart_upload.go b/api/handler/multipart_upload.go index bb109275..73014853 100644 --- a/api/handler/multipart_upload.go +++ b/api/handler/multipart_upload.go @@ -138,7 +138,7 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re } } - p.Info.Encryption, err = formEncryptionParams(r) + p.Info.Encryption, err = h.formEncryptionParams(r) if err != nil { h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err, additional...) return @@ -223,7 +223,7 @@ func (h *handler) UploadPartHandler(w http.ResponseWriter, r *http.Request) { ContentSHA256Hash: r.Header.Get(api.AmzContentSha256), } - p.Info.Encryption, err = formEncryptionParams(r) + p.Info.Encryption, err = h.formEncryptionParams(r) if err != nil { h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err, additional...) return @@ -323,7 +323,7 @@ func (h *handler) UploadPartCopy(w http.ResponseWriter, r *http.Request) { return } - srcEncryptionParams, err := formCopySourceEncryptionParams(r) + srcEncryptionParams, err := h.formCopySourceEncryptionParams(r) if err != nil { h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) return @@ -348,7 +348,7 @@ func (h *handler) UploadPartCopy(w http.ResponseWriter, r *http.Request) { Range: srcRange, } - p.Info.Encryption, err = formEncryptionParams(r) + p.Info.Encryption, err = h.formEncryptionParams(r) if err != nil { h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err, additional...) return @@ -593,7 +593,7 @@ func (h *handler) ListPartsHandler(w http.ResponseWriter, r *http.Request) { PartNumberMarker: partNumberMarker, } - p.Info.Encryption, err = formEncryptionParams(r) + p.Info.Encryption, err = h.formEncryptionParams(r) if err != nil { h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) return @@ -629,7 +629,7 @@ func (h *handler) AbortMultipartUploadHandler(w http.ResponseWriter, r *http.Req Key: reqInfo.ObjectName, } - p.Encryption, err = formEncryptionParams(r) + p.Encryption, err = h.formEncryptionParams(r) if err != nil { h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) return diff --git a/api/handler/put.go b/api/handler/put.go index 5d997436..cb245673 100644 --- a/api/handler/put.go +++ b/api/handler/put.go @@ -228,7 +228,7 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) { metadata[api.ContentLanguage] = contentLanguage } - encryptionParams, err := formEncryptionParams(r) + encryptionParams, err := h.formEncryptionParams(r) if err != nil { h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) return @@ -363,15 +363,15 @@ func (h *handler) getBodyReader(r *http.Request) (io.ReadCloser, error) { return chunkReader, nil } -func formEncryptionParams(r *http.Request) (enc encryption.Params, err error) { - return formEncryptionParamsBase(r, false) +func (h *handler) formEncryptionParams(r *http.Request) (enc encryption.Params, err error) { + return h.formEncryptionParamsBase(r, false) } -func formCopySourceEncryptionParams(r *http.Request) (enc encryption.Params, err error) { - return formEncryptionParamsBase(r, true) +func (h *handler) formCopySourceEncryptionParams(r *http.Request) (enc encryption.Params, err error) { + return h.formEncryptionParamsBase(r, true) } -func formEncryptionParamsBase(r *http.Request, isCopySource bool) (enc encryption.Params, err error) { +func (h *handler) formEncryptionParamsBase(r *http.Request, isCopySource bool) (enc encryption.Params, err error) { var sseCustomerAlgorithm, sseCustomerKey, sseCustomerKeyMD5 string if isCopySource { sseCustomerAlgorithm = r.Header.Get(api.AmzCopySourceServerSideEncryptionCustomerAlgorithm) @@ -387,7 +387,17 @@ func formEncryptionParamsBase(r *http.Request, isCopySource bool) (enc encryptio return } - if r.TLS == nil { + needCheckTLS := true + if tlsTerminationStr := r.Header.Get(h.cfg.TLSTerminationHeader()); len(tlsTerminationStr) > 0 { + tlsTermination, err := strconv.ParseBool(tlsTerminationStr) + if err != nil { + h.reqLogger(r.Context()).Warn(logs.WarnInvalidTypeTLSTerminationHeader, zap.Error(err)) + } else { + needCheckTLS = !tlsTermination + } + } + + if needCheckTLS && r.TLS == nil { return enc, apierr.GetAPIError(apierr.ErrInsecureSSECustomerRequest) } diff --git a/cmd/s3-gw/app.go b/cmd/s3-gw/app.go index 01986eed..7af302bb 100644 --- a/cmd/s3-gw/app.go +++ b/cmd/s3-gw/app.go @@ -129,6 +129,7 @@ type ( retryStrategy handler.RetryStrategy tombstoneMembersSize int tombstoneLifetime uint64 + tlsTerminationHeader string } maxClientsConfig struct { @@ -316,6 +317,7 @@ func (s *appSettings) update(v *viper.Viper, log *zap.Logger) { httpLoggingUseGzip := v.GetBool(cfgHTTPLoggingGzip) tombstoneMembersSize := fetchTombstoneMembersSize(v) tombstoneLifetime := fetchTombstoneLifetime(v) + tlsTerminationHeader := v.GetString(cfgSSECTLSTerminationHeader) s.mu.Lock() defer s.mu.Unlock() @@ -347,6 +349,7 @@ func (s *appSettings) update(v *viper.Viper, log *zap.Logger) { s.vhsNamespacesEnabled = vhsNamespacesEnabled s.tombstoneMembersSize = tombstoneMembersSize s.tombstoneLifetime = tombstoneLifetime + s.tlsTerminationHeader = tlsTerminationHeader } func (s *appSettings) prepareVHSNamespaces(v *viper.Viper, log *zap.Logger, defaultNamespaces []string) map[string]bool { @@ -541,6 +544,12 @@ func (s *appSettings) RetryStrategy() handler.RetryStrategy { return s.retryStrategy } +func (s *appSettings) TLSTerminationHeader() string { + s.mu.RLock() + defer s.mu.RUnlock() + return s.tlsTerminationHeader +} + func (s *appSettings) AccessBoxContainer() (cid.ID, bool) { if s.accessbox != nil { return *s.accessbox, true diff --git a/cmd/s3-gw/app_settings.go b/cmd/s3-gw/app_settings.go index edf87691..1650b6b3 100644 --- a/cmd/s3-gw/app_settings.go +++ b/cmd/s3-gw/app_settings.go @@ -59,9 +59,10 @@ const ( defaultAccessBoxCacheRemovingCheckInterval = 5 * time.Minute - defaultNamespaceHeader = "X-Frostfs-Namespace" - defaultVHSHeader = "X-Frostfs-S3-VHS" - defaultServernameHeader = "X-Frostfs-Servername" + defaultNamespaceHeader = "X-Frostfs-Namespace" + defaultVHSHeader = "X-Frostfs-S3-VHS" + defaultServernameHeader = "X-Frostfs-Servername" + defaultTLSTerminationHeader = "X-Frostfs-TLS-Termination" defaultMultinetFallbackDelay = 300 * time.Millisecond @@ -280,6 +281,9 @@ const ( // Settings. // Server. cfgReconnectInterval = "reconnect_interval" + // SSE-C. + cfgSSECTLSTerminationHeader = "sse_c.tls_termination_header" + // envPrefix is an environment variables prefix used for configuration. envPrefix = "S3_GW" ) @@ -946,6 +950,9 @@ func newSettings() *viper.Viper { // multinet v.SetDefault(cfgMultinetFallbackDelay, defaultMultinetFallbackDelay) + // sse-c + v.SetDefault(cfgSSECTLSTerminationHeader, defaultTLSTerminationHeader) + // Bind flags if err := bindFlags(v, flags); err != nil { panic(fmt.Errorf("bind flags: %w", err)) diff --git a/config/config.env b/config/config.env index 44a89715..986cfdf6 100644 --- a/config/config.env +++ b/config/config.env @@ -269,3 +269,6 @@ S3_GW_MULTINET_FALLBACK_DELAY=300ms # List of subnets and IP addresses to use as source for those subnets S3_GW_MULTINET_SUBNETS_1_MASK=1.2.3.4/24 S3_GW_MULTINET_SUBNETS_1_SOURCE_IPS=1.2.3.4 1.2.3.5 + +# Header for determining the termination of TLS. +S3_GW_SSE_C_TLS_TERMINATION_TLS_HEADER=X-Frostfs-TLS-Termination diff --git a/config/config.yaml b/config/config.yaml index 8f169463..a4fc2382 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -318,3 +318,6 @@ multinet: source_ips: - 1.2.3.4 - 1.2.3.5 + +sse_c: + tls_termination_header: X-Frostfs-TLS-Termination diff --git a/docs/configuration.md b/docs/configuration.md index 0f98747f..99ebaf2e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -196,6 +196,7 @@ There are some custom types used for brevity: | `containers` | [Containers configuration](#containers-section) | | `vhs` | [VHS configuration](#vhs-section) | | `multinet` | [Multinet configuration](#multinet-section) | +| `sse_c` | [SSE-C configuration](#sse_c-section) | ### General section @@ -681,7 +682,7 @@ web: | Parameter | Type | SIGHUP reload | Default value | Description | |-----------------------|------------|---------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `read_timeout` | `duration` | no | `0` | The maximum duration for reading the entire request, including the body. A zero or negative value means there will be no timeout. | +| `read_timeout` | `duration` | no | `0` | The maximum duration for reading the entire request, including the body. A zero or negative value means there will be no timeout. | | `read_header_timeout` | `duration` | no | `30s` | The amount of time allowed to read request headers. If `read_header_timeout` is zero, the value of `read_timeout` is used. If both are zero, there is no timeout. | | `write_timeout` | `duration` | no | `0` | The maximum duration before timing out writes of the response. A zero or negative value means there will be no timeout. | | `idle_timeout` | `duration` | no | `30s` | The maximum amount of time to wait for the next request when keep-alives are enabled. If `idle_timeout` is zero, the value of `read_timeout` is used. If both are zero, there is no timeout. | @@ -858,3 +859,16 @@ multinet: |--------------|------------|---------------|---------------|----------------------------------------------------------------------| | `mask` | `string` | yes | | Destination subnet. | | `source_ips` | `[]string` | yes | | Array of source IP addresses to use when dialing destination subnet. | + +# `sse_c` section + +Configuration of SSE-C. + +```yaml +sse_c: + tls_termination_header: X-Frostfs-TLS-Termination +``` + +| Parameter | Type | SIGHUP reload | Default value | Description | +| ------------------------ | -------- | ------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `tls_termination_header` | `string` | yes | `X-Frostfs-TLS-Termination` | The header for determining whether TLS needs to be checked. If the system requests come through a proxy server and TLS can terminate at the proxy level, you should use this header to disable TLS verification at SSE-C. | diff --git a/internal/logs/logs.go b/internal/logs/logs.go index 0c47ef45..1070b91a 100644 --- a/internal/logs/logs.go +++ b/internal/logs/logs.go @@ -181,4 +181,5 @@ const ( FailedToPutTombstoneObject = "failed to put tombstone object" FailedToCreateWorkerPool = "failed to create worker pool" FailedToListAllObjectRelations = "failed to list all object relations" + WarnInvalidTypeTLSTerminationHeader = "invalid type of value of tls termination header" )