mirror of
https://github.com/status-im/consul.git
synced 2025-01-09 21:35:52 +00:00
f52d76f096
With an optional interface that providers can use to indicate if they use an intermediate cert in the primary DC. This removes the need to look up the provider config when renewing the intermediate.
607 lines
17 KiB
Go
607 lines
17 KiB
Go
package ca
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
vaultapi "github.com/hashicorp/vault/api"
|
|
"github.com/mitchellh/mapstructure"
|
|
|
|
"github.com/hashicorp/consul/agent/connect"
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
"github.com/hashicorp/consul/logging"
|
|
)
|
|
|
|
const VaultCALeafCertRole = "leaf-cert"
|
|
|
|
var ErrBackendNotMounted = fmt.Errorf("backend not mounted")
|
|
var ErrBackendNotInitialized = fmt.Errorf("backend not initialized")
|
|
|
|
type VaultProvider struct {
|
|
config *structs.VaultCAProviderConfig
|
|
client *vaultapi.Client
|
|
|
|
shutdown func()
|
|
|
|
isPrimary bool
|
|
clusterID string
|
|
spiffeID *connect.SpiffeIDSigning
|
|
setupIntermediatePKIPathDone bool
|
|
logger hclog.Logger
|
|
}
|
|
|
|
func NewVaultProvider() *VaultProvider {
|
|
return &VaultProvider{shutdown: func() {}}
|
|
}
|
|
|
|
func vaultTLSConfig(config *structs.VaultCAProviderConfig) *vaultapi.TLSConfig {
|
|
return &vaultapi.TLSConfig{
|
|
CACert: config.CAFile,
|
|
CAPath: config.CAPath,
|
|
ClientCert: config.CertFile,
|
|
ClientKey: config.KeyFile,
|
|
Insecure: config.TLSSkipVerify,
|
|
TLSServerName: config.TLSServerName,
|
|
}
|
|
}
|
|
|
|
// Configure sets up the provider using the given configuration.
|
|
func (v *VaultProvider) Configure(cfg ProviderConfig) error {
|
|
config, err := ParseVaultCAConfig(cfg.RawConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
clientConf := &vaultapi.Config{
|
|
Address: config.Address,
|
|
}
|
|
err = clientConf.ConfigureTLS(vaultTLSConfig(config))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
client, err := vaultapi.NewClient(clientConf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client.SetToken(config.Token)
|
|
v.config = config
|
|
v.client = client
|
|
v.isPrimary = cfg.IsPrimary
|
|
v.clusterID = cfg.ClusterID
|
|
v.spiffeID = connect.SpiffeIDSigningForCluster(&structs.CAConfiguration{ClusterID: v.clusterID})
|
|
|
|
// Look up the token to see if we can auto-renew its lease.
|
|
secret, err := client.Auth().Token().LookupSelf()
|
|
if err != nil {
|
|
return err
|
|
} else if secret == nil {
|
|
return fmt.Errorf("Could not look up Vault provider token: not found")
|
|
}
|
|
var token struct {
|
|
Renewable bool
|
|
TTL int
|
|
}
|
|
if err := mapstructure.Decode(secret.Data, &token); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set up a renewer to renew the token automatically, if supported.
|
|
if token.Renewable {
|
|
lifetimeWatcher, err := client.NewLifetimeWatcher(&vaultapi.LifetimeWatcherInput{
|
|
Secret: &vaultapi.Secret{
|
|
Auth: &vaultapi.SecretAuth{
|
|
ClientToken: config.Token,
|
|
Renewable: token.Renewable,
|
|
LeaseDuration: secret.LeaseDuration,
|
|
},
|
|
},
|
|
Increment: token.TTL,
|
|
RenewBehavior: vaultapi.RenewBehaviorIgnoreErrors,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("Error beginning Vault provider token renewal: %v", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.TODO())
|
|
v.shutdown = cancel
|
|
go v.renewToken(ctx, lifetimeWatcher)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// renewToken uses a vaultapi.Renewer to repeatedly renew our token's lease.
|
|
func (v *VaultProvider) renewToken(ctx context.Context, watcher *vaultapi.LifetimeWatcher) {
|
|
go watcher.Start()
|
|
defer watcher.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
|
|
case err := <-watcher.DoneCh():
|
|
if err != nil {
|
|
v.logger.Error("Error renewing token for Vault provider", "error", err)
|
|
}
|
|
|
|
// Watcher routine has finished, so start it again.
|
|
go watcher.Start()
|
|
|
|
case <-watcher.RenewCh():
|
|
v.logger.Info("Successfully renewed token for Vault provider")
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetLogger implements the NeedsLogger interface so the provider can log important messages.
|
|
func (v *VaultProvider) SetLogger(logger hclog.Logger) {
|
|
v.logger = logger.
|
|
ResetNamed(logging.Connect).
|
|
Named(logging.CA).
|
|
Named(logging.Vault)
|
|
}
|
|
|
|
// State implements Provider. Vault provider needs no state other than the
|
|
// user-provided config currently.
|
|
func (v *VaultProvider) State() (map[string]string, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
// ActiveRoot returns the active root CA certificate.
|
|
func (v *VaultProvider) ActiveRoot() (string, error) {
|
|
return v.getCA(v.config.RootPKIPath)
|
|
}
|
|
|
|
// GenerateRoot mounts and initializes a new root PKI backend if needed.
|
|
func (v *VaultProvider) GenerateRoot() error {
|
|
if !v.isPrimary {
|
|
return fmt.Errorf("provider is not the root certificate authority")
|
|
}
|
|
|
|
// Set up the root PKI backend if necessary.
|
|
_, err := v.ActiveRoot()
|
|
switch err {
|
|
case ErrBackendNotMounted:
|
|
err := v.client.Sys().Mount(v.config.RootPKIPath, &vaultapi.MountInput{
|
|
Type: "pki",
|
|
Description: "root CA backend for Consul Connect",
|
|
Config: vaultapi.MountConfigInput{
|
|
MaxLeaseTTL: "8760h",
|
|
},
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fallthrough
|
|
case ErrBackendNotInitialized:
|
|
uid, err := connect.CompactUID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = v.client.Logical().Write(v.config.RootPKIPath+"root/generate/internal", map[string]interface{}{
|
|
"common_name": connect.CACN("vault", uid, v.clusterID, v.isPrimary),
|
|
"uri_sans": v.spiffeID.URI().String(),
|
|
"key_type": v.config.PrivateKeyType,
|
|
"key_bits": v.config.PrivateKeyBits,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GenerateIntermediateCSR creates a private key and generates a CSR
|
|
// for another datacenter's root to sign, overwriting the intermediate backend
|
|
// in the process.
|
|
func (v *VaultProvider) GenerateIntermediateCSR() (string, error) {
|
|
if v.isPrimary {
|
|
return "", fmt.Errorf("provider is the root certificate authority, " +
|
|
"cannot generate an intermediate CSR")
|
|
}
|
|
|
|
return v.generateIntermediateCSR()
|
|
}
|
|
|
|
func (v *VaultProvider) setupIntermediatePKIPath() error {
|
|
if v.setupIntermediatePKIPathDone {
|
|
return nil
|
|
}
|
|
mounts, err := v.client.Sys().ListMounts()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Mount the backend if it isn't mounted already.
|
|
if _, ok := mounts[v.config.IntermediatePKIPath]; !ok {
|
|
err := v.client.Sys().Mount(v.config.IntermediatePKIPath, &vaultapi.MountInput{
|
|
Type: "pki",
|
|
Description: "intermediate CA backend for Consul Connect",
|
|
Config: vaultapi.MountConfigInput{
|
|
MaxLeaseTTL: v.config.IntermediateCertTTL.String(),
|
|
},
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Create the role for issuing leaf certs if it doesn't exist yet
|
|
rolePath := v.config.IntermediatePKIPath + "roles/" + VaultCALeafCertRole
|
|
role, err := v.client.Logical().Read(rolePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if role == nil {
|
|
_, err := v.client.Logical().Write(rolePath, map[string]interface{}{
|
|
"allow_any_name": true,
|
|
"allowed_uri_sans": "spiffe://*",
|
|
"key_type": "any",
|
|
"max_ttl": v.config.LeafCertTTL.String(),
|
|
"no_store": true,
|
|
"require_cn": false,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
v.setupIntermediatePKIPathDone = true
|
|
return nil
|
|
}
|
|
|
|
func (v *VaultProvider) generateIntermediateCSR() (string, error) {
|
|
err := v.setupIntermediatePKIPath()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Generate a new intermediate CSR for the root to sign.
|
|
uid, err := connect.CompactUID()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
data, err := v.client.Logical().Write(v.config.IntermediatePKIPath+"intermediate/generate/internal", map[string]interface{}{
|
|
"common_name": connect.CACN("vault", uid, v.clusterID, v.isPrimary),
|
|
"key_type": v.config.PrivateKeyType,
|
|
"key_bits": v.config.PrivateKeyBits,
|
|
"uri_sans": v.spiffeID.URI().String(),
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if data == nil || data.Data["csr"] == "" {
|
|
return "", fmt.Errorf("got empty value when generating intermediate CSR")
|
|
}
|
|
csr, ok := data.Data["csr"].(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("csr result is not a string")
|
|
}
|
|
|
|
return csr, nil
|
|
}
|
|
|
|
// SetIntermediate writes the incoming intermediate and root certificates to the
|
|
// intermediate backend (as a chain).
|
|
func (v *VaultProvider) SetIntermediate(intermediatePEM, rootPEM string) error {
|
|
if v.isPrimary {
|
|
return fmt.Errorf("cannot set an intermediate using another root in the primary datacenter")
|
|
}
|
|
|
|
err := validateSetIntermediate(
|
|
intermediatePEM, rootPEM,
|
|
"", // we don't have access to the private key directly
|
|
v.spiffeID,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = v.client.Logical().Write(v.config.IntermediatePKIPath+"intermediate/set-signed", map[string]interface{}{
|
|
"certificate": fmt.Sprintf("%s\n%s", intermediatePEM, rootPEM),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ActiveIntermediate returns the current intermediate certificate.
|
|
func (v *VaultProvider) ActiveIntermediate() (string, error) {
|
|
if err := v.setupIntermediatePKIPath(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
cert, err := v.getCA(v.config.IntermediatePKIPath)
|
|
|
|
// This error is expected when calling initializeSecondaryCA for the
|
|
// first time. It means that the backend is mounted and ready, but
|
|
// there is no intermediate.
|
|
// This error is swallowed because there is nothing the caller can do
|
|
// about it. The caller needs to handle the empty cert though and
|
|
// create an intermediate CA.
|
|
if err == ErrBackendNotInitialized {
|
|
return "", nil
|
|
}
|
|
return cert, err
|
|
}
|
|
|
|
// getCA returns the raw CA cert for the given endpoint if there is one.
|
|
// We have to use the raw NewRequest call here instead of Logical().Read
|
|
// 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(path string) (string, error) {
|
|
req := v.client.NewRequest("GET", "/v1/"+path+"/ca/pem")
|
|
resp, err := v.client.RawRequest(req)
|
|
if resp != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
if resp != nil && resp.StatusCode == http.StatusNotFound {
|
|
return "", ErrBackendNotMounted
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
bytes, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
root := string(bytes)
|
|
if root == "" {
|
|
return "", ErrBackendNotInitialized
|
|
}
|
|
|
|
return root, nil
|
|
}
|
|
|
|
// GenerateIntermediate 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) {
|
|
csr, err := v.generateIntermediateCSR()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Sign the CSR with the root backend.
|
|
intermediate, err := v.client.Logical().Write(v.config.RootPKIPath+"root/sign-intermediate", map[string]interface{}{
|
|
"csr": csr,
|
|
"use_csr_values": true,
|
|
"format": "pem_bundle",
|
|
"ttl": v.config.IntermediateCertTTL.String(),
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if intermediate == nil || intermediate.Data["certificate"] == "" {
|
|
return "", fmt.Errorf("got empty value when generating intermediate certificate")
|
|
}
|
|
|
|
// Set the intermediate backend to use the new certificate.
|
|
_, err = v.client.Logical().Write(v.config.IntermediatePKIPath+"intermediate/set-signed", map[string]interface{}{
|
|
"certificate": intermediate.Data["certificate"],
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return v.ActiveIntermediate()
|
|
}
|
|
|
|
// Sign calls the configured role in the intermediate PKI backend to issue
|
|
// a new leaf certificate based on the provided CSR, with the issuing
|
|
// intermediate CA cert attached.
|
|
func (v *VaultProvider) Sign(csr *x509.CertificateRequest) (string, error) {
|
|
var pemBuf bytes.Buffer
|
|
if err := pem.Encode(&pemBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr.Raw}); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Use the leaf cert role to sign a new cert for this CSR.
|
|
response, err := v.client.Logical().Write(v.config.IntermediatePKIPath+"sign/"+VaultCALeafCertRole, map[string]interface{}{
|
|
"csr": pemBuf.String(),
|
|
"ttl": v.config.LeafCertTTL.String(),
|
|
})
|
|
if err != nil {
|
|
return "", fmt.Errorf("error issuing cert: %v", err)
|
|
}
|
|
if response == nil || response.Data["certificate"] == "" || response.Data["issuing_ca"] == "" {
|
|
return "", fmt.Errorf("certificate info returned from Vault was blank")
|
|
}
|
|
|
|
cert, ok := response.Data["certificate"].(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("certificate was not a string")
|
|
}
|
|
ca, ok := response.Data["issuing_ca"].(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("issuing_ca was not a string")
|
|
}
|
|
|
|
return fmt.Sprintf("%s\n%s", cert, ca), nil
|
|
}
|
|
|
|
// SignIntermediate returns a signed CA certificate with a path length constraint
|
|
// of 0 to ensure that the certificate cannot be used to generate further CA certs.
|
|
func (v *VaultProvider) SignIntermediate(csr *x509.CertificateRequest) (string, error) {
|
|
err := validateSignIntermediate(csr, v.spiffeID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var pemBuf bytes.Buffer
|
|
err = pem.Encode(&pemBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr.Raw})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Sign the CSR with the root backend.
|
|
data, err := v.client.Logical().Write(v.config.RootPKIPath+"root/sign-intermediate", map[string]interface{}{
|
|
"csr": pemBuf.String(),
|
|
"use_csr_values": true,
|
|
"format": "pem_bundle",
|
|
"max_path_length": 0,
|
|
"ttl": v.config.IntermediateCertTTL.String(),
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if data == nil || data.Data["certificate"] == "" {
|
|
return "", fmt.Errorf("got empty value when generating intermediate certificate")
|
|
}
|
|
|
|
intermediate, ok := data.Data["certificate"].(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("signed intermediate result is not a string")
|
|
}
|
|
|
|
return intermediate, nil
|
|
}
|
|
|
|
// CrossSignCA takes a CA certificate and cross-signs it to form a trust chain
|
|
// back to our active root.
|
|
func (v *VaultProvider) CrossSignCA(cert *x509.Certificate) (string, error) {
|
|
rootPEM, err := v.ActiveRoot()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
rootCert, err := connect.ParseCert(rootPEM)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error parsing root cert: %v", err)
|
|
}
|
|
if rootCert.NotAfter.Before(time.Now()) {
|
|
return "", fmt.Errorf("root certificate is expired")
|
|
}
|
|
|
|
var pemBuf bytes.Buffer
|
|
err = pem.Encode(&pemBuf, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Have the root PKI backend sign this cert.
|
|
response, err := v.client.Logical().Write(v.config.RootPKIPath+"root/sign-self-issued", map[string]interface{}{
|
|
"certificate": pemBuf.String(),
|
|
})
|
|
if err != nil {
|
|
return "", fmt.Errorf("error having Vault cross-sign cert: %v", err)
|
|
}
|
|
if response == nil || response.Data["certificate"] == "" {
|
|
return "", fmt.Errorf("certificate info returned from Vault was blank")
|
|
}
|
|
|
|
xcCert, ok := response.Data["certificate"].(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("certificate was not a string")
|
|
}
|
|
|
|
return xcCert, nil
|
|
}
|
|
|
|
// SupportsCrossSigning implements Provider
|
|
func (v *VaultProvider) SupportsCrossSigning() (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
// Cleanup unmounts the configured intermediate PKI backend. It's fine to tear
|
|
// this down and recreate it on small config changes because the intermediate
|
|
// certs get bundled with the leaf certs, so there's no cost to the CA changing.
|
|
func (v *VaultProvider) Cleanup(providerTypeChange bool, otherConfig map[string]interface{}) error {
|
|
v.Stop()
|
|
|
|
if !providerTypeChange {
|
|
newConfig, err := ParseVaultCAConfig(otherConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// if the intermeidate PKI path isn't changing we don't want to delete it as
|
|
// Cleanup is called after initializing the new provider
|
|
if newConfig.IntermediatePKIPath == v.config.IntermediatePKIPath {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
err := v.client.Sys().Unmount(v.config.IntermediatePKIPath)
|
|
|
|
switch err {
|
|
case ErrBackendNotMounted, ErrBackendNotInitialized:
|
|
// suppress these errors if we didn't finish initialization before
|
|
return nil
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Stop shuts down the token renew goroutine.
|
|
func (v *VaultProvider) Stop() {
|
|
v.shutdown()
|
|
}
|
|
|
|
func (v *VaultProvider) PrimaryUsesIntermediate() {}
|
|
|
|
func ParseVaultCAConfig(raw map[string]interface{}) (*structs.VaultCAProviderConfig, error) {
|
|
config := structs.VaultCAProviderConfig{
|
|
CommonCAProviderConfig: defaultCommonConfig(),
|
|
}
|
|
|
|
decodeConf := &mapstructure.DecoderConfig{
|
|
DecodeHook: structs.ParseDurationFunc(),
|
|
Result: &config,
|
|
WeaklyTypedInput: true,
|
|
}
|
|
|
|
decoder, err := mapstructure.NewDecoder(decodeConf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := decoder.Decode(raw); err != nil {
|
|
return nil, fmt.Errorf("error decoding config: %s", err)
|
|
}
|
|
|
|
if config.Token == "" {
|
|
return nil, fmt.Errorf("must provide a Vault token")
|
|
}
|
|
|
|
if config.RootPKIPath == "" {
|
|
return nil, fmt.Errorf("must provide a valid path to a root PKI backend")
|
|
}
|
|
if !strings.HasSuffix(config.RootPKIPath, "/") {
|
|
config.RootPKIPath += "/"
|
|
}
|
|
|
|
if config.IntermediatePKIPath == "" {
|
|
return nil, fmt.Errorf("must provide a valid path for the intermediate PKI backend")
|
|
}
|
|
if !strings.HasSuffix(config.IntermediatePKIPath, "/") {
|
|
config.IntermediatePKIPath += "/"
|
|
}
|
|
|
|
if err := config.CommonCAProviderConfig.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &config, nil
|
|
}
|