connect: add validations around intermediate cert ttl (#7213)

pull/6569/head
Hans Hasselberg 2020-02-11 00:05:49 +01:00 committed by GitHub
parent 1edcdafeaf
commit 6739fe6e83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 171 additions and 50 deletions

View File

@ -10,10 +10,7 @@ import (
) )
func ParseConsulCAConfig(raw map[string]interface{}) (*structs.ConsulCAProviderConfig, error) { func ParseConsulCAConfig(raw map[string]interface{}) (*structs.ConsulCAProviderConfig, error) {
config := structs.ConsulCAProviderConfig{ config := defaultConsulCAProviderConfig()
CommonCAProviderConfig: defaultCommonConfig(),
}
decodeConf := &mapstructure.DecoderConfig{ decodeConf := &mapstructure.DecoderConfig{
DecodeHook: structs.ParseDurationFunc(), DecodeHook: structs.ParseDurationFunc(),
Result: &config, Result: &config,
@ -37,9 +34,19 @@ func ParseConsulCAConfig(raw map[string]interface{}) (*structs.ConsulCAProviderC
return nil, err return nil, err
} }
if err := config.Validate(); err != nil {
return nil, err
}
return &config, nil return &config, nil
} }
func defaultConsulCAProviderConfig() structs.ConsulCAProviderConfig {
return structs.ConsulCAProviderConfig{
CommonCAProviderConfig: defaultCommonConfig(),
IntermediateCertTTL: 24 * 365 * time.Hour,
}
}
func defaultCommonConfig() structs.CommonCAProviderConfig { func defaultCommonConfig() structs.CommonCAProviderConfig {
return structs.CommonCAProviderConfig{ return structs.CommonCAProviderConfig{
LeafCertTTL: 3 * 24 * time.Hour, LeafCertTTL: 3 * 24 * time.Hour,

View File

@ -69,7 +69,7 @@ func testConsulCAConfig() *structs.CAConfiguration {
Config: map[string]interface{}{ Config: map[string]interface{}{
// Tests duration parsing after msgpack type mangling during raft apply. // Tests duration parsing after msgpack type mangling during raft apply.
"LeafCertTTL": []uint8("72h"), "LeafCertTTL": []uint8("72h"),
"IntermediateCertTTL": []uint8("72h"), "IntermediateCertTTL": []uint8("288h"),
}, },
} }
} }

View File

@ -51,7 +51,7 @@ func TestStructs_CAConfiguration_MsgpackEncodeDecode(t *testing.T) {
"PrivateKey": "key", "PrivateKey": "key",
"RootCert": "cert", "RootCert": "cert",
"RotationPeriod": "5m", "RotationPeriod": "5m",
"IntermediateCertTTL": "30m", "IntermediateCertTTL": "90h",
"DisableCrossSigning": true, "DisableCrossSigning": true,
}, },
}, },
@ -60,7 +60,7 @@ func TestStructs_CAConfiguration_MsgpackEncodeDecode(t *testing.T) {
PrivateKey: "key", PrivateKey: "key",
RootCert: "cert", RootCert: "cert",
RotationPeriod: 5 * time.Minute, RotationPeriod: 5 * time.Minute,
IntermediateCertTTL: 30 * time.Minute, IntermediateCertTTL: 90 * time.Hour,
DisableCrossSigning: true, DisableCrossSigning: true,
}, },
parseFunc: func(t *testing.T, raw map[string]interface{}) interface{} { parseFunc: func(t *testing.T, raw map[string]interface{}) interface{} {

View File

@ -11,11 +11,13 @@ import (
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"strings" "strings"
"time"
) )
const ( const (
DefaultPrivateKeyType = "ec" DefaultPrivateKeyType = "ec"
DefaultPrivateKeyBits = 256 DefaultPrivateKeyBits = 256
DefaultIntermediateCertTTL = 24 * 365 * time.Hour
) )
func pemEncodeKey(key []byte, blockType string) (string, error) { func pemEncodeKey(key []byte, blockType string) (string, error) {

View File

@ -368,7 +368,7 @@ func testCAConfigSet(t testing.T, a TestAgentRPC,
"PrivateKey": ca.SigningKey, "PrivateKey": ca.SigningKey,
"RootCert": ca.RootCert, "RootCert": ca.RootCert,
"RotationPeriod": 180 * 24 * time.Hour, "RotationPeriod": 180 * 24 * time.Hour,
"IntermediateCertTTL": 72 * time.Hour, "IntermediateCertTTL": 288 * time.Hour,
}, },
} }
args := &structs.CARequest{ args := &structs.CARequest{

View File

@ -76,7 +76,8 @@ func TestConnectCAConfig(t *testing.T) {
"Provider": "consul", "Provider": "consul",
"Config": { "Config": {
"LeafCertTTL": "72h", "LeafCertTTL": "72h",
"RotationPeriod": "1h" "RotationPeriod": "1h",
"IntermediateCertTTL": "288h"
} }
}`, }`,
wantErr: false, wantErr: false,
@ -84,8 +85,9 @@ func TestConnectCAConfig(t *testing.T) {
Provider: "consul", Provider: "consul",
ClusterID: connect.TestClusterID, ClusterID: connect.TestClusterID,
Config: map[string]interface{}{ Config: map[string]interface{}{
"LeafCertTTL": "72h", "LeafCertTTL": "72h",
"RotationPeriod": "1h", "RotationPeriod": "1h",
"IntermediateCertTTL": "288h",
}, },
}, },
}, },
@ -97,7 +99,7 @@ func TestConnectCAConfig(t *testing.T) {
"Config": { "Config": {
"LeafCertTTL": "72h", "LeafCertTTL": "72h",
"RotationPeriod": "1h", "RotationPeriod": "1h",
"IntermediateCertTTL": "2h" "IntermediateCertTTL": "288h"
} }
}`, }`,
wantErr: false, wantErr: false,
@ -107,7 +109,7 @@ func TestConnectCAConfig(t *testing.T) {
Config: map[string]interface{}{ Config: map[string]interface{}{
"LeafCertTTL": "72h", "LeafCertTTL": "72h",
"RotationPeriod": "1h", "RotationPeriod": "1h",
"IntermediateCertTTL": "2h", "IntermediateCertTTL": "288h",
}, },
}, },
}, },
@ -118,7 +120,8 @@ func TestConnectCAConfig(t *testing.T) {
"Provider": "consul", "Provider": "consul",
"Config": { "Config": {
"LeafCertTTL": "72h", "LeafCertTTL": "72h",
"RotationPeriod": "1h" "RotationPeriod": "1h",
"IntermediateCertTTL": "288h"
}, },
"ForceWithoutCrossSigning": true "ForceWithoutCrossSigning": true
}`, }`,
@ -127,8 +130,9 @@ func TestConnectCAConfig(t *testing.T) {
Provider: "consul", Provider: "consul",
ClusterID: connect.TestClusterID, ClusterID: connect.TestClusterID,
Config: map[string]interface{}{ Config: map[string]interface{}{
"LeafCertTTL": "72h", "LeafCertTTL": "72h",
"RotationPeriod": "1h", "RotationPeriod": "1h",
"IntermediateCertTTL": "288h",
}, },
ForceWithoutCrossSigning: true, ForceWithoutCrossSigning: true,
}, },
@ -145,7 +149,8 @@ func TestConnectCAConfig(t *testing.T) {
"provider": "consul", "provider": "consul",
"config": { "config": {
"LeafCertTTL": "72h", "LeafCertTTL": "72h",
"RotationPeriod": "1h" "RotationPeriod": "1h",
"IntermediateCertTTL": "288h"
}, },
"force_without_cross_signing": true "force_without_cross_signing": true
}`, }`,
@ -154,8 +159,9 @@ func TestConnectCAConfig(t *testing.T) {
Provider: "consul", Provider: "consul",
ClusterID: connect.TestClusterID, ClusterID: connect.TestClusterID,
Config: map[string]interface{}{ Config: map[string]interface{}{
"LeafCertTTL": "72h", "LeafCertTTL": "72h",
"RotationPeriod": "1h", "RotationPeriod": "1h",
"IntermediateCertTTL": "288h",
}, },
ForceWithoutCrossSigning: true, ForceWithoutCrossSigning: true,
}, },
@ -179,7 +185,8 @@ func TestConnectCAConfig(t *testing.T) {
"Provider": "consul", "Provider": "consul",
"config": { "config": {
"LeafCertTTL": "72h", "LeafCertTTL": "72h",
"RotationPeriod": "1h" "RotationPeriod": "1h",
"IntermediateCertTTL": "288h"
}, },
"State": { "State": {
"foo": "bar" "foo": "bar"
@ -190,8 +197,9 @@ func TestConnectCAConfig(t *testing.T) {
Provider: "consul", Provider: "consul",
ClusterID: connect.TestClusterID, ClusterID: connect.TestClusterID,
Config: map[string]interface{}{ Config: map[string]interface{}{
"LeafCertTTL": "72h", "LeafCertTTL": "72h",
"RotationPeriod": "1h", "RotationPeriod": "1h",
"IntermediateCertTTL": "288h",
}, },
State: map[string]string{ State: map[string]string{
"foo": "bar", "foo": "bar",
@ -211,6 +219,7 @@ func TestConnectCAConfig(t *testing.T) {
enabled = true enabled = true
ca_provider = "consul" ca_provider = "consul"
ca_config { ca_config {
intermediate_cert_ttl = "288h"
test_state { test_state {
` + tc.initialState + ` ` + tc.initialState + `
} }

View File

@ -35,10 +35,6 @@ var (
// maxRetryBackoff is the maximum number of seconds to wait between failed blocking // maxRetryBackoff is the maximum number of seconds to wait between failed blocking
// queries when backing off. // queries when backing off.
maxRetryBackoff = 256 maxRetryBackoff = 256
// intermediateCertRenewInterval is the interval at which the expiration
// of the intermediate cert is checked and renewed if necessary.
intermediateCertRenewInterval = time.Hour
) )
// initializeCAConfig is used to initialize the CA config if necessary // initializeCAConfig is used to initialize the CA config if necessary
@ -49,25 +45,36 @@ func (s *Server) initializeCAConfig() (*structs.CAConfiguration, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if config != nil { if config == nil {
return config, nil config = s.config.CAConfig
} if config.ClusterID == "" {
id, err := uuid.GenerateUUID()
config = s.config.CAConfig if err != nil {
if config.ClusterID == "" { return nil, err
id, err := uuid.GenerateUUID() }
if err != nil { config.ClusterID = id
return nil, err
} }
config.ClusterID = id } else if _, ok := config.Config["IntermediateCertTTL"]; !ok {
dup := *config
copied := make(map[string]interface{})
for k, v := range dup.Config {
copied[k] = v
}
copied["IntermediateCertTTL"] = connect.DefaultIntermediateCertTTL.String()
dup.Config = copied
config = &dup
} else {
return config, nil
} }
req := structs.CARequest{ req := structs.CARequest{
Op: structs.CAOpSetConfig, Op: structs.CAOpSetConfig,
Config: config, Config: config,
} }
if _, err = s.raftApply(structs.ConnectCARequestType, req); err != nil { if resp, err := s.raftApply(structs.ConnectCARequestType, req); err != nil {
return nil, err return nil, err
} else if respErr, ok := resp.(error); ok {
return nil, respErr
} }
return config, nil return config, nil
@ -643,7 +650,7 @@ func (s *Server) secondaryIntermediateCertRenewalWatch(ctx context.Context) erro
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil return nil
case <-time.After(intermediateCertRenewInterval): case <-time.After(structs.IntermediateCertRenewInterval):
retryLoopBackoff(ctx.Done(), func() error { retryLoopBackoff(ctx.Done(), func() error {
s.caProviderReconfigurationLock.Lock() s.caProviderReconfigurationLock.Lock()
defer s.caProviderReconfigurationLock.Unlock() defer s.caProviderReconfigurationLock.Unlock()

View File

@ -175,9 +175,16 @@ func waitForActiveCARoot(t *testing.T, srv *Server, expect *structs.CARoot) {
} }
func TestLeader_SecondaryCA_IntermediateRenew(t *testing.T) { func TestLeader_SecondaryCA_IntermediateRenew(t *testing.T) {
t.Parallel() // no parallel execution because we change globals
origInterval := structs.IntermediateCertRenewInterval
origMinTTL := structs.MinLeafCertTTL
defer func() {
structs.IntermediateCertRenewInterval = origInterval
structs.MinLeafCertTTL = origMinTTL
}()
intermediateCertRenewInterval = time.Millisecond structs.IntermediateCertRenewInterval = time.Millisecond
structs.MinLeafCertTTL = time.Second
require := require.New(t) require := require.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) { dir1, s1 := testServerWithConfig(t, func(c *Config) {
@ -188,7 +195,7 @@ func TestLeader_SecondaryCA_IntermediateRenew(t *testing.T) {
"PrivateKey": "", "PrivateKey": "",
"RootCert": "", "RootCert": "",
"RotationPeriod": "2160h", "RotationPeriod": "2160h",
"LeafCertTTL": "72h", "LeafCertTTL": "5s",
// The retry loop only retries for 7sec max and // The retry loop only retries for 7sec max and
// the ttl needs to be below so that it // the ttl needs to be below so that it
// triggers definitely. // triggers definitely.

View File

@ -160,7 +160,7 @@ func testServerConfig(t *testing.T) (string, *Config) {
"RootCert": "", "RootCert": "",
"RotationPeriod": "2160h", "RotationPeriod": "2160h",
"LeafCertTTL": "72h", "LeafCertTTL": "72h",
"IntermediateCertTTL": "72h", "IntermediateCertTTL": "288h",
}, },
} }

View File

@ -351,17 +351,20 @@ type CommonCAProviderConfig struct {
PrivateKeyBits int PrivateKeyBits int
} }
var MinLeafCertTTL = time.Hour
var MaxLeafCertTTL = 365 * 24 * time.Hour
func (c CommonCAProviderConfig) Validate() error { func (c CommonCAProviderConfig) Validate() error {
if c.SkipValidate { if c.SkipValidate {
return nil return nil
} }
if c.LeafCertTTL < time.Hour { if c.LeafCertTTL < MinLeafCertTTL {
return fmt.Errorf("leaf cert TTL must be greater than 1h") return fmt.Errorf("leaf cert TTL must be greater or equal than %s", MinLeafCertTTL)
} }
if c.LeafCertTTL > 365*24*time.Hour { if c.LeafCertTTL > MaxLeafCertTTL {
return fmt.Errorf("leaf cert TTL must be less than 1 year") return fmt.Errorf("leaf cert TTL must be less than %s", MaxLeafCertTTL)
} }
switch c.PrivateKeyType { switch c.PrivateKeyType {
@ -395,6 +398,40 @@ type ConsulCAProviderConfig struct {
DisableCrossSigning bool DisableCrossSigning bool
} }
// intermediateCertRenewInterval is the interval at which the expiration
// of the intermediate cert is checked and renewed if necessary.
var IntermediateCertRenewInterval = time.Hour
func (c *ConsulCAProviderConfig) Validate() error {
if c.IntermediateCertTTL < (3 * IntermediateCertRenewInterval) {
// Intermediate Certificates are checked every
// hour(intermediateCertRenewInterval) if they are about to
// expire. Recreating an intermediate certs is started once
// more than half its lifetime has passed.
// If it would be 2h, worst case is that the check happens
// right before half time and when the check happens again, the
// certificate is very close to expiring, leaving only a small
// timeframe to renew. 3h leaves more than 30min to recreate.
// Right now the minimum LeafCertTTL is 1h, which means this
// check not strictly needed, because the same thing is covered
// in the next check too. But just in case minimum LeafCertTTL
// changes at some point, this validation must still be
// performed.
return fmt.Errorf("Intermediate Cert TTL must be greater or equal than %dh", 3*int(IntermediateCertRenewInterval.Hours()))
}
if c.IntermediateCertTTL < (3 * c.CommonCAProviderConfig.LeafCertTTL) {
// Intermediate Certificates are being sent to the proxy when
// the Leaf Certificate changes because they are bundled
// together.
// That means that the Intermediate Certificate TTL must be at
// a minimum of 3 * Leaf Certificate TTL to ensure that the new
// Intermediate is being set together with the Leaf Certificate
// before it expires.
return fmt.Errorf("Intermediate Cert TTL must be greater or equal than 3 * LeafCertTTL (>=%s).", 3*c.CommonCAProviderConfig.LeafCertTTL)
}
return nil
}
// CAConsulProviderState is used to track the built-in Consul CA provider's state. // CAConsulProviderState is used to track the built-in Consul CA provider's state.
type CAConsulProviderState struct { type CAConsulProviderState struct {
ID string ID string

View File

@ -59,3 +59,55 @@ func TestCAConfiguration_GetCommonConfig(t *testing.T) {
}) })
} }
} }
func TestCAProviderConfig_Validate(t *testing.T) {
tests := []struct {
name string
cfg *ConsulCAProviderConfig
wantErr bool
wantMsg string
}{
{
name: "defaults",
cfg: &ConsulCAProviderConfig{},
wantErr: true,
wantMsg: "Intermediate Cert TTL must be greater or equal than 3h",
},
{
name: "intermediate cert ttl too short",
cfg: &ConsulCAProviderConfig{
CommonCAProviderConfig: CommonCAProviderConfig{LeafCertTTL: 2 * time.Hour},
IntermediateCertTTL: 4 * time.Hour,
},
wantErr: true,
wantMsg: "Intermediate Cert TTL must be greater or equal than 3 * LeafCertTTL (>=6h0m0s).",
},
{
name: "intermediate cert ttl too short",
cfg: &ConsulCAProviderConfig{
CommonCAProviderConfig: CommonCAProviderConfig{LeafCertTTL: 5 * time.Hour},
IntermediateCertTTL: 15*time.Hour - 1,
},
wantErr: true,
wantMsg: "Intermediate Cert TTL must be greater or equal than 3 * LeafCertTTL (>=15h0m0s).",
},
{
name: "good intermediate and leaf cert TTL",
cfg: &ConsulCAProviderConfig{
CommonCAProviderConfig: CommonCAProviderConfig{LeafCertTTL: 1 * time.Hour},
IntermediateCertTTL: 4 * time.Hour,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.cfg.Validate()
if err == nil {
require.False(t, tt.wantErr)
return
}
require.Equal(t, err.Error(), tt.wantMsg)
})
}
}

View File

@ -50,5 +50,5 @@ func TestConnectCASetConfigCommand(t *testing.T) {
parsed, err := ca.ParseConsulCAConfig(reply.Config) parsed, err := ca.ParseConsulCAConfig(reply.Config)
require.NoError(err) require.NoError(err)
require.Equal(24*time.Hour, parsed.RotationPeriod) require.Equal(24*time.Hour, parsed.RotationPeriod)
require.Equal(36*time.Hour, parsed.IntermediateCertTTL) require.Equal(288*time.Hour, parsed.IntermediateCertTTL)
} }

View File

@ -4,6 +4,6 @@
"PrivateKey": "", "PrivateKey": "",
"RootCert": "", "RootCert": "",
"RotationPeriod": "24h", "RotationPeriod": "24h",
"IntermediateCertTTL": "36h" "IntermediateCertTTL": "288h"
} }
} }