Skip to content

Commit

Permalink
make kubernetes_ca_cert optional and allow TLS client to use the host…
Browse files Browse the repository at this point in the history
…'s root CA set on no CA certificates provided (#238)

- allow TLS client to use the host's root CA set when no CA certificates are provided and `disable_local_ca_jwt` is true if running Vault in a Kubernetes pod
- validate the configuration's provided CA PEM bundle
---------

Co-authored-by: Ben Ash <[email protected]>
Co-authored-by: Ben Ash <[email protected]>
  • Loading branch information
3 people authored Mar 27, 2024
1 parent 26978d2 commit da08a6a
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 20 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
* `k8s.io/api` v0.29.1 -> v0.29.2
* `k8s.io/apimachinery` v0.29.1 -> v0.29.2

### Improvements

* Allow TLS client to use the host's root CA set when no CA certificates are provided and
`disable_local_ca_jwt` is true if running Vault in a Kubernetes pod. Additionally, validate the
configuration's provided CA PEM bundle. [GH-238](https://github.com/hashicorp/vault-plugin-auth-kubernetes/pull/238)

## 0.18.0 (Feb 2, 2024)

### Changes
Expand Down
43 changes: 32 additions & 11 deletions backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,16 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend,
return b, nil
}

var getDefaultHTTPClient = cleanhttp.DefaultPooledClient
// getDefaultTLSConfig returns a http.Client with the supported, default
// tls.Config from getDefaultTLSConfig() set on the
// cleanhttp.DefaultPooledTransport() http.Transport.
func getDefaultHTTPClient() *http.Client {
transport := cleanhttp.DefaultPooledTransport()
transport.TLSClientConfig = getDefaultTLSConfig()
return &http.Client{
Transport: transport,
}
}

func getDefaultTLSConfig() *tls.Config {
return &tls.Config{
Expand Down Expand Up @@ -433,21 +442,24 @@ func (b *kubeAuthBackend) updateTLSConfig(config *kubeConfig) error {
caCertBytes = []byte(data)
}

certPool := x509.NewCertPool()
if len(caCertBytes) > 0 {
var certPool *x509.CertPool
if len(caCertBytes) == 0 {
// since the CA chain is not configured, we use the system's cert pool.
var err error
certPool, err = x509.SystemCertPool()
if err != nil {
return err
}
} else {
// since we have a CA chain configured, we create a new x509.CertPool with its
// contents.
certPool = x509.NewCertPool()
if ok := certPool.AppendCertsFromPEM(caCertBytes); !ok {
b.Logger().Warn("Configured CA PEM data contains no valid certificates, TLS verification will fail")
}
} else {
// provide an empty certPool
b.Logger().Warn("No CA certificates configured, TLS verification will fail")
// TODO: think about supporting host root CA certificates via a configuration toggle,
// in which case RootCAs should be set to nil
}

// only refresh the Root CAs if they have changed since the last full update.
if !b.tlsConfig.RootCAs.Equal(certPool) {
b.Logger().Trace("Root CA certificate pool has changed, updating the client's transport")
setTLSClientConfig := func() error {
transport, ok := b.httpClient.Transport.(*http.Transport)
if !ok {
// should never happen
Expand All @@ -456,6 +468,15 @@ func (b *kubeAuthBackend) updateTLSConfig(config *kubeConfig) error {

b.tlsConfig.RootCAs = certPool
transport.TLSClientConfig = b.tlsConfig
return nil
}

// only refresh the Root CAs if they have changed since the last full update.
if b.tlsConfig.RootCAs == nil {
return setTLSClientConfig()
} else if !b.tlsConfig.RootCAs.Equal(certPool) {
b.Logger().Trace("Root CA certificate pool has changed, updating the client's transport")
return setTLSClientConfig()
} else {
b.Logger().Trace("Root CA certificate pool is unchanged, no update required")
}
Expand Down
22 changes: 22 additions & 0 deletions backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ func Test_kubeAuthBackend_updateTLSConfig(t *testing.T) {
localCertPool := getTestCertPool(t, testLocalCACert)
otherCertPool := getTestCertPool(t, testOtherCACert)

systemCertPool, err := x509.SystemCertPool()
if err != nil {
t.Fatal(err)
}

type testConfig struct {
config *kubeConfig
expectTLSConfig *tls.Config
Expand Down Expand Up @@ -173,6 +178,23 @@ func Test_kubeAuthBackend_updateTLSConfig(t *testing.T) {
},
wantErr: false,
},
{
name: "ca-certs-not-set",
httpClient: getDefaultHTTPClient(),
tlsConfig: getDefaultTLSConfig(),
configs: []testConfig{
{
config: &kubeConfig{
DisableLocalCAJwt: true,
},
expectTLSConfig: &tls.Config{
MinVersion: minTLSVersion,
RootCAs: systemCertPool,
},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
14 changes: 14 additions & 0 deletions common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,19 @@ pEDcN/HcXM47TP7XgWW0rfQli3RucuqMV7LHvvpiGIWwfutrK9g7Py91W2JbQCA0
D14XDzgsruCwlWAP1FMvLMIPhSknpIJd9Xql+0/Ae1yl9f3Uamj3mDtBKg8/U5nJ
0wU=
-----END CERTIFICATE-----
`

testInvalidCACert = `
-----BEGIN CERTIFICATE-----
MIIC5zCCAc+gAwIBAgIBATANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwptaW5p
a3ViZUNBMB4XDTE3MDgxMDIzMTQ1NVoXDTI3MDgwODIzMTQ1NVowFTETMBEGA1UE
AxMKbWluaWt1YmVDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN8d
w2p/KXRkm+vzOO0eT1vYBWP7fKsnng9/g5nnXAJlt9NxpOSolRcyItm/04R0E1jx
jpgsdzkybc+QU5ZiszOYN833/D5hCNVAABVivpDd2P8wVKXN46cB99e24etUVBqG
5aR0Ku3IBsJjCN9efhF+XRCA2gy/KaXMdKJhHfdtc8hCr7G9+2wO2G58FLmIfEyH
owviOGt0BSnCtMpsA8ZgGQyfqgSd5u466aCv6oj0MyzsMnfS38niM53Rlv4IY6ol
taYbWXtCNndQ2S687qE0qTCxhE95Bm2Nfkqct4R1798sJz83xNv8hALvxr/vPK/J
2XkIm3oo3YKG4n/CHXcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgKkMB0GA1UdJQQW
MBQGCCsGAQUFBwMCBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
`
)
36 changes: 29 additions & 7 deletions path_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ func pathConfig(b *kubeAuthBackend) *framework.Path {
Type: framework.TypeString,
Description: "Host must be a host string, a host:port pair, or a URL to the base of the Kubernetes API server.",
},

"kubernetes_ca_cert": {
Type: framework.TypeString,
Description: "PEM encoded CA cert for use by the TLS client used to talk with the API.",
Type: framework.TypeString,
Description: `Optional PEM encoded CA cert for use by the TLS client used to talk with the API.
If it is not set and disable_local_ca_jwt is true, the system's trusted CA certificate pool will be used.`,
DisplayAttrs: &framework.DisplayAttributes{
Name: "Kubernetes CA Certificate",
},
Expand Down Expand Up @@ -159,16 +159,38 @@ func (b *kubeAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Requ
return logical.ErrorResponse("no host provided"), nil
}

disableLocalJWT := data.Get("disable_local_ca_jwt").(bool)
disableLocalCAJwt := data.Get("disable_local_ca_jwt").(bool)
pemList := data.Get("pem_keys").([]string)
caCert := data.Get("kubernetes_ca_cert").(string)
issuer := data.Get("issuer").(string)
disableIssValidation := data.Get("disable_iss_validation").(bool)
tokenReviewer := data.Get("token_reviewer_jwt").(string)
useAnnotationsAsAliasMetadata := data.Get("use_annotations_as_alias_metadata").(bool)

if disableLocalJWT && caCert == "" {
return logical.ErrorResponse("kubernetes_ca_cert must be given when disable_local_ca_jwt is true"), nil
// hasCerts returns true if caCert contains at least one valid certificate. It
// does not check if any of the certificates from caCert are CAs, although that
// might be something that we want in the future.
hasCerts := func(certBundle string) bool {
var b *pem.Block
rest := []byte(certBundle)
for {
b, rest = pem.Decode(rest)
if b == nil {
break
}

if pem.EncodeToMemory(b) != nil {
return true
}
}

return false
}

if caCert != "" && !hasCerts(caCert) {
return logical.ErrorResponse(
"The provided CA PEM data contains no valid certificates",
), nil
}

config := &kubeConfig{
Expand All @@ -179,7 +201,7 @@ func (b *kubeAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Requ
TokenReviewerJWT: tokenReviewer,
Issuer: issuer,
DisableISSValidation: disableIssValidation,
DisableLocalCAJwt: disableLocalJWT,
DisableLocalCAJwt: disableLocalCAJwt,
UseAnnotationsAsAliasMetadata: useAnnotationsAsAliasMetadata,
}

Expand Down
58 changes: 56 additions & 2 deletions path_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ func TestConfig(t *testing.T) {
t.Fatalf("got unexpected error: %v", resp.Error())
}

// test invalid cert
// test invalid pem_keys
data = map[string]interface{}{
"pem_keys": "bad",
"kubernetes_host": "host",
Expand All @@ -195,6 +195,27 @@ func TestConfig(t *testing.T) {
t.Fatalf("got unexpected error: %v", resp.Error())
}

// test invalid kubernetes_ca_cert
data = map[string]interface{}{
"kubernetes_ca_cert": testInvalidCACert,
"kubernetes_host": "host",
}

req = &logical.Request{
Operation: logical.UpdateOperation,
Path: configPath,
Storage: storage,
Data: data,
}

resp, err = b.HandleRequest(context.Background(), req)
if resp == nil || !resp.IsError() {
t.Fatal("expected error")
}
if resp.Error().Error() != "The provided CA PEM data contains no valid certificates" {
t.Fatalf("got unexpected error: %v", resp.Error())
}

// Test success with no certs
data = map[string]interface{}{
"kubernetes_host": "host",
Expand Down Expand Up @@ -458,7 +479,7 @@ func TestConfig_LocalCaJWT(t *testing.T) {
DisableLocalCAJwt: false,
},
},
"CA and disable local default": {
"disable local default, CA set": {
config: map[string]interface{}{
"kubernetes_host": "host",
"kubernetes_ca_cert": testCACert,
Expand All @@ -474,6 +495,39 @@ func TestConfig_LocalCaJWT(t *testing.T) {
DisableLocalCAJwt: true,
},
},
"disable local default, JWT set": {
config: map[string]interface{}{
"kubernetes_host": "host",
"token_reviewer_jwt": jwtGoodDataToken,
"disable_local_ca_jwt": true,
},
setupInClusterFiles: true,
expected: &kubeConfig{
PublicKeys: []crypto.PublicKey{},
PEMKeys: []string{},
Host: "host",
CACert: "",
TokenReviewerJWT: jwtGoodDataToken,
DisableISSValidation: true,
DisableLocalCAJwt: true,
},
},
"disable local default, no CA or JWT": {
config: map[string]interface{}{
"kubernetes_host": "host",
"disable_local_ca_jwt": true,
},
setupInClusterFiles: true,
expected: &kubeConfig{
PublicKeys: []crypto.PublicKey{},
PEMKeys: []string{},
Host: "host",
CACert: "",
TokenReviewerJWT: "",
DisableISSValidation: true,
DisableLocalCAJwt: true,
},
},
}

for name, tc := range testCases {
Expand Down

0 comments on commit da08a6a

Please sign in to comment.