[#562] Support TLS termination header for SSE-C

The TLS termination header added 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.

Signed-off-by: Roman Loginov <r.loginov@yadro.com>
This commit is contained in:
Roman Loginov 2024-12-03 15:36:40 +03:00
parent 3cd88d6204
commit e1579f16c0
14 changed files with 74 additions and 22 deletions

View file

@ -41,6 +41,7 @@ type (
RetryMaxAttempts() int RetryMaxAttempts() int
RetryMaxBackoff() time.Duration RetryMaxBackoff() time.Duration
RetryStrategy() RetryStrategy RetryStrategy() RetryStrategy
TLSTerminationHeader() string
} }
FrostFSID interface { FrostFSID interface {

View file

@ -98,7 +98,7 @@ func (h *handler) GetObjectAttributesHandler(w http.ResponseWriter, r *http.Requ
} }
info := extendedInfo.ObjectInfo info := extendedInfo.ObjectInfo
encryptionParams, err := formEncryptionParams(r) encryptionParams, err := h.formEncryptionParams(r)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err)
return return

View file

@ -103,12 +103,12 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
} }
srcObjInfo := extendedSrcObjInfo.ObjectInfo srcObjInfo := extendedSrcObjInfo.ObjectInfo
srcEncryptionParams, err := formCopySourceEncryptionParams(r) srcEncryptionParams, err := h.formCopySourceEncryptionParams(r)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err)
return return
} }
dstEncryptionParams, err := formEncryptionParams(r) dstEncryptionParams, err := h.formEncryptionParams(r)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err)
return return

View file

@ -202,7 +202,7 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
encryptionParams, err := formEncryptionParams(r) encryptionParams, err := h.formEncryptionParams(r)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err)
return return

View file

@ -140,6 +140,10 @@ func (c *configMock) RetryStrategy() RetryStrategy {
return RetryStrategyConstant return RetryStrategyConstant
} }
func (c *configMock) TLSTerminationHeader() string {
return "X-Frostfs-TLS-Termination"
}
func prepareHandlerContext(t *testing.T) *handlerContext { func prepareHandlerContext(t *testing.T) *handlerContext {
hc, err := prepareHandlerContextBase(layer.DefaultCachesConfigs(zap.NewExample())) hc, err := prepareHandlerContextBase(layer.DefaultCachesConfigs(zap.NewExample()))
require.NoError(t, err) require.NoError(t, err)

View file

@ -51,7 +51,7 @@ func (h *handler) HeadObjectHandler(w http.ResponseWriter, r *http.Request) {
} }
info := extendedInfo.ObjectInfo info := extendedInfo.ObjectInfo
encryptionParams, err := formEncryptionParams(r) encryptionParams, err := h.formEncryptionParams(r)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err)
return return

View file

@ -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 { if err != nil {
h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err, additional...) h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err, additional...)
return return
@ -223,7 +223,7 @@ func (h *handler) UploadPartHandler(w http.ResponseWriter, r *http.Request) {
ContentSHA256Hash: r.Header.Get(api.AmzContentSha256), ContentSHA256Hash: r.Header.Get(api.AmzContentSha256),
} }
p.Info.Encryption, err = formEncryptionParams(r) p.Info.Encryption, err = h.formEncryptionParams(r)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err, additional...) h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err, additional...)
return return
@ -323,7 +323,7 @@ func (h *handler) UploadPartCopy(w http.ResponseWriter, r *http.Request) {
return return
} }
srcEncryptionParams, err := formCopySourceEncryptionParams(r) srcEncryptionParams, err := h.formCopySourceEncryptionParams(r)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err)
return return
@ -348,7 +348,7 @@ func (h *handler) UploadPartCopy(w http.ResponseWriter, r *http.Request) {
Range: srcRange, Range: srcRange,
} }
p.Info.Encryption, err = formEncryptionParams(r) p.Info.Encryption, err = h.formEncryptionParams(r)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err, additional...) h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err, additional...)
return return
@ -593,7 +593,7 @@ func (h *handler) ListPartsHandler(w http.ResponseWriter, r *http.Request) {
PartNumberMarker: partNumberMarker, PartNumberMarker: partNumberMarker,
} }
p.Info.Encryption, err = formEncryptionParams(r) p.Info.Encryption, err = h.formEncryptionParams(r)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err)
return return
@ -629,7 +629,7 @@ func (h *handler) AbortMultipartUploadHandler(w http.ResponseWriter, r *http.Req
Key: reqInfo.ObjectName, Key: reqInfo.ObjectName,
} }
p.Encryption, err = formEncryptionParams(r) p.Encryption, err = h.formEncryptionParams(r)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err)
return return

View file

@ -228,7 +228,7 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
metadata[api.ContentLanguage] = contentLanguage metadata[api.ContentLanguage] = contentLanguage
} }
encryptionParams, err := formEncryptionParams(r) encryptionParams, err := h.formEncryptionParams(r)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err)
return return
@ -353,15 +353,15 @@ func (h *handler) getBodyReader(r *http.Request) (io.ReadCloser, error) {
return chunkReader, nil return chunkReader, nil
} }
func formEncryptionParams(r *http.Request) (enc encryption.Params, err error) { func (h *handler) formEncryptionParams(r *http.Request) (enc encryption.Params, err error) {
return formEncryptionParamsBase(r, false) return h.formEncryptionParamsBase(r, false)
} }
func formCopySourceEncryptionParams(r *http.Request) (enc encryption.Params, err error) { func (h *handler) formCopySourceEncryptionParams(r *http.Request) (enc encryption.Params, err error) {
return formEncryptionParamsBase(r, true) 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 var sseCustomerAlgorithm, sseCustomerKey, sseCustomerKeyMD5 string
if isCopySource { if isCopySource {
sseCustomerAlgorithm = r.Header.Get(api.AmzCopySourceServerSideEncryptionCustomerAlgorithm) sseCustomerAlgorithm = r.Header.Get(api.AmzCopySourceServerSideEncryptionCustomerAlgorithm)
@ -377,7 +377,17 @@ func formEncryptionParamsBase(r *http.Request, isCopySource bool) (enc encryptio
return return
} }
if r.TLS == nil { needCheckTLS := true
if tlsTerminationParam := r.Header.Get(h.cfg.TLSTerminationHeader()); len(tlsTerminationParam) > 0 {
tlsTermination, err := strconv.ParseBool(tlsTerminationParam)
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) return enc, apierr.GetAPIError(apierr.ErrInsecureSSECustomerRequest)
} }

View file

@ -119,6 +119,7 @@ type (
vhsNamespacesEnabled map[string]bool vhsNamespacesEnabled map[string]bool
retryMaxBackoff time.Duration retryMaxBackoff time.Duration
retryStrategy handler.RetryStrategy retryStrategy handler.RetryStrategy
tlsTerminationHeader string
} }
maxClientsConfig struct { maxClientsConfig struct {
@ -275,6 +276,7 @@ func (s *appSettings) update(v *viper.Viper, log *zap.Logger) {
httpLoggingMaxLogSize := v.GetInt(cfgHTTPLoggingMaxLogSize) httpLoggingMaxLogSize := v.GetInt(cfgHTTPLoggingMaxLogSize)
httpLoggingOutputPath := v.GetString(cfgHTTPLoggingDestination) httpLoggingOutputPath := v.GetString(cfgHTTPLoggingDestination)
httpLoggingUseGzip := v.GetBool(cfgHTTPLoggingGzip) httpLoggingUseGzip := v.GetBool(cfgHTTPLoggingGzip)
tlsTerminationHeader := v.GetString(cfgSSECTLSTerminationHeader)
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -304,6 +306,7 @@ func (s *appSettings) update(v *viper.Viper, log *zap.Logger) {
s.vhsHeader = vhsHeader s.vhsHeader = vhsHeader
s.servernameHeader = servernameHeader s.servernameHeader = servernameHeader
s.vhsNamespacesEnabled = vhsNamespacesEnabled s.vhsNamespacesEnabled = vhsNamespacesEnabled
s.tlsTerminationHeader = tlsTerminationHeader
} }
func (s *appSettings) prepareVHSNamespaces(v *viper.Viper, log *zap.Logger, defaultNamespaces []string) map[string]bool { func (s *appSettings) prepareVHSNamespaces(v *viper.Viper, log *zap.Logger, defaultNamespaces []string) map[string]bool {
@ -498,6 +501,12 @@ func (s *appSettings) RetryStrategy() handler.RetryStrategy {
return s.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) { func (s *appSettings) AccessBoxContainer() (cid.ID, bool) {
if s.accessbox != nil { if s.accessbox != nil {
return *s.accessbox, true return *s.accessbox, true

View file

@ -59,9 +59,10 @@ const (
defaultAccessBoxCacheRemovingCheckInterval = 5 * time.Minute defaultAccessBoxCacheRemovingCheckInterval = 5 * time.Minute
defaultNamespaceHeader = "X-Frostfs-Namespace" defaultNamespaceHeader = "X-Frostfs-Namespace"
defaultVHSHeader = "X-Frostfs-S3-VHS" defaultVHSHeader = "X-Frostfs-S3-VHS"
defaultServernameHeader = "X-Frostfs-Servername" defaultServernameHeader = "X-Frostfs-Servername"
defaultTLSTerminationHeader = "X-Frostfs-TLS-Termination"
defaultMultinetFallbackDelay = 300 * time.Millisecond defaultMultinetFallbackDelay = 300 * time.Millisecond
@ -273,6 +274,9 @@ const ( // Settings.
// Server. // Server.
cfgReconnectInterval = "reconnect_interval" cfgReconnectInterval = "reconnect_interval"
// SSE-C.
cfgSSECTLSTerminationHeader = "sse_c.tls_termination_header"
// envPrefix is an environment variables prefix used for configuration. // envPrefix is an environment variables prefix used for configuration.
envPrefix = "S3_GW" envPrefix = "S3_GW"
) )
@ -909,6 +913,9 @@ func newSettings() *viper.Viper {
// multinet // multinet
v.SetDefault(cfgMultinetFallbackDelay, defaultMultinetFallbackDelay) v.SetDefault(cfgMultinetFallbackDelay, defaultMultinetFallbackDelay)
// sse-c
v.SetDefault(cfgSSECTLSTerminationHeader, defaultTLSTerminationHeader)
// Bind flags // Bind flags
if err := bindFlags(v, flags); err != nil { if err := bindFlags(v, flags); err != nil {
panic(fmt.Errorf("bind flags: %w", err)) panic(fmt.Errorf("bind flags: %w", err))

View file

@ -263,3 +263,6 @@ S3_GW_MULTINET_FALLBACK_DELAY=300ms
# List of subnets and IP addresses to use as source for those subnets # 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_MASK=1.2.3.4/24
S3_GW_MULTINET_SUBNETS_1_SOURCE_IPS=1.2.3.4 1.2.3.5 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

View file

@ -311,3 +311,6 @@ multinet:
source_ips: source_ips:
- 1.2.3.4 - 1.2.3.4
- 1.2.3.5 - 1.2.3.5
sse_c:
tls_termination_header: X-Frostfs-TLS-Termination

View file

@ -196,6 +196,7 @@ There are some custom types used for brevity:
| `containers` | [Containers configuration](#containers-section) | | `containers` | [Containers configuration](#containers-section) |
| `vhs` | [VHS configuration](#vhs-section) | | `vhs` | [VHS configuration](#vhs-section) |
| `multinet` | [Multinet configuration](#multinet-section) | | `multinet` | [Multinet configuration](#multinet-section) |
| `sse_c` | [SSE-C configuration](#sse_c-section) |
### General section ### General section
@ -674,7 +675,7 @@ web:
| Parameter | Type | SIGHUP reload | Default value | Description | | 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. | | `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. | | `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. | | `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. |
@ -851,3 +852,16 @@ multinet:
|--------------|------------|---------------|---------------|----------------------------------------------------------------------| |--------------|------------|---------------|---------------|----------------------------------------------------------------------|
| `mask` | `string` | yes | | Destination subnet. | | `mask` | `string` | yes | | Destination subnet. |
| `source_ips` | `[]string` | yes | | Array of source IP addresses to use when dialing 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. |

View file

@ -178,4 +178,5 @@ const (
MultinetDialSuccess = "multinet dial successful" MultinetDialSuccess = "multinet dial successful"
MultinetDialFail = "multinet dial failed" MultinetDialFail = "multinet dial failed"
FailedToParseHTTPTime = "failed to parse http time, header is ignored" FailedToParseHTTPTime = "failed to parse http time, header is ignored"
WarnInvalidTypeTLSTerminationHeader = "invalid type of value of tls termination header"
) )