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

Istio Citadel Agent integrates with Vault CA providers #10638

Merged
merged 10 commits into from
Jan 1, 2019
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't pass vault specific params in this func, better to get those env variable in vault client.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting the CA provider inside vault client has pros and cons.

  • Pros: less perimeters.
  • Cons: CA provider is not exposed at Node Agent. The code is difficult to understand because how the CA provider is configured is hidden somewhere.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those variables are CA provider specific, since we already passed CAProviderName as input param, setting those variables should be pushed to lower layer(each plugin). let's revisit this after e2e works.

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"
lei-tang marked this conversation as resolved.
Show resolved Hide resolved

"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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

csr doesn't contain identity info(which is only available in saToken), both google ca and citadel extract identity from saToken, will vault support the same logic ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is up to Vault provider. Will sync up offline on this comment.

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
}