Merge pull request #4400 from hashicorp/leaf-cert-ttl

Add configurable leaf cert TTL to Connect CA
This commit is contained in:
Kyle Havlovitz 2018-07-25 17:53:25 -07:00 committed by GitHub
commit ecc02c6aee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 176 additions and 44 deletions

View File

@ -544,6 +544,9 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
"token": "Token",
"root_pki_path": "RootPKIPath",
"intermediate_pki_path": "IntermediatePKIPath",
// Common CA config
"leaf_cert_ttl": "LeafCertTTL",
})
}

View File

@ -2602,7 +2602,8 @@ func TestFullConfig(t *testing.T) {
"connect": {
"ca_provider": "consul",
"ca_config": {
"RotationPeriod": "90h"
"RotationPeriod": "90h",
"LeafCertTTL": "1h"
},
"enabled": true,
"proxy_defaults": {
@ -3073,7 +3074,8 @@ func TestFullConfig(t *testing.T) {
connect {
ca_provider = "consul"
ca_config {
"RotationPeriod" = "90h"
rotation_period = "90h"
leaf_cert_ttl = "1h"
}
enabled = true
proxy_defaults {
@ -3687,6 +3689,7 @@ func TestFullConfig(t *testing.T) {
ConnectCAProvider: "consul",
ConnectCAConfig: map[string]interface{}{
"RotationPeriod": "90h",
"LeafCertTTL": "1h",
},
ConnectProxyAllowManagedRoot: false,
ConnectProxyAllowManagedAPIRegistration: false,

View File

@ -214,8 +214,7 @@ func (c *ConsulProvider) Sign(csr *x509.CertificateRequest) (string, error) {
x509.ExtKeyUsageClientAuth,
x509.ExtKeyUsageServerAuth,
},
// todo(kyhavlov): add a way to set the cert lifetime here from the CA config
NotAfter: effectiveNow.Add(3 * 24 * time.Hour),
NotAfter: effectiveNow.Add(c.config.LeafCertTTL),
NotBefore: effectiveNow,
AuthorityKeyId: keyId,
SubjectKeyId: keyId,

View File

@ -10,10 +10,12 @@ import (
)
func ParseConsulCAConfig(raw map[string]interface{}) (*structs.ConsulCAProviderConfig, error) {
var config structs.ConsulCAProviderConfig
config := structs.ConsulCAProviderConfig{
CommonCAProviderConfig: defaultCommonConfig(),
}
decodeConf := &mapstructure.DecoderConfig{
DecodeHook: ParseDurationFunc(),
ErrorUnused: true,
Result: &config,
WeaklyTypedInput: true,
}
@ -31,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")
}
if err := config.CommonCAProviderConfig.Validate(); err != nil {
return nil, err
}
return &config, nil
}
@ -75,3 +81,9 @@ func Uint8ToString(bs []uint8) string {
}
return string(b)
}
func defaultCommonConfig() structs.CommonCAProviderConfig {
return structs.CommonCAProviderConfig{
LeafCertTTL: 3 * 24 * time.Hour,
}
}

View File

@ -117,12 +117,13 @@ func TestConsulCAProvider_Bootstrap_WithCert(t *testing.T) {
func TestConsulCAProvider_SignLeaf(t *testing.T) {
t.Parallel()
assert := assert.New(t)
require := require.New(t)
conf := testConsulCAConfig()
conf.Config["LeafCertTTL"] = "1h"
delegate := newMockDelegate(t, conf)
provider, err := NewConsulProvider(conf.Config, delegate)
assert.NoError(err)
require.NoError(err)
spiffeService := &connect.SpiffeIDService{
Host: "node1",
@ -136,20 +137,21 @@ func TestConsulCAProvider_SignLeaf(t *testing.T) {
raw, _ := connect.TestCSR(t, spiffeService)
csr, err := connect.ParseCSR(raw)
assert.NoError(err)
require.NoError(err)
cert, err := provider.Sign(csr)
assert.NoError(err)
require.NoError(err)
parsed, err := connect.ParseCert(cert)
assert.NoError(err)
assert.Equal(parsed.URIs[0], spiffeService.URI())
assert.Equal(parsed.Subject.CommonName, "foo")
assert.Equal(uint64(2), parsed.SerialNumber.Uint64())
require.NoError(err)
require.Equal(parsed.URIs[0], spiffeService.URI())
require.Equal(parsed.Subject.CommonName, "foo")
require.Equal(uint64(2), parsed.SerialNumber.Uint64())
// Ensure the cert is valid now and expires within the correct limit.
assert.True(parsed.NotAfter.Sub(time.Now()) < 3*24*time.Hour)
assert.True(parsed.NotBefore.Before(time.Now()))
now := time.Now()
require.True(parsed.NotAfter.Sub(now) < time.Hour)
require.True(parsed.NotBefore.Before(now))
}
// Generate a new cert for another service and make sure
@ -159,20 +161,20 @@ func TestConsulCAProvider_SignLeaf(t *testing.T) {
raw, _ := connect.TestCSR(t, spiffeService)
csr, err := connect.ParseCSR(raw)
assert.NoError(err)
require.NoError(err)
cert, err := provider.Sign(csr)
assert.NoError(err)
require.NoError(err)
parsed, err := connect.ParseCert(cert)
assert.NoError(err)
assert.Equal(parsed.URIs[0], spiffeService.URI())
assert.Equal(parsed.Subject.CommonName, "bar")
assert.Equal(parsed.SerialNumber.Uint64(), uint64(2))
require.NoError(err)
require.Equal(parsed.URIs[0], spiffeService.URI())
require.Equal(parsed.Subject.CommonName, "bar")
require.Equal(parsed.SerialNumber.Uint64(), uint64(2))
// Ensure the cert is valid now and expires within the correct limit.
assert.True(parsed.NotAfter.Sub(time.Now()) < 3*24*time.Hour)
assert.True(parsed.NotBefore.Before(time.Now()))
require.True(parsed.NotAfter.Sub(time.Now()) < 3*24*time.Hour)
require.True(parsed.NotBefore.Before(time.Now()))
}
}

View File

@ -172,7 +172,7 @@ func (v *VaultProvider) GenerateIntermediate() (string, error) {
"allow_any_name": true,
"allowed_uri_sans": "spiffe://*",
"key_type": "any",
"max_ttl": "72h",
"max_ttl": v.config.LeafCertTTL.String(),
"require_cn": false,
})
if err != nil {
@ -227,6 +227,7 @@ func (v *VaultProvider) Sign(csr *x509.CertificateRequest) (string, error) {
// 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)
@ -283,10 +284,12 @@ func (v *VaultProvider) Cleanup() error {
}
func ParseVaultCAConfig(raw map[string]interface{}) (*structs.VaultCAProviderConfig, error) {
var config structs.VaultCAProviderConfig
config := structs.VaultCAProviderConfig{
CommonCAProviderConfig: defaultCommonConfig(),
}
decodeConf := &mapstructure.DecoderConfig{
ErrorUnused: true,
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
Result: &config,
WeaklyTypedInput: true,
}
@ -318,5 +321,9 @@ func ParseVaultCAConfig(raw map[string]interface{}) (*structs.VaultCAProviderCon
config.IntermediatePKIPath += "/"
}
if err := config.CommonCAProviderConfig.Validate(); err != nil {
return nil, err
}
return &config, nil
}

View File

@ -16,6 +16,10 @@ import (
)
func testVaultCluster(t *testing.T) (*VaultProvider, *vault.Core, net.Listener) {
return testVaultClusterWithConfig(t, nil)
}
func testVaultClusterWithConfig(t *testing.T, rawConf map[string]interface{}) (*VaultProvider, *vault.Core, net.Listener) {
if err := vault.AddTestLogicalBackend("pki", pki.Factory); err != nil {
t.Fatal(err)
}
@ -23,12 +27,17 @@ func testVaultCluster(t *testing.T) (*VaultProvider, *vault.Core, net.Listener)
ln, addr := vaulthttp.TestServer(t, core)
provider, err := NewVaultProvider(map[string]interface{}{
conf := map[string]interface{}{
"Address": addr,
"Token": token,
"RootPKIPath": "pki-root/",
"IntermediatePKIPath": "pki-intermediate/",
}, "asdf")
}
for k, v := range rawConf {
conf[k] = v
}
provider, err := NewVaultProvider(conf, "asdf")
if err != nil {
t.Fatal(err)
}
@ -87,7 +96,9 @@ func TestVaultCAProvider_SignLeaf(t *testing.T) {
t.Parallel()
require := require.New(t)
provider, core, listener := testVaultCluster(t)
provider, core, listener := testVaultClusterWithConfig(t, map[string]interface{}{
"LeafCertTTL": "1h",
})
defer core.Shutdown()
defer listener.Close()
client, err := vaultapi.NewClient(&vaultapi.Config{
@ -120,8 +131,9 @@ func TestVaultCAProvider_SignLeaf(t *testing.T) {
firstSerial = parsed.SerialNumber.Uint64()
// Ensure the cert is valid now and expires within the correct limit.
require.True(parsed.NotAfter.Sub(time.Now()) < 3*24*time.Hour)
require.True(parsed.NotBefore.Before(time.Now()))
now := time.Now()
require.True(parsed.NotAfter.Sub(now) < time.Hour)
require.True(parsed.NotBefore.Before(now))
}
// Generate a new cert for another service and make sure
@ -142,7 +154,7 @@ func TestVaultCAProvider_SignLeaf(t *testing.T) {
require.NotEqual(firstSerial, parsed.SerialNumber.Uint64())
// Ensure the cert is valid now and expires within the correct limit.
require.True(parsed.NotAfter.Sub(time.Now()) < 3*24*time.Hour)
require.True(parsed.NotAfter.Sub(time.Now()) < time.Hour)
require.True(parsed.NotBefore.Before(time.Now()))
}
}

View File

@ -67,6 +67,7 @@ func TestConnectCAConfig(t *testing.T) {
expected := &structs.ConsulCAProviderConfig{
RotationPeriod: 90 * 24 * time.Hour,
}
expected.LeafCertTTL = 72 * time.Hour
// Get the initial config.
{
@ -88,7 +89,8 @@ func TestConnectCAConfig(t *testing.T) {
{
"Provider": "consul",
"Config": {
"RotationPeriod": 3600000000000
"LeafCertTTL": "72h",
"RotationPeriod": "1h"
}
}`))
req, _ := http.NewRequest("PUT", "/v1/connect/ca/configuration", body)

View File

@ -438,6 +438,7 @@ func DefaultConfig() *Config {
Provider: "consul",
Config: map[string]interface{}{
"RotationPeriod": "2160h",
"LeafCertTTL": "72h",
},
},

View File

@ -32,10 +32,6 @@ var (
// caRootPruneInterval is how often we check for stale CARoots to remove.
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
// are supported.
minAutopilotVersion = version.Must(version.NewVersion("0.8.0"))
@ -601,14 +597,25 @@ func (s *Server) pruneCARoots() error {
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 {
return err
}
var newRoots structs.CARoots
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)
continue
}

View File

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

View File

@ -1,7 +1,10 @@
package structs
import (
"fmt"
"time"
"github.com/mitchellh/mapstructure"
)
// IndexedCARoots is the list of currently trusted CA Roots.
@ -192,7 +195,54 @@ type CAConfiguration struct {
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 {
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 {
CommonCAProviderConfig `mapstructure:",squash"`
PrivateKey string
RootCert string
RotationPeriod time.Duration
@ -208,6 +258,8 @@ type CAConsulProviderState struct {
}
type VaultCAProviderConfig struct {
CommonCAProviderConfig `mapstructure:",squash"`
Address string
Token string
RootPKIPath string

View File

@ -21,8 +21,15 @@ type CAConfig struct {
ModifyIndex uint64
}
// CommonCAProviderConfig is the common options available to all CA providers.
type CommonCAProviderConfig struct {
LeafCertTTL time.Duration
}
// ConsulCAProviderConfig is the config for the built-in Consul CA provider.
type ConsulCAProviderConfig struct {
CommonCAProviderConfig `mapstructure:",squash"`
PrivateKey string
RootCert string
RotationPeriod time.Duration

View File

@ -63,6 +63,7 @@ func TestAPI_ConnectCAConfig_get_set(t *testing.T) {
expected := &ConsulCAProviderConfig{
RotationPeriod: 90 * 24 * time.Hour,
}
expected.LeafCertTTL = 72 * time.Hour
// This fails occasionally if server doesn't have time to bootstrap CA so
// retry

View File

@ -91,8 +91,7 @@ $ curl \
{
"Provider": "consul",
"Config": {
"PrivateKey": null,
"RootCert": null,
"LeafCertTTL": "72h",
"RotationPeriod": "2160h"
},
"CreateIndex": 5,
@ -133,8 +132,10 @@ providers, see [Provider Config](/docs/connect/ca.html).
{
"Provider": "consul",
"Config": {
"LeafCertTTL": "72h",
"PrivateKey": "-----BEGIN RSA PRIVATE KEY-----...",
"RootCert": "-----BEGIN CERTIFICATE-----...",
"RotationPeriod": "2160h"
}
}
```

View File

@ -728,6 +728,21 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass
`write` access to this backend, as well as permission to mount the backend at this path if it is not
already mounted.
#### Common CA Config Options
<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 upper bound on the
lease duration of a leaf certificate issued for a service. In most cases a new leaf certificate will be
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_allow_managed_registration"></a><a href="#connect_proxy_allow_managed_registration">`allow_managed_api_registration`</a> Allows managed proxies to be configured with services that are registered via the Agent HTTP API. Enabling this would allow anyone with permission to register a service to define a command to execute for the proxy. By default, this is false to protect against arbitrary process execution.

View File

@ -88,6 +88,7 @@ $ curl http://localhost:8500/v1/connect/ca/configuration
{
"Provider": "consul",
"Config": {
"LeafCertTTL": "72h",
"RotationPeriod": "2160h"
},
"CreateIndex": 5,

View File

@ -53,6 +53,9 @@ is used if configuring in an agent configuration file.
bootstrap with the ".consul" TLD. The cluster identifier can be found
using the [CA List Roots endpoint](/api/connect/ca.html#list-ca-root-certificates).
There are also [common CA configuration options](/docs/agent/options.html#common-ca-config-options)
that are supported by all CA providers.
## Specifying a Custom Private Key and Root Certificate
By default, a root certificate and private key will be automatically
@ -69,6 +72,7 @@ $ curl localhost:8500/v1/connect/ca/configuration
{
"Provider": "consul",
"Config": {
"LeafCertTTL": "72h",
"RotationPeriod": "2160h"
},
"CreateIndex": 5,
@ -99,6 +103,7 @@ $ jq -n --arg key "$(cat root.key)" --arg cert "$(cat root.crt)" '
{
"Provider": "consul",
"Config": {
"LeafCertTTL": "72h",
"PrivateKey": $key,
"RootCert": $cert,
"RotationPeriod": "2160h"
@ -113,6 +118,7 @@ $ cat ca_config.json
{
"Provider": "consul",
"Config": {
"LeafCertTTL": "72h",
"PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEArqiy1c3pbT3cSkjdEM1APALUareU...",
"RootCert": "-----BEGIN CERTIFICATE-----\nMIIDijCCAnKgAwIBAgIJAOFZ66em1qC7MA0GCSqGSIb3...",
"RotationPeriod": "2160h"