Skip to content

Commit

Permalink
Manual backport of #19095
Browse files Browse the repository at this point in the history
  • Loading branch information
cthain committed Oct 11, 2023
1 parent 737213f commit 16c1f2a
Show file tree
Hide file tree
Showing 4 changed files with 315 additions and 107 deletions.
3 changes: 3 additions & 0 deletions .changelog/19095.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
ca: ensure Vault CA provider respects Vault Enterprise namespace configuration.
```
141 changes: 93 additions & 48 deletions agent/connect/ca/provider_vault.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package ca

import (
Expand All @@ -8,7 +11,6 @@ import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"

Expand Down Expand Up @@ -96,7 +98,7 @@ func vaultTLSConfig(config *structs.VaultCAProviderConfig) *vaultapi.TLSConfig {
// Configure sets up the provider using the given configuration.
// Configure supports being called multiple times to re-configure the provider.
func (v *VaultProvider) Configure(cfg ProviderConfig) error {
config, err := ParseVaultCAConfig(cfg.RawConfig)
config, err := ParseVaultCAConfig(cfg.RawConfig, v.isPrimary)
if err != nil {
return err
}
Expand Down Expand Up @@ -187,11 +189,11 @@ func (v *VaultProvider) Configure(cfg ProviderConfig) error {
}

func (v *VaultProvider) ValidateConfigUpdate(prevRaw, nextRaw map[string]interface{}) error {
prev, err := ParseVaultCAConfig(prevRaw)
prev, err := ParseVaultCAConfig(prevRaw, v.isPrimary)
if err != nil {
return fmt.Errorf("failed to parse existing CA config: %w", err)
}
next, err := ParseVaultCAConfig(nextRaw)
next, err := ParseVaultCAConfig(nextRaw, v.isPrimary)
if err != nil {
return fmt.Errorf("failed to parse new CA config: %w", err)
}
Expand Down Expand Up @@ -274,10 +276,10 @@ func (v *VaultProvider) State() (map[string]string, error) {
return nil, nil
}

// GenerateRoot mounts and initializes a new root PKI backend if needed.
func (v *VaultProvider) GenerateRoot() (RootResult, error) {
// GenerateCAChain mounts and initializes a new root PKI backend if needed.
func (v *VaultProvider) GenerateCAChain() (string, error) {
if !v.isPrimary {
return RootResult{}, fmt.Errorf("provider is not the root certificate authority")
return "", fmt.Errorf("provider is not the root certificate authority")
}

// Set up the root PKI backend if necessary.
Expand All @@ -297,15 +299,15 @@ func (v *VaultProvider) GenerateRoot() (RootResult, error) {
},
})
if err != nil {
return RootResult{}, fmt.Errorf("failed to mount root CA backend: %w", err)
return "", fmt.Errorf("failed to mount root CA backend: %w", err)
}

// We want to initialize afterwards
fallthrough
case ErrBackendNotInitialized:
uid, err := connect.CompactUID()
if err != nil {
return RootResult{}, err
return "", err
}
resp, err := v.writeNamespaced(v.config.RootPKINamespace, v.config.RootPKIPath+"root/generate/internal", map[string]interface{}{
"common_name": connect.CACN("vault", uid, v.clusterID, v.isPrimary),
Expand All @@ -314,23 +316,23 @@ func (v *VaultProvider) GenerateRoot() (RootResult, error) {
"key_bits": v.config.PrivateKeyBits,
})
if err != nil {
return RootResult{}, fmt.Errorf("failed to initialize root CA: %w", err)
return "", fmt.Errorf("failed to initialize root CA: %w", err)
}
var ok bool
rootPEM, ok = resp.Data["certificate"].(string)
if !ok {
return RootResult{}, fmt.Errorf("unexpected response from Vault: %v", resp.Data["certificate"])
return "", fmt.Errorf("unexpected response from Vault: %v", resp.Data["certificate"])
}

default:
if err != nil {
return RootResult{}, fmt.Errorf("unexpected error while setting root PKI backend: %w", err)
return "", fmt.Errorf("unexpected error while setting root PKI backend: %w", err)
}
}

rootChain, err := v.getCAChain(v.config.RootPKINamespace, v.config.RootPKIPath)
if err != nil {
return RootResult{}, err
return "", err
}

// Workaround for a bug in the Vault PKI API.
Expand All @@ -339,7 +341,7 @@ func (v *VaultProvider) GenerateRoot() (RootResult, error) {
rootChain = rootPEM
}

return RootResult{PEM: rootChain}, nil
return rootChain, nil
}

// GenerateIntermediateCSR creates a private key and generates a CSR
Expand Down Expand Up @@ -422,6 +424,9 @@ func (v *VaultProvider) setupIntermediatePKIPath() error {
"require_cn": false,
})

// enable auto-tidy with tidy_expired_issuers
v.autotidyIssuers(v.config.IntermediatePKIPath)

return err
}

Expand Down Expand Up @@ -493,7 +498,7 @@ func (v *VaultProvider) SetIntermediate(intermediatePEM, rootPEM, keyId string)
}

// ActiveIntermediate returns the current intermediate certificate.
func (v *VaultProvider) ActiveIntermediate() (string, error) {
func (v *VaultProvider) ActiveLeafSigningCert() (string, error) {
cert, err := v.getCA(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath)

// This error is expected when calling initializeSecondaryCA for the
Expand All @@ -513,7 +518,7 @@ func (v *VaultProvider) ActiveIntermediate() (string, error) {
// because the endpoint only returns the raw PEM contents of the CA cert
// and not the typical format of the secrets endpoints.
func (v *VaultProvider) getCA(namespace, path string) (string, error) {
resp, err := v.client.WithNamespace(namespace).Logical().ReadRaw(path + "/ca/pem")
resp, err := v.client.WithNamespace(v.getNamespace(namespace)).Logical().ReadRaw(path + "/ca/pem")
if resp != nil {
defer resp.Body.Close()
}
Expand All @@ -539,7 +544,7 @@ func (v *VaultProvider) getCA(namespace, path string) (string, error) {

// TODO: refactor to remove duplication with getCA
func (v *VaultProvider) getCAChain(namespace, path string) (string, error) {
resp, err := v.client.WithNamespace(namespace).Logical().ReadRaw(path + "/ca_chain")
resp, err := v.client.WithNamespace(v.getNamespace(namespace)).Logical().ReadRaw(path + "/ca_chain")
if resp != nil {
defer resp.Body.Close()
}
Expand All @@ -559,10 +564,10 @@ func (v *VaultProvider) getCAChain(namespace, path string) (string, error) {
return root, nil
}

// GenerateIntermediate mounts the configured intermediate PKI backend if
// GenerateLeafSigningCert mounts the configured intermediate PKI backend if
// necessary, then generates and signs a new CA CSR using the root PKI backend
// and updates the intermediate backend to use that new certificate.
func (v *VaultProvider) GenerateIntermediate() (string, error) {
func (v *VaultProvider) GenerateLeafSigningCert() (string, error) {
csr, keyId, err := v.generateIntermediateCSR()
if err != nil {
return "", err
Expand Down Expand Up @@ -598,7 +603,7 @@ func (v *VaultProvider) GenerateIntermediate() (string, error) {
}
}

return v.ActiveIntermediate()
return v.ActiveLeafSigningCert()
}

// setDefaultIntermediateIssuer updates the default issuer for
Expand Down Expand Up @@ -816,7 +821,7 @@ func (v *VaultProvider) Cleanup(providerTypeChange bool, otherConfig map[string]
v.Stop()

if !providerTypeChange {
newConfig, err := ParseVaultCAConfig(otherConfig)
newConfig, err := ParseVaultCAConfig(otherConfig, v.isPrimary)
if err != nil {
return err
}
Expand Down Expand Up @@ -844,34 +849,72 @@ func (v *VaultProvider) Stop() {
v.stopWatcher()
}

func (v *VaultProvider) PrimaryUsesIntermediate() {}

// We use raw path here
func (v *VaultProvider) mountNamespaced(namespace, path string, mountInfo *vaultapi.MountInput) error {
return v.client.WithNamespace(namespace).Sys().Mount(path, mountInfo)
return v.client.WithNamespace(v.getNamespace(namespace)).Sys().Mount(path, mountInfo)
}

func (v *VaultProvider) tuneMountNamespaced(namespace, path string, mountConfig *vaultapi.MountConfigInput) error {
return v.client.WithNamespace(namespace).Sys().TuneMount(path, *mountConfig)
return v.client.WithNamespace(v.getNamespace(namespace)).Sys().TuneMount(path, *mountConfig)
}

func (v *VaultProvider) unmountNamespaced(namespace, path string) error {
return v.client.WithNamespace(namespace).Sys().Unmount(path)
return v.client.WithNamespace(v.getNamespace(namespace)).Sys().Unmount(path)
}

func (v *VaultProvider) readNamespaced(namespace string, resource string) (*vaultapi.Secret, error) {
return v.client.WithNamespace(namespace).Logical().Read(resource)
return v.client.WithNamespace(v.getNamespace(namespace)).Logical().Read(resource)
}

func (v *VaultProvider) writeNamespaced(namespace string, resource string, data map[string]interface{}) (*vaultapi.Secret, error) {
return v.client.WithNamespace(namespace).Logical().Write(resource, data)
return v.client.WithNamespace(v.getNamespace(namespace)).Logical().Write(resource, data)
}

func (v *VaultProvider) deleteNamespaced(namespace string, resource string) (*vaultapi.Secret, error) {
return v.client.WithNamespace(namespace).Logical().Delete(resource)
return v.client.WithNamespace(v.getNamespace(namespace)).Logical().Delete(resource)
}

func (v *VaultProvider) getNamespace(namespace string) string {
if namespace != "" {
return namespace
}
return v.baseNamespace
}

// autotidyIssuers sets Vault's auto-tidy to remove expired issuers
// Returns a boolean on success for testing (as there is no post-facto way of
// checking if it is set). Logs at info level on failure to set and why,
// returning the log message for test purposes as well.
func (v *VaultProvider) autotidyIssuers(path string) (bool, string) {
s, err := v.client.Logical().Write(path+"/config/auto-tidy",
map[string]interface{}{
"enabled": true,
"tidy_expired_issuers": true,
})
var errStr string
if err != nil {
errStr = err.Error()
switch {
case strings.Contains(errStr, "404"):
errStr = "vault versions < 1.12 don't support auto-tidy"
case strings.Contains(errStr, "400"):
errStr = "vault versions < 1.13 don't support the tidy_expired_issuers field"
case strings.Contains(errStr, "403"):
errStr = "permission denied on auto-tidy path in vault"
}
v.logger.Info("Unable to enable Vault's auto-tidy feature for expired issuers", "reason", errStr, "path", path)
}
// return values for tests
tidySet := false
if s != nil {
if tei, ok := s.Data["tidy_expired_issuers"]; ok {
tidySet, _ = tei.(bool)
}
}
return tidySet, errStr
}

func ParseVaultCAConfig(raw map[string]interface{}) (*structs.VaultCAProviderConfig, error) {
func ParseVaultCAConfig(raw map[string]interface{}, isPrimary bool) (*structs.VaultCAProviderConfig, error) {
config := structs.VaultCAProviderConfig{
CommonCAProviderConfig: defaultCommonConfig(),
}
Expand Down Expand Up @@ -902,10 +945,10 @@ func ParseVaultCAConfig(raw map[string]interface{}) (*structs.VaultCAProviderCon
return nil, fmt.Errorf("only one of Vault token or Vault auth method can be provided, but not both")
}

if config.RootPKIPath == "" {
if isPrimary && config.RootPKIPath == "" {
return nil, fmt.Errorf("must provide a valid path to a root PKI backend")
}
if !strings.HasSuffix(config.RootPKIPath, "/") {
if config.RootPKIPath != "" && !strings.HasSuffix(config.RootPKIPath, "/") {
config.RootPKIPath += "/"
}

Expand Down Expand Up @@ -940,6 +983,14 @@ func vaultLogin(client *vaultapi.Client, authMethod *structs.VaultAuthMethod) (*
return resp, nil
}

// Note the authMethod's parameters (Params) is populated from a freeform map
// in the configuration where they could hardcode values to be passed directly
// to the `auth/*/login` endpoint. Each auth method's authentication code
// needs to handle two cases:
// - The legacy case (which should be deprecated) where the user has
// hardcoded login values directly (eg. a `jwt` string)
// - The case where they use the configuration option used in the
// vault agent's auth methods.
func configureVaultAuthMethod(authMethod *structs.VaultAuthMethod) (VaultAuthenticator, error) {
if authMethod.MountPath == "" {
authMethod.MountPath = authMethod.Type
Expand All @@ -949,20 +1000,18 @@ func configureVaultAuthMethod(authMethod *structs.VaultAuthMethod) (VaultAuthent
switch authMethod.Type {
case VaultAuthMethodTypeAWS:
return NewAWSAuthClient(authMethod), nil
case VaultAuthMethodTypeAzure:
return NewAzureAuthClient(authMethod)
case VaultAuthMethodTypeGCP:
return NewGCPAuthClient(authMethod)
case VaultAuthMethodTypeJWT:
return NewJwtAuthClient(authMethod)
case VaultAuthMethodTypeAppRole:
return NewAppRoleAuthClient(authMethod)
case VaultAuthMethodTypeAliCloud:
return NewAliCloudAuthClient(authMethod)
case VaultAuthMethodTypeKubernetes:
// For the Kubernetes Auth method, we will try to read the JWT token
// from the default service account file location if jwt was not provided.
if jwt, ok := authMethod.Params["jwt"]; !ok || jwt == "" {
serviceAccountToken, err := os.ReadFile(defaultK8SServiceAccountTokenPath)
if err != nil {
return nil, err
}

authMethod.Params["jwt"] = string(serviceAccountToken)
}
return NewVaultAPIAuthClient(authMethod, loginPath), nil
return NewK8sAuthClient(authMethod)
// These auth methods require a username for the login API path.
case VaultAuthMethodTypeLDAP, VaultAuthMethodTypeUserpass, VaultAuthMethodTypeOkta, VaultAuthMethodTypeRadius:
// Get username from the params.
Expand All @@ -984,12 +1033,8 @@ func configureVaultAuthMethod(authMethod *structs.VaultAuthMethod) (VaultAuthent
return nil, fmt.Errorf("'token' auth method is not supported via auth method configuration; " +
"please provide the token with the 'token' parameter in the CA configuration")
// The rest of the auth methods use auth/<auth method path> login API path.
case VaultAuthMethodTypeAliCloud,
VaultAuthMethodTypeAppRole,
VaultAuthMethodTypeAzure,
VaultAuthMethodTypeCloudFoundry,
case VaultAuthMethodTypeCloudFoundry,
VaultAuthMethodTypeGitHub,
VaultAuthMethodTypeJWT,
VaultAuthMethodTypeKerberos,
VaultAuthMethodTypeTLS:
return NewVaultAPIAuthClient(authMethod, loginPath), nil
Expand Down
Loading

0 comments on commit 16c1f2a

Please sign in to comment.