github.com/outbrain/consul@v1.4.5/agent/connect/ca/provider_vault.go (about)

     1  package ca
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/x509"
     6  	"encoding/pem"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"strings"
    11  
    12  	"github.com/hashicorp/consul/agent/connect"
    13  	"github.com/hashicorp/consul/agent/structs"
    14  	"github.com/hashicorp/go-uuid"
    15  	vaultapi "github.com/hashicorp/vault/api"
    16  	"github.com/mitchellh/mapstructure"
    17  )
    18  
    19  const VaultCALeafCertRole = "leaf-cert"
    20  
    21  var ErrBackendNotMounted = fmt.Errorf("backend not mounted")
    22  var ErrBackendNotInitialized = fmt.Errorf("backend not initialized")
    23  
    24  type VaultProvider struct {
    25  	config    *structs.VaultCAProviderConfig
    26  	client    *vaultapi.Client
    27  	isRoot    bool
    28  	clusterId string
    29  }
    30  
    31  func vaultTLSConfig(config *structs.VaultCAProviderConfig) *vaultapi.TLSConfig {
    32  	return &vaultapi.TLSConfig{
    33  		CACert:        config.CAFile,
    34  		CAPath:        config.CAPath,
    35  		ClientCert:    config.CertFile,
    36  		ClientKey:     config.KeyFile,
    37  		Insecure:      config.TLSSkipVerify,
    38  		TLSServerName: config.TLSServerName,
    39  	}
    40  }
    41  
    42  // Configure sets up the provider using the given configuration.
    43  func (v *VaultProvider) Configure(clusterId string, isRoot bool, rawConfig map[string]interface{}) error {
    44  	config, err := ParseVaultCAConfig(rawConfig)
    45  	if err != nil {
    46  		return err
    47  	}
    48  
    49  	clientConf := &vaultapi.Config{
    50  		Address: config.Address,
    51  	}
    52  	err = clientConf.ConfigureTLS(vaultTLSConfig(config))
    53  	if err != nil {
    54  		return err
    55  	}
    56  	client, err := vaultapi.NewClient(clientConf)
    57  	if err != nil {
    58  		return err
    59  	}
    60  
    61  	client.SetToken(config.Token)
    62  	v.config = config
    63  	v.client = client
    64  	v.isRoot = isRoot
    65  	v.clusterId = clusterId
    66  
    67  	return nil
    68  }
    69  
    70  // ActiveRoot returns the active root CA certificate.
    71  func (v *VaultProvider) ActiveRoot() (string, error) {
    72  	return v.getCA(v.config.RootPKIPath)
    73  }
    74  
    75  // GenerateRoot mounts and initializes a new root PKI backend if needed.
    76  func (v *VaultProvider) GenerateRoot() error {
    77  	if !v.isRoot {
    78  		return fmt.Errorf("provider is not the root certificate authority")
    79  	}
    80  
    81  	// Set up the root PKI backend if necessary.
    82  	_, err := v.ActiveRoot()
    83  	switch err {
    84  	case ErrBackendNotMounted:
    85  		err := v.client.Sys().Mount(v.config.RootPKIPath, &vaultapi.MountInput{
    86  			Type:        "pki",
    87  			Description: "root CA backend for Consul Connect",
    88  			Config: vaultapi.MountConfigInput{
    89  				MaxLeaseTTL: "8760h",
    90  			},
    91  		})
    92  
    93  		if err != nil {
    94  			return err
    95  		}
    96  
    97  		fallthrough
    98  	case ErrBackendNotInitialized:
    99  		spiffeID := connect.SpiffeIDSigning{ClusterID: v.clusterId, Domain: "consul"}
   100  		uuid, err := uuid.GenerateUUID()
   101  		if err != nil {
   102  			return err
   103  		}
   104  		_, err = v.client.Logical().Write(v.config.RootPKIPath+"root/generate/internal", map[string]interface{}{
   105  			"common_name": fmt.Sprintf("Vault CA Root Authority %s", uuid),
   106  			"uri_sans":    spiffeID.URI().String(),
   107  		})
   108  		if err != nil {
   109  			return err
   110  		}
   111  	default:
   112  		if err != nil {
   113  			return err
   114  		}
   115  	}
   116  
   117  	return nil
   118  }
   119  
   120  // GenerateIntermediateCSR creates a private key and generates a CSR
   121  // for another datacenter's root to sign, overwriting the intermediate backend
   122  // in the process.
   123  func (v *VaultProvider) GenerateIntermediateCSR() (string, error) {
   124  	if v.isRoot {
   125  		return "", fmt.Errorf("provider is the root certificate authority, " +
   126  			"cannot generate an intermediate CSR")
   127  	}
   128  
   129  	return v.generateIntermediateCSR()
   130  }
   131  
   132  func (v *VaultProvider) generateIntermediateCSR() (string, error) {
   133  	mounts, err := v.client.Sys().ListMounts()
   134  	if err != nil {
   135  		return "", err
   136  	}
   137  
   138  	// Mount the backend if it isn't mounted already.
   139  	if _, ok := mounts[v.config.IntermediatePKIPath]; !ok {
   140  		err := v.client.Sys().Mount(v.config.IntermediatePKIPath, &vaultapi.MountInput{
   141  			Type:        "pki",
   142  			Description: "intermediate CA backend for Consul Connect",
   143  			Config: vaultapi.MountConfigInput{
   144  				MaxLeaseTTL: "2160h",
   145  			},
   146  		})
   147  
   148  		if err != nil {
   149  			return "", err
   150  		}
   151  	}
   152  
   153  	// Create the role for issuing leaf certs if it doesn't exist yet
   154  	rolePath := v.config.IntermediatePKIPath + "roles/" + VaultCALeafCertRole
   155  	role, err := v.client.Logical().Read(rolePath)
   156  	if err != nil {
   157  		return "", err
   158  	}
   159  	spiffeID := connect.SpiffeIDSigning{ClusterID: v.clusterId, Domain: "consul"}
   160  	if role == nil {
   161  		_, err := v.client.Logical().Write(rolePath, map[string]interface{}{
   162  			"allow_any_name":   true,
   163  			"allowed_uri_sans": "spiffe://*",
   164  			"key_type":         "any",
   165  			"max_ttl":          v.config.LeafCertTTL.String(),
   166  			"no_store":         true,
   167  			"require_cn":       false,
   168  		})
   169  		if err != nil {
   170  			return "", err
   171  		}
   172  	}
   173  
   174  	// Generate a new intermediate CSR for the root to sign.
   175  	data, err := v.client.Logical().Write(v.config.IntermediatePKIPath+"intermediate/generate/internal", map[string]interface{}{
   176  		"common_name": "Vault CA Intermediate Authority",
   177  		"key_bits":    224,
   178  		"key_type":    "ec",
   179  		"uri_sans":    spiffeID.URI().String(),
   180  	})
   181  	if err != nil {
   182  		return "", err
   183  	}
   184  	if data == nil || data.Data["csr"] == "" {
   185  		return "", fmt.Errorf("got empty value when generating intermediate CSR")
   186  	}
   187  	csr, ok := data.Data["csr"].(string)
   188  	if !ok {
   189  		return "", fmt.Errorf("csr result is not a string")
   190  	}
   191  
   192  	return csr, nil
   193  }
   194  
   195  // SetIntermediate writes the incoming intermediate and root certificates to the
   196  // intermediate backend (as a chain).
   197  func (v *VaultProvider) SetIntermediate(intermediatePEM, rootPEM string) error {
   198  	if v.isRoot {
   199  		return fmt.Errorf("cannot set an intermediate using another root in the primary datacenter")
   200  	}
   201  
   202  	_, err := v.client.Logical().Write(v.config.IntermediatePKIPath+"intermediate/set-signed", map[string]interface{}{
   203  		"certificate": fmt.Sprintf("%s\n%s", intermediatePEM, rootPEM),
   204  	})
   205  	if err != nil {
   206  		return err
   207  	}
   208  
   209  	return nil
   210  }
   211  
   212  // ActiveIntermediate returns the current intermediate certificate.
   213  func (v *VaultProvider) ActiveIntermediate() (string, error) {
   214  	return v.getCA(v.config.IntermediatePKIPath)
   215  }
   216  
   217  // getCA returns the raw CA cert for the given endpoint if there is one.
   218  // We have to use the raw NewRequest call here instead of Logical().Read
   219  // because the endpoint only returns the raw PEM contents of the CA cert
   220  // and not the typical format of the secrets endpoints.
   221  func (v *VaultProvider) getCA(path string) (string, error) {
   222  	req := v.client.NewRequest("GET", "/v1/"+path+"/ca/pem")
   223  	resp, err := v.client.RawRequest(req)
   224  	if resp != nil {
   225  		defer resp.Body.Close()
   226  	}
   227  	if resp != nil && resp.StatusCode == http.StatusNotFound {
   228  		return "", ErrBackendNotMounted
   229  	}
   230  	if err != nil {
   231  		return "", err
   232  	}
   233  
   234  	bytes, err := ioutil.ReadAll(resp.Body)
   235  	if err != nil {
   236  		return "", err
   237  	}
   238  
   239  	root := string(bytes)
   240  	if root == "" {
   241  		return "", ErrBackendNotInitialized
   242  	}
   243  
   244  	return root, nil
   245  }
   246  
   247  // GenerateIntermediate mounts the configured intermediate PKI backend if
   248  // necessary, then generates and signs a new CA CSR using the root PKI backend
   249  // and updates the intermediate backend to use that new certificate.
   250  func (v *VaultProvider) GenerateIntermediate() (string, error) {
   251  	csr, err := v.generateIntermediateCSR()
   252  	if err != nil {
   253  		return "", err
   254  	}
   255  
   256  	// Sign the CSR with the root backend.
   257  	intermediate, err := v.client.Logical().Write(v.config.RootPKIPath+"root/sign-intermediate", map[string]interface{}{
   258  		"csr":    csr,
   259  		"format": "pem_bundle",
   260  	})
   261  	if err != nil {
   262  		return "", err
   263  	}
   264  	if intermediate == nil || intermediate.Data["certificate"] == "" {
   265  		return "", fmt.Errorf("got empty value when generating intermediate certificate")
   266  	}
   267  
   268  	// Set the intermediate backend to use the new certificate.
   269  	_, err = v.client.Logical().Write(v.config.IntermediatePKIPath+"intermediate/set-signed", map[string]interface{}{
   270  		"certificate": intermediate.Data["certificate"],
   271  	})
   272  	if err != nil {
   273  		return "", err
   274  	}
   275  
   276  	return v.ActiveIntermediate()
   277  }
   278  
   279  // Sign calls the configured role in the intermediate PKI backend to issue
   280  // a new leaf certificate based on the provided CSR, with the issuing
   281  // intermediate CA cert attached.
   282  func (v *VaultProvider) Sign(csr *x509.CertificateRequest) (string, error) {
   283  	var pemBuf bytes.Buffer
   284  	if err := pem.Encode(&pemBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr.Raw}); err != nil {
   285  		return "", err
   286  	}
   287  
   288  	// Use the leaf cert role to sign a new cert for this CSR.
   289  	response, err := v.client.Logical().Write(v.config.IntermediatePKIPath+"sign/"+VaultCALeafCertRole, map[string]interface{}{
   290  		"csr": pemBuf.String(),
   291  		"ttl": v.config.LeafCertTTL.String(),
   292  	})
   293  	if err != nil {
   294  		return "", fmt.Errorf("error issuing cert: %v", err)
   295  	}
   296  	if response == nil || response.Data["certificate"] == "" || response.Data["issuing_ca"] == "" {
   297  		return "", fmt.Errorf("certificate info returned from Vault was blank")
   298  	}
   299  
   300  	cert, ok := response.Data["certificate"].(string)
   301  	if !ok {
   302  		return "", fmt.Errorf("certificate was not a string")
   303  	}
   304  	ca, ok := response.Data["issuing_ca"].(string)
   305  	if !ok {
   306  		return "", fmt.Errorf("issuing_ca was not a string")
   307  	}
   308  
   309  	return fmt.Sprintf("%s\n%s", cert, ca), nil
   310  }
   311  
   312  // SignIntermediate returns a signed CA certificate with a path length constraint
   313  // of 0 to ensure that the certificate cannot be used to generate further CA certs.
   314  func (v *VaultProvider) SignIntermediate(csr *x509.CertificateRequest) (string, error) {
   315  	var pemBuf bytes.Buffer
   316  	err := pem.Encode(&pemBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr.Raw})
   317  	if err != nil {
   318  		return "", err
   319  	}
   320  
   321  	// Sign the CSR with the root backend.
   322  	data, err := v.client.Logical().Write(v.config.RootPKIPath+"root/sign-intermediate", map[string]interface{}{
   323  		"csr":             pemBuf.String(),
   324  		"format":          "pem_bundle",
   325  		"max_path_length": 0,
   326  	})
   327  	if err != nil {
   328  		return "", err
   329  	}
   330  	if data == nil || data.Data["certificate"] == "" {
   331  		return "", fmt.Errorf("got empty value when generating intermediate certificate")
   332  	}
   333  
   334  	intermediate, ok := data.Data["certificate"].(string)
   335  	if !ok {
   336  		return "", fmt.Errorf("signed intermediate result is not a string")
   337  	}
   338  
   339  	return intermediate, nil
   340  }
   341  
   342  // CrossSignCA takes a CA certificate and cross-signs it to form a trust chain
   343  // back to our active root.
   344  func (v *VaultProvider) CrossSignCA(cert *x509.Certificate) (string, error) {
   345  	var pemBuf bytes.Buffer
   346  	err := pem.Encode(&pemBuf, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
   347  	if err != nil {
   348  		return "", err
   349  	}
   350  
   351  	// Have the root PKI backend sign this cert.
   352  	response, err := v.client.Logical().Write(v.config.RootPKIPath+"root/sign-self-issued", map[string]interface{}{
   353  		"certificate": pemBuf.String(),
   354  	})
   355  	if err != nil {
   356  		return "", fmt.Errorf("error having Vault cross-sign cert: %v", err)
   357  	}
   358  	if response == nil || response.Data["certificate"] == "" {
   359  		return "", fmt.Errorf("certificate info returned from Vault was blank")
   360  	}
   361  
   362  	xcCert, ok := response.Data["certificate"].(string)
   363  	if !ok {
   364  		return "", fmt.Errorf("certificate was not a string")
   365  	}
   366  
   367  	return xcCert, nil
   368  }
   369  
   370  // Cleanup unmounts the configured intermediate PKI backend. It's fine to tear
   371  // this down and recreate it on small config changes because the intermediate
   372  // certs get bundled with the leaf certs, so there's no cost to the CA changing.
   373  func (v *VaultProvider) Cleanup() error {
   374  	return v.client.Sys().Unmount(v.config.IntermediatePKIPath)
   375  }
   376  
   377  func ParseVaultCAConfig(raw map[string]interface{}) (*structs.VaultCAProviderConfig, error) {
   378  	config := structs.VaultCAProviderConfig{
   379  		CommonCAProviderConfig: defaultCommonConfig(),
   380  	}
   381  
   382  	decodeConf := &mapstructure.DecoderConfig{
   383  		DecodeHook:       structs.ParseDurationFunc(),
   384  		Result:           &config,
   385  		WeaklyTypedInput: true,
   386  	}
   387  
   388  	decoder, err := mapstructure.NewDecoder(decodeConf)
   389  	if err != nil {
   390  		return nil, err
   391  	}
   392  
   393  	if err := decoder.Decode(raw); err != nil {
   394  		return nil, fmt.Errorf("error decoding config: %s", err)
   395  	}
   396  
   397  	if config.Token == "" {
   398  		return nil, fmt.Errorf("must provide a Vault token")
   399  	}
   400  
   401  	if config.RootPKIPath == "" {
   402  		return nil, fmt.Errorf("must provide a valid path to a root PKI backend")
   403  	}
   404  	if !strings.HasSuffix(config.RootPKIPath, "/") {
   405  		config.RootPKIPath += "/"
   406  	}
   407  
   408  	if config.IntermediatePKIPath == "" {
   409  		return nil, fmt.Errorf("must provide a valid path for the intermediate PKI backend")
   410  	}
   411  	if !strings.HasSuffix(config.IntermediatePKIPath, "/") {
   412  		config.IntermediatePKIPath += "/"
   413  	}
   414  
   415  	if err := config.CommonCAProviderConfig.Validate(); err != nil {
   416  		return nil, err
   417  	}
   418  
   419  	return &config, nil
   420  }