From 9369c1eff40e7fb6f821194ce1a6973a3ec7605f Mon Sep 17 00:00:00 2001 From: Ying Li Date: Mon, 26 Mar 2018 17:38:10 -0700 Subject: [PATCH] Stop encrypting the raft root CA key entirely based on env vars, since that feature was deprecated almost a year ago. Rely on MTLS for encryption in transit and raft log encryption for encryption at rest. Signed-off-by: Ying Li --- ca/certificates.go | 66 +---------------- ca/certificates_test.go | 79 -------------------- manager/manager.go | 116 ++---------------------------- manager/manager_test.go | 155 ---------------------------------------- 4 files changed, 7 insertions(+), 409 deletions(-) diff --git a/ca/certificates.go b/ca/certificates.go index a5b31d9cce..6bad7f9a9c 100644 --- a/ca/certificates.go +++ b/ca/certificates.go @@ -52,13 +52,6 @@ const ( RootKeySize = 256 // RootKeyAlgo defines the default algorithm for the root CA Key RootKeyAlgo = "ecdsa" - // PassphraseENVVar defines the environment variable to look for the - // root CA private key material encryption key - PassphraseENVVar = "SWARM_ROOT_CA_PASSPHRASE" - // PassphraseENVVarPrev defines the alternate environment variable to look for the - // root CA private key material encryption key. It can be used for seamless - // KEK rotations. - PassphraseENVVarPrev = "SWARM_ROOT_CA_PASSPHRASE_PREV" // RootCAExpiration represents the default expiration for the root CA in seconds (20 years) RootCAExpiration = "630720000s" // DefaultNodeCertExpiration represents the default expiration for node certificates (3 months) @@ -642,28 +635,10 @@ func newLocalSigner(keyBytes, certBytes []byte, certExpiry time.Duration, rootPo return nil, errors.Wrap(err, "error while validating signing CA certificate against roots and intermediates") } - var ( - passphraseStr string - passphrase, passphrasePrev []byte - priv crypto.Signer - ) - - // Attempt two distinct passphrases, so we can do a hitless passphrase rotation - if passphraseStr = os.Getenv(PassphraseENVVar); passphraseStr != "" { - passphrase = []byte(passphraseStr) - } - - if p := os.Getenv(PassphraseENVVarPrev); p != "" { - passphrasePrev = []byte(p) - } - - // Attempt to decrypt the current private-key with the passphrases provided - priv, err = keyutils.ParsePrivateKeyPEMWithPassword(keyBytes, passphrase) + // The key should not be encrypted, but it could be in PKCS8 format rather than PKCS1 + priv, err := keyutils.ParsePrivateKeyPEMWithPassword(keyBytes, nil) if err != nil { - priv, err = keyutils.ParsePrivateKeyPEMWithPassword(keyBytes, passphrasePrev) - if err != nil { - return nil, errors.Wrap(err, "malformed private key") - } + return nil, errors.Wrap(err, "malformed private key") } // We will always use the first certificate inside of the root bundle as the active one @@ -676,17 +651,6 @@ func newLocalSigner(keyBytes, certBytes []byte, certExpiry time.Duration, rootPo return nil, err } - // If the key was loaded from disk unencrypted, but there is a passphrase set, - // ensure it is encrypted, so it doesn't hit raft in plain-text - // we don't have to check for nil, because if we couldn't pem-decode the bytes, then parsing above would have failed - keyBlock, _ := pem.Decode(keyBytes) - if passphraseStr != "" && !keyutils.IsEncryptedPEMBlock(keyBlock) { - keyBytes, err = EncryptECPrivateKey(keyBytes, passphraseStr) - if err != nil { - return nil, errors.Wrap(err, "unable to encrypt signing CA key material") - } - } - return &LocalSigner{Cert: certBytes, Key: keyBytes, Signer: signer, parsedCert: parsedCerts[0], cryptoSigner: priv}, nil } @@ -977,30 +941,6 @@ func GenerateNewCSR() ([]byte, []byte, error) { return csr, key, err } -// EncryptECPrivateKey receives a PEM encoded private key and returns an encrypted -// AES256 version using a passphrase -// TODO: Make this method generic to handle RSA keys -func EncryptECPrivateKey(key []byte, passphraseStr string) ([]byte, error) { - passphrase := []byte(passphraseStr) - - keyBlock, _ := pem.Decode(key) - if keyBlock == nil { - // This RootCA does not have a valid signer. - return nil, errors.New("error while decoding PEM key") - } - - encryptedPEMBlock, err := keyutils.EncryptPEMBlock(keyBlock.Bytes, passphrase) - if err != nil { - return nil, err - } - - if encryptedPEMBlock.Headers == nil { - return nil, errors.New("unable to encrypt key - invalid PEM file produced") - } - - return pem.EncodeToMemory(encryptedPEMBlock), nil -} - // NormalizePEMs takes a bundle of PEM-encoded certificates in a certificate bundle, // decodes them, removes headers, and re-encodes them to make sure that they have // consistent whitespace. Note that this is intended to normalize x509 certificates diff --git a/ca/certificates_test.go b/ca/certificates_test.go index 20269829c9..205534a6ef 100644 --- a/ca/certificates_test.go +++ b/ca/certificates_test.go @@ -27,8 +27,6 @@ import ( "github.com/docker/go-events" "github.com/docker/swarmkit/api" "github.com/docker/swarmkit/ca" - "github.com/docker/swarmkit/ca/keyutils" - "github.com/docker/swarmkit/ca/pkcs8" cautils "github.com/docker/swarmkit/ca/testutils" "github.com/docker/swarmkit/connectionbroker" "github.com/docker/swarmkit/fips" @@ -45,9 +43,6 @@ import ( ) func init() { - os.Setenv(ca.PassphraseENVVar, "") - os.Setenv(ca.PassphraseENVVarPrev, "") - ca.RenewTLSExponentialBackoff = events.ExponentialBackoffConfig{ Base: 250 * time.Millisecond, Factor: 250 * time.Millisecond, @@ -232,21 +227,6 @@ some random garbage\n require.Error(t, err) } -func TestEncryptECPrivateKey(t *testing.T) { - tempBaseDir, err := ioutil.TempDir("", "swarm-ca-test-") - assert.NoError(t, err) - defer os.RemoveAll(tempBaseDir) - - _, key, err := ca.GenerateNewCSR() - assert.NoError(t, err) - encryptedKey, err := ca.EncryptECPrivateKey(key, "passphrase") - assert.NoError(t, err) - - keyBlock, _ := pem.Decode(encryptedKey) - assert.NotNil(t, keyBlock) - assert.True(t, pkcs8.IsEncryptedPEMBlock(keyBlock)) -} - func TestParseValidateAndSignCSR(t *testing.T) { rootCA, err := ca.CreateRootCA("rootCN") assert.NoError(t, err) @@ -1332,65 +1312,6 @@ func TestRootCAWithCrossSignedIntermediates(t *testing.T) { checkValidateAgainstAllRoots(tlsCert) } -func TestNewRootCAWithPassphrase(t *testing.T) { - defer os.Setenv(ca.PassphraseENVVar, "") - defer os.Setenv(ca.PassphraseENVVarPrev, "") - - rootCA, err := ca.CreateRootCA("rootCN") - assert.NoError(t, err) - rcaSigner, err := rootCA.Signer() - assert.NoError(t, err) - - // Ensure that we're encrypting the Key bytes out of NewRoot if there - // is a passphrase set as an env Var - os.Setenv(ca.PassphraseENVVar, "password1") - newRootCA, err := ca.NewRootCA(rootCA.Certs, rcaSigner.Cert, rcaSigner.Key, ca.DefaultNodeCertExpiration, nil) - assert.NoError(t, err) - nrcaSigner, err := newRootCA.Signer() - assert.NoError(t, err) - assert.NotEqual(t, rcaSigner.Key, nrcaSigner.Key) - assert.Equal(t, rootCA.Certs, newRootCA.Certs) - assert.NotContains(t, string(rcaSigner.Key), string(nrcaSigner.Key)) - keyBlock, _ := pem.Decode(nrcaSigner.Key) - assert.NotNil(t, keyBlock) - assert.True(t, keyutils.IsEncryptedPEMBlock(keyBlock)) - - // Ensure that we're decrypting the Key bytes out of NewRoot if there - // is a passphrase set as an env Var - anotherNewRootCA, err := ca.NewRootCA(newRootCA.Certs, nrcaSigner.Cert, nrcaSigner.Key, ca.DefaultNodeCertExpiration, nil) - assert.NoError(t, err) - anrcaSigner, err := anotherNewRootCA.Signer() - assert.NoError(t, err) - assert.Equal(t, newRootCA, anotherNewRootCA) - assert.NotContains(t, string(rcaSigner.Key), string(anrcaSigner.Key)) - keyBlock, _ = pem.Decode(anrcaSigner.Key) - assert.NotNil(t, keyBlock) - assert.True(t, keyutils.IsEncryptedPEMBlock(keyBlock)) - - // Ensure that we cant decrypt the Key bytes out of NewRoot if there - // is a wrong passphrase set as an env Var - os.Setenv(ca.PassphraseENVVar, "password2") - anotherNewRootCA, err = ca.NewRootCA(newRootCA.Certs, nrcaSigner.Cert, nrcaSigner.Key, ca.DefaultNodeCertExpiration, nil) - assert.Error(t, err) - - // Ensure that we cant decrypt the Key bytes out of NewRoot if there - // is a wrong passphrase set as an env Var - os.Setenv(ca.PassphraseENVVarPrev, "password2") - anotherNewRootCA, err = ca.NewRootCA(newRootCA.Certs, nrcaSigner.Cert, nrcaSigner.Key, ca.DefaultNodeCertExpiration, nil) - assert.Error(t, err) - - // Ensure that we can decrypt the Key bytes out of NewRoot if there - // is a wrong passphrase set as an env Var, but a valid as Prev - os.Setenv(ca.PassphraseENVVarPrev, "password1") - anotherNewRootCA, err = ca.NewRootCA(newRootCA.Certs, nrcaSigner.Cert, nrcaSigner.Key, ca.DefaultNodeCertExpiration, nil) - assert.NoError(t, err) - assert.Equal(t, newRootCA, anotherNewRootCA) - assert.NotContains(t, string(rcaSigner.Key), string(anrcaSigner.Key)) - keyBlock, _ = pem.Decode(anrcaSigner.Key) - assert.NotNil(t, keyBlock) - assert.True(t, keyutils.IsEncryptedPEMBlock(keyBlock)) -} - type certTestCase struct { cert []byte errorStr string diff --git a/manager/manager.go b/manager/manager.go index 0e897bcd14..820ad6631c 100644 --- a/manager/manager.go +++ b/manager/manager.go @@ -2,7 +2,6 @@ package manager import ( "crypto/tls" - "encoding/pem" "fmt" "net" "os" @@ -17,7 +16,6 @@ import ( gmetrics "github.com/docker/go-metrics" "github.com/docker/swarmkit/api" "github.com/docker/swarmkit/ca" - "github.com/docker/swarmkit/ca/keyutils" "github.com/docker/swarmkit/connectionbroker" "github.com/docker/swarmkit/identity" "github.com/docker/swarmkit/log" @@ -806,109 +804,6 @@ func (m *Manager) watchForClusterChanges(ctx context.Context) error { return nil } -// rotateRootCAKEK will attempt to rotate the key-encryption-key for root CA key-material in raft. -// If there is no passphrase set in ENV, it returns. -// If there is plain-text root key-material, and a passphrase set, it encrypts it. -// If there is encrypted root key-material and it is using the current passphrase, it returns. -// If there is encrypted root key-material, and it is using the previous passphrase, it -// re-encrypts it with the current passphrase. -func (m *Manager) rotateRootCAKEK(ctx context.Context, clusterID string) error { - // If we don't have a KEK, we won't ever be rotating anything - strPassphrase := os.Getenv(ca.PassphraseENVVar) - strPassphrasePrev := os.Getenv(ca.PassphraseENVVarPrev) - if strPassphrase == "" && strPassphrasePrev == "" { - return nil - } - if strPassphrase != "" { - log.G(ctx).Warn("Encrypting the root CA key in swarm using environment variables is deprecated. " + - "Support for decrypting or rotating the key will be removed in the future.") - } - - passphrase := []byte(strPassphrase) - passphrasePrev := []byte(strPassphrasePrev) - - s := m.raftNode.MemoryStore() - var ( - cluster *api.Cluster - err error - finalKey []byte - ) - // Retrieve the cluster identified by ClusterID - return s.Update(func(tx store.Tx) error { - cluster = store.GetCluster(tx, clusterID) - if cluster == nil { - return fmt.Errorf("cluster not found: %s", clusterID) - } - - // Try to get the private key from the cluster - privKeyPEM := cluster.RootCA.CAKey - if len(privKeyPEM) == 0 { - // We have no PEM root private key in this cluster. - log.G(ctx).Warnf("cluster %s does not have private key material", clusterID) - return nil - } - - // Decode the PEM private key - keyBlock, _ := pem.Decode(privKeyPEM) - if keyBlock == nil { - return fmt.Errorf("invalid PEM-encoded private key inside of cluster %s", clusterID) - } - - if keyutils.IsEncryptedPEMBlock(keyBlock) { - // PEM encryption does not have a digest, so sometimes decryption doesn't - // error even with the wrong passphrase. So actually try to parse it into a valid key. - _, err := keyutils.ParsePrivateKeyPEMWithPassword(privKeyPEM, []byte(passphrase)) - if err == nil { - // This key is already correctly encrypted with the correct KEK, nothing to do here - return nil - } - - // This key is already encrypted, but failed with current main passphrase. - // Let's try to decrypt with the previous passphrase, and parse into a valid key, for the - // same reason as above. - _, err = keyutils.ParsePrivateKeyPEMWithPassword(privKeyPEM, []byte(passphrasePrev)) - if err != nil { - // We were not able to decrypt either with the main or backup passphrase, error - return err - } - // ok the above passphrase is correct, so decrypt the PEM block so we can re-encrypt - - // since the key was successfully decrypted above, there will be no error doing PEM - // decryption - unencryptedDER, _ := keyutils.DecryptPEMBlock(keyBlock, []byte(passphrasePrev)) - unencryptedKeyBlock := &pem.Block{ - Type: keyBlock.Type, - Bytes: unencryptedDER, - } - - // we were able to decrypt the key with the previous passphrase - if the current passphrase is empty, - // the we store the decrypted key in raft - finalKey = pem.EncodeToMemory(unencryptedKeyBlock) - - // the current passphrase is not empty, so let's encrypt with the new one and store it in raft - if strPassphrase != "" { - finalKey, err = ca.EncryptECPrivateKey(finalKey, strPassphrase) - if err != nil { - log.G(ctx).WithError(err).Debugf("failed to rotate the key-encrypting-key for the root key material of cluster %s", clusterID) - return err - } - } - } else if strPassphrase != "" { - // If this key is not encrypted, and the passphrase is not nil, then we have to encrypt it - finalKey, err = ca.EncryptECPrivateKey(privKeyPEM, strPassphrase) - if err != nil { - log.G(ctx).WithError(err).Debugf("failed to rotate the key-encrypting-key for the root key material of cluster %s", clusterID) - return err - } - } else { - return nil // don't update if it's not encrypted and we don't want it encrypted - } - - log.G(ctx).Infof("Updating the encryption on the root key material of cluster %s", clusterID) - cluster.RootCA.CAKey = finalKey - return store.UpdateCluster(tx, cluster) - }) -} - // getLeaderNodeID is a small helper function returning a string with the // leader's node ID. it is only used for logging, and should not be relied on // to give a node ID for actual operational purposes (because it returns errors @@ -1013,7 +908,10 @@ func (m *Manager) becomeLeader(ctx context.Context) { initialCAConfig := ca.DefaultCAConfig() initialCAConfig.ExternalCAs = m.config.ExternalCAs - var unlockKeys []*api.EncryptionKey + var ( + unlockKeys []*api.EncryptionKey + err error + ) if m.config.AutoLockManagers { unlockKeys = []*api.EncryptionKey{{ Subsystem: ca.ManagerRole, @@ -1066,12 +964,6 @@ func (m *Manager) becomeLeader(ctx context.Context) { return nil }) - // Attempt to rotate the key-encrypting-key of the root CA key-material - err := m.rotateRootCAKEK(ctx, clusterID) - if err != nil { - log.G(ctx).WithError(err).Error("root key-encrypting-key rotation failed") - } - m.replicatedOrchestrator = replicated.NewReplicatedOrchestrator(s) m.constraintEnforcer = constraintenforcer.New(s) m.globalOrchestrator = global.NewGlobalOrchestrator(s) diff --git a/manager/manager_test.go b/manager/manager_test.go index 8fc4c30d37..03b035efa2 100644 --- a/manager/manager_test.go +++ b/manager/manager_test.go @@ -423,158 +423,3 @@ func TestManagerLockUnlock(t *testing.T) { // error. <-done } - -// Tests manager rotates encryption of root key data in the raft store -func TestManagerEncryptsDecryptsRootKeyMaterial(t *testing.T) { - tc := cautils.NewTestCA(t) - defer tc.Stop() - - temp, err := ioutil.TempFile("", "test-socket") - require.NoError(t, err) - require.NoError(t, temp.Close()) - require.NoError(t, os.Remove(temp.Name())) - - defer os.RemoveAll(temp.Name()) - - stateDir, err := ioutil.TempDir("", "test-raft") - require.NoError(t, err) - defer os.RemoveAll(stateDir) - - managerSecurityConfig, err := tc.NewNodeConfig(ca.ManagerRole) - require.NoError(t, err) - - _, _, err = managerSecurityConfig.KeyReader().Read() - require.NoError(t, err) - - config := Config{ - RemoteAPI: &RemoteAddrs{ListenAddr: "127.0.0.1:0"}, - ControlAPI: temp.Name(), - StateDir: stateDir, - SecurityConfig: managerSecurityConfig, - RootCAPaths: tc.Paths.RootCA, - } - done := make(chan error) - defer close(done) - - var m *Manager - startManager := func() { - m, err = New(&config) - require.NoError(t, err) - require.NotNil(t, m) - - go func() { - done <- m.Run(tc.Context) - }() - } - - startManager() - - var clusterID string - // wait for cluster data to be there - err = testutils.PollFunc(nil, func() error { - // using store.Update just because it returns an error, as opposed to store.View - return m.raftNode.MemoryStore().Update(func(tx store.Tx) error { - clusters, err := store.FindClusters(tx, store.All) - if err != nil { - return err - } - if len(clusters) != 1 { - return fmt.Errorf("expected 1 cluster, got %d", len(clusters)) - } - clusterID = clusters[0].ID - return nil - }) - }) - - os.Setenv(ca.PassphraseENVVar, "kek") - defer os.Unsetenv(ca.PassphraseENVVar) - - // restart - m.Stop(tc.Context, false) - <-done - startManager() - - // wait for the key to be encrypted in the raft store - err = testutils.PollFunc(nil, func() error { - return m.raftNode.MemoryStore().Update(func(tx store.Tx) error { - cluster := store.GetCluster(tx, clusterID) - if cluster == nil { - return fmt.Errorf("cluster gone") - } - keyBlock, _ := pem.Decode(cluster.RootCA.CAKey) - if keyBlock == nil { - return fmt.Errorf("could not pem decode root key") - } - if !keyutils.IsEncryptedPEMBlock(keyBlock) { - return fmt.Errorf("root key material not encrypted yet") - } - _, err = keyutils.DecryptPEMBlock(keyBlock, []byte("kek")) - return err - }) - }) - require.NoError(t, err) - - os.Unsetenv(ca.PassphraseENVVar) - os.Setenv(ca.PassphraseENVVarPrev, "kek") - defer os.Unsetenv(ca.PassphraseENVVarPrev) - - // restart - m.Stop(tc.Context, false) - <-done - startManager() - - // wait for the key to be decrypted in the raft store - pollDecrypted := func() error { - return testutils.PollFunc(nil, func() error { - // wait until we are leader first, because otherwise the raft node could still be catching - // up on all the logs on disk and hence not have processed the "encrypt CA key" log yet - if !m.raftNode.IsLeader() { - return fmt.Errorf("node is not leader yet") - } - return m.raftNode.MemoryStore().Update(func(tx store.Tx) error { - cluster := store.GetCluster(tx, clusterID) - if cluster == nil { - return fmt.Errorf("cluster gone") - } - keyBlock, _ := pem.Decode(cluster.RootCA.CAKey) - if keyBlock == nil { - return fmt.Errorf("could not pem decode root key") - } - if keyutils.IsEncryptedPEMBlock(keyBlock) { - return fmt.Errorf("root key material not decrypted yet") - } - return nil - }) - }) - } - require.NoError(t, pollDecrypted()) - - // update the key to that can be "decrypted" with both "" and "kek" as the password. This - // doesn't actually match the root CA certificate, and hence the security config can't be - // updated, but we're just checking that the CA key is decrypted. - require.NoError(t, m.raftNode.MemoryStore().Update(func(tx store.Tx) error { - cluster := store.GetCluster(tx, clusterID) - if cluster == nil { - return fmt.Errorf("cluster gone") - } - cluster.RootCA.CAKey = []byte(` ------BEGIN ENCRYPTED PRIVATE KEY----- -MIHeMEkGCSqGSIb3DQEFDTA8MBsGCSqGSIb3DQEFDDAOBAiLGJtiTmJ3rQICCAAw -HQYJYIZIAWUDBAEqBBBeDoliB0Qe73DdcMeFCuRzBIGQP/iFMPj9BJ/81GV//fMp -KPozbY0EWodXt7KArbeROd5+uWw1muLANUa3KkkXyQhmzlR2Zv3Y/kBuPay9RweU -md94ZD/HY9K+ISv4tIA7u8gp2Hqr0elfG0QqBuwrh688ZF5jii6umZzXtLVVMvWd -NF7w1CA6b8w1aTIklVjv0AJ9tgtGQb9phVigPAdyyw6v ------END ENCRYPTED PRIVATE KEY----- -`) - return store.UpdateCluster(tx, cluster) - })) - - // restart - m.Stop(tc.Context, false) - <-done - startManager() - require.NoError(t, pollDecrypted()) - - m.Stop(tc.Context, false) - <-done -}