connect/ca: check LeafCertTTL when rotating expired roots

This commit is contained in:
Kyle Havlovitz 2018-07-20 16:04:04 -07:00
parent 6465b13b7d
commit ce10de036e
No known key found for this signature in database
GPG Key ID: 8A5E6B173056AD6C
6 changed files with 80 additions and 13 deletions

View File

@ -33,6 +33,10 @@ func ParseConsulCAConfig(raw map[string]interface{}) (*structs.ConsulCAProviderC
return nil, fmt.Errorf("must provide a private key when providing a root cert") return nil, fmt.Errorf("must provide a private key when providing a root cert")
} }
if err := config.CommonCAProviderConfig.Validate(); err != nil {
return nil, err
}
return &config, nil return &config, nil
} }

View File

@ -172,7 +172,7 @@ func (v *VaultProvider) GenerateIntermediate() (string, error) {
"allow_any_name": true, "allow_any_name": true,
"allowed_uri_sans": "spiffe://*", "allowed_uri_sans": "spiffe://*",
"key_type": "any", "key_type": "any",
"max_ttl": "72h", "max_ttl": fmt.Sprintf("%.0fm", v.config.LeafCertTTL.Minutes()),
"require_cn": false, "require_cn": false,
}) })
if err != nil { if err != nil {
@ -227,7 +227,7 @@ func (v *VaultProvider) Sign(csr *x509.CertificateRequest) (string, error) {
// Use the leaf cert role to sign a new cert for this CSR. // 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{}{ response, err := v.client.Logical().Write(v.config.IntermediatePKIPath+"sign/"+VaultCALeafCertRole, map[string]interface{}{
"csr": pemBuf.String(), "csr": pemBuf.String(),
"ttl": fmt.Sprintf("%.0fh", v.config.LeafCertTTL.Hours()), "ttl": fmt.Sprintf("%.0fm", v.config.LeafCertTTL.Minutes()),
}) })
if err != nil { if err != nil {
return "", fmt.Errorf("error issuing cert: %v", err) return "", fmt.Errorf("error issuing cert: %v", err)
@ -321,5 +321,9 @@ func ParseVaultCAConfig(raw map[string]interface{}) (*structs.VaultCAProviderCon
config.IntermediatePKIPath += "/" config.IntermediatePKIPath += "/"
} }
if err := config.CommonCAProviderConfig.Validate(); err != nil {
return nil, err
}
return &config, nil return &config, nil
} }

View File

@ -32,10 +32,6 @@ var (
// caRootPruneInterval is how often we check for stale CARoots to remove. // caRootPruneInterval is how often we check for stale CARoots to remove.
caRootPruneInterval = time.Hour caRootPruneInterval = time.Hour
// caRootExpireDuration is the duration after which an inactive root is considered
// "expired". Currently this is based on the default leaf cert TTL of 3 days.
caRootExpireDuration = 7 * 24 * time.Hour
// minAutopilotVersion is the minimum Consul version in which Autopilot features // minAutopilotVersion is the minimum Consul version in which Autopilot features
// are supported. // are supported.
minAutopilotVersion = version.Must(version.NewVersion("0.8.0")) minAutopilotVersion = version.Must(version.NewVersion("0.8.0"))
@ -601,14 +597,25 @@ func (s *Server) pruneCARoots() error {
return nil return nil
} }
idx, roots, err := s.fsm.State().CARoots(nil) state := s.fsm.State()
idx, roots, err := state.CARoots(nil)
if err != nil {
return err
}
_, caConf, err := state.CAConfig()
if err != nil {
return err
}
common, err := caConf.GetCommonConfig()
if err != nil { if err != nil {
return err return err
} }
var newRoots structs.CARoots var newRoots structs.CARoots
for _, r := range roots { for _, r := range roots {
if !r.Active && !r.RotatedOutAt.IsZero() && time.Now().Sub(r.RotatedOutAt) > caRootExpireDuration { if !r.Active && !r.RotatedOutAt.IsZero() && time.Now().Sub(r.RotatedOutAt) > common.LeafCertTTL*2 {
s.logger.Printf("[INFO] connect: pruning old unused root CA (ID: %s)", r.ID) s.logger.Printf("[INFO] connect: pruning old unused root CA (ID: %s)", r.ID)
continue continue
} }

View File

@ -1008,7 +1008,6 @@ func TestLeader_ACL_Initialization(t *testing.T) {
func TestLeader_CARootPruning(t *testing.T) { func TestLeader_CARootPruning(t *testing.T) {
t.Parallel() t.Parallel()
caRootExpireDuration = 500 * time.Millisecond
caRootPruneInterval = 200 * time.Millisecond caRootPruneInterval = 200 * time.Millisecond
require := require.New(t) require := require.New(t)
@ -1036,9 +1035,11 @@ func TestLeader_CARootPruning(t *testing.T) {
newConfig := &structs.CAConfiguration{ newConfig := &structs.CAConfiguration{
Provider: "consul", Provider: "consul",
Config: map[string]interface{}{ Config: map[string]interface{}{
"LeafCertTTL": 500 * time.Millisecond,
"PrivateKey": newKey, "PrivateKey": newKey,
"RootCert": "", "RootCert": "",
"RotationPeriod": 90 * 24 * time.Hour, "RotationPeriod": 90 * 24 * time.Hour,
"SkipValidate": true,
}, },
} }
{ {
@ -1056,7 +1057,7 @@ func TestLeader_CARootPruning(t *testing.T) {
require.NoError(err) require.NoError(err)
require.Len(roots, 2) require.Len(roots, 2)
time.Sleep(caRootExpireDuration * 2) time.Sleep(2 * time.Second)
// Now the old root should be pruned. // Now the old root should be pruned.
_, roots, err = s1.fsm.State().CARoots(nil) _, roots, err = s1.fsm.State().CARoots(nil)

View File

@ -1,7 +1,10 @@
package structs package structs
import ( import (
"fmt"
"time" "time"
"github.com/mitchellh/mapstructure"
) )
// IndexedCARoots is the list of currently trusted CA Roots. // IndexedCARoots is the list of currently trusted CA Roots.
@ -192,8 +195,49 @@ type CAConfiguration struct {
RaftIndex RaftIndex
} }
func (c *CAConfiguration) GetCommonConfig() (*CommonCAProviderConfig, error) {
if c == nil {
return nil, fmt.Errorf("config map was nil")
}
var config CommonCAProviderConfig
decodeConf := &mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
Result: &config,
}
decoder, err := mapstructure.NewDecoder(decodeConf)
if err != nil {
return nil, err
}
if err := decoder.Decode(c.Config); err != nil {
return nil, fmt.Errorf("error decoding config: %s", err)
}
return &config, nil
}
type CommonCAProviderConfig struct { type CommonCAProviderConfig struct {
LeafCertTTL time.Duration LeafCertTTL time.Duration
SkipValidate bool
}
func (c CommonCAProviderConfig) Validate() error {
if c.SkipValidate {
return nil
}
if c.LeafCertTTL < time.Hour {
return fmt.Errorf("leaf cert TTL must be greater than 1h")
}
if c.LeafCertTTL > 365*24*time.Hour {
return fmt.Errorf("leaf cert TTL must be less than 1 year")
}
return nil
} }
type ConsulCAProviderConfig struct { type ConsulCAProviderConfig struct {

View File

@ -721,9 +721,16 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass
<p>There are also a number of common configuration options supported by all providers:</p> <p>There are also a number of common configuration options supported by all providers:</p>
* <a name="ca_leaf_cert_ttl"></a><a href="#ca_leaf_cert_ttl">`leaf_cert_ttl`</a> The lease duration of * <a name="ca_leaf_cert_ttl"></a><a href="#ca_leaf_cert_ttl">`leaf_cert_ttl`</a> The upper bound on the
a leaf certificate issued for a service, after which a new certificate will be requested by the proxy. lease duration of a leaf certificate issued for a service. In most cases a new leaf certificate will be
Defaults to `72h`. requested by a proxy before this limit is reached. This is also the effective limit on how long a server
outage can last (with no leader) before network connections will start being rejected, and as a result the
defaults is `72h` to last through a weekend without intervention. This value cannot be lower than 1 hour
or higher than 1 year.
This value is also used when rotating out old root certificates from the cluster. When a root certificate
has been inactive (rotated out) for more than twice the *current* `leaf_cert_ttl`, it will be removed from
the trusted list.
* <a name="connect_proxy"></a><a href="#connect_proxy">`proxy`</a> This object allows setting options for the Connect proxies. The following sub-keys are available: * <a name="connect_proxy"></a><a href="#connect_proxy">`proxy`</a> This object allows setting options for the Connect proxies. The following sub-keys are available: