Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 3 additions & 63 deletions ca/certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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
Expand Down
79 changes: 0 additions & 79 deletions ca/certificates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
116 changes: 4 additions & 112 deletions manager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package manager

import (
"crypto/tls"
"encoding/pem"
"fmt"
"net"
"os"
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
Loading