blob: 5e78928c49f65906c3918d8237909e85669f7709 [file] [log] [blame]
// 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 istioagent
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"net"
"os"
"path"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"
"time"
)
import (
core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/reflection"
meshconfig "istio.io/api/mesh/v1alpha1"
pkgenv "istio.io/pkg/env"
"istio.io/pkg/log"
)
import (
"github.com/apache/dubbo-go-pixiu/pilot/pkg/model"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/xds"
testutil "github.com/apache/dubbo-go-pixiu/pilot/test/util"
"github.com/apache/dubbo-go-pixiu/pilot/test/xdstest"
"github.com/apache/dubbo-go-pixiu/pkg/bootstrap"
"github.com/apache/dubbo-go-pixiu/pkg/bootstrap/platform"
"github.com/apache/dubbo-go-pixiu/pkg/config/mesh"
"github.com/apache/dubbo-go-pixiu/pkg/envoy"
"github.com/apache/dubbo-go-pixiu/pkg/file"
"github.com/apache/dubbo-go-pixiu/pkg/istio-agent/grpcxds"
"github.com/apache/dubbo-go-pixiu/pkg/security"
"github.com/apache/dubbo-go-pixiu/pkg/spiffe"
"github.com/apache/dubbo-go-pixiu/pkg/test"
"github.com/apache/dubbo-go-pixiu/pkg/test/env"
"github.com/apache/dubbo-go-pixiu/pkg/test/util/retry"
"github.com/apache/dubbo-go-pixiu/security/pkg/credentialfetcher/plugin"
"github.com/apache/dubbo-go-pixiu/security/pkg/nodeagent/cache"
camock "github.com/apache/dubbo-go-pixiu/security/pkg/nodeagent/caclient/providers/mock"
"github.com/apache/dubbo-go-pixiu/security/pkg/nodeagent/cafile"
"github.com/apache/dubbo-go-pixiu/security/pkg/nodeagent/sds"
"github.com/apache/dubbo-go-pixiu/security/pkg/nodeagent/test/mock"
pkiutil "github.com/apache/dubbo-go-pixiu/security/pkg/pki/util"
"github.com/apache/dubbo-go-pixiu/security/pkg/stsservice"
stsmock "github.com/apache/dubbo-go-pixiu/security/pkg/stsservice/mock"
stsserver "github.com/apache/dubbo-go-pixiu/security/pkg/stsservice/server"
"github.com/apache/dubbo-go-pixiu/tests/util/leak"
)
func TestAgent(t *testing.T) {
wd := t.TempDir()
mktemp := t.TempDir
// Normally we call leak checker first. Here we call it after TempDir to avoid the (extremely
// rare) race condition of a certificate being written at the same time the cleanup occurs, which
// causes the test to fail. By checking for the leak first, we ensure all of our activities have
// fully cleanup up.
// https://github.com/golang/go/issues/43547
leak.Check(t)
// We run in the temp dir to ensure that when we are writing to the hardcoded ./etc/certs we
// don't collide with other tests
if err := os.Chdir(wd); err != nil {
t.Fatal(err)
}
certDir := filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot")
fakeSpiffeID := "spiffe://cluster.local/ns/fake-namespace/sa/fake-sa"
// As a hack, we are using the serving sets as client cert, so the identity here is istiod not a spiffe
preProvisionID := "istiod.dubbo-system.svc"
checkCertsWritten := func(t *testing.T, dir string) {
retry.UntilSuccessOrFail(t, func() error {
// Ensure we output the certs
if names := filenames(t, dir); !reflect.DeepEqual(names, []string{"cert-chain.pem", "key.pem", "root-cert.pem"}) {
return fmt.Errorf("expected certs to be output, but got %v", names)
}
return nil
}, retry.Delay(time.Millisecond*10), retry.Timeout(time.Second*5))
}
// NOTE: Long test names may result in weird errors on bind; see https://github.com/golang/go/issues/6895
t.Run("Kubernetes defaults", func(t *testing.T) {
// XDS and CA are both using JWT authentication and TLS. Root certificates distributed in
// configmap to each namespace.
Setup(t).Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
})
t.Run("RSA", func(t *testing.T) {
// All of the other tests use ECC for speed. Here we make sure RSA still works
Setup(t, func(a AgentTest) AgentTest {
a.Security.ECCSigAlg = ""
return a
}).Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
})
t.Run("Kubernetes defaults output key and cert", func(t *testing.T) {
// same as "Kubernetes defaults", but also output the key and cert. This can be used for tools
// that expect certs as files, like Prometheus.
dir := mktemp()
sds := Setup(t, func(a AgentTest) AgentTest {
a.Security.OutputKeyCertToDir = dir
a.Security.SecretRotationGracePeriodRatio = 1
return a
}).Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
// Ensure we output the certs
checkCertsWritten(t, dir)
// We rotate immediately, so we expect these files to be constantly updated
go sds[security.WorkloadKeyCertResourceName].DrainResponses()
expectFileChanged(t, filepath.Join(dir, "cert-chain.pem"), filepath.Join(dir, "key.pem"))
})
t.Run("Kubernetes defaults - output key and cert without SDS", func(t *testing.T) {
dir := mktemp()
Setup(t, func(a AgentTest) AgentTest {
a.Security.OutputKeyCertToDir = dir
a.Security.SecretRotationGracePeriodRatio = 1
return a
})
// We start the agent, but never send a single SDS request
// This behavior is useful for supporting writing the certs to disk without Envoy
checkCertsWritten(t, dir)
// TODO: this does not actually work, rotation is tied to SDS currently
// expectFileChanged(t, filepath.Join(dir, "cert-chain.pem"))
// expectFileChanged(t, filepath.Join(dir, "key.pem"))
})
t.Run("File mounted certs", func(t *testing.T) {
// User sets FileMountedCerts. They also need to set ISTIO_META_TLS_CLIENT* to specify the
// file paths. CA communication is disabled. mTLS is always used for authentication with
// Istiod, never JWT.
dir := mktemp()
copyCerts(t, dir)
cfg := security.SdsCertificateConfig{
CertificatePath: filepath.Join(dir, "cert-chain.pem"),
PrivateKeyPath: filepath.Join(dir, "key.pem"),
CaCertificatePath: filepath.Join(dir, "root-cert.pem"),
}
Setup(t, func(a AgentTest) AgentTest {
// Ensure we use the mTLS certs for XDS
a.XdsAuthenticator.Set("", preProvisionID)
// Ensure we don't try to connect to CA
a.CaAuthenticator.Set("", "")
a.Security.CertChainFilePath = cfg.CertificatePath
a.Security.KeyFilePath = cfg.PrivateKeyPath
a.Security.RootCertFilePath = cfg.CaCertificatePath
a.ProxyConfig.ProxyMetadata = map[string]string{}
a.ProxyConfig.ProxyMetadata[MetadataClientCertChain] = filepath.Join(dir, "cert-chain.pem")
a.ProxyConfig.ProxyMetadata[MetadataClientCertKey] = filepath.Join(dir, "key.pem")
a.ProxyConfig.ProxyMetadata[MetadataClientRootCert] = filepath.Join(dir, "root-cert.pem")
a.Security.FileMountedCerts = true
return a
}).Check(t, cfg.GetRootResourceName(), cfg.GetResourceName())
})
t.Run("File mounted certs with bogus token path", func(t *testing.T) {
// User sets FileMountedCerts and a bogus token path.
// They also need to set ISTIO_META_TLS_CLIENT* to specify the file paths.
// CA communication is disabled. mTLS is used for authentication with Istiod.
dir := mktemp()
copyCerts(t, dir)
cfg := security.SdsCertificateConfig{
CertificatePath: filepath.Join(dir, "cert-chain.pem"),
PrivateKeyPath: filepath.Join(dir, "key.pem"),
CaCertificatePath: filepath.Join(dir, "root-cert.pem"),
}
Setup(t, func(a AgentTest) AgentTest {
// Ensure we use the mTLS certs for XDS
a.XdsAuthenticator.Set("", preProvisionID)
// Ensure we don't try to connect to CA
a.CaAuthenticator.Set("", "")
a.Security.CertChainFilePath = cfg.CertificatePath
a.Security.KeyFilePath = cfg.PrivateKeyPath
a.Security.RootCertFilePath = cfg.CaCertificatePath
a.Security.CredFetcher = plugin.CreateTokenPlugin(filepath.Join(env.IstioSrc, "pkg/istio-agent/testdata/token"))
a.ProxyConfig.ProxyMetadata = map[string]string{}
a.ProxyConfig.ProxyMetadata[MetadataClientCertChain] = filepath.Join(dir, "cert-chain.pem")
a.ProxyConfig.ProxyMetadata[MetadataClientCertKey] = filepath.Join(dir, "key.pem")
a.ProxyConfig.ProxyMetadata[MetadataClientRootCert] = filepath.Join(dir, "root-cert.pem")
a.Security.FileMountedCerts = true
return a
}).Check(t, cfg.GetRootResourceName(), cfg.GetResourceName())
})
t.Run("OS CA Certs are able to be accessed", func(t *testing.T) {
// Try loading an OS CA Cert from OS CA Certs file paths.
dir := mktemp()
copyCertsWithOSRootCA(t, dir)
osRootPath := security.GetOSRootFilePath()
caRootCert := filepath.Base(osRootPath)
cfg := security.SdsCertificateConfig{
CertificatePath: filepath.Join(dir, "cert-chain.pem"),
PrivateKeyPath: filepath.Join(dir, "key.pem"),
CaCertificatePath: filepath.Join(dir, caRootCert),
}
Setup(t, func(a AgentTest) AgentTest {
// Ensure we use the mTLS certs for XDS
a.XdsAuthenticator.Set("", preProvisionID)
// Ensure we don't try to connect to CA
a.CaAuthenticator.Set("", "")
a.ProxyConfig.ProxyMetadata = map[string]string{}
a.ProxyConfig.ProxyMetadata[MetadataClientCertChain] = filepath.Join(dir, "cert-chain.pem")
a.ProxyConfig.ProxyMetadata[MetadataClientCertKey] = filepath.Join(dir, "key.pem")
a.ProxyConfig.ProxyMetadata[MetadataClientRootCert] = filepath.Join(dir, caRootCert)
a.Security.FileMountedCerts = true
a.Security.CARootPath = cafile.CACertFilePath
return a
}).Check(t, cfg.GetRootResourceName(), cfg.GetResourceName())
})
t.Run("Implicit file mounted certs", func(t *testing.T) {
// User mounts certificates in /etc/certs (hardcoded path). CA communication is disabled.
// mTLS is always used for authentication with Istiod, never JWT.
copyCerts(t, "./etc/certs")
t.Cleanup(func() {
_ = os.RemoveAll("./etc/certs")
})
Setup(t, func(a AgentTest) AgentTest {
// Ensure we use the token
a.XdsAuthenticator.Set("fake", "")
// Ensure we don't try to connect to CA
a.CaAuthenticator.Set("", "")
a.Security.CertChainFilePath = security.DefaultCertChainFilePath
a.Security.KeyFilePath = security.DefaultKeyFilePath
a.Security.RootCertFilePath = security.DefaultRootCertFilePath
return a
}).Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
})
t.Run("GKE workload certificates", func(t *testing.T) {
dir := mktemp()
dir = filepath.Join(dir, "./var/run/secrets/workload-spiffe-credentials")
// The cert and key names of GKE workload certificates differ from
// the default file mounted cert and key names.
copyGkeWorkloadCerts(t, dir)
cfg := security.SdsCertificateConfig{
CertificatePath: filepath.Join(dir, "certificates.pem"),
PrivateKeyPath: filepath.Join(dir, "private_key.pem"),
CaCertificatePath: filepath.Join(dir, "ca_certificates.pem"),
}
Setup(t, func(a AgentTest) AgentTest {
// Ensure we use the token
a.XdsAuthenticator.Set("fake", "")
// Ensure we don't try to connect to CA
a.CaAuthenticator.Set("", "")
a.Security.CertChainFilePath = cfg.CertificatePath
a.Security.KeyFilePath = cfg.PrivateKeyPath
a.Security.RootCertFilePath = cfg.CaCertificatePath
a.Security.FileMountedCerts = true
return a
}).Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
})
t.Run("External SDS socket", func(t *testing.T) {
dir := mktemp()
copyCerts(t, dir)
secOpts := &security.Options{}
secOpts.RootCertFilePath = filepath.Join(dir, "/root-cert.pem")
secOpts.CertChainFilePath = filepath.Join(dir, "/cert-chain.pem")
secOpts.KeyFilePath = filepath.Join(dir, "/key.pem")
secretCache, err := cache.NewSecretManagerClient(nil, secOpts)
if err != nil {
t.Fatal(err)
}
defer secretCache.Close()
// this SDS Server listens on the well-known socket path serving the certs copied to the temp directory,
// and acts as the external SDS Server that the Agent will detect at startup
sdsServer := sds.NewServer(secOpts, secretCache, nil)
defer sdsServer.Stop()
Setup(t).Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
t.Cleanup(func() {
_ = os.RemoveAll(dir)
})
})
t.Run("Unhealthy SDS socket", func(t *testing.T) {
dir := filepath.Dir(security.WorkloadIdentitySocketPath)
os.MkdirAll(dir, 0o755)
// starting an unresponsive listener on the socket
l, err := net.Listen("unix", security.WorkloadIdentitySocketPath)
if err != nil {
t.Fatal(err)
}
defer l.Close()
Setup(t).Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
t.Cleanup(func() {
_ = os.RemoveAll(dir)
})
})
t.Run("Workload certificates", func(t *testing.T) {
dir := security.WorkloadIdentityCredentialsPath
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
copyCerts(t, dir)
Setup(t).Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
t.Cleanup(func() {
_ = os.RemoveAll(dir)
})
})
t.Run("VMs", func(t *testing.T) {
// Bootstrap sets up a short lived JWT token and root certificate. The initial run will fetch
// a certificate and write it to disk. This will be used (by mTLS authenticator) for future
// requests to both the CA and XDS.
dir := mktemp()
t.Run("initial run", func(t *testing.T) {
a := Setup(t, func(a AgentTest) AgentTest {
a.XdsAuthenticator.Set("", fakeSpiffeID)
a.Security.OutputKeyCertToDir = dir
a.Security.ProvCert = dir
return a
})
a.Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
})
t.Run("reboot", func(t *testing.T) {
// Switch the JWT to a bogus path, to simulate the VM being rebooted
a := Setup(t, func(a AgentTest) AgentTest {
a.XdsAuthenticator.Set("", fakeSpiffeID)
a.CaAuthenticator.Set("", fakeSpiffeID)
a.Security.OutputKeyCertToDir = dir
a.Security.ProvCert = dir
a.Security.CredFetcher = plugin.CreateTokenPlugin(filepath.Join(env.IstioSrc, "pkg/istio-agent/testdata/token"))
return a
})
// Ensure we can still make requests
a.Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
a.Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
})
})
t.Run("VMs to etc/certs", func(t *testing.T) {
// Handle special edge case where we output certs to /etc/certs, which has magic implicit
// reading from file logic
dir := "./etc/certs"
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_ = os.RemoveAll(dir)
})
a := Setup(t, func(a AgentTest) AgentTest {
a.XdsAuthenticator.Set("", fakeSpiffeID)
a.Security.OutputKeyCertToDir = dir
a.Security.ProvCert = dir
a.Security.SecretRotationGracePeriodRatio = 1
return a
})
sds := a.Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
// Ensure we output the certs
checkCertsWritten(t, dir)
// The provisioning certificates should not be touched
go sds[security.WorkloadKeyCertResourceName].DrainResponses()
expectFileChanged(t, filepath.Join(dir, "cert-chain.pem"), filepath.Join(dir, "key.pem"))
})
t.Run("VMs provisioned certificates - short lived", func(t *testing.T) {
// User has certificates pre-provisioned on the VM by some sort of tooling, pointed to by
// PROV_CERT. These are used for mTLS auth with XDS and CA. Certificates are short lived,
// OUTPUT_CERT = PROV_CERT. This is the same as "VMs", just skipping the initial
// JWT exchange.
dir := mktemp()
copyCerts(t, dir)
sds := Setup(t, func(a AgentTest) AgentTest {
a.CaAuthenticator.Set("", preProvisionID)
a.XdsAuthenticator.Set("", fakeSpiffeID)
a.Security.OutputKeyCertToDir = dir
a.Security.ProvCert = dir
a.Security.SecretRotationGracePeriodRatio = 1
return a
}).Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
// The provisioning certificates should not be touched
go sds[security.WorkloadKeyCertResourceName].DrainResponses()
expectFileChanged(t, filepath.Join(dir, "cert-chain.pem"), filepath.Join(dir, "key.pem"))
})
t.Run("VMs provisioned certificates - long lived", func(t *testing.T) {
// User has certificates pre-provisioned on the VM by some sort of tooling, pointed to by
// PROV_CERT. These are used for mTLS auth with XDS and CA. Certificates are long lived, we
// always use the same certificate for control plane authentication and the short lived
// certificates returned from the CA for workload authentication
dir := mktemp()
copyCerts(t, dir)
sds := Setup(t, func(a AgentTest) AgentTest {
a.CaAuthenticator.Set("", preProvisionID)
a.XdsAuthenticator.Set("", preProvisionID)
a.Security.ProvCert = dir
a.Security.SecretRotationGracePeriodRatio = 1
return a
}).Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
// The provisioning certificates should not be touched
go sds[security.WorkloadKeyCertResourceName].DrainResponses()
expectFileUnchanged(t, filepath.Join(dir, "cert-chain.pem"), filepath.Join(dir, "key.pem"))
})
t.Run("Token exchange", func(t *testing.T) {
// This is used in environments where the CA expects a different token type than K8s jwt, and
// exchanges it for another form using TokenExchanger
Setup(t, func(a AgentTest) AgentTest {
a.CaAuthenticator.Set("some-token", "")
a.Security.TokenExchanger = camock.NewMockTokenExchangeServer(map[string]string{"fake": "some-token"})
return a
}).Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
})
t.Run("Token exchange with credential fetcher", func(t *testing.T) {
// This is used with platform credentials, where the platform provides some underlying
// identity (typically for VMs, not Kubernetes), which is exchanged with TokenExchanger
// before sending to the CA.
dir := mktemp()
a := Setup(t, func(a AgentTest) AgentTest {
a.CaAuthenticator.Set("some-token", "")
a.XdsAuthenticator.Set("", fakeSpiffeID)
a.Security.TokenExchanger = camock.NewMockTokenExchangeServer(map[string]string{"platform-cred": "some-token"})
a.Security.CredFetcher = plugin.CreateMockPlugin("platform-cred")
a.Security.OutputKeyCertToDir = dir
a.Security.ProvCert = dir
a.AgentConfig.XDSRootCerts = filepath.Join(certDir, "root-cert.pem")
return a
})
a.Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
})
t.Run("Token exchange with credential fetcher downtime", func(t *testing.T) {
// This ensures our pre-warming is resilient to temporary downtime of the CA
dir := mktemp()
a := Setup(t, func(a AgentTest) AgentTest {
// Make CA deny all requests to simulate downtime
a.CaAuthenticator.Set("", "")
a.XdsAuthenticator.Set("", fakeSpiffeID)
a.Security.TokenExchanger = camock.NewMockTokenExchangeServer(map[string]string{"platform-cred": "some-token"})
a.Security.CredFetcher = plugin.CreateMockPlugin("platform-cred")
a.Security.OutputKeyCertToDir = dir
a.Security.ProvCert = dir
a.AgentConfig.XDSRootCerts = filepath.Join(certDir, "root-cert.pem")
return a
})
go func() {
// Wait until we get a failure
if err := retry.UntilSuccess(func() error {
if a.CaAuthenticator.Failures.Load() < 2 {
return fmt.Errorf("not enough failures yet")
}
return nil
}); err != nil {
// never got failures, we cannot fail in goroutine so just log it and the outer
// function will fail
log.Error(err)
return
}
// Bring the CA back up
a.CaAuthenticator.Set("some-token", "")
}()
a.Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
})
envoyReady := func(t test.Failer, name string, port int) {
retry.UntilSuccessOrFail(t, func() error {
code, _, _ := env.HTTPGet(fmt.Sprintf("http://localhost:%d/ready", port))
if code != 200 {
return fmt.Errorf("envoy %q is not ready", name)
}
return nil
}, retry.Timeout(time.Second*150))
}
t.Run("Envoy lifecycle", func(t *testing.T) {
Setup(t, func(a AgentTest) AgentTest {
a.envoyEnable = true
a.ProxyConfig.StatusPort = 15020
a.ProxyConfig.ProxyAdminPort = 15000
a.AgentConfig.EnvoyPrometheusPort = 15090
a.AgentConfig.EnvoyStatusPort = 15021
a.AgentConfig.ProxyXDSDebugViaAgent = false // uses a fixed port
return a
}).Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
envoyReady(t, "first agent", 15000)
Setup(t, func(a AgentTest) AgentTest {
a.envoyEnable = true
a.ProxyConfig.StatusPort = 25020
a.ProxyConfig.ProxyAdminPort = 25000
a.AgentConfig.EnvoyPrometheusPort = 25090
a.AgentConfig.EnvoyStatusPort = 25021
a.AgentConfig.ProxyXDSDebugViaAgent = false // uses a fixed port
return a
}).Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
envoyReady(t, "second agent", 25000)
})
t.Run("Envoy bootstrap discovery", func(t *testing.T) {
Setup(t, func(a AgentTest) AgentTest {
a.envoyEnable = true
a.ProxyConfig.StatusPort = 15020
a.ProxyConfig.ProxyAdminPort = 15000
a.AgentConfig.EnvoyPrometheusPort = 15090
a.AgentConfig.EnvoyStatusPort = 15021
a.AgentConfig.EnableDynamicBootstrap = true
return a
}).Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
envoyReady(t, "bootstrap discovery", 15000)
})
t.Run("gRPC XDS bootstrap", func(t *testing.T) {
bootstrapPath := path.Join(mktemp(), "grpc-bootstrap.json")
a := Setup(t, func(a AgentTest) AgentTest {
a.Security.OutputKeyCertToDir = "/cert/path"
a.AgentConfig.GRPCBootstrapPath = bootstrapPath
a.envoyEnable = false
return a
})
generated, err := grpcxds.LoadBootstrap(bootstrapPath)
if err != nil {
t.Fatalf("could not read bootstrap config: %v", err)
}
// goldenfile determinism
for _, k := range []string{"PROV_CERT", "PROXY_CONFIG"} {
delete(generated.Node.Metadata.Fields, k)
}
got, err := json.MarshalIndent(generated, "", " ")
if err != nil {
t.Fatalf("failed to marshal bootstrap: %v", err)
}
got = []byte(strings.ReplaceAll(string(got), a.agent.cfg.XdsUdsPath, "etc/istio/XDS"))
testutil.CompareContent(t, got, filepath.Join(env.IstioSrc, "pkg/istio-agent/testdata/grpc-bootstrap.json"))
})
t.Run("ROOT CA change", func(t *testing.T) {
dir := mktemp()
rootCertFileName := "root-cert.pem"
// use a invalid root cert, XDS will fail with `authentication handshake failed`
localRootCert := filepath.Join(env.IstioSrc, "./tests/testdata/local/etc/certs/root-cert.pem")
if err := file.Copy(localRootCert, dir, rootCertFileName); err != nil {
t.Fatalf("failed to init root CA: %v", err)
}
a := Setup(t, func(a AgentTest) AgentTest {
a.AgentConfig.XDSRootCerts = path.Join(dir, rootCertFileName)
return a
})
meta := proxyConfigToMetadata(t, a.ProxyConfig)
if err := test.Wrap(func(t test.Failer) {
conn := setupDownstreamConnectionUDS(t, a.AgentConfig.XdsUdsPath)
xdsc := xds.NewAdsTest(t, conn).WithMetadata(meta)
_ = xdsc.RequestResponseAck(t, nil)
}); err == nil {
t.Fatalf("connect success with wrong CA")
}
// change ROOT CA, XDS will success
if err := file.Copy(path.Join(certDir, rootCertFileName), dir, rootCertFileName); err != nil {
t.Fatalf("failed to change root CA: %v", err)
}
a.Check(t, security.WorkloadKeyCertResourceName, security.RootCertReqResourceName)
})
t.Run("GCP", func(t *testing.T) {
os.MkdirAll(filepath.Join(wd, "var/run/secrets/tokens"), 0o755)
os.WriteFile(filepath.Join(wd, "var/run/secrets/tokens/istio-token"), []byte("test-token"), 0o644)
a := Setup(t, func(a AgentTest) AgentTest {
a.envoyEnable = true
a.enableSTS = true
a.XdsAuthenticator.Set("Fake STS", "")
a.ProxyConfig.ProxyBootstrapTemplatePath = filepath.Join(env.IstioSrc, "./tools/packaging/common/gcp_envoy_bootstrap.json")
a.AgentConfig.Platform = &fakePlatform{meta: map[string]string{
"gcp_project": "my-sd-project",
}}
a.AgentConfig.XDSRootCerts = filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/root-cert.pem")
return a
})
// We cannot actually check that envoy is ready, since it depends on RTDS and Istiod does not implement this.
// So instead just make sure it authenticated, which ensures the full STS flow properly functions
retry.UntilOrFail(t, func() bool { return a.XdsAuthenticator.Successes.Load() > 0 })
})
}
type AgentTest struct {
ProxyConfig *meshconfig.ProxyConfig
Security security.Options
AgentConfig AgentOptions
XdsAuthenticator *security.FakeAuthenticator
CaAuthenticator *security.FakeAuthenticator
envoyEnable bool
enableSTS bool
agent *Agent
}
func Setup(t *testing.T, opts ...func(a AgentTest) AgentTest) *AgentTest {
d := t.TempDir()
resp := AgentTest{
XdsAuthenticator: security.NewFakeAuthenticator("xds").Set("fake", ""),
CaAuthenticator: security.NewFakeAuthenticator("ca").Set("fake", ""),
ProxyConfig: mesh.DefaultProxyConfig(),
}
// Run through opts one time just to get the authenticators.
for _, opt := range opts {
resp = opt(resp)
}
ca := setupCa(t, resp.CaAuthenticator)
resp.Security = security.Options{
CAEndpoint: ca.URL,
CAProviderName: "Citadel",
TrustDomain: "cluster.local",
CredFetcher: plugin.CreateTokenPlugin(filepath.Join(env.IstioSrc, "pkg/istio-agent/testdata/token")),
WorkloadNamespace: "namespace",
ServiceAccount: "sa",
// Signing in 2048 bit RSA is extremely slow when running with -race enabled, sometimes taking 5s+ in
// our CI, causing flakes. We use ECC as the default to speed this up.
ECCSigAlg: string(pkiutil.EcdsaSigAlg),
CARootPath: cafile.CACertFilePath,
}
proxy := &model.Proxy{
ID: "pod1.fake-namespace",
DNSDomain: "fake-namespace.svc.cluster.local",
Type: model.SidecarProxy,
IPAddresses: []string{"127.0.0.1"},
}
resp.ProxyConfig = mesh.DefaultProxyConfig()
resp.ProxyConfig.DiscoveryAddress = setupDiscovery(t, resp.XdsAuthenticator, ca.KeyCertBundle.GetRootCertPem())
rootCert := filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/root-cert.pem")
resp.AgentConfig = AgentOptions{
ProxyXDSDebugViaAgent: true,
CARootCerts: rootCert,
XDSRootCerts: rootCert,
XdsUdsPath: filepath.Join(d, "XDS"),
ServiceNode: proxy.ServiceNode(),
}
// Set-up envoy defaults
resp.ProxyConfig.ProxyBootstrapTemplatePath = filepath.Join(env.IstioSrc, "./tools/packaging/common/envoy_bootstrap.json")
resp.ProxyConfig.ConfigPath = d
resp.ProxyConfig.BinaryPath = filepath.Join(env.LocalOut, "envoy")
if path, exists := pkgenv.RegisterStringVar("ENVOY_PATH", "", "Specifies the path to an Envoy binary.").Lookup(); exists {
resp.ProxyConfig.BinaryPath = path
}
resp.ProxyConfig.TerminationDrainDuration = nil // no need to be graceful in a test
resp.AgentConfig.ProxyIPAddresses = []string{"127.0.0.1"} // ensures IPv4 binding
resp.AgentConfig.Platform = &platform.Unknown{} // disable discovery
// Run through opts again to apply settings
for _, opt := range opts {
resp = opt(resp)
}
if resp.enableSTS {
tokenManager := stsmock.CreateFakeTokenManager()
tokenManager.SetRespStsParam(stsservice.StsResponseParameters{
AccessToken: "Fake STS",
IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
TokenType: "Bearer",
ExpiresIn: 60,
Scope: "example.com",
})
stsServer, err := stsserver.NewServer(stsserver.Config{
LocalHostAddr: "localhost",
LocalPort: 0,
}, tokenManager)
if err != nil {
t.Fatal(err)
}
resp.Security.STSPort = stsServer.Port
t.Cleanup(stsServer.Stop)
}
a := NewAgent(resp.ProxyConfig, &resp.AgentConfig, &resp.Security, envoy.ProxyConfig{TestOnly: !resp.envoyEnable})
t.Cleanup(a.Close)
ctx, done := context.WithCancel(context.Background())
wait, err := a.Run(ctx)
if err != nil {
t.Fatal(err)
}
// First signal to terminate, then wait for completion (reverse order semantics).
t.Cleanup(wait)
t.Cleanup(done)
resp.agent = a
return &resp
}
func (a *AgentTest) Check(t *testing.T, expectedSDS ...string) map[string]*xds.AdsTest {
// Ensure we can send XDS requests
meta := proxyConfigToMetadata(t, a.ProxyConfig)
// We add a retry around XDS since some of the auth methods are eventually consistent, relying on
// the CSR flow to complete first. This mirrors Envoy, which will also retry indefinitely
var resp *discovery.DiscoveryResponse
retry.UntilSuccessOrFail(t, func() error {
return test.Wrap(func(t test.Failer) {
conn := setupDownstreamConnectionUDS(t, a.AgentConfig.XdsUdsPath)
xdsc := xds.NewAdsTest(t, conn).WithMetadata(meta)
resp = xdsc.RequestResponseAck(t, nil)
})
}, retry.Timeout(time.Second*15), retry.Delay(time.Millisecond*200))
sdsStreams := map[string]*xds.AdsTest{}
gotKeys := []string{}
for _, res := range xdstest.ExtractSecretResources(t, resp.Resources) {
sds := xds.NewSdsTest(t, setupDownstreamConnectionUDS(t, security.WorkloadIdentitySocketPath)).
WithMetadata(meta).
WithTimeout(time.Second * 20) // CSR can be extremely slow with race detection enabled due to 2048 RSA
sds.RequestResponseAck(t, &discovery.DiscoveryRequest{ResourceNames: []string{res}})
sdsStreams[res] = sds
gotKeys = append(gotKeys, res)
}
if !reflect.DeepEqual(expectedSDS, gotKeys) {
t.Errorf("expected SDS resources %v got %v", expectedSDS, gotKeys)
}
return sdsStreams
}
func copyCerts(t *testing.T, dir string) {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := file.Copy(filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/cert-chain.pem"), dir, "cert-chain.pem"); err != nil {
t.Fatal(err)
}
if err := file.Copy(filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/key.pem"), dir, "key.pem"); err != nil {
t.Fatal(err)
}
if err := file.Copy(filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/root-cert.pem"), dir, "root-cert.pem"); err != nil {
t.Fatal(err)
}
}
func copyGkeWorkloadCerts(t *testing.T, dir string) {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := file.Copy(filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/cert-chain.pem"), dir, "certificates.pem"); err != nil {
t.Fatal(err)
}
if err := file.Copy(filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/key.pem"), dir, "private_key.pem"); err != nil {
t.Fatal(err)
}
if err := file.Copy(filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/root-cert.pem"), dir, "ca_certificates.pem"); err != nil {
t.Fatal(err)
}
}
func copyCertsWithOSRootCA(t *testing.T, dir string) {
caRootPath := security.GetOSRootFilePath()
if caRootPath == "" {
t.Fatal("OS CA Root Cert could not be found.")
}
caRootCert := filepath.Base(caRootPath)
if caRootCert == "" {
t.Fatal("OS CA Root Cert couldn't be found.")
}
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := file.Copy(filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/cert-chain.pem"), dir, "cert-chain.pem"); err != nil {
t.Fatal(err)
}
if err := file.Copy(filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/key.pem"), dir, "key.pem"); err != nil {
t.Fatal(err)
}
if err := file.Copy(filepath.Join(caRootPath), dir, caRootCert); err != nil {
t.Fatal(err)
}
}
func expectFileChanged(t *testing.T, files ...string) {
t.Helper()
initials := [][]byte{}
for _, f := range files {
initials = append(initials, testutil.ReadFile(t, f))
}
retry.UntilSuccessOrFail(t, func() error {
for i, f := range files {
now, err := os.ReadFile(f)
if err != nil {
return err
}
if reflect.DeepEqual(initials[i], now) {
return fmt.Errorf("file is unchanged")
}
}
return nil
}, retry.Delay(time.Millisecond*10), retry.Timeout(time.Second*15))
}
func expectFileUnchanged(t *testing.T, files ...string) {
t.Helper()
initials := [][]byte{}
for _, f := range files {
initials = append(initials, testutil.ReadFile(t, f))
}
for attempt := 0; attempt < 10; attempt++ {
time.Sleep(time.Millisecond * 10)
for i, f := range files {
now := testutil.ReadFile(t, f)
if !reflect.DeepEqual(initials[i], now) {
t.Fatalf("file is changed!")
}
}
}
}
func filenames(t *testing.T, dir string) []string {
t.Helper()
res := []string{}
contents, err := os.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
for _, f := range contents {
res = append(res, f.Name())
}
sort.Strings(res)
return res
}
func proxyConfigToMetadata(t *testing.T, proxyConfig *meshconfig.ProxyConfig) model.NodeMetadata {
t.Helper()
m := map[string]interface{}{}
for k, v := range proxyConfig.ProxyMetadata {
if strings.HasPrefix(k, bootstrap.IstioMetaPrefix) {
m[strings.TrimPrefix(k, bootstrap.IstioMetaPrefix)] = v
}
}
b, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
meta := model.NodeMetadata{}
if err := json.Unmarshal(b, &meta); err != nil {
t.Fatal(err)
}
pc := (*model.NodeMetaProxyConfig)(proxyConfig)
meta.Namespace = "fake-namespace"
meta.ServiceAccount = "fake-sa"
meta.ProxyConfig = pc
return meta
}
func setupCa(t *testing.T, auth *security.FakeAuthenticator) *mock.CAServer {
t.Helper()
opt := tlsOptions(t)
s, err := mock.NewCAServerWithKeyCert(0,
testutil.ReadFile(t, filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/ca-key.pem")),
testutil.ReadFile(t, filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/ca-cert.pem")),
opt)
if err != nil {
t.Fatal(err)
}
t.Cleanup(s.GRPCServer.Stop)
s.Authenticators = []security.Authenticator{auth}
return s
}
func tlsOptions(t *testing.T, extraRoots ...[]byte) grpc.ServerOption {
t.Helper()
cert, err := tls.LoadX509KeyPair(
filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/cert-chain.pem"),
filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/key.pem"))
if err != nil {
t.Fatal(err)
}
peerCertVerifier := spiffe.NewPeerCertVerifier()
if err := peerCertVerifier.AddMappingFromPEM("cluster.local",
testutil.ReadFile(t, filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/root-cert.pem"))); err != nil {
t.Fatal(err)
}
for _, r := range extraRoots {
if err := peerCertVerifier.AddMappingFromPEM("cluster.local", r); err != nil {
t.Fatal(err)
}
}
return grpc.Creds(credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.VerifyClientCertIfGiven,
ClientCAs: peerCertVerifier.GetGeneralCertPool(),
VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
err := peerCertVerifier.VerifyPeerCert(rawCerts, verifiedChains)
if err != nil {
log.Infof("Could not verify certificate: %v", err)
}
return err
},
}))
}
func setupDiscovery(t *testing.T, auth *security.FakeAuthenticator, certPem []byte) string {
t.Helper()
l, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal(err)
}
opt := tlsOptions(t, certPem)
// Set up a simple service to make sure we have mTLS requested
ds := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{ConfigString: `
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: app
namespace: default
spec:
hosts:
- app.com
ports:
- number: 80
name: http
protocol: HTTP
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: plaintext
namespace: default
spec:
host: app.com
trafficPolicy:
tls:
mode: ISTIO_MUTUAL
`})
ds.Discovery.Authenticators = []security.Authenticator{auth}
grpcServer := grpc.NewServer(opt)
reflection.Register(grpcServer)
ds.Discovery.Register(grpcServer)
go func() {
_ = grpcServer.Serve(l)
}()
t.Cleanup(grpcServer.Stop)
return net.JoinHostPort("localhost", fmt.Sprint(l.Addr().(*net.TCPAddr).Port))
}
type fakePlatform struct {
meta map[string]string
labels map[string]string
}
func (f *fakePlatform) Metadata() map[string]string {
return f.meta
}
func (f *fakePlatform) Locality() *core.Locality {
return &core.Locality{}
}
func (f *fakePlatform) Labels() map[string]string {
return f.labels
}
func (f *fakePlatform) IsKubernetes() bool {
return true
}