Skip to content

Commit

Permalink
Istio CA integrates with Vault CA providers (#10638)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
lei-tang authored and istio-testing committed Jan 1, 2019
1 parent fe94f7f commit f97a89f
Show file tree
Hide file tree
Showing 9 changed files with 677 additions and 8 deletions.
27 changes: 25 additions & 2 deletions security/cmd/node_agent_k8s/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down
3 changes: 2 additions & 1 deletion security/pkg/nodeagent/cache/secretcache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
7 changes: 6 additions & 1 deletion security/pkg/nodeagent/caclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion security/pkg/nodeagent/caclient/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
226 changes: 226 additions & 0 deletions security/pkg/nodeagent/caclient/providers/vault/client.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit f97a89f

Please sign in to comment.