connect: check if intermediate cert needs to be renewed. (#6835)

Currently when using the built-in CA provider for Connect, root certificates are valid for 10 years, however secondary DCs get intermediates that are valid for only 1 year. There is no mechanism currently short of rotating the root in the primary that will cause the secondary DCs to renew their intermediates.
This PR adds a check that renews the cert if it is half way through its validity period.

In order to be able to test these changes, a new configuration option was added: IntermediateCertTTL which is set extremely low in the tests.
This commit is contained in:
Hans Hasselberg 2020-01-17 23:27:13 +01:00 committed by GitHub
parent 87f32c8ba6
commit 804eb17094
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 480 additions and 138 deletions

View File

@ -621,9 +621,10 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
if connectCAConfig != nil {
lib.TranslateKeys(connectCAConfig, map[string]string{
// Consul CA config
"private_key": "PrivateKey",
"root_cert": "RootCert",
"rotation_period": "RotationPeriod",
"private_key": "PrivateKey",
"root_cert": "RootCert",
"rotation_period": "RotationPeriod",
"intermediate_cert_ttl": "IntermediateCertTTL",
// Vault CA config
"address": "Address",

View File

@ -3766,6 +3766,7 @@ func TestFullConfig(t *testing.T) {
"ca_provider": "consul",
"ca_config": {
"rotation_period": "90h",
"intermediate_cert_ttl": "8760h",
"leaf_cert_ttl": "1h",
"csr_max_per_second": 100,
"csr_max_concurrent": 2
@ -4367,6 +4368,7 @@ func TestFullConfig(t *testing.T) {
ca_provider = "consul"
ca_config {
rotation_period = "90h"
intermediate_cert_ttl = "8760h"
leaf_cert_ttl = "1h"
# hack float since json parses numbers as float and we have to
# assert against the same thing
@ -5079,10 +5081,11 @@ func TestFullConfig(t *testing.T) {
ExposeMaxPort: 2222,
ConnectCAProvider: "consul",
ConnectCAConfig: map[string]interface{}{
"RotationPeriod": "90h",
"LeafCertTTL": "1h",
"CSRMaxPerSecond": float64(100),
"CSRMaxConcurrent": float64(2),
"RotationPeriod": "90h",
"IntermediateCertTTL": "8760h",
"LeafCertTTL": "1h",
"CSRMaxPerSecond": float64(100),
"CSRMaxConcurrent": float64(2),
},
DNSAddrs: []net.Addr{tcpAddr("93.95.95.81:7001"), udpAddr("93.95.95.81:7001")},
DNSARecordLimit: 29907,

View File

@ -480,7 +480,7 @@ func (c *ConsulProvider) SignIntermediate(csr *x509.CertificateRequest) (string,
x509.KeyUsageDigitalSignature,
IsCA: true,
MaxPathLenZero: true,
NotAfter: effectiveNow.AddDate(1, 0, 0),
NotAfter: effectiveNow.Add(c.config.IntermediateCertTTL),
NotBefore: effectiveNow,
SubjectKeyId: subjectKeyID,
}

View File

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

View File

@ -361,9 +361,10 @@ func testCAConfigSet(t testing.T, a TestAgentRPC,
newConfig := &structs.CAConfiguration{
Provider: "consul",
Config: map[string]interface{}{
"PrivateKey": ca.SigningKey,
"RootCert": ca.RootCert,
"RotationPeriod": 180 * 24 * time.Hour,
"PrivateKey": ca.SigningKey,
"RootCert": ca.RootCert,
"RotationPeriod": 180 * 24 * time.Hour,
"IntermediateCertTTL": 72 * time.Hour,
},
}
args := &structs.CARequest{

View File

@ -89,6 +89,28 @@ func TestConnectCAConfig(t *testing.T) {
},
},
},
{
name: "basic with IntermediateCertTTL",
body: `
{
"Provider": "consul",
"Config": {
"LeafCertTTL": "72h",
"RotationPeriod": "1h",
"IntermediateCertTTL": "2h"
}
}`,
wantErr: false,
wantCfg: structs.CAConfiguration{
Provider: "consul",
ClusterID: connect.TestClusterID,
Config: map[string]interface{}{
"LeafCertTTL": "72h",
"RotationPeriod": "1h",
"IntermediateCertTTL": "2h",
},
},
},
{
name: "force without cross sign CamelCase",
body: `
@ -211,7 +233,6 @@ func TestConnectCAConfig(t *testing.T) {
}
require.NoError(err)
}
// The config should be updated now.
{
req, _ := http.NewRequest("GET", "/v1/connect/ca/configuration", nil)

View File

@ -549,8 +549,9 @@ func DefaultConfig() *Config {
CAConfig: &structs.CAConfiguration{
Provider: "consul",
Config: map[string]interface{}{
"RotationPeriod": "2160h",
"LeafCertTTL": "72h",
"RotationPeriod": "2160h",
"LeafCertTTL": "72h",
"IntermediateCertTTL": "8760h", // 365 * 24h
},
},

View File

@ -1280,9 +1280,10 @@ func TestFSM_CAConfig(t *testing.T) {
Config: &structs.CAConfiguration{
Provider: "consul",
Config: map[string]interface{}{
"PrivateKey": "asdf",
"RootCert": "qwer",
"RotationPeriod": 90 * 24 * time.Hour,
"PrivateKey": "asdf",
"RootCert": "qwer",
"RotationPeriod": 90 * 24 * time.Hour,
"IntermediateCertTTL": 365 * 24 * time.Hour,
},
},
}
@ -1314,6 +1315,9 @@ func TestFSM_CAConfig(t *testing.T) {
if got, want := conf.RotationPeriod, 90*24*time.Hour; got != want {
t.Fatalf("got %v, want %v", got, want)
}
if got, want := conf.IntermediateCertTTL, 365*24*time.Hour; got != want {
t.Fatalf("got %v, want %v", got, want)
}
// Now use CAS and provide an old index
req.Config.Provider = "static"

View File

@ -34,6 +34,10 @@ var (
// maxRetryBackoff is the maximum number of seconds to wait between failed blocking
// queries when backing off.
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
@ -119,6 +123,8 @@ func (s *Server) createCAProvider(conf *structs.CAConfiguration) (ca.Provider, e
return p, nil
}
// getCAProvider is being called while holding caProviderReconfigurationLock
// which means it must never take that lock itself or call anything that does.
func (s *Server) getCAProvider() (ca.Provider, *structs.CARoot) {
retries := 0
var result ca.Provider
@ -144,6 +150,8 @@ func (s *Server) getCAProvider() (ca.Provider, *structs.CARoot) {
return result, resultRoot
}
// setCAProvider is being called while holding caProviderReconfigurationLock
// which means it must never take that lock itself or call anything that does.
func (s *Server) setCAProvider(newProvider ca.Provider, root *structs.CARoot) {
s.caProviderLock.Lock()
defer s.caProviderLock.Unlock()
@ -169,6 +177,9 @@ func (s *Server) initializeCA() error {
if err != nil {
return err
}
s.caProviderReconfigurationLock.Lock()
defer s.caProviderReconfigurationLock.Unlock()
s.setCAProvider(provider, nil)
// If this isn't the primary DC, run the secondary DC routine if the primary has already been upgraded to at least 1.6.0
@ -209,6 +220,8 @@ func (s *Server) initializeCA() error {
}
// initializeRootCA runs the initialization logic for a root CA.
// It is being called while holding caProviderReconfigurationLock
// which means it must never take that lock itself or call anything that does.
func (s *Server) initializeRootCA(provider ca.Provider, conf *structs.CAConfiguration) error {
pCfg := ca.ProviderConfig{
ClusterID: conf.ClusterID,
@ -315,6 +328,8 @@ func (s *Server) initializeRootCA(provider ca.Provider, conf *structs.CAConfigur
// initializeSecondaryCA runs the routine for generating an intermediate CA CSR and getting
// it signed by the primary DC if the root CA of the primary DC has changed since the last
// intermediate.
// It is being called while holding caProviderReconfigurationLock
// which means it must never take that lock itself or call anything that does.
func (s *Server) initializeSecondaryCA(provider ca.Provider, primaryRoots structs.IndexedCARoots) error {
activeIntermediate, err := provider.ActiveIntermediate()
if err != nil {
@ -344,7 +359,7 @@ func (s *Server) initializeSecondaryCA(provider ca.Provider, primaryRoots struct
storedRootID, err = connect.CalculateCertFingerprint(storedRoot)
if err != nil {
return fmt.Errorf("error parsing root fingerprint: %v, %#v", err, primaryRoots)
return fmt.Errorf("error parsing root fingerprint: %v, %#v", err, storedRoot)
}
intermediateCert, err := connect.ParseCert(activeIntermediate)
@ -394,34 +409,10 @@ func (s *Server) initializeSecondaryCA(provider ca.Provider, primaryRoots struct
newIntermediate := false
if needsNewIntermediate {
csr, err := provider.GenerateIntermediateCSR()
if err != nil {
if err := s.getIntermediateCASigned(provider, newActiveRoot); err != nil {
return err
}
var intermediatePEM string
if err := s.forwardDC("ConnectCA.SignIntermediate", s.config.PrimaryDatacenter, s.generateCASignRequest(csr), &intermediatePEM); err != nil {
// this is a failure in the primary and shouldn't be capable of erroring out our establishing leadership
s.logger.Printf("[WARN] connect: Primary datacenter refused to sign our intermediate CA certificate: %v", err)
return nil
}
if err := provider.SetIntermediate(intermediatePEM, newActiveRoot.RootCert); err != nil {
return fmt.Errorf("Failed to set the intermediate certificate with the CA provider: %v", err)
}
intermediateCert, err := connect.ParseCert(intermediatePEM)
if err != nil {
return fmt.Errorf("error parsing intermediate cert: %v", err)
}
// Append the new intermediate to our local active root entry. This is
// where the root representations start to diverge.
newActiveRoot.IntermediateCerts = append(newActiveRoot.IntermediateCerts, intermediatePEM)
newActiveRoot.SigningKeyID = connect.EncodeSigningKeyID(intermediateCert.SubjectKeyId)
newIntermediate = true
s.logger.Printf("[INFO] connect: received new intermediate certificate from primary datacenter")
} else {
// Discard the primary's representation since our local one is
// sufficiently up to date.
@ -435,67 +426,110 @@ func (s *Server) initializeSecondaryCA(provider ca.Provider, primaryRoots struct
return err
}
if activeRoot == nil || activeRoot.ID != newActiveRoot.ID || newIntermediate {
idx, oldRoots, err := state.CARoots(nil)
if err != nil {
if err := s.persistNewRoot(provider, newActiveRoot); err != nil {
return err
}
_, config, err := state.CAConfig(nil)
if err != nil {
return err
}
if config == nil {
return fmt.Errorf("local CA not initialized yet")
}
newConf := *config
newConf.ClusterID = newActiveRoot.ExternalTrustDomain
// Persist any state the provider needs us to
newConf.State, err = provider.State()
if err != nil {
return fmt.Errorf("error getting provider state: %v", err)
}
// Copy the root list and append the new active root, updating the old root
// with the time it was rotated out.
var newRoots structs.CARoots
for _, r := range oldRoots {
newRoot := *r
if newRoot.Active {
newRoot.Active = false
newRoot.RotatedOutAt = time.Now()
}
if newRoot.ExternalTrustDomain == "" {
newRoot.ExternalTrustDomain = config.ClusterID
}
newRoots = append(newRoots, &newRoot)
}
newRoots = append(newRoots, newActiveRoot)
args := &structs.CARequest{
Op: structs.CAOpSetRootsAndConfig,
Index: idx,
Roots: newRoots,
Config: &newConf,
}
resp, err := s.raftApply(structs.ConnectCARequestType, &args)
if err != nil {
return err
}
if respErr, ok := resp.(error); ok {
return respErr
}
if respOk, ok := resp.(bool); ok && !respOk {
return fmt.Errorf("could not atomically update roots and config")
}
s.logger.Printf("[INFO] connect: updated root certificates from primary datacenter")
}
s.setCAProvider(provider, newActiveRoot)
return nil
}
// persistNewRoot is being called while holding caProviderReconfigurationLock
// which means it must never take that lock itself or call anything that does.
func (s *Server) persistNewRoot(provider ca.Provider, newActiveRoot *structs.CARoot) error {
state := s.fsm.State()
idx, oldRoots, err := state.CARoots(nil)
if err != nil {
return err
}
_, config, err := state.CAConfig(nil)
if err != nil {
return err
}
if config == nil {
return fmt.Errorf("local CA not initialized yet")
}
newConf := *config
newConf.ClusterID = newActiveRoot.ExternalTrustDomain
// Persist any state the provider needs us to
newConf.State, err = provider.State()
if err != nil {
return fmt.Errorf("error getting provider state: %v", err)
}
// Copy the root list and append the new active root, updating the old root
// with the time it was rotated out.
var newRoots structs.CARoots
for _, r := range oldRoots {
newRoot := *r
if newRoot.Active {
newRoot.Active = false
newRoot.RotatedOutAt = time.Now()
}
if newRoot.ExternalTrustDomain == "" {
newRoot.ExternalTrustDomain = config.ClusterID
}
newRoots = append(newRoots, &newRoot)
}
newRoots = append(newRoots, newActiveRoot)
args := &structs.CARequest{
Op: structs.CAOpSetRootsAndConfig,
Index: idx,
Roots: newRoots,
Config: &newConf,
}
resp, err := s.raftApply(structs.ConnectCARequestType, &args)
if err != nil {
return err
}
if respErr, ok := resp.(error); ok {
return respErr
}
if respOk, ok := resp.(bool); ok && !respOk {
return fmt.Errorf("could not atomically update roots and config")
}
s.logger.Printf("[INFO] connect: updated root certificates from primary datacenter")
return nil
}
// getIntermediateCASigned is being called while holding caProviderReconfigurationLock
// which means it must never take that lock itself or call anything that does.
func (s *Server) getIntermediateCASigned(provider ca.Provider, newActiveRoot *structs.CARoot) error {
csr, err := provider.GenerateIntermediateCSR()
if err != nil {
return err
}
var intermediatePEM string
if err := s.forwardDC("ConnectCA.SignIntermediate", s.config.PrimaryDatacenter, s.generateCASignRequest(csr), &intermediatePEM); err != nil {
// this is a failure in the primary and shouldn't be capable of erroring out our establishing leadership
s.logger.Printf("[WARN] connect: Primary datacenter refused to sign our intermediate CA certificate: %v", err)
return nil
}
if err := provider.SetIntermediate(intermediatePEM, newActiveRoot.RootCert); err != nil {
return fmt.Errorf("Failed to set the intermediate certificate with the CA provider: %v", err)
}
intermediateCert, err := connect.ParseCert(intermediatePEM)
if err != nil {
return fmt.Errorf("error parsing intermediate cert: %v", err)
}
// Append the new intermediate to our local active root entry. This is
// where the root representations start to diverge.
newActiveRoot.IntermediateCerts = append(newActiveRoot.IntermediateCerts, intermediatePEM)
newActiveRoot.SigningKeyID = connect.EncodeSigningKeyID(intermediateCert.SubjectKeyId)
s.logger.Printf("[INFO] connect: received new intermediate certificate from primary datacenter")
return nil
}
func (s *Server) generateCASignRequest(csr string) *structs.CASignRequest {
return &structs.CASignRequest{
Datacenter: s.config.PrimaryDatacenter,
@ -510,6 +544,7 @@ func (s *Server) startConnectLeader() {
if s.config.ConnectEnabled && s.config.Datacenter != s.config.PrimaryDatacenter {
s.leaderRoutineManager.Start(secondaryCARootWatchRoutineName, s.secondaryCARootWatch)
s.leaderRoutineManager.Start(intentionReplicationRoutineName, s.replicateIntentions)
s.leaderRoutineManager.Start(secondaryCertRenewWatchRoutineName, s.secondaryIntermediateCertRenewalWatch)
}
s.leaderRoutineManager.Start(caRootPruningRoutineName, s.runCARootPruning)
@ -591,6 +626,70 @@ func (s *Server) pruneCARoots() error {
return nil
}
// secondaryIntermediateCertRenewalWatch checks the intermediate cert for
// expiration. As soon as more than half the time a cert is valid has passed,
// it will try to renew it.
func (s *Server) secondaryIntermediateCertRenewalWatch(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return nil
case <-time.After(intermediateCertRenewInterval):
retryLoopBackoff(ctx.Done(), func() error {
s.caProviderReconfigurationLock.Lock()
defer s.caProviderReconfigurationLock.Unlock()
provider, _ := s.getCAProvider()
if provider == nil {
// this happens when leadership is being revoked and this go routine will be stopped
return nil
}
if !s.configuredSecondaryCA() {
return fmt.Errorf("secondary CA is not yet configured.")
}
state := s.fsm.State()
_, activeRoot, err := state.CARootActive(nil)
if err != nil {
return err
}
activeIntermediate, err := provider.ActiveIntermediate()
if err != nil {
return err
}
if activeIntermediate == "" {
return fmt.Errorf("secondary datacenter doesn't have an active intermediate.")
}
intermediateCert, err := connect.ParseCert(activeIntermediate)
if err != nil {
return fmt.Errorf("error parsing active intermediate cert: %v", err)
}
if lessThanHalfTimePassed(time.Now(), intermediateCert.NotBefore,
intermediateCert.NotAfter) {
return nil
}
if err := s.getIntermediateCASigned(provider, activeRoot); err != nil {
return err
}
if err := s.persistNewRoot(provider, activeRoot); err != nil {
return err
}
s.setCAProvider(provider, activeRoot)
return nil
}, func(err error) {
s.logger.Printf("[ERR] connect: %s: %v", secondaryCertRenewWatchRoutineName, err)
})
}
}
}
// secondaryCARootWatch maintains a blocking query to the primary datacenter's
// ConnectCA.Roots endpoint to monitor when it needs to request a new signed
// intermediate certificate.
@ -642,7 +741,7 @@ func (s *Server) secondaryCARootWatch(ctx context.Context) error {
args.QueryOptions.MinQueryIndex = nextIndexVal(args.QueryOptions.MinQueryIndex, roots.QueryMeta.Index)
return nil
}, func(err error) {
s.logger.Printf("[ERR] connect: %v", err)
s.logger.Printf("[ERR] connect: %s: %v", secondaryCARootWatchRoutineName, err)
})
return nil
@ -699,7 +798,7 @@ func (s *Server) replicateIntentions(ctx context.Context) error {
args.QueryOptions.MinQueryIndex = nextIndexVal(args.QueryOptions.MinQueryIndex, remote.QueryMeta.Index)
return nil
}, func(err error) {
s.logger.Printf("[ERR] connect: error replicating intentions: %v", err)
s.logger.Printf("[ERR] connect: %s: %v", intentionReplicationRoutineName, err)
})
return nil
}
@ -816,6 +915,8 @@ func nextIndexVal(prevIdx, idx uint64) uint64 {
}
// initializeSecondaryProvider configures the given provider for a secondary, non-root datacenter.
// It is being called while holding caProviderReconfigurationLock which means
// it must never take that lock itself or call anything that does.
func (s *Server) initializeSecondaryProvider(provider ca.Provider, roots structs.IndexedCARoots) error {
if roots.TrustDomain == "" {
return fmt.Errorf("trust domain from primary datacenter is not initialized")
@ -845,8 +946,26 @@ func (s *Server) initializeSecondaryProvider(provider ca.Provider, roots structs
return nil
}
// configuredSecondaryCA is being called while holding caProviderReconfigurationLock
// which means it must never take that lock itself or call anything that does.
func (s *Server) configuredSecondaryCA() bool {
s.actingSecondaryLock.RLock()
defer s.actingSecondaryLock.RUnlock()
return s.actingSecondaryCA
}
// halfTime returns a duration that is half the time between notBefore and
// notAfter.
func halfTime(notBefore, notAfter time.Time) time.Duration {
interval := notAfter.Sub(notBefore)
return interval / 2
}
// lessThanHalfTimePassed decides if half the time between notBefore and
// notAfter has passed relative to now.
// lessThanHalfTimePassed is being called while holding caProviderReconfigurationLock
// which means it must never take that lock itself or call anything that does.
func lessThanHalfTimePassed(now, notBefore, notAfter time.Time) bool {
t := notBefore.Add(halfTime(notBefore, notAfter))
return t.Sub(now) > 0
}

View File

@ -162,6 +162,138 @@ func TestLeader_SecondaryCA_Initialize(t *testing.T) {
}
}
func waitForActiveCARoot(t *testing.T, srv *Server, expect *structs.CARoot) {
retry.Run(t, func(r *retry.R) {
_, root := srv.getCAProvider()
if root == nil {
r.Fatal("no root")
}
if root.ID != expect.ID {
r.Fatalf("current active root is %s; waiting for %s", root.ID, expect.ID)
}
})
}
func TestLeader_SecondaryCA_IntermediateRenew(t *testing.T) {
t.Parallel()
intermediateCertRenewInterval = time.Millisecond
require := require.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.Build = "1.6.0"
c.CAConfig = &structs.CAConfiguration{
Provider: "consul",
Config: map[string]interface{}{
"PrivateKey": "",
"RootCert": "",
"RotationPeriod": "2160h",
"LeafCertTTL": "72h",
// The retry loop only retries for 7sec max and
// the ttl needs to be below so that it
// triggers definitely.
// Since certs are created so that they are
// valid from 1minute in the past, we need to
// account for that, otherwise it will be
// expired immediately.
"IntermediateCertTTL": time.Minute + (5 * time.Second),
},
}
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
testrpc.WaitForLeader(t, s1.RPC, "dc1")
// dc2 as a secondary DC
dir2, s2 := testServerWithConfig(t, func(c *Config) {
c.Datacenter = "dc2"
c.PrimaryDatacenter = "dc1"
c.Build = "1.6.0"
})
defer os.RemoveAll(dir2)
defer s2.Shutdown()
// Create the WAN link
joinWAN(t, s2, s1)
testrpc.WaitForLeader(t, s2.RPC, "dc2")
// Get the original intermediate
secondaryProvider, _ := s2.getCAProvider()
intermediatePEM, err := secondaryProvider.ActiveIntermediate()
require.NoError(err)
cert, err := connect.ParseCert(intermediatePEM)
require.NoError(err)
currentCertSerialNumber := cert.SerialNumber
currentCertAuthorityKeyId := cert.AuthorityKeyId
// Capture the current root
var originalRoot *structs.CARoot
{
rootList, activeRoot, err := getTestRoots(s1, "dc1")
require.NoError(err)
require.Len(rootList.Roots, 1)
originalRoot = activeRoot
}
waitForActiveCARoot(t, s1, originalRoot)
waitForActiveCARoot(t, s2, originalRoot)
// Wait for dc2's intermediate to be refreshed.
// It is possible that test fails when the blocking query doesn't return.
// When https://github.com/hashicorp/consul/pull/3777 is merged
// however, defaultQueryTime will be configurable and we con lower it
// so that it returns for sure.
retry.Run(t, func(r *retry.R) {
secondaryProvider, _ := s2.getCAProvider()
intermediatePEM, err = secondaryProvider.ActiveIntermediate()
r.Check(err)
cert, err := connect.ParseCert(intermediatePEM)
r.Check(err)
if cert.SerialNumber.Cmp(currentCertSerialNumber) == 0 || !reflect.DeepEqual(cert.AuthorityKeyId, currentCertAuthorityKeyId) {
currentCertSerialNumber = cert.SerialNumber
currentCertAuthorityKeyId = cert.AuthorityKeyId
r.Fatal("not a renewed intermediate")
}
})
require.NoError(err)
// Get the new root from dc1 and validate a chain of:
// dc2 leaf -> dc2 intermediate -> dc1 root
_, caRoot := s1.getCAProvider()
// Have dc2 sign a leaf cert and make sure the chain is correct.
spiffeService := &connect.SpiffeIDService{
Host: "node1",
Namespace: "default",
Datacenter: "dc1",
Service: "foo",
}
raw, _ := connect.TestCSR(t, spiffeService)
leafCsr, err := connect.ParseCSR(raw)
require.NoError(err)
leafPEM, err := secondaryProvider.Sign(leafCsr)
require.NoError(err)
cert, err = connect.ParseCert(leafPEM)
require.NoError(err)
// Check that the leaf signed by the new intermediate can be verified using the
// returned cert chain (signed intermediate + remote root).
intermediatePool := x509.NewCertPool()
intermediatePool.AppendCertsFromPEM([]byte(intermediatePEM))
rootPool := x509.NewCertPool()
rootPool.AppendCertsFromPEM([]byte(caRoot.RootCert))
_, err = cert.Verify(x509.VerifyOptions{
Intermediates: intermediatePool,
Roots: rootPool,
})
require.NoError(err)
}
func TestLeader_SecondaryCA_IntermediateRefresh(t *testing.T) {
t.Parallel()
@ -214,9 +346,10 @@ func TestLeader_SecondaryCA_IntermediateRefresh(t *testing.T) {
newConfig := &structs.CAConfiguration{
Provider: "consul",
Config: map[string]interface{}{
"PrivateKey": newKey,
"RootCert": "",
"RotationPeriod": 90 * 24 * time.Hour,
"PrivateKey": newKey,
"RootCert": "",
"RotationPeriod": 90 * 24 * time.Hour,
"IntermediateCertTTL": 72 * 24 * time.Hour,
},
}
{
@ -1292,3 +1425,13 @@ func readTestData(t *testing.T, name string) string {
}
return string(bs)
}
func TestLeader_lessThanHalfTimePassed(t *testing.T) {
now := time.Now()
require.False(t, lessThanHalfTimePassed(now, now.Add(-10*time.Second), now.Add(-5*time.Second)))
require.False(t, lessThanHalfTimePassed(now, now.Add(-10*time.Second), now))
require.False(t, lessThanHalfTimePassed(now, now.Add(-10*time.Second), now.Add(5*time.Second)))
require.False(t, lessThanHalfTimePassed(now, now.Add(-10*time.Second), now.Add(10*time.Second)))
require.True(t, lessThanHalfTimePassed(now, now.Add(-10*time.Second), now.Add(20*time.Second)))
}

View File

@ -89,16 +89,17 @@ const (
)
const (
legacyACLReplicationRoutineName = "legacy ACL replication"
aclPolicyReplicationRoutineName = "ACL policy replication"
aclRoleReplicationRoutineName = "ACL role replication"
aclTokenReplicationRoutineName = "ACL token replication"
aclTokenReapingRoutineName = "acl token reaping"
aclUpgradeRoutineName = "legacy ACL token upgrade"
caRootPruningRoutineName = "CA root pruning"
configReplicationRoutineName = "config entry replication"
intentionReplicationRoutineName = "intention replication"
secondaryCARootWatchRoutineName = "secondary CA roots watch"
legacyACLReplicationRoutineName = "legacy ACL replication"
aclPolicyReplicationRoutineName = "ACL policy replication"
aclRoleReplicationRoutineName = "ACL role replication"
aclTokenReplicationRoutineName = "ACL token replication"
aclTokenReapingRoutineName = "acl token reaping"
aclUpgradeRoutineName = "legacy ACL token upgrade"
caRootPruningRoutineName = "CA root pruning"
configReplicationRoutineName = "config entry replication"
intentionReplicationRoutineName = "intention replication"
secondaryCARootWatchRoutineName = "secondary CA roots watch"
secondaryCertRenewWatchRoutineName = "secondary cert renew watch"
)
var (
@ -126,6 +127,8 @@ type Server struct {
// autopilotWaitGroup is used to block until Autopilot shuts down.
autopilotWaitGroup sync.WaitGroup
// caProviderReconfigurationLock guards the provider reconfiguration.
caProviderReconfigurationLock sync.Mutex
// caProvider is the current CA provider in use for Connect. This is
// only non-nil when we are the leader.
caProvider ca.Provider

View File

@ -155,10 +155,11 @@ func testServerConfig(t *testing.T) (string, *Config) {
ClusterID: connect.TestClusterID,
Provider: structs.ConsulCAProvider,
Config: map[string]interface{}{
"PrivateKey": "",
"RootCert": "",
"RotationPeriod": "2160h",
"LeafCertTTL": "72h",
"PrivateKey": "",
"RootCert": "",
"RotationPeriod": "2160h",
"LeafCertTTL": "72h",
"IntermediateCertTTL": "72h",
},
}

View File

@ -424,9 +424,10 @@ func (c CommonCAProviderConfig) Validate() error {
type ConsulCAProviderConfig struct {
CommonCAProviderConfig `mapstructure:",squash"`
PrivateKey string
RootCert string
RotationPeriod time.Duration
PrivateKey string
RootCert string
RotationPeriod time.Duration
IntermediateCertTTL time.Duration
// DisableCrossSigning is really only useful in test code to use the built in
// provider while exercising logic that depends on the CA provider ability to

View File

@ -39,9 +39,10 @@ type CommonCAProviderConfig struct {
type ConsulCAProviderConfig struct {
CommonCAProviderConfig `mapstructure:",squash"`
PrivateKey string
RootCert string
RotationPeriod time.Duration
PrivateKey string
RootCert string
RotationPeriod time.Duration
IntermediateCertTTL time.Duration
}
// ParseConsulCAConfig takes a raw config map and returns a parsed

View File

@ -62,7 +62,8 @@ func TestAPI_ConnectCAConfig_get_set(t *testing.T) {
s.WaitForSerfCheck(t)
expected := &ConsulCAProviderConfig{
RotationPeriod: 90 * 24 * time.Hour,
RotationPeriod: 90 * 24 * time.Hour,
IntermediateCertTTL: 365 * 24 * time.Hour,
}
expected.LeafCertTTL = 72 * time.Hour
@ -83,15 +84,19 @@ func TestAPI_ConnectCAConfig_get_set(t *testing.T) {
// Change a config value and update
conf.Config["PrivateKey"] = ""
conf.Config["RotationPeriod"] = 120 * 24 * time.Hour
conf.Config["IntermediateCertTTL"] = 300 * 24 * time.Hour
// Pass through some state as if the provider stored it so we can make sure
// we can read it again.
conf.Config["test_state"] = map[string]string{"foo": "bar"}
_, err = connect.CASetConfig(conf, nil)
r.Check(err)
updated, _, err := connect.CAGetConfig(nil)
r.Check(err)
expected.RotationPeriod = 120 * 24 * time.Hour
expected.IntermediateCertTTL = 300 * 24 * time.Hour
parsed, err = ParseConsulCAConfig(updated.Config)
r.Check(err)
require.Equal(r, expected, parsed)

View File

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

View File

@ -3,6 +3,7 @@
"Config": {
"PrivateKey": "",
"RootCert": "",
"RotationPeriod": "24h"
"RotationPeriod": "24h",
"IntermediateCertTTL": "36h"
}
}
}

View File

@ -16,10 +16,6 @@ import (
)
const (
// blockSize is the size of the allocated port block. ports are given out
// consecutively from that block and after that point in a LRU fashion.
blockSize = 1500
// maxBlocks is the number of available port blocks before exclusions.
maxBlocks = 30
@ -32,6 +28,10 @@ const (
)
var (
// blockSize is the size of the allocated port block. ports are given out
// consecutively from that block and after that point in a LRU fashion.
blockSize int
// effectiveMaxBlocks is the number of available port blocks.
// lowPort + effectiveMaxBlocks * blockSize must be less than 65535.
effectiveMaxBlocks int
@ -71,6 +71,17 @@ var (
// initialize is used to initialize freeport.
func initialize() {
var err error
blockSize = 1500
limit, err := systemLimit()
if err != nil {
panic("freeport: error getting system limit: " + err.Error())
}
if limit > 0 && limit < blockSize {
logf("INFO", "blockSize %d too big for system limit %d. Adjusting...", blockSize, limit)
blockSize = limit - 3
}
effectiveMaxBlocks, err = adjustMaxBlocks()
if err != nil {
panic("freeport: ephemeral port range detection failed: " + err.Error())

View File

@ -185,10 +185,10 @@ func TestTakeReturn(t *testing.T) {
c.Close()
}
}()
for _, port := range allPorts {
for i, port := range allPorts {
ln, err := net.ListenTCP("tcp", tcpAddr("127.0.0.1", port))
if err != nil {
t.Fatalf("err: %v", err)
t.Fatalf("%d err: %v", i, err)
}
leaked = append(leaked, ln)
}

View File

@ -0,0 +1,11 @@
// +build !windows
package freeport
import "golang.org/x/sys/unix"
func systemLimit() (int, error) {
var limit unix.Rlimit
err := unix.Getrlimit(unix.RLIMIT_NOFILE, &limit)
return int(limit.Cur), err
}

View File

@ -0,0 +1,7 @@
// +build windows
package freeport
func systemLimit() (int, error) {
return 0, nil
}

View File

@ -24,7 +24,7 @@ func WaitForLeader(t *testing.T, rpc rpcFn, dc string) {
r.Fatalf("No leader")
}
if out.Index < 2 {
r.Fatalf("Consul index should be at least 2")
r.Fatalf("Consul index should be at least 2 in %s", dc)
}
})
}

View File

@ -97,7 +97,8 @@ $ curl \
"Provider": "consul",
"Config": {
"LeafCertTTL": "72h",
"RotationPeriod": "2160h"
"RotationPeriod": "2160h",
"IntermediateCertTTL": "8760h"
},
"CreateIndex": 5,
"ModifyIndex": 5
@ -148,7 +149,8 @@ providers, see [Provider Config](/docs/connect/ca.html).
"LeafCertTTL": "72h",
"PrivateKey": "-----BEGIN RSA PRIVATE KEY-----...",
"RootCert": "-----BEGIN CERTIFICATE-----...",
"RotationPeriod": "2160h"
"RotationPeriod": "2160h",
"IntermediateCertTTL": "8760h"
},
"ForceWithoutCrossSigning": false
}

View File

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

View File

@ -73,7 +73,8 @@ $ curl localhost:8500/v1/connect/ca/configuration
"Provider": "consul",
"Config": {
"LeafCertTTL": "72h",
"RotationPeriod": "2160h"
"RotationPeriod": "2160h",
"IntermediateCertTTL": "8760h"
},
"CreateIndex": 5,
"ModifyIndex": 5
@ -106,7 +107,8 @@ $ jq -n --arg key "$(cat root.key)" --arg cert "$(cat root.crt)" '
"LeafCertTTL": "72h",
"PrivateKey": $key,
"RootCert": $cert,
"RotationPeriod": "2160h"
"RotationPeriod": "2160h",
"IntermediateCertTTL": "8760h"
}
}' > ca_config.json
```
@ -121,7 +123,8 @@ $ cat ca_config.json
"LeafCertTTL": "72h",
"PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEArqiy1c3pbT3cSkjdEM1APALUareU...",
"RootCert": "-----BEGIN CERTIFICATE-----\nMIIDijCCAnKgAwIBAgIJAOFZ66em1qC7MA0GCSqGSIb3...",
"RotationPeriod": "2160h"
"RotationPeriod": "2160h",
"IntermediateCertTTL": "8760h"
}
}