add root_cert_ttl option for consul connect, vault ca providers (#11428)

* add root_cert_ttl option for consul connect, vault ca providers

Signed-off-by: FFMMM <FFMMM@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Chris S. Kim <ckim@hashicorp.com>

* add changelog, pr feedback

Signed-off-by: FFMMM <FFMMM@users.noreply.github.com>

* Update .changelog/11428.txt, more docs

Co-authored-by: Daniel Nephin <dnephin@hashicorp.com>

* Update website/content/docs/agent/options.mdx

Co-authored-by: Kyle Havlovitz <kylehav@gmail.com>

Co-authored-by: Chris S. Kim <ckim@hashicorp.com>
Co-authored-by: Daniel Nephin <dnephin@hashicorp.com>
Co-authored-by: Kyle Havlovitz <kylehav@gmail.com>
This commit is contained in:
FFMMM 2021-11-02 11:02:10 -07:00 committed by GitHub
parent 51d8417545
commit 4ddf973a31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 220 additions and 23 deletions

3
.changelog/11428.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
ca: Add a configurable TTL for Connect CA root certificates. The configuration is supported by the Vault and Consul providers.
```

View File

@ -724,6 +724,7 @@ func (b *builder) build() (rt RuntimeConfig, err error) {
"csr_max_concurrent": "CSRMaxConcurrent",
"private_key_type": "PrivateKeyType",
"private_key_bits": "PrivateKeyBits",
"root_cert_ttl": "RootCertTTL",
})
}

View File

@ -1623,6 +1623,7 @@ func (c *RuntimeConfig) ConnectCAConfiguration() (*structs.CAConfiguration, erro
Config: map[string]interface{}{
"LeafCertTTL": structs.DefaultLeafCertTTL,
"IntermediateCertTTL": structs.DefaultIntermediateCertTTL,
"RootCertTTL": structs.DefaultRootCertTTL,
},
}

View File

@ -3162,6 +3162,65 @@ func TestLoad_IntegrationWithFlags(t *testing.T) {
}
},
})
run(t, testCase{
desc: "test connect vault provider configuration with root cert ttl",
args: []string{
`-data-dir=` + dataDir,
},
json: []string{`{
"connect": {
"enabled": true,
"ca_provider": "vault",
"ca_config": {
"ca_file": "/capath/ca.pem",
"ca_path": "/capath/",
"cert_file": "/certpath/cert.pem",
"key_file": "/certpath/key.pem",
"tls_server_name": "server.name",
"tls_skip_verify": true,
"token": "abc",
"root_pki_path": "consul-vault",
"root_cert_ttl": "96360h",
"intermediate_pki_path": "connect-intermediate"
}
}
}`},
hcl: []string{`
connect {
enabled = true
ca_provider = "vault"
ca_config {
ca_file = "/capath/ca.pem"
ca_path = "/capath/"
cert_file = "/certpath/cert.pem"
key_file = "/certpath/key.pem"
tls_server_name = "server.name"
tls_skip_verify = true
root_pki_path = "consul-vault"
token = "abc"
intermediate_pki_path = "connect-intermediate"
root_cert_ttl = "96360h"
}
}
`},
expected: func(rt *RuntimeConfig) {
rt.DataDir = dataDir
rt.ConnectEnabled = true
rt.ConnectCAProvider = "vault"
rt.ConnectCAConfig = map[string]interface{}{
"CAFile": "/capath/ca.pem",
"CAPath": "/capath/",
"CertFile": "/certpath/cert.pem",
"KeyFile": "/certpath/key.pem",
"TLSServerName": "server.name",
"TLSSkipVerify": true,
"Token": "abc",
"RootPKIPath": "consul-vault",
"RootCertTTL": "96360h",
"IntermediatePKIPath": "connect-intermediate",
}
},
})
run(t, testCase{
desc: "Connect AWS CA provider configuration",
args: []string{
@ -5472,6 +5531,7 @@ func TestLoad_FullConfig(t *testing.T) {
ConnectCAConfig: map[string]interface{}{
"IntermediateCertTTL": "8760h",
"LeafCertTTL": "1h",
"RootCertTTL": "96360h",
"CSRMaxPerSecond": float64(100),
"CSRMaxConcurrent": float64(2),
},
@ -6652,6 +6712,7 @@ func TestConnectCAConfiguration(t *testing.T) {
Config: map[string]interface{}{
"LeafCertTTL": "72h",
"IntermediateCertTTL": "8760h", // 365 * 24h
"RootCertTTL": "87600h", // 365 * 10 * 24h
},
},
},
@ -6668,6 +6729,7 @@ func TestConnectCAConfiguration(t *testing.T) {
Config: map[string]interface{}{
"LeafCertTTL": "72h",
"IntermediateCertTTL": "8760h", // 365 * 24h
"RootCertTTL": "87600h", // 365 * 10 * 24h
"cluster_id": "adfe7697-09b4-413a-ac0a-fa81ed3a3001",
},
},
@ -6691,6 +6753,7 @@ func TestConnectCAConfiguration(t *testing.T) {
Config: map[string]interface{}{
"LeafCertTTL": "72h",
"IntermediateCertTTL": "8760h", // 365 * 24h
"RootCertTTL": "87600h", // 365 * 10 * 24h
},
},
},
@ -6699,6 +6762,7 @@ func TestConnectCAConfiguration(t *testing.T) {
ConnectEnabled: true,
ConnectCAConfig: map[string]interface{}{
"foo": "bar",
"RootCertTTL": "8761h", // 365 * 24h + 1
},
},
expected: &structs.CAConfiguration{
@ -6706,6 +6770,7 @@ func TestConnectCAConfiguration(t *testing.T) {
Config: map[string]interface{}{
"LeafCertTTL": "72h",
"IntermediateCertTTL": "8760h", // 365 * 24h
"RootCertTTL": "8761h", // 365 * 24h + 1
"foo": "bar",
},
},

View File

@ -200,6 +200,7 @@ connect {
ca_config {
intermediate_cert_ttl = "8760h"
leaf_cert_ttl = "1h"
root_cert_ttl = "96360h"
# hack float since json parses numbers as float and we have to
# assert against the same thing
csr_max_per_second = 100.0

View File

@ -200,6 +200,7 @@
"connect": {
"ca_provider": "consul",
"ca_config": {
"root_cert_ttl": "96360h",
"intermediate_cert_ttl": "8760h",
"leaf_cert_ttl": "1h",
"csr_max_per_second": 100,

View File

@ -96,7 +96,7 @@ func (c *ConsulProvider) Configure(cfg ProviderConfig) error {
fmt.Sprintf("%s,%s", config.PrivateKey, config.RootCert),
}
// Check if there any entries with old ID schemes.
// Check if there are any entries with old ID schemes.
for _, oldID := range oldIDs {
_, providerState, err = c.Delegate.State().CAProviderState(oldID)
if err != nil {
@ -194,7 +194,7 @@ func (c *ConsulProvider) GenerateRoot() error {
return fmt.Errorf("error computing next serial number: %v", err)
}
ca, err := c.generateCA(newState.PrivateKey, nextSerial)
ca, err := c.generateCA(newState.PrivateKey, nextSerial, c.config.RootCertTTL)
if err != nil {
return fmt.Errorf("error generating CA: %v", err)
}
@ -616,7 +616,7 @@ func (c *ConsulProvider) incrementAndGetNextSerialNumber() (uint64, error) {
}
// generateCA makes a new root CA using the current private key
func (c *ConsulProvider) generateCA(privateKey string, sn uint64) (string, error) {
func (c *ConsulProvider) generateCA(privateKey string, sn uint64, rootCertTTL time.Duration) (string, error) {
stateStore := c.Delegate.State()
_, config, err := stateStore.CAConfig(nil)
if err != nil {
@ -652,7 +652,7 @@ func (c *ConsulProvider) generateCA(privateKey string, sn uint64) (string, error
x509.KeyUsageCRLSign |
x509.KeyUsageDigitalSignature,
IsCA: true,
NotAfter: time.Now().AddDate(10, 0, 0),
NotAfter: time.Now().Add(rootCertTTL),
NotBefore: time.Now(),
AuthorityKeyId: keyId,
SubjectKeyId: keyId,

View File

@ -52,5 +52,6 @@ func defaultCommonConfig() structs.CommonCAProviderConfig {
IntermediateCertTTL: 24 * 365 * time.Hour,
PrivateKeyType: connect.DefaultPrivateKeyType,
PrivateKeyBits: connect.DefaultPrivateKeyBits,
RootCertTTL: 10 * 24 * 365 * time.Hour,
}
}

View File

@ -43,8 +43,9 @@ func testConsulCAConfig() *structs.CAConfiguration {
Provider: "consul",
Config: map[string]interface{}{
// Tests duration parsing after msgpack type mangling during raft apply.
"LeafCertTTL": []uint8("72h"),
"IntermediateCertTTL": []uint8("288h"),
"LeafCertTTL": []byte("72h"),
"IntermediateCertTTL": []byte("288h"),
"RootCertTTL": []byte("87600h"),
},
}
}
@ -88,6 +89,14 @@ func TestConsulCAProvider_Bootstrap(t *testing.T) {
require.Equal(parsed.URIs[0].String(), fmt.Sprintf("spiffe://%s.consul", conf.ClusterID))
requireNotEncoded(t, parsed.SubjectKeyId)
requireNotEncoded(t, parsed.AuthorityKeyId)
// test that the root cert ttl is the same as the expected value
// notice that we allow a margin of "error" of 10 minutes between the
// generateCA() creation and this check
defaultRootCertTTL, err := time.ParseDuration(structs.DefaultRootCertTTL)
require.NoError(err)
expectedNotAfter := time.Now().Add(defaultRootCertTTL).UTC()
require.WithinDuration(expectedNotAfter, parsed.NotAfter, 10*time.Minute, "expected parsed cert ttl to be the same as the value configured")
}
func TestConsulCAProvider_Bootstrap_WithCert(t *testing.T) {
@ -95,7 +104,7 @@ func TestConsulCAProvider_Bootstrap_WithCert(t *testing.T) {
// Make sure setting a custom private key/root cert works.
require := require.New(t)
rootCA := connect.TestCA(t, nil)
rootCA := connect.TestCAWithTTL(t, nil, 5*time.Hour)
conf := testConsulCAConfig()
conf.Config = map[string]interface{}{
"PrivateKey": rootCA.SigningKey,
@ -110,6 +119,18 @@ func TestConsulCAProvider_Bootstrap_WithCert(t *testing.T) {
root, err := provider.ActiveRoot()
require.NoError(err)
require.Equal(root, rootCA.RootCert)
// Should be a valid cert
parsed, err := connect.ParseCert(root)
require.NoError(err)
// test that the default root cert ttl was not applied to the provided cert
defaultRootCertTTL, err := time.ParseDuration(structs.DefaultRootCertTTL)
require.NoError(err)
defaultNotAfter := time.Now().Add(defaultRootCertTTL).UTC()
// we can't compare given the "delta" between the time the cert is generated
// and when we start the test; so just look at the years for now, given different years
require.NotEqualf(defaultNotAfter.Year(), parsed.NotAfter.Year(), "parsed cert ttl expected to be different from default root cert ttl")
}
func TestConsulCAProvider_SignLeaf(t *testing.T) {

View File

@ -34,6 +34,7 @@ func TestStructs_CAConfiguration_MsgpackEncodeDecode(t *testing.T) {
CSRMaxConcurrent: 55,
PrivateKeyType: "rsa",
PrivateKeyBits: 4096,
RootCertTTL: 10 * 24 * 365 * time.Hour,
}
cases := map[string]testcase{

View File

@ -170,7 +170,11 @@ func (v *VaultProvider) GenerateRoot() error {
Type: "pki",
Description: "root CA backend for Consul Connect",
Config: vaultapi.MountConfigInput{
MaxLeaseTTL: "8760h",
// the max lease ttl denotes the maximum ttl that secrets are created from the engine
// the default lease ttl is the kind of ttl that will *reliably* set the ttl to v.config.RootCertTTL
// https://www.vaultproject.io/docs/secrets/pki#configure-a-ca-certificate
MaxLeaseTTL: v.config.RootCertTTL.String(),
DefaultLeaseTTL: v.config.RootCertTTL.String(),
},
})

View File

@ -89,28 +89,51 @@ func TestVaultCAProvider_Bootstrap(t *testing.T) {
SkipIfVaultNotPresent(t)
provider, testVault := testVaultProvider(t)
defer testVault.Stop()
client := testVault.client
providerWDefaultRootCertTtl, testvault1 := testVaultProviderWithConfig(t, true, map[string]interface{}{
"LeafCertTTL": "1h",
})
defer testvault1.Stop()
client1 := testvault1.client
providerCustomRootCertTtl, testvault2 := testVaultProviderWithConfig(t, true, map[string]interface{}{
"LeafCertTTL": "1h",
"RootCertTTL": "8761h",
})
defer testvault2.Stop()
client2 := testvault2.client
require := require.New(t)
cases := []struct {
certFunc func() (string, error)
backendPath string
rootCaCreation bool
provider *VaultProvider
client *vaultapi.Client
expectedRootCertTTL string
}{
{
certFunc: provider.ActiveRoot,
certFunc: providerWDefaultRootCertTtl.ActiveRoot,
backendPath: "pki-root/",
rootCaCreation: true,
client: client1,
provider: providerWDefaultRootCertTtl,
expectedRootCertTTL: structs.DefaultRootCertTTL,
},
{
certFunc: provider.ActiveIntermediate,
certFunc: providerCustomRootCertTtl.ActiveIntermediate,
backendPath: "pki-intermediate/",
rootCaCreation: false,
provider: providerCustomRootCertTtl,
client: client2,
expectedRootCertTTL: "8761h",
},
}
// Verify the root and intermediate certs match the ones in the vault backends
for _, tc := range cases {
provider := tc.provider
client := tc.client
cert, err := tc.certFunc()
require.NoError(err)
req := client.NewRequest("GET", "/v1/"+tc.backendPath+"ca/pem")
@ -126,6 +149,15 @@ func TestVaultCAProvider_Bootstrap(t *testing.T) {
require.True(parsed.IsCA)
require.Len(parsed.URIs, 1)
require.Equal(fmt.Sprintf("spiffe://%s.consul", provider.clusterID), parsed.URIs[0].String())
// test that the root cert ttl as applied
if tc.rootCaCreation {
rootCertTTL, err := time.ParseDuration(tc.expectedRootCertTTL)
require.NoError(err)
expectedNotAfter := time.Now().Add(rootCertTTL).UTC()
require.WithinDuration(expectedNotAfter, parsed.NotAfter, 10*time.Minute, "expected parsed cert ttl to be the same as the value configured")
}
}
}

View File

@ -40,6 +40,7 @@ func makeConfig(kc KeyConfig) structs.CommonCAProviderConfig {
return structs.CommonCAProviderConfig{
LeafCertTTL: 3 * 24 * time.Hour,
IntermediateCertTTL: 365 * 24 * time.Hour,
RootCertTTL: 10 * 365 * 24 * time.Hour,
PrivateKeyType: kc.keyType,
PrivateKeyBits: kc.keyBits,
}

View File

@ -65,7 +65,7 @@ func (s *HTTPHandlers) ConnectCAConfiguration(resp http.ResponseWriter, req *htt
}
}
// GEt /v1/connect/ca/configuration
// GET /v1/connect/ca/configuration
func (s *HTTPHandlers) ConnectCAConfigurationGet(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// Method is tested in ConnectCAConfiguration
var args structs.DCSpecificRequest

View File

@ -494,6 +494,7 @@ func DefaultConfig() *Config {
Config: map[string]interface{}{
"LeafCertTTL": structs.DefaultLeafCertTTL,
"IntermediateCertTTL": structs.DefaultIntermediateCertTTL,
"RootCertTTL": structs.DefaultRootCertTTL,
},
},

View File

@ -12,7 +12,8 @@ import (
const (
DefaultLeafCertTTL = "72h"
DefaultIntermediateCertTTL = "8760h" // 365 * 24h
DefaultIntermediateCertTTL = "8760h" // ~ 1 year = 365 * 24h
DefaultRootCertTTL = "87600h" // ~ 10 years = 365 * 24h * 10
)
// IndexedCARoots is the list of currently trusted CA Roots.
@ -326,6 +327,7 @@ func (c *CAConfiguration) GetCommonConfig() (*CommonCAProviderConfig, error) {
type CommonCAProviderConfig struct {
LeafCertTTL time.Duration
IntermediateCertTTL time.Duration
RootCertTTL time.Duration
SkipValidate bool
@ -380,6 +382,12 @@ func (c CommonCAProviderConfig) Validate() error {
return nil
}
// it's sufficient to check that the root cert ttl >= intermediate cert ttl
// since intermediate cert ttl >= 3* leaf cert ttl; so root cert ttl >= 3 * leaf cert ttl > leaf cert ttl
if c.RootCertTTL < c.IntermediateCertTTL {
return fmt.Errorf("root cert TTL is set and is not greater than intermediate cert ttl. root cert ttl: %s, intermediate cert ttl: %s", c.RootCertTTL, c.IntermediateCertTTL)
}
if c.LeafCertTTL < MinLeafCertTTL {
return fmt.Errorf("leaf cert TTL must be greater or equal than %s", MinLeafCertTTL)
}

View File

@ -80,6 +80,7 @@ func TestCAProviderConfig_Validate(t *testing.T) {
cfg: &CommonCAProviderConfig{
LeafCertTTL: 2 * time.Hour,
IntermediateCertTTL: 4 * time.Hour,
RootCertTTL: 5 * time.Hour,
},
wantErr: true,
wantMsg: "Intermediate Cert TTL must be greater or equal than 3 * LeafCertTTL (>=6h0m0s).",
@ -89,6 +90,7 @@ func TestCAProviderConfig_Validate(t *testing.T) {
cfg: &CommonCAProviderConfig{
LeafCertTTL: 5 * time.Hour,
IntermediateCertTTL: 15*time.Hour - 1,
RootCertTTL: 15 * time.Hour,
},
wantErr: true,
wantMsg: "Intermediate Cert TTL must be greater or equal than 3 * LeafCertTTL (>=15h0m0s).",
@ -98,6 +100,7 @@ func TestCAProviderConfig_Validate(t *testing.T) {
cfg: &CommonCAProviderConfig{
LeafCertTTL: 1 * time.Hour,
IntermediateCertTTL: 4 * time.Hour,
RootCertTTL: 5 * time.Hour,
},
wantErr: true,
wantMsg: "private key type must be either 'ec' or 'rsa'",
@ -107,6 +110,7 @@ func TestCAProviderConfig_Validate(t *testing.T) {
cfg: &CommonCAProviderConfig{
LeafCertTTL: 1 * time.Hour,
IntermediateCertTTL: 4 * time.Hour,
RootCertTTL: 5 * time.Hour,
PrivateKeyType: "ec",
},
wantErr: true,
@ -117,11 +121,36 @@ func TestCAProviderConfig_Validate(t *testing.T) {
cfg: &CommonCAProviderConfig{
LeafCertTTL: 1 * time.Hour,
IntermediateCertTTL: 4 * time.Hour,
RootCertTTL: 5 * time.Hour,
PrivateKeyType: "ec",
PrivateKeyBits: 256,
},
wantErr: false,
},
{
name: "good root cert/ intermediate TTLs",
cfg: &CommonCAProviderConfig{
LeafCertTTL: 1 * time.Hour,
IntermediateCertTTL: 4 * time.Hour,
RootCertTTL: 5 * time.Hour,
PrivateKeyType: "ec",
PrivateKeyBits: 256,
},
wantErr: false,
wantMsg: "",
},
{
name: "bad root cert/ intermediate TTLs",
cfg: &CommonCAProviderConfig{
LeafCertTTL: 1 * time.Hour,
IntermediateCertTTL: 4 * time.Hour,
RootCertTTL: 3 * time.Hour,
PrivateKeyType: "ec",
PrivateKeyBits: 256,
},
wantErr: true,
wantMsg: "root cert TTL is set and is not greater than intermediate cert ttl. root cert ttl: 3h0m0s, intermediate cert ttl: 4h0m0s",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -38,6 +38,7 @@ type CAConfig struct {
// CommonCAProviderConfig is the common options available to all CA providers.
type CommonCAProviderConfig struct {
LeafCertTTL time.Duration
RootCertTTL time.Duration
SkipValidate bool
CSRMaxPerSecond float32
CSRMaxConcurrent int

View File

@ -66,6 +66,7 @@ func TestAPI_ConnectCAConfig_get_set(t *testing.T) {
IntermediateCertTTL: 365 * 24 * time.Hour,
}
expected.LeafCertTTL = 72 * time.Hour
expected.RootCertTTL = 10 * 365 * 24 * time.Hour
// This fails occasionally if server doesn't have time to bootstrap CA so
// retry
@ -84,6 +85,7 @@ func TestAPI_ConnectCAConfig_get_set(t *testing.T) {
// Change a config value and update
conf.Config["PrivateKey"] = ""
conf.Config["IntermediateCertTTL"] = 300 * 24 * time.Hour
conf.Config["RootCertTTL"] = 11 * 365 * 24 * time.Hour
// Pass through some state as if the provider stored it so we can make sure
// we can read it again.
@ -95,6 +97,7 @@ func TestAPI_ConnectCAConfig_get_set(t *testing.T) {
updated, _, err := connect.CAGetConfig(nil)
r.Check(err)
expected.IntermediateCertTTL = 300 * 24 * time.Hour
expected.RootCertTTL = 11 * 365 * 24 * time.Hour
parsed, err = ParseConsulCAConfig(updated.Config)
r.Check(err)
require.Equal(r, expected, parsed)

View File

@ -224,6 +224,7 @@ type TestServer struct {
// callback function to modify the configuration. If there is an error
// configuring or starting the server, the server will NOT be running when the
// function returns (thus you do not need to stop it).
// This function will call the `consul` binary in GOPATH.
func NewTestServerConfigT(t TestingTB, cb ServerConfigCallback) (*TestServer, error) {
path, err := exec.LookPath("consul")
if err != nil || path == "" {

View File

@ -1267,6 +1267,18 @@ bind_addr = "{{ GetPrivateInterfaces | include \"network\" \"10.0.0.0/8\" | attr
for more than twice the _current_ `leaf_cert_ttl`, it will be removed
from the trusted list.
- `root_cert_ttl` ((#ca_root_cert_ttl)) The time to live (TTL) for a root certificate.
Defaults to 10 years as `87600h`. This value, if provided, needs to be higher than the
intermediate certificate TTL.
This setting currently applies only to the consul connect and Vault CA providers. It is
ignored for the AWS acm pca provider. The value for root certificates issued by the AWS
CA provider is 5 years and not configurable at this time.
For the Vault provider, this value is only used if the backend is not initialized at first.
This value is also applied on the `ca set-config` command.
- `private_key_type` ((#ca_private_key_type)) The type of key to generate
for this CA. This is only used when the provider is generating a new key. If
`private_key` is set for the Consul provider, or existing root or intermediate

View File

@ -35,6 +35,16 @@ The following configuration options are supported by all CA providers:
for more than twice the _current_ `leaf_cert_ttl`, it will be removed
from the trusted list.
- `RootCertTTL` / `root_cert_ttl` (`duration: "87600h"`) The time to live (TTL) for a root certificate.
Defaults to 10 years as `87600h`. This value, if provided, needs to be higher than the
intermediate certificate TTL.
This setting currently applies only to the consul connect and Vault CA providers. It is
ignored for the AWS acm pca provider. The value for root certificates issued by the AWS
CA provider is 5 years and not configurable at this time.
For the Vault provider, this value is only used if the backend is not initialized at first.
- `PrivateKeyType` / `private_key_type` (`string: "ec"`) - The type of key to generate
for this CA. This is only used when the provider is generating a new key. If
`private_key` is set for the Consul provider, or existing root or intermediate