Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TT-13629]: OAS upstream SSL configuration #6840

Merged
merged 13 commits into from
Feb 7, 2025
4 changes: 4 additions & 0 deletions apidef/oas/linter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ func TestXTykGateway_Lint(t *testing.T) {
BasicAuth: nil,
OAuth: nil,
}

settings.Upstream.TLSTransport.MinVersion = "1.2"
settings.Upstream.TLSTransport.MaxVersion = "1.2"
settings.Upstream.TLSTransport.Ciphers = []string{"TLS_RSA_WITH_RC4_128_SHA"}
}

// Encode data to json
Expand Down
6 changes: 0 additions & 6 deletions apidef/oas/oas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,6 @@ func TestOAS_ExtractTo_ResetAPIDefinition(t *testing.T) {
"APIDefinition.UptimeTests.Config.RecheckWait",
"APIDefinition.Proxy.DisableStripSlash",
"APIDefinition.Proxy.CheckHostAgainstUptimeTests",
"APIDefinition.Proxy.Transport.SSLInsecureSkipVerify",
"APIDefinition.Proxy.Transport.SSLCipherSuites[0]",
"APIDefinition.Proxy.Transport.SSLMinVersion",
"APIDefinition.Proxy.Transport.SSLMaxVersion",
"APIDefinition.Proxy.Transport.SSLForceCommonNameCheck",
"APIDefinition.Proxy.Transport.ProxyURL",
"APIDefinition.DisableQuota",
"APIDefinition.AuthProvider.Name",
"APIDefinition.AuthProvider.StorageEngine",
Expand Down
43 changes: 43 additions & 0 deletions apidef/oas/schema/x-tyk-api-gateway.json
Original file line number Diff line number Diff line change
Expand Up @@ -1366,6 +1366,12 @@
},
"preserveHostHeader": {
"$ref": "#/definitions/X-Tyk-PreserveHostHeader"
},
"tlsTransport": {
"$ref": "#/definitions/X-Tyk-TLSTransport"
},
"proxy": {
"$ref": "#/definitions/X-Tyk-Proxy"
}
},
"anyOf": [
Expand Down Expand Up @@ -2360,6 +2366,43 @@
}
]
},
"X-Tyk-TLSTransport": {
"type": "object",
"properties": {
"insecureSkipVerify": {
"type": "boolean"
},
"ciphers": {
"type": "array",
"items": {
"type": "string"
}
},
"minVersion": {
"type": "string",
"enum": ["1.0", "1.1", "1.2", "1.3"]
},
"maxVersion": {
"type": "string",
"enum": ["1.0", "1.1", "1.2", "1.3"]
},
"forceCommonNameCheck": {
"type": "boolean"
}
}
},
"X-Tyk-Proxy": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
},
"url": {
"type": "string"
}
},
"required": ["enabled"]
},
"X-Tyk-PreserveHostHeader": {
"type": "object",
"properties": {
Expand Down
157 changes: 156 additions & 1 deletion apidef/oas/upstream.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package oas

import (
"crypto/tls"
"sort"
"strings"

Expand Down Expand Up @@ -38,6 +39,13 @@ type Upstream struct {

// PreserveHostHeader contains the configuration for preserving the host header.
PreserveHostHeader *PreserveHostHeader `bson:"preserveHostHeader,omitempty" json:"preserveHostHeader,omitempty"`
// TLSTransport contains the configuration for TLS transport settings.
// Tyk classic API definition: `proxy.transport`
TLSTransport *TLSTransport `bson:"tlsTransport,omitempty" json:"tlsTransport,omitempty"`

// Proxy contains the configuration for an internal proxy.
// Tyk classic API definition: `proxy.proxy_url`
Proxy *Proxy `bson:"proxy,omitempty" json:"proxy,omitempty"`
}

// Fill fills *Upstream from apidef.APIDefinition.
Expand Down Expand Up @@ -98,8 +106,23 @@ func (u *Upstream) Fill(api apidef.APIDefinition) {
u.Authentication = nil
}

u.fillLoadBalancing(api)
if u.TLSTransport == nil {
u.TLSTransport = &TLSTransport{}
}
u.TLSTransport.Fill(api)
if ShouldOmit(u.TLSTransport) {
u.TLSTransport = nil
}

if u.Proxy == nil {
u.Proxy = &Proxy{}
}
u.Proxy.Fill(api)
if ShouldOmit(u.Proxy) {
u.Proxy = nil
}

u.fillLoadBalancing(api)
u.fillPreserveHostHeader(api)
}

Expand Down Expand Up @@ -175,6 +198,22 @@ func (u *Upstream) ExtractTo(api *apidef.APIDefinition) {

u.loadBalancingExtractTo(api)

if u.TLSTransport == nil {
u.TLSTransport = &TLSTransport{}
defer func() {
u.TLSTransport = nil
}()
}
u.TLSTransport.ExtractTo(api)

if u.Proxy == nil {
u.Proxy = &Proxy{}
defer func() {
u.Proxy = nil
}()
}
u.Proxy.ExtractTo(api)

u.preserveHostHeaderExtractTo(api)
}

Expand Down Expand Up @@ -211,6 +250,122 @@ func (u *Upstream) loadBalancingExtractTo(api *apidef.APIDefinition) {
u.LoadBalancing.ExtractTo(api)
}

// TLSTransport contains the configuration for TLS transport settings.
// This struct allows you to specify a custom proxy and set the minimum TLS versions and any SSL ciphers.
//
// Example:
//
// {
// "proxy_url": "http(s)://proxy.url:1234",
// "minVersion": "1.0",
// "maxVersion": "1.0",
// "ciphers": [
// "TLS_RSA_WITH_AES_128_GCM_SHA256",
// "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA"
// ],
// "insecureSkipVerify": true,
// "forceCommonNameCheck": false
// }
//
// Tyk classic API definition: `proxy.transport`
type TLSTransport struct {
// InsecureSkipVerify controls whether a client verifies the server's certificate chain and host name.
// If InsecureSkipVerify is true, crypto/tls accepts any certificate presented by the server and any host name in that certificate.
// In this mode, TLS is susceptible to machine-in-the-middle attacks unless custom verification is used.
// This should be used only for testing or in combination with VerifyConnection or VerifyPeerCertificate.
//
// Tyk classic API definition: `proxy.transport.ssl_insecure_skip_verify`
InsecureSkipVerify bool `bson:"insecureSkipVerify,omitempty" json:"insecureSkipVerify,omitempty"`

// Ciphers is a list of SSL ciphers to be used. If unset, the default ciphers will be used.
//
// Tyk classic API definition: `proxy.transport.ssl_ciphers`
Ciphers []string `bson:"ciphers,omitempty" json:"ciphers,omitempty"`

// MinVersion is the minimum SSL/TLS version that is acceptable.
// Tyk classic API definition: `proxy.transport.ssl_min_version`
MinVersion string `bson:"minVersion,omitempty" json:"minVersion,omitempty"`

// MaxVersion is the maximum SSL/TLS version that is acceptable.
MaxVersion string `bson:"maxVersion,omitempty" json:"maxVersion,omitempty"`

// ForceCommonNameCheck forces the validation of the hostname against the certificate Common Name.
//
// Tyk classic API definition: `proxy.transport.ssl_force_common_name_check`
ForceCommonNameCheck bool `bson:"forceCommonNameCheck,omitempty" json:"forceCommonNameCheck,omitempty"`
}

// Fill fills *TLSTransport from apidef.ServiceDiscoveryConfiguration.
func (t *TLSTransport) Fill(api apidef.APIDefinition) {
t.ForceCommonNameCheck = api.Proxy.Transport.SSLForceCommonNameCheck
t.Ciphers = api.Proxy.Transport.SSLCipherSuites
t.MaxVersion = t.tlsVersionToString(api.Proxy.Transport.SSLMaxVersion)
t.MinVersion = t.tlsVersionToString(api.Proxy.Transport.SSLMinVersion)
t.InsecureSkipVerify = api.Proxy.Transport.SSLInsecureSkipVerify
}

// ExtractTo extracts *TLSTransport into *apidef.ServiceDiscoveryConfiguration.
func (t *TLSTransport) ExtractTo(api *apidef.APIDefinition) {
api.Proxy.Transport.SSLForceCommonNameCheck = t.ForceCommonNameCheck
api.Proxy.Transport.SSLCipherSuites = t.Ciphers
api.Proxy.Transport.SSLMaxVersion = t.tlsVersionFromString(t.MaxVersion)
api.Proxy.Transport.SSLMinVersion = t.tlsVersionFromString(t.MinVersion)
api.Proxy.Transport.SSLInsecureSkipVerify = t.InsecureSkipVerify
}

// tlsVersionFromString converts v in the form of 1.2/1.3 to the version int
func (t *TLSTransport) tlsVersionFromString(v string) uint16 {
switch v {
case "1.0":
return tls.VersionTLS10
case "1.1":
return tls.VersionTLS11
case "1.2":
return tls.VersionTLS12
case "1.3":
return tls.VersionTLS13
default:
return 0
}
}

// tlsVersionFromString converts v from version into to the form 1.0/1.1
func (t *TLSTransport) tlsVersionToString(v uint16) string {
switch v {
case tls.VersionTLS10:
return "1.0"
case tls.VersionTLS11:
return "1.1"
kofoworola marked this conversation as resolved.
Show resolved Hide resolved
case tls.VersionTLS12:
return "1.2"
case tls.VersionTLS13:
return "1.3"
default:
return ""
}
}

// Proxy contains the configuration for an internal proxy.
//
// Tyk classic API definition: `proxy.proxy_url`
type Proxy struct {
// Enabled determines if the proxy is active.
Enabled bool `bson:"enabled" json:"enabled"`

// URL specifies the URL of the internal proxy.
URL string `bson:"url" json:"url"`
}

// Fill fills *Proxy from apidef.ServiceDiscoveryConfiguration.
func (p *Proxy) Fill(api apidef.APIDefinition) {
p.URL = api.Proxy.Transport.ProxyURL
}

// ExtractTo extracts *Proxy into *apidef.ServiceDiscoveryConfiguration.
func (p *Proxy) ExtractTo(api *apidef.APIDefinition) {
api.Proxy.Transport.ProxyURL = p.URL
}

// ServiceDiscovery holds configuration required for service discovery.
type ServiceDiscovery struct {
// Enabled activates Service Discovery.
Expand Down
55 changes: 55 additions & 0 deletions apidef/oas/upstream_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package oas

import (
"crypto/tls"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -608,6 +609,60 @@ func TestUpstreamRequestSigning(t *testing.T) {
})
}

func TestTLSTransportProxy(t *testing.T) {
t.Run("with tls settings", func(t *testing.T) {
transport := TLSTransport{
InsecureSkipVerify: true,
MinVersion: "1.2",
MaxVersion: "1.3",
ForceCommonNameCheck: true,
}

var convertedAPI apidef.APIDefinition
var resultTransport TLSTransport

convertedAPI.SetDisabledFlags()
transport.ExtractTo(&convertedAPI)

assert.Equal(t, transport.InsecureSkipVerify, convertedAPI.Proxy.Transport.SSLInsecureSkipVerify)
assert.Equal(t, uint16(tls.VersionTLS12), convertedAPI.Proxy.Transport.SSLMinVersion)
assert.Equal(t, uint16(tls.VersionTLS13), convertedAPI.Proxy.Transport.SSLMaxVersion)
assert.Equal(t, transport.ForceCommonNameCheck, convertedAPI.Proxy.Transport.SSLForceCommonNameCheck)

resultTransport.Fill(convertedAPI)

assert.Equal(t, transport, resultTransport)
})

t.Run("emmpty tls settings", func(t *testing.T) {
var emptyTlsTransport TLSTransport
var convertedAPI apidef.APIDefinition
var resultTransport TLSTransport

convertedAPI.SetDisabledFlags()
emptyTlsTransport.ExtractTo(&convertedAPI)
resultTransport.Fill(convertedAPI)

assert.Equal(t, emptyTlsTransport, resultTransport)
})

t.Run("proxy settings", func(t *testing.T) {
proxyTransport := Proxy{
URL: "proxy-url",
}
var convertedAPI apidef.APIDefinition
var resultProxy Proxy

convertedAPI.SetDisabledFlags()
proxyTransport.ExtractTo(&convertedAPI)

assert.Equal(t, "proxy-url", convertedAPI.Proxy.Transport.ProxyURL)

resultProxy.Fill(convertedAPI)
assert.Equal(t, proxyTransport, resultProxy)
})
}

func TestLoadBalancing(t *testing.T) {
t.Parallel()
t.Run("fill", func(t *testing.T) {
Expand Down
12 changes: 7 additions & 5 deletions gateway/cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type APIAllCertificateBasics struct {
Certs []*certs.CertificateBasics `json:"certs"`
}

// Deprecated: use tls.CipherSuites() now
var cipherSuites = map[string]uint16{
"TLS_RSA_WITH_RC4_128_SHA": 0x0005,
"TLS_RSA_WITH_3DES_EDE_CBC_SHA": 0x000a,
Expand Down Expand Up @@ -581,12 +582,13 @@ func (gw *Gateway) certHandler(w http.ResponseWriter, r *http.Request) {
}

func getCipherAliases(ciphers []string) (cipherCodes []uint16) {
for k, v := range cipherSuites {
for _, str := range ciphers {
if str == k {
cipherCodes = append(cipherCodes, v)
}
for _, v := range ciphers {
id, err := crypto.ResolveCipher(v)
if err != nil {
log.Debugf("cipher %s not found; skipped", v)
continue
}
cipherCodes = append(cipherCodes, id)
}
return cipherCodes
}
4 changes: 2 additions & 2 deletions gateway/cert_old_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ func TestProxyTransport(t *testing.T) {
// force creating new transport on each reque
globalConf.MaxConnTime = -1

globalConf.ProxySSLCipherSuites = []string{"TLS_RSA_WITH_AES_128_CBC_SHA"}
globalConf.ProxySSLCipherSuites = []string{"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"}
ts.Gw.SetConfig(globalConf)
ts.Gw.BuildAndLoadAPI(func(spec *APISpec) {
spec.Proxy.ListenPath = "/"
Expand All @@ -300,7 +300,7 @@ func TestProxyTransport(t *testing.T) {
// force creating new transport on each reque
globalConf.MaxConnTime = -1

globalConf.ProxySSLCipherSuites = []string{"TLS_RSA_WITH_RC4_128_SHA"}
globalConf.ProxySSLCipherSuites = []string{"TLS_AES_256_GCM_SHA384"}
ts.Gw.SetConfig(globalConf)
ts.Gw.BuildAndLoadAPI(func(spec *APISpec) {
spec.Proxy.ListenPath = "/"
Expand Down
Loading
Loading