blob: ecf61780dde20637619df7edcb2083aff7224bb9 [file] [log] [blame]
//go:build integ
// +build integ
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package security
import (
"fmt"
"net/http"
"os"
"path"
"strings"
"testing"
)
import (
"github.com/apache/dubbo-go-pixiu/pkg/http/headers"
"github.com/apache/dubbo-go-pixiu/pkg/test"
echoClient "github.com/apache/dubbo-go-pixiu/pkg/test/echo"
"github.com/apache/dubbo-go-pixiu/pkg/test/env"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/echo"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/echo/check"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/echo/echotest"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/echo/match"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/istio"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/namespace"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/resource"
ingressutil "github.com/apache/dubbo-go-pixiu/tests/integration/security/sds_ingress/util"
sdstlsutil "github.com/apache/dubbo-go-pixiu/tests/integration/security/sds_tls_origination/util"
"github.com/apache/dubbo-go-pixiu/tests/integration/security/util"
)
// TestSimpleTlsOrigination test SIMPLE TLS mode with TLS origination happening at Gateway proxy
// It uses CredentialName set in DestinationRule API to fetch secrets from k8s API server
func TestSimpleTlsOrigination(t *testing.T) {
framework.NewTest(t).
RequiresSingleNetwork(). // https://github.com/istio/istio/issues/37134
Features("security.egress.tls.sds").
Run(func(t framework.TestContext) {
var (
credName = "tls-credential-cacert"
fakeCredName = "fake-tls-credential-cacert"
credNameMissing = "tls-credential-not-created-cacert"
)
credentialA := ingressutil.IngressCredential{
CaCert: MustReadCert(t, "root-cert.pem"),
}
CredentialB := ingressutil.IngressCredential{
CaCert: sdstlsutil.FakeRoot,
}
// Add kubernetes secret to provision key/cert for gateway.
ingressutil.CreateIngressKubeSecret(t, credName, ingressutil.TLS, credentialA, false)
// Add kubernetes secret to provision key/cert for gateway.
ingressutil.CreateIngressKubeSecret(t, fakeCredName, ingressutil.TLS, CredentialB, false)
// Set up Host Namespace
host := apps.External.Config().ClusterLocalFQDN()
testCases := []TLSTestCase{
// Use CA certificate stored as k8s secret with the same issuing CA as server's CA.
// This root certificate can validate the server cert presented by the echoboot server instance.
{
Name: "simple",
StatusCode: http.StatusOK,
CredentialToUse: strings.TrimSuffix(credName, "-cacert"),
Gateway: true,
},
// Use CA certificate stored as k8s secret with different issuing CA as server's CA.
// This root certificate cannot validate the server cert presented by the echoboot server instance.
{
Name: "fake root",
StatusCode: http.StatusServiceUnavailable,
CredentialToUse: strings.TrimSuffix(fakeCredName, "-cacert"),
Gateway: false,
},
// Set up an UpstreamCluster with a CredentialName when secret doesn't even exist in dubbo-system ns.
// Secret fetching error at Gateway, results in a 503 response.
{
Name: "missing secret",
StatusCode: http.StatusServiceUnavailable,
CredentialToUse: strings.TrimSuffix(credNameMissing, "-cacert"),
Gateway: false,
},
}
CreateGateway(t, t, apps.Namespace1, apps.External)
for _, tc := range testCases {
t.NewSubTest(tc.Name).Run(func(t framework.TestContext) {
CreateDestinationRule(t, apps.External, "SIMPLE", tc.CredentialToUse)
newGatewayTest(t).
Run(func(t framework.TestContext, from echo.Instance, to echo.Target) {
callOpt := CallOpts(to, host, tc)
from.CallOrFail(t, callOpt)
})
})
}
})
}
// TestMutualTlsOrigination test MUTUAL TLS mode with TLS origination happening at Gateway proxy
// It uses CredentialName set in DestinationRule API to fetch secrets from k8s API server
func TestMutualTlsOrigination(t *testing.T) {
framework.NewTest(t).
RequiresSingleNetwork(). // https://github.com/istio/istio/issues/37134
Features("security.egress.mtls.sds").
Run(func(t framework.TestContext) {
var (
credNameGeneric = "mtls-credential-generic"
credNameNotGeneric = "mtls-credential-not-generic"
fakeCredNameA = "fake-mtls-credential-a"
fakeCredNameB = "fake-mtls-credential-b"
credNameMissing = "mtls-credential-not-created"
simpleCredName = "tls-credential-simple-cacert"
)
// Add kubernetes secret to provision key/cert for gateway.
ingressutil.CreateIngressKubeSecret(t, credNameGeneric, ingressutil.Mtls, ingressutil.IngressCredential{
Certificate: MustReadCert(t, "cert-chain.pem"),
PrivateKey: MustReadCert(t, "key.pem"),
CaCert: MustReadCert(t, "root-cert.pem"),
}, false)
ingressutil.CreateIngressKubeSecret(t, credNameNotGeneric, ingressutil.Mtls, ingressutil.IngressCredential{
Certificate: MustReadCert(t, "cert-chain.pem"),
PrivateKey: MustReadCert(t, "key.pem"),
CaCert: MustReadCert(t, "root-cert.pem"),
}, true)
// Configured with an invalid ClientCert
ingressutil.CreateIngressKubeSecret(t, fakeCredNameA, ingressutil.Mtls, ingressutil.IngressCredential{
Certificate: sdstlsutil.FakeCert,
PrivateKey: MustReadCert(t, "key.pem"),
CaCert: MustReadCert(t, "root-cert.pem"),
}, false)
// Configured with an invalid ClientCert and PrivateKey
ingressutil.CreateIngressKubeSecret(t, fakeCredNameB, ingressutil.Mtls, ingressutil.IngressCredential{
Certificate: sdstlsutil.FakeCert,
PrivateKey: sdstlsutil.FakeKey,
CaCert: MustReadCert(t, "root-cert.pem"),
}, false)
ingressutil.CreateIngressKubeSecret(t, simpleCredName, ingressutil.TLS, ingressutil.IngressCredential{
CaCert: MustReadCert(t, "root-cert.pem"),
}, false)
// Set up Host Namespace
host := apps.External.Config().ClusterLocalFQDN()
testCases := []TLSTestCase{
// Use CA certificate and client certs stored as k8s secret with the same issuing CA as server's CA.
// This root certificate can validate the server cert presented by the echoboot server instance and server CA can
// validate the client cert. Secret is of type generic.
{
Name: "generic",
StatusCode: http.StatusOK,
CredentialToUse: strings.TrimSuffix(credNameGeneric, "-cacert"),
Gateway: true,
},
// Use CA certificate and client certs stored as k8s secret with the same issuing CA as server's CA.
// This root certificate can validate the server cert presented by the echoboot server instance and server CA can
// validate the client cert. Secret is not of type generic.
{
Name: "non-generic",
StatusCode: http.StatusOK,
CredentialToUse: strings.TrimSuffix(credNameNotGeneric, "-cacert"),
Gateway: true,
},
// Use CA certificate and client certs stored as k8s secret with the same issuing CA as server's CA.
// This root certificate can validate the server cert presented by the echoboot server instance and server CA
// cannot validate the client cert. Returns 503 response as TLS handshake fails.
{
Name: "invalid client cert",
StatusCode: http.StatusServiceUnavailable,
CredentialToUse: strings.TrimSuffix(fakeCredNameA, "-cacert"),
Gateway: false,
},
// Set up an UpstreamCluster with a CredentialName when secret doesn't even exist in dubbo-system ns.
// Secret fetching error at Gateway, results in a 503 response.
{
Name: "missing",
StatusCode: http.StatusServiceUnavailable,
CredentialToUse: strings.TrimSuffix(credNameMissing, "-cacert"),
Gateway: false,
},
{
Name: "no client certs",
StatusCode: http.StatusServiceUnavailable,
CredentialToUse: strings.TrimSuffix(simpleCredName, "-cacert"),
Gateway: false,
},
}
CreateGateway(t, t, apps.Namespace1, apps.External)
for _, tc := range testCases {
t.NewSubTest(tc.Name).Run(func(t framework.TestContext) {
CreateDestinationRule(t, apps.External, "MUTUAL", tc.CredentialToUse)
newGatewayTest(t).
Run(func(t framework.TestContext, from echo.Instance, to echo.Target) {
callOpt := CallOpts(to, host, tc)
from.CallOrFail(t, callOpt)
})
})
}
})
}
const (
Gateway = `
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: istio-egressgateway-sds
spec:
selector:
istio: egressgateway
servers:
- port:
number: 443
name: https-sds
protocol: HTTPS
hosts:
- {{ .to.Config.ClusterLocalFQDN }}
tls:
mode: ISTIO_MUTUAL
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: egressgateway-for-server-sds
spec:
host: istio-egressgateway.dubbo-system.svc.cluster.local
subsets:
- name: server
trafficPolicy:
portLevelSettings:
- port:
number: 443
tls:
mode: ISTIO_MUTUAL
sni: {{ .to.Config.ClusterLocalFQDN }}
`
VirtualService = `
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: route-via-egressgateway-sds
spec:
hosts:
- {{ .to.Config.ClusterLocalFQDN }}
gateways:
- istio-egressgateway-sds
- mesh
http:
- match:
- gateways:
- mesh # from sidecars, route to egress gateway service
port: 80
route:
- destination:
host: istio-egressgateway.dubbo-system.svc.cluster.local
subset: server
port:
number: 443
weight: 100
- match:
- gateways:
- istio-egressgateway-sds
port: 443
route:
- destination:
host: {{ .to.Config.ClusterLocalFQDN }}
port:
number: 443
weight: 100
headers:
request:
add:
handled-by-egress-gateway: "true"
`
)
// We want to test out TLS origination at Gateway, to do so traffic from client in client namespace is first
// routed to egress-gateway service in dubbo-system namespace and then from egress-gateway to server in server namespace.
// TLS origination at Gateway happens using DestinationRule with CredentialName reading k8s secret at the gateway proxy.
func CreateGateway(t test.Failer, ctx resource.Context, clientNamespace namespace.Instance, to echo.Instances) {
args := map[string]interface{}{"to": to}
ctx.ConfigIstio().Eval(clientNamespace.Name(), args, Gateway, VirtualService).ApplyOrFail(t)
}
const (
// Destination Rule configs
DestinationRuleConfig = `
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: originate-tls-for-server-sds-{{.CredentialName}}
spec:
host: "{{ .to.Config.ClusterLocalFQDN }}"
trafficPolicy:
portLevelSettings:
- port:
number: 443
tls:
mode: {{.Mode}}
credentialName: {{.CredentialName}}
sni: {{ .to.Config.ClusterLocalFQDN }}
`
)
// Create the DestinationRule for TLS origination at Gateway by reading secret in dubbo-system namespace.
func CreateDestinationRule(t framework.TestContext, to echo.Instances,
destinationRuleMode string, credentialName string) {
args := map[string]interface{}{
"to": to,
"Mode": destinationRuleMode,
"CredentialName": credentialName,
}
// Get namespace for gateway pod.
istioCfg := istio.DefaultConfigOrFail(t, t)
systemNS := namespace.ClaimOrFail(t, t, istioCfg.SystemNamespace)
t.ConfigKube(t.Clusters().Default()).Eval(systemNS.Name(), args, DestinationRuleConfig).ApplyOrFail(t)
}
type TLSTestCase struct {
Name string
StatusCode int
CredentialToUse string
Gateway bool // true if the request is expected to be routed through gateway
}
func CallOpts(to echo.Target, host string, tc TLSTestCase) echo.CallOptions {
return echo.CallOptions{
To: to,
Count: util.CallsPerCluster * to.MustWorkloads().Len(),
Port: echo.Port{
Name: "http",
},
HTTP: echo.HTTP{
Headers: headers.New().WithHost(host).Build(),
},
Check: check.And(
check.NoErrorAndStatus(tc.StatusCode),
check.Each(func(r echoClient.Response) error {
if _, f := r.RequestHeaders["Handled-By-Egress-Gateway"]; tc.Gateway && !f {
return fmt.Errorf("expected to be handled by gateway. response: %s", r)
}
return nil
})),
}
}
func MustReadCert(t test.Failer, f string) string {
b, err := os.ReadFile(path.Join(env.IstioSrc, "tests/testdata/certs/dns", f))
if err != nil {
t.Fatalf("failed to read %v: %v", f, err)
}
return string(b)
}
func newGatewayTest(t framework.TestContext) *echotest.T {
return echotest.New(t, apps.All).
WithDefaultFilters().
FromMatch(match.And(
match.NotNaked,
match.Or(
match.ServiceName(apps.A.NamespacedName()),
match.Not(match.RegularPod)))).
ToMatch(match.ServiceName(apps.External.NamespacedName()))
}