From f97a89fa1676cf55fb8d343ae92f453a98ac5724 Mon Sep 17 00:00:00 2001 From: lei-tang <32078630+lei-tang@users.noreply.github.com> Date: Tue, 1 Jan 2019 13:47:40 +0800 Subject: [PATCH] Istio CA integrates with Vault CA providers (#10638) * Istio integrates with Vault CA providers * Add tests to the Vault CA client * Add the support to TLS Vault connection * Add a test for a TLS Vault server * Change to use a Vault TLS server in the endpoints-jenkins project * Change the vault client name * Fix goimports error and logf statements * Configure the test Vault server * Change naming in client.go * Add flags of Vault CA config --- security/cmd/node_agent_k8s/main.go | 27 +- .../pkg/nodeagent/cache/secretcache_test.go | 3 +- security/pkg/nodeagent/caclient/client.go | 7 +- .../pkg/nodeagent/caclient/client_test.go | 2 +- .../caclient/providers/vault/client.go | 226 ++++++++++ .../caclient/providers/vault/client_test.go | 400 ++++++++++++++++++ security/pkg/nodeagent/sds/server.go | 12 + .../nodeagent/secretfetcher/secretfetcher.go | 5 +- .../secretfetcher/secretfetcher_test.go | 3 +- 9 files changed, 677 insertions(+), 8 deletions(-) create mode 100644 security/pkg/nodeagent/caclient/providers/vault/client.go create mode 100644 security/pkg/nodeagent/caclient/providers/vault/client_test.go diff --git a/security/cmd/node_agent_k8s/main.go b/security/cmd/node_agent_k8s/main.go index f85f73e6687b..44187bc58f3d 100644 --- a/security/cmd/node_agent_k8s/main.go +++ b/security/cmd/node_agent_k8s/main.go @@ -44,6 +44,18 @@ const ( // The trust domain corresponds to the trust root of a system. // Refer to https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md#21-trust-domain trustDomain = "Trust_Domain" + + // The environmental variable name for Vault CA address. + vaultAddress = "VAULT_ADDR" + + // The environmental variable name for Vault auth path. + vaultAuthPath = "VAULT_AUTH_PATH" + + // The environmental variable name for Vault role. + vaultRole = "VAULT_ROLE" + + // The environmental variable name for Vault sign CSR path. + vaultSignCsrPath = "VAULT_SIGN_CSR_PATH" ) var ( @@ -103,7 +115,8 @@ var ( func newSecretCache(serverOptions sds.Options) (workloadSecretCache, gatewaySecretCache *cache.SecretCache) { if serverOptions.EnableWorkloadSDS { - wSecretFetcher, err := secretfetcher.NewSecretFetcher(false, serverOptions.CAEndpoint, serverOptions.CAProviderName, true, nil) + wSecretFetcher, err := secretfetcher.NewSecretFetcher(false, serverOptions.CAEndpoint, + serverOptions.CAProviderName, true, nil, "", "", "", "") if err != nil { log.Errorf("failed to create secretFetcher for workload proxy: %v", err) os.Exit(1) @@ -116,7 +129,8 @@ func newSecretCache(serverOptions sds.Options) (workloadSecretCache, gatewaySecr } if serverOptions.EnableIngressGatewaySDS { - gSecretFetcher, err := secretfetcher.NewSecretFetcher(true, "", "", false, nil) + gSecretFetcher, err := secretfetcher.NewSecretFetcher(true, "", "", + false, nil, "", "", "", "") if err != nil { log.Errorf("failed to create secretFetcher for gateway proxy: %v", err) os.Exit(1) @@ -169,6 +183,15 @@ func init() { rootCmd.PersistentFlags().DurationVar(&workloadSdsCacheOptions.EvictionDuration, "secretEvictionDuration", 24*time.Hour, "Secret eviction time duration") + rootCmd.PersistentFlags().StringVar(&serverOptions.VaultAddress, "vaultAddress", os.Getenv(vaultAddress), + "Vault address") + rootCmd.PersistentFlags().StringVar(&serverOptions.VaultRole, "vaultRole", os.Getenv(vaultRole), + "Vault role") + rootCmd.PersistentFlags().StringVar(&serverOptions.VaultAuthPath, "vaultAuthPath", os.Getenv(vaultAuthPath), + "Vault auth path") + rootCmd.PersistentFlags().StringVar(&serverOptions.VaultSignCsrPath, "vaultSignCsrPath", os.Getenv(vaultSignCsrPath), + "Vault sign CSR path") + // Attach the Istio logging options to the command. loggingOptions.AttachCobraFlags(rootCmd) } diff --git a/security/pkg/nodeagent/cache/secretcache_test.go b/security/pkg/nodeagent/cache/secretcache_test.go index f492659733b2..7a22a535ffd5 100644 --- a/security/pkg/nodeagent/cache/secretcache_test.go +++ b/security/pkg/nodeagent/cache/secretcache_test.go @@ -186,7 +186,8 @@ func TestWorkloadAgentRefreshSecret(t *testing.T) { // TestGatewayAgentGenerateSecret verifies that ingress gateway agent manages secret cache correctly. func TestGatewayAgentGenerateSecret(t *testing.T) { client := fake.NewSimpleClientset() - fetcher, err := secretfetcher.NewSecretFetcher(true, "", "", false, client) + fetcher, err := secretfetcher.NewSecretFetcher(true, "", "", false, client, + "", "", "", "") if err != nil { t.Errorf("failed to create secretFetcher for gateway proxy: %v", err) } diff --git a/security/pkg/nodeagent/caclient/client.go b/security/pkg/nodeagent/caclient/client.go index 37603f6195e1..70b3a587f310 100644 --- a/security/pkg/nodeagent/caclient/client.go +++ b/security/pkg/nodeagent/caclient/client.go @@ -28,11 +28,13 @@ import ( caClientInterface "istio.io/istio/security/pkg/nodeagent/caclient/interface" citadel "istio.io/istio/security/pkg/nodeagent/caclient/providers/citadel" gca "istio.io/istio/security/pkg/nodeagent/caclient/providers/google" + vault "istio.io/istio/security/pkg/nodeagent/caclient/providers/vault" ) const ( googleCAName = "GoogleCA" citadelName = "Citadel" + vaultCAName = "VaultCA" ns = "istio-system" retryInterval = time.Second * 2 @@ -44,10 +46,13 @@ type configMap interface { } // NewCAClient create an CA client. -func NewCAClient(endpoint, CAProviderName string, tlsFlag bool) (caClientInterface.Client, error) { +func NewCAClient(endpoint, CAProviderName string, tlsFlag bool, vaultAddr, vaultRole, + vaultAuthPath, vaultSignCsrPath string) (caClientInterface.Client, error) { switch CAProviderName { case googleCAName: return gca.NewGoogleCAClient(endpoint, tlsFlag) + case vaultCAName: + return vault.NewVaultClient(tlsFlag, nil, vaultAddr, vaultRole, vaultAuthPath, vaultSignCsrPath) case citadelName: cs, err := createClientSet() if err != nil { diff --git a/security/pkg/nodeagent/caclient/client_test.go b/security/pkg/nodeagent/caclient/client_test.go index 74a728aa8994..ed4769df594b 100644 --- a/security/pkg/nodeagent/caclient/client_test.go +++ b/security/pkg/nodeagent/caclient/client_test.go @@ -46,7 +46,7 @@ func TestNewCAClient(t *testing.T) { } for id, tc := range testCases { - _, err := NewCAClient("abc:0", tc.provider, false) + _, err := NewCAClient("abc:0", tc.provider, false, "", "", "", "") if err.Error() != tc.expectedErr { t.Errorf("Test case [%s]: Get error (%s) different from expected error (%s).", id, err.Error(), tc.expectedErr) diff --git a/security/pkg/nodeagent/caclient/providers/vault/client.go b/security/pkg/nodeagent/caclient/providers/vault/client.go new file mode 100644 index 000000000000..d9b08ece02e7 --- /dev/null +++ b/security/pkg/nodeagent/caclient/providers/vault/client.go @@ -0,0 +1,226 @@ +// Copyright 2018 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 caclient + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + "strconv" + + "github.com/hashicorp/vault/api" + + "istio.io/istio/pkg/log" + caClientInterface "istio.io/istio/security/pkg/nodeagent/caclient/interface" +) + +type vaultClient struct { + enableTLS bool + tlsRootCert []byte + + vaultAddr string + vaultLoginRole string + vaultLoginPath string + vaultSignCsrPath string + + client *api.Client +} + +// NewVaultClient create a CA client for the Vault provider 1. +func NewVaultClient(tls bool, tlsRootCert []byte, + vaultAddr, vaultLoginRole, vaultLoginPath, vaultSignCsrPath string) (caClientInterface.Client, error) { + c := &vaultClient{ + enableTLS: tls, + tlsRootCert: tlsRootCert, + vaultAddr: vaultAddr, + vaultLoginRole: vaultLoginRole, + vaultLoginPath: vaultLoginPath, + vaultSignCsrPath: vaultSignCsrPath, + } + + var client *api.Client + var err error + if tls { + client, err = createVaultTLSClient(vaultAddr, tlsRootCert) + } else { + client, err = createVaultClient(vaultAddr) + } + if err != nil { + return nil, err + } + c.client = client + + return c, nil +} + +// CSR Sign calls Vault to sign a CSR. +func (c *vaultClient) CSRSign(ctx context.Context, csrPEM []byte, saToken string, + certValidTTLInSec int64) ([]string /*PEM-encoded certificate chain*/, error) { + token, err := loginVaultK8sAuthMethod(c.client, c.vaultLoginPath, c.vaultLoginRole, saToken) + if err != nil { + return nil, fmt.Errorf("failed to login Vault: %v", err) + } + c.client.SetToken(token) + certChain, err := signCsrByVault(c.client, c.vaultSignCsrPath, certValidTTLInSec, csrPEM) + if err != nil { + return nil, fmt.Errorf("failed to sign CSR: %v", err) + } + + if len(certChain) <= 1 { + log.Errorf("certificate chain length is %d, expected more than 1", len(certChain)) + return nil, fmt.Errorf("invalid certificate chain in the response") + } + + return certChain, nil +} + +// createVaultClient creates a client to a Vault server +// vaultAddr: the address of the Vault server (e.g., "http://127.0.0.1:8200"). +func createVaultClient(vaultAddr string) (*api.Client, error) { + config := api.DefaultConfig() + config.Address = vaultAddr + + client, err := api.NewClient(config) + if err != nil { + log.Errorf("failed to create a Vault client: %v", err) + return nil, err + } + + return client, nil +} + +// createVaultTLSClient creates a client to a Vault server +// vaultAddr: the address of the Vault server (e.g., "https://127.0.0.1:8200"). +func createVaultTLSClient(vaultAddr string, tlsRootCert []byte) (*api.Client, error) { + // Load the system default root certificates. + pool, err := x509.SystemCertPool() + if err != nil { + log.Errorf("could not get SystemCertPool: %v", err) + return nil, fmt.Errorf("could not get SystemCertPool: %v", err) + } + if tlsRootCert != nil && len(tlsRootCert) > 0 { + ok := pool.AppendCertsFromPEM(tlsRootCert) + if !ok { + return nil, fmt.Errorf("failed to append a certificate (%v) to the certificate pool", string(tlsRootCert[:])) + } + } + tlsConfig := &tls.Config{ + RootCAs: pool, + } + + transport := &http.Transport{TLSClientConfig: tlsConfig} + httpClient := &http.Client{Transport: transport} + + config := api.DefaultConfig() + config.Address = vaultAddr + config.HttpClient = httpClient + + client, err := api.NewClient(config) + if err != nil { + log.Errorf("failed to create a Vault client: %v", err) + return nil, err + } + + return client, nil +} + +// loginVaultK8sAuthMethod logs into the Vault k8s auth method with the service account and +// returns the auth client token. +// loginPath: the path of the login +// role: the login role +// jwt: the service account used for login +func loginVaultK8sAuthMethod(client *api.Client, loginPath, role, sa string) (string, error) { + resp, err := client.Logical().Write( + loginPath, + map[string]interface{}{ + "jwt": sa, + "role": role, + }) + + if err != nil { + log.Errorf("failed to login Vault: %v", err) + return "", err + } + if resp == nil { + log.Errorf("login response is nil") + return "", fmt.Errorf("login response is nil") + } + if resp.Auth == nil { + log.Errorf("login response auth field is nil") + return "", fmt.Errorf("login response auth field is nil") + } + return resp.Auth.ClientToken, nil +} + +// signCsrByVault signs the CSR and return the signed certifcate and the CA certificate chain +// Return the signed certificate chain when succeed. +// client: the Vault client +// csrSigningPath: the path for signing a CSR +// csr: the CSR to be signed, in pem format +func signCsrByVault(client *api.Client, csrSigningPath string, certTTLInSec int64, csr []byte) ([]string, error) { + m := map[string]interface{}{ + "format": "pem", + "csr": string(csr[:]), + "ttl": strconv.FormatInt(certTTLInSec, 10) + "s", + } + res, err := client.Logical().Write(csrSigningPath, m) + if err != nil { + log.Errorf("failed to post to %v: %v", csrSigningPath, err) + return nil, fmt.Errorf("failed to post to %v: %v", csrSigningPath, err) + } + if res == nil { + log.Error("sign response is nil") + return nil, fmt.Errorf("sign response is nil") + } + if res.Data == nil { + log.Error("sign response has a nil Data field") + return nil, fmt.Errorf("sign response has a nil Data field") + } + //Extract the certificate and the certificate chain + certificate, ok := res.Data["certificate"] + if !ok { + log.Error("no certificate in the CSR response") + return nil, fmt.Errorf("no certificate in the CSR response") + } + cert, ok := certificate.(string) + if !ok { + log.Error("the certificate in the CSR response is not a string") + return nil, fmt.Errorf("the certificate in the CSR response is not a string") + } + caChain, ok := res.Data["ca_chain"] + if !ok { + log.Error("no certificate chain in the CSR response") + return nil, fmt.Errorf("no certificate chain in the CSR response") + } + chain, ok := caChain.([]interface{}) + if !ok { + log.Error("the certificate chain in the CSR response is of unexpected format") + return nil, fmt.Errorf("the certificate chain in the CSR response is of unexpected format") + } + var certChain []string + certChain = append(certChain, cert) + for idx, c := range chain { + _, ok := c.(string) + if !ok { + log.Errorf("the certificate in the certificate chain %v is not a string", idx) + return nil, fmt.Errorf("the certificate in the certificate chain %v is not a string", idx) + } + certChain = append(certChain, c.(string)) + } + + return certChain, nil +} diff --git a/security/pkg/nodeagent/caclient/providers/vault/client_test.go b/security/pkg/nodeagent/caclient/providers/vault/client_test.go new file mode 100644 index 000000000000..8c4c59838f79 --- /dev/null +++ b/security/pkg/nodeagent/caclient/providers/vault/client_test.go @@ -0,0 +1,400 @@ +// Copyright 2018 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 caclient + +import ( + "context" + "encoding/json" + "encoding/pem" + "io/ioutil" + "net/http" + "net/http/httptest" + "reflect" + "regexp" + "testing" +) + +// vaultAuthHeaderName is the name of the header containing the token. +const vaultAuthHeaderName = "X-Vault-Token" + +var ( + vaultLoginResp = ` + { + "auth": { + "client_token": "fake-vault-token" + } + } + ` + vaultSignResp = ` + { + "data": { + "certificate": "fake-certificate", + "ca_chain": ["fake-ca1", "fake-ca2"] + } + } + ` + vaultServerTLSCert = ` +-----BEGIN CERTIFICATE----- +MIIC3jCCAcagAwIBAgIRAIcSFH1jneS0XPz5r2QDbigwDQYJKoZIhvcNAQELBQAw +EDEOMAwGA1UEChMFVmF1bHQwIBcNMTgxMjI2MDkwMDU3WhgPMjExODEyMDIwOTAw +NTdaMBAxDjAMBgNVBAoTBVZhdWx0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA2q5lfJCLAOTEjX3xV8qMLEX8zUQpd0AjD6zzOMzx51GVM7Plf7CJmaDq +yloRz3zcrTEltHUrln5fvouvp4TetOlqEU979vvccnFLgXrSpn+Zt/EyjE0rUYY3 +5e2qxy9bP2E7zJSKONIT6zRDd2zUQGH3zUem1ZG0GFY1ZL5qFSOIy+PvuQ4u8HCa +1CcnHmI613fVDbFbaxuF2G2MIwCZ/Fg6KBd9kgU7uCOvkbR4AtRe0ntwweIjOIas +FiohPQzVY4obrYZiTV43HT4lGti7ySn2c96UnRSnmHLWyBb7cafd4WZN/t+OmYSd +ooxCVQ2Zqub6NlZ5OySYOz/0BJq6DQIDAQABozEwLzAOBgNVHQ8BAf8EBAMCBaAw +DAYDVR0TAQH/BAIwADAPBgNVHREECDAGhwQj6fn5MA0GCSqGSIb3DQEBCwUAA4IB +AQBORvUcW0wgg/Wo1aKFaZQuPPFVLjOZat0QpCJYNDhsSIO4Y0JS+Y1cEIkvXB3S +Q3D7IfNP0gh1fhtP/d45LQSPqpyJF5vKWAvwa/LSPKpw2+Zys4oDahcH+SEKiQco +IhkkHNEgC4LEKEaGvY4A8Cw7uWWquUJB16AapSSnkeD2vTcxErfCO59yR7yEWDa6 +8j6QNzmGNj2YXtT86+Mmedhfh65Rrh94mhAPQHBAdCNGCUwZ6zHPQ6Z1rj+x3Wm9 +gqpveVq2olloNbnLNmM3V6F9mqSZACgADmRqf42bixeHczkTfRDKThJcpY5U44vy +w4Nm32yDWhD6AC68rDkXX68m +-----END CERTIFICATE----- + ` + testCsr1 = ` +-----BEGIN CERTIFICATE REQUEST----- +MIICojCCAYoCAQAwEzERMA8GA1UEAxMId29ya2xvYWQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCtUKHNG598mQ0wo5+AfZhn2yA8HhL1QV0XERJgBU2p +PbH/4yIHq++kugWWbj4REE7OPvKjJRdo8yJ9OpjDXA8s5t7fchdr6BePLF6+GfkQ +ACmnKAziRHMg22Zy+crdVEiyrMAzwujbiBxiI5hcHHB15TX+6lAxaLZJ3BLC4NBd +YHUeEwvuBV4zLLvKSVE6jFQIvxHKk/Nh/sJvvvSIOWmXPgS6raFPKPTDJ3MjFyCU +VEz8/HWyaEptX4C91NQxa7/CIJ/DYXtKVbP+jXGaLrLQUX+2r95H2cU604OfMz2Z +PmYgYUovtb93llwgLKoJk3MjIGEvy4AluGqegrDe5ghfAgMBAAGgSjBIBgkqhkiG +9w0BCQ4xOzA5MDcGA1UdEQQwMC6GLHNwaWZmZTovL2NsdXN0ZXIubG9jYWwvbnMv +ZGVmYXVsdC9zYS9kZWZhdWx0MA0GCSqGSIb3DQEBCwUAA4IBAQCRnzNqI46M1FJL +IWaQsZj7QeJPrPmuwcGzQ5qRlXBmxAe95N+9DKpmiTwU0tOz375EEjXwVYvs1cZT +d75Br1kaAMT70LnPUxvSjlcTNItLwlu6LoH/BuaFa5VL1dKFvjRQC3aKFKD634pX +U82yKWa7kAVPWJAizoz+wf0RIF2KEp0wpd/FPQJaFkAiTrC8rwEhPIfKTLads4HL +5pWcfODn5eMC7+htiteWsfdhK8Bxjz0VyzSs3BbgAHs+LFkIBGkKe0sl/ii96Bik +SQYzPWVk89gu6nKV+fS2pA9C8dAnYOzVu9XXc+PGlcIhjnuS+/P74hN5D3aIGljW +7WsYeEkp +-----END CERTIFICATE REQUEST----- + ` + citadelSANonTLS = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3Nlcn" + + "ZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2" + + "UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubm" + + "FtZSI6InZhdWx0LWNpdGFkZWwtc2EtdG9rZW4tYjdyemsiLCJrdWJlcm5ldGVzLmlvL3" + + "NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoidmF1bHQtY2l0YWRlbC" + + "1zYSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Ln" + + "VpZCI6IjczMmFhMDYyLTBjYWYtMTFlOS1iNWFkLTQyMDEwYThhMDAwYyIsInN1YiI6In" + + "N5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OnZhdWx0LWNpdGFkZWwtc2EifQ.BYX" + + "fKQHG3Eu384EY4KlnhFEk6iLZZHVnX03FIrC-xR-tft2AZP0wpGeRNmMMKMiFzXfBQ8j" + + "XzarGgPdoWFjVy0R1HuozX-g7WCAkhlMR38IhHr7EFOkue3_73dGNHAXoCQ4C9eAduDn" + + "r_yBClB3JMeoJXIS2tvbwZ4BrHJepu7zXJalbWE2n0oucOH2JLIrp_wcA0yCNu6wFXEX" + + "S7ghVsiDHKyL1_SmzsZ4gKyhlDUB1UAIbQ9XghXIAK_5Tmo_cKGbZ0MeqJeVUkDr2w-3" + + "ZRrnQD8lUEwhkGlgkIjEKAY4yKFliEOIDTft_gz0h6t9zGCn5OmhNolXQ4dIbsEcFgA" + + citadelSATLS = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3" + + "NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3" + + "BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZX" + + "QubmFtZSI6InZhdWx0LWNpdGFkZWwtc2EtdG9rZW4tcmZxZGoiLCJrdWJlcm5ldGVzLm" + + "lvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoidmF1bHQtY2l0YW" + + "RlbC1zYSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW" + + "50LnVpZCI6IjIzOTk5YzY1LTA4ZjMtMTFlOS1hYzAzLTQyMDEwYThhMDA3OSIsInN1Yi" + + "I6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OnZhdWx0LWNpdGFkZWwtc2EifQ" + + ".RNH1QbapJKPmktV3tCnpiz7hoYpv1TM6LXzThOtaDp7LFpeANZcJ1zVQdys3Ednlkry" + + "kGMepEjsdNuT6ndHfh8jRJAZuNWNPGrhxz4BeUaOqZg3v7AzJlMeFKjY_fiTYYd2gBZZ" + + "xkpv1FvAPihHYng2NeN2nKbiZbsnZNU1qFdvbgCISaFqTf0dh75OzgCX_1Fh6HOA7ANf" + + "7p522PDW_BRln0RTwUJovCpGeiNCGdujGiNLDZyBcdtikY5ry_KXTdrVAcTUvI6lxwRb" + + "ONNfuN8hrIDl95vJjhUlE-O-_cx8qWtXNdqJlMje1SsiPCL4uq70OepG_I4aSzC2o8aD" + + "tlQ" + + fakeCert = []string{"fake-certificate", "fake-ca1", "fake-ca2"} + vaultNonTLSAddr = "http://35.247.15.29:8200" + vaultTLSAddr = "https://35.233.249.249:8200" +) + +type mockVaultServer struct { + httpServer *httptest.Server + loginRole string + token string + vaultLoginResp string + vaultSignResp string +} + +type clientConfig struct { + tls bool + tlsCert []byte + vaultAddr string + vaultLoginRole string + vaultLoginPath string + vaultSignCsrPath string + clientToken string + csr []byte +} + +type loginRequest struct { + Jwt string `json:"jwt"` + Role string `json:"role"` +} + +type signRequest struct { + Format string `json:"format"` + Csr string `json:"csr"` +} + +func TestClientOnMockVaultCA(t *testing.T) { + testCases := map[string]struct { + cliConfig clientConfig + expectedCert []string + expectedErr string + }{ + "Valid certs 1": { + cliConfig: clientConfig{tls: false, tlsCert: []byte{}, vaultLoginPath: "login", + vaultSignCsrPath: "sign", clientToken: "fake-client-token", csr: []byte{01}}, + expectedCert: fakeCert, + expectedErr: "", + }, + "Valid certs 1 (TLS)": { + cliConfig: clientConfig{tls: true, vaultLoginPath: "login", vaultSignCsrPath: "sign", + clientToken: "fake-client-token", csr: []byte{01}}, + expectedCert: fakeCert, + expectedErr: "", + }, + "Wrong Vault addr": { + cliConfig: clientConfig{tls: false, tlsCert: []byte{}, vaultAddr: "wrong-vault-addr", + vaultLoginPath: "login", vaultSignCsrPath: "wrong-sign-path", + clientToken: "fake-client-token", csr: []byte{01}}, + expectedCert: nil, + expectedErr: "failed to login Vault", + }, + "Wrong login path": { + cliConfig: clientConfig{tls: false, tlsCert: []byte{}, vaultLoginPath: "wrong-login-path", + vaultSignCsrPath: "sign", clientToken: "fake-client-token", csr: []byte{01}}, + expectedCert: nil, + expectedErr: "failed to login Vault", + }, + "Wrong client token": { + cliConfig: clientConfig{tls: false, tlsCert: []byte{}, vaultLoginPath: "login", + vaultSignCsrPath: "sign", clientToken: "wrong-client-token", csr: []byte{01}}, + expectedCert: nil, + expectedErr: "failed to login Vault", + }, + "Wrong sign path": { + cliConfig: clientConfig{tls: false, tlsCert: []byte{}, vaultLoginPath: "login", + vaultSignCsrPath: "wrong-sign-path", clientToken: "fake-client-token", csr: []byte{01}}, + expectedCert: nil, + expectedErr: "failed to sign CSR", + }, + } + + ch := make(chan *mockVaultServer) + go func() { + // create a test TLS Vault server + server := newMockVaultServer(t, true, "", "fake-client-token", vaultLoginResp, vaultSignResp) + ch <- server + }() + s1 := <-ch + defer s1.httpServer.Close() + + go func() { + // create a test non-TLS Vault server + server := newMockVaultServer(t, false, "", "fake-client-token", vaultLoginResp, vaultSignResp) + ch <- server + }() + s2 := <-ch + defer s2.httpServer.Close() + + for id, tc := range testCases { + if len(tc.cliConfig.vaultAddr) == 0 { + // If the address of Vault is not set by the test case, use that of the test server. + if tc.cliConfig.tls { + tc.cliConfig.vaultAddr = s1.httpServer.URL + } else { + tc.cliConfig.vaultAddr = s2.httpServer.URL + } + } + if tc.cliConfig.tls { + tc.cliConfig.tlsCert = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: s1.httpServer.Certificate().Raw}) + if tc.cliConfig.tlsCert == nil { + t.Errorf("invalid TLS certificate") + } + } + cli, err := NewVaultClient(tc.cliConfig.tls, tc.cliConfig.tlsCert, tc.cliConfig.vaultAddr, tc.cliConfig.vaultLoginRole, + tc.cliConfig.vaultLoginPath, tc.cliConfig.vaultSignCsrPath) + if err != nil { + t.Errorf("Test case [%s]: failed to create ca client: %v", id, err) + } + + resp, err := cli.CSRSign(context.Background(), tc.cliConfig.csr, tc.cliConfig.clientToken, 1) + if err != nil { + match, _ := regexp.MatchString(tc.expectedErr+".+", err.Error()) + if !match { + t.Errorf("Test case [%s]: error (%s) does not match expected error (%s)", id, err.Error(), tc.expectedErr) + } + } else { + if tc.expectedErr != "" { + t.Errorf("Test case [%s]: expect error: %s but got no error", id, tc.expectedErr) + } else if !reflect.DeepEqual(resp, tc.expectedCert) { + t.Errorf("Test case [%s]: resp: got %+v, expected %v", id, resp, tc.expectedCert) + } + } + } +} + +func TestClientOnExampleHttpVaultCA(t *testing.T) { + testCases := map[string]struct { + cliConfig clientConfig + }{ + "Valid certs 1": { + cliConfig: clientConfig{vaultAddr: vaultNonTLSAddr, vaultLoginPath: "auth/kubernetes/login", + vaultLoginRole: "istio-cert", vaultSignCsrPath: "istio_ca/sign/istio-pki-role", + clientToken: citadelSANonTLS, csr: []byte(testCsr1)}, + }, + } + + for id, tc := range testCases { + var vaultAddr string + vaultAddr = tc.cliConfig.vaultAddr + cli, err := NewVaultClient(false, []byte{}, vaultAddr, tc.cliConfig.vaultLoginRole, + tc.cliConfig.vaultLoginPath, tc.cliConfig.vaultSignCsrPath) + if err != nil { + t.Errorf("Test case [%s]: failed to create ca client: %v", id, err) + } + + resp, err := cli.CSRSign(context.Background(), tc.cliConfig.csr, tc.cliConfig.clientToken, 1) + if err != nil { + t.Errorf("Test case [%s]: error (%v) is not expected", id, err.Error()) + } else { + if len(resp) != 3 { + t.Errorf("Test case [%s]: the certificate chain length (%v) is unexpected", id, len(resp)) + } + } + } +} + +func TestClientOnExampleHttpsVaultCA(t *testing.T) { + testCases := map[string]struct { + cliConfig clientConfig + }{ + "Valid certs 1": { + cliConfig: clientConfig{vaultAddr: vaultTLSAddr, vaultLoginPath: "auth/kubernetes/login", + vaultLoginRole: "istio-cert", vaultSignCsrPath: "istio_ca/sign/istio-pki-role", + clientToken: citadelSATLS, csr: []byte(testCsr1)}, + }, + } + + for id, tc := range testCases { + var vaultAddr string + vaultAddr = tc.cliConfig.vaultAddr + cli, err := NewVaultClient(true, []byte(vaultServerTLSCert), vaultAddr, tc.cliConfig.vaultLoginRole, + tc.cliConfig.vaultLoginPath, tc.cliConfig.vaultSignCsrPath) + if err != nil { + t.Errorf("Test case [%s]: failed to create ca client: %v", id, err) + } + + resp, err := cli.CSRSign(context.Background(), tc.cliConfig.csr, tc.cliConfig.clientToken, 1) + if err != nil { + t.Errorf("Test case [%s]: error (%v) is not expected", id, err.Error()) + } else { + if len(resp) != 3 { + t.Errorf("Test case [%s]: the certificate chain length (%v) is unexpected", id, len(resp)) + } + } + } +} + +// newMockVaultServer creates a mock Vault server for testing purpose. +// token: required access token +func newMockVaultServer(t *testing.T, tls bool, loginRole, token, loginResp, signResp string) *mockVaultServer { + vaultServer := &mockVaultServer{ + loginRole: loginRole, + token: token, + vaultLoginResp: loginResp, + vaultSignResp: signResp, + } + + handler := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + t.Logf("request: %+v", *req) + switch req.URL.Path { + case "/v1/login": + t.Logf("%v", req.URL) + body, err := ioutil.ReadAll(req.Body) + if err != nil { + t.Logf("failed to read the request body: %v", err) + resp.WriteHeader(http.StatusBadRequest) + return + } + loginReq := loginRequest{} + err = json.Unmarshal(body, &loginReq) + if err != nil { + t.Logf("failed to parse the request body: %v", err) + resp.WriteHeader(http.StatusBadRequest) + return + } + if vaultServer.loginRole != loginReq.Role { + t.Logf("invalid login role: %v", loginReq.Role) + resp.WriteHeader(http.StatusBadRequest) + return + } + if vaultServer.token != loginReq.Jwt { + t.Logf("invalid login token: %v", loginReq.Jwt) + resp.WriteHeader(http.StatusBadRequest) + return + } + resp.Header().Set("Content-Type", "application/json") + resp.Write([]byte(vaultServer.vaultLoginResp)) + break + case "/v1/sign": + t.Logf("%v", req.URL) + if req.Header.Get(vaultAuthHeaderName) != "fake-vault-token" { + t.Logf("the vault token is invalid: %v", req.Header.Get(vaultAuthHeaderName)) + resp.WriteHeader(http.StatusBadRequest) + return + } + body, err := ioutil.ReadAll(req.Body) + if err != nil { + t.Logf("failed to read the request body: %v", err) + resp.WriteHeader(http.StatusBadRequest) + return + } + signReq := signRequest{} + err = json.Unmarshal(body, &signReq) + if err != nil { + t.Logf("failed to parse the request body: %v", err) + resp.WriteHeader(http.StatusBadRequest) + return + } + if "pem" != signReq.Format { + t.Logf("invalid sign format: %v", signReq.Format) + resp.WriteHeader(http.StatusBadRequest) + return + } + if len(signReq.Csr) == 0 { + t.Logf("empty CSR") + resp.WriteHeader(http.StatusBadRequest) + return + } + resp.Header().Set("Content-Type", "application/json") + resp.Write([]byte(vaultServer.vaultSignResp)) + break + default: + t.Logf("The request contains invalid path: %v", req.URL) + resp.WriteHeader(http.StatusNotFound) + } + }) + + if tls { + vaultServer.httpServer = httptest.NewTLSServer(handler) + } else { + vaultServer.httpServer = httptest.NewServer(handler) + } + + t.Logf("Serving Vault at: %v", vaultServer.httpServer.URL) + + return vaultServer +} diff --git a/security/pkg/nodeagent/sds/server.go b/security/pkg/nodeagent/sds/server.go index 9562742a9b4d..fe343a4abaa7 100644 --- a/security/pkg/nodeagent/sds/server.go +++ b/security/pkg/nodeagent/sds/server.go @@ -62,6 +62,18 @@ type Options struct { // PluginNames is plugins' name for certain authentication provider. PluginNames []string + + // The Vault CA address. + VaultAddress string + + // The Vault auth path. + VaultAuthPath string + + // The Vault role. + VaultRole string + + // The Vault sign CSR path. + VaultSignCsrPath string } // Server is the gPRC server that exposes SDS through UDS. diff --git a/security/pkg/nodeagent/secretfetcher/secretfetcher.go b/security/pkg/nodeagent/secretfetcher/secretfetcher.go index 42ba6861c0de..3602789d5f18 100644 --- a/security/pkg/nodeagent/secretfetcher/secretfetcher.go +++ b/security/pkg/nodeagent/secretfetcher/secretfetcher.go @@ -92,7 +92,8 @@ func createClientset() kubernetes.Interface { } // NewSecretFetcher returns a pointer to a newly constructed SecretFetcher instance. -func NewSecretFetcher(ingressGatewayAgent bool, endpoint, CAProviderName string, tlsFlag bool, clientSet kubernetes.Interface) (*SecretFetcher, error) { +func NewSecretFetcher(ingressGatewayAgent bool, endpoint, CAProviderName string, tlsFlag bool, + clientSet kubernetes.Interface, vaultAddr, vaultRole, vaultAuthPath, vaultSignCsrPath string) (*SecretFetcher, error) { ret := &SecretFetcher{} if ingressGatewayAgent { @@ -119,7 +120,7 @@ func NewSecretFetcher(ingressGatewayAgent bool, endpoint, CAProviderName string, // TODO(jimmycyj): add handler for UpdateFunc. }) } else { - caClient, err := ca.NewCAClient(endpoint, CAProviderName, tlsFlag) + caClient, err := ca.NewCAClient(endpoint, CAProviderName, tlsFlag, vaultAddr, vaultRole, vaultAuthPath, vaultSignCsrPath) if err != nil { log.Errorf("failed to create caClient: %v", err) return ret, fmt.Errorf("failed to create caClient") diff --git a/security/pkg/nodeagent/secretfetcher/secretfetcher_test.go b/security/pkg/nodeagent/secretfetcher/secretfetcher_test.go index bc3ba4056f39..9edb1842eaad 100644 --- a/security/pkg/nodeagent/secretfetcher/secretfetcher_test.go +++ b/security/pkg/nodeagent/secretfetcher/secretfetcher_test.go @@ -44,7 +44,8 @@ var ( // find secret by name, and delete secret by name. func TestSecretFetcher(t *testing.T) { client := fake.NewSimpleClientset() - gSecretFetcher, err := NewSecretFetcher(true, "", "", false, client) + gSecretFetcher, err := NewSecretFetcher(true, "", "", false, + client, "", "", "", "") if err != nil { t.Errorf("failed to create secretFetcher for gateway proxy: %v", err) }