Add the root rotation mechanism to the CA config endpoint
This commit is contained in:
parent
a585a0ba10
commit
bbfcb278e1
|
@ -38,6 +38,18 @@ func ParseSigner(pemValue string) (crypto.Signer, error) {
|
||||||
case "EC PRIVATE KEY":
|
case "EC PRIVATE KEY":
|
||||||
return x509.ParseECPrivateKey(block.Bytes)
|
return x509.ParseECPrivateKey(block.Bytes)
|
||||||
|
|
||||||
|
case "PRIVATE KEY":
|
||||||
|
signer, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pk, ok := signer.(crypto.Signer)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("private key is not a valid format")
|
||||||
|
}
|
||||||
|
|
||||||
|
return pk, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown PEM block type for signing key: %s", block.Type)
|
return nil, fmt.Errorf("unknown PEM block type for signing key: %s", block.Type)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,10 @@ import (
|
||||||
// an external CA that provides leaf certificate signing for
|
// an external CA that provides leaf certificate signing for
|
||||||
// given SpiffeIDServices.
|
// given SpiffeIDServices.
|
||||||
type CAProvider interface {
|
type CAProvider interface {
|
||||||
SetConfiguration(raw map[string]interface{}) error
|
|
||||||
ActiveRoot() (*structs.CARoot, error)
|
ActiveRoot() (*structs.CARoot, error)
|
||||||
ActiveIntermediate() (*structs.CARoot, error)
|
ActiveIntermediate() (*structs.CARoot, error)
|
||||||
GenerateIntermediate() (*structs.CARoot, error)
|
GenerateIntermediate() (*structs.CARoot, error)
|
||||||
Sign(*SpiffeIDService, *x509.CertificateRequest) (*structs.IssuedCert, error)
|
Sign(*SpiffeIDService, *x509.CertificateRequest) (*structs.IssuedCert, error)
|
||||||
|
//SignCA(*x509.CertificateRequest) (*structs.IssuedCert, error)
|
||||||
|
Teardown() error
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package consul
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/acl"
|
"github.com/hashicorp/consul/acl"
|
||||||
"github.com/hashicorp/consul/agent/connect"
|
"github.com/hashicorp/consul/agent/connect"
|
||||||
|
@ -60,9 +61,95 @@ func (s *ConnectCA) ConfigurationSet(
|
||||||
return acl.ErrPermissionDenied
|
return acl.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit
|
// Exit early if it's a no-op change
|
||||||
// todo(kyhavlov): trigger a bootstrap here when the provider changes
|
state := s.srv.fsm.State()
|
||||||
args.Op = structs.CAOpSetConfig
|
_, config, err := state.CAConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if args.Config.Provider == config.Provider && reflect.DeepEqual(args.Config.Config, config.Config) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new instance of the provider described by the config
|
||||||
|
// and get the current active root CA. This acts as a good validation
|
||||||
|
// of the config and makes sure the provider is functioning correctly
|
||||||
|
// before we commit any changes to Raft.
|
||||||
|
newProvider, err := s.srv.createCAProvider(args.Config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not initialize provider: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newActiveRoot, err := newProvider.ActiveRoot()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare the new provider's root CA ID to the current one. If they
|
||||||
|
// match, just update the existing provider with the new config.
|
||||||
|
// If they don't match, begin the root rotation process.
|
||||||
|
_, root, err := state.CARootActive(nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if root != nil && root.ID == newActiveRoot.ID {
|
||||||
|
args.Op = structs.CAOpSetConfig
|
||||||
|
resp, err := s.srv.raftApply(structs.ConnectCARequestType, args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if respErr, ok := resp.(error); ok {
|
||||||
|
return respErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the config has been committed, update the local provider instance
|
||||||
|
s.srv.setCAProvider(newProvider)
|
||||||
|
|
||||||
|
s.srv.logger.Printf("[INFO] connect: provider config updated")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, we know the config change has trigged a root rotation,
|
||||||
|
// either by swapping the provider type or changing the provider's config
|
||||||
|
// to use a different root certificate.
|
||||||
|
|
||||||
|
// If it's a config change that would trigger a rotation (different provider/root):
|
||||||
|
// -1. Create an instance of the provider described by the new config
|
||||||
|
// 2. Get the intermediate from the new provider
|
||||||
|
// 3. Generate a CSR for the new intermediate, call SignCA on the old/current provider
|
||||||
|
// to get the cross-signed intermediate
|
||||||
|
// ~4. Get the active root for the new provider, append the intermediate from step 3
|
||||||
|
// to its list of intermediates
|
||||||
|
// -5. Update the roots and CA config in the state store at the same time, finally switching
|
||||||
|
// to the new provider
|
||||||
|
// -6. Call teardown on the old provider, so it can clean up whatever it needs to
|
||||||
|
|
||||||
|
/*_, err := newProvider.ActiveIntermediate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}*/
|
||||||
|
|
||||||
|
// Update the roots and CA config in the state store at the same time
|
||||||
|
idx, roots, err := state.CARoots(nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var newRoots structs.CARoots
|
||||||
|
for _, r := range roots {
|
||||||
|
newRoot := *r
|
||||||
|
if newRoot.Active {
|
||||||
|
newRoot.Active = false
|
||||||
|
}
|
||||||
|
newRoots = append(newRoots, &newRoot)
|
||||||
|
}
|
||||||
|
newRoots = append(newRoots, newActiveRoot)
|
||||||
|
|
||||||
|
args.Op = structs.CAOpSetRootsAndConfig
|
||||||
|
args.Index = idx
|
||||||
|
args.Roots = newRoots
|
||||||
resp, err := s.srv.raftApply(structs.ConnectCARequestType, args)
|
resp, err := s.srv.raftApply(structs.ConnectCARequestType, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -71,6 +158,17 @@ func (s *ConnectCA) ConfigurationSet(
|
||||||
return respErr
|
return respErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the config has been committed, update the local provider instance
|
||||||
|
// and call teardown on the old provider
|
||||||
|
oldProvider := s.srv.getCAProvider()
|
||||||
|
s.srv.setCAProvider(newProvider)
|
||||||
|
|
||||||
|
if err := oldProvider.Teardown(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.srv.logger.Printf("[INFO] connect: CA rotated to the new root under %q provider", args.Config.Provider)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,28 +29,94 @@ type ConsulCAProviderConfig struct {
|
||||||
type ConsulCAProvider struct {
|
type ConsulCAProvider struct {
|
||||||
config *ConsulCAProviderConfig
|
config *ConsulCAProviderConfig
|
||||||
|
|
||||||
// todo(kyhavlov): store these directly in the state store
|
id string
|
||||||
// and pass a reference to the state to this provider instead of
|
|
||||||
// having these values here
|
|
||||||
srv *Server
|
srv *Server
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewConsulCAProvider returns a new instance of the Consul CA provider,
|
||||||
|
// bootstrapping its state in the state store necessary
|
||||||
func NewConsulCAProvider(rawConfig map[string]interface{}, srv *Server) (*ConsulCAProvider, error) {
|
func NewConsulCAProvider(rawConfig map[string]interface{}, srv *Server) (*ConsulCAProvider, error) {
|
||||||
provider := &ConsulCAProvider{srv: srv}
|
conf, err := decodeConfig(rawConfig)
|
||||||
provider.SetConfiguration(rawConfig)
|
|
||||||
|
|
||||||
return provider, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ConsulCAProvider) SetConfiguration(raw map[string]interface{}) error {
|
|
||||||
conf, err := decodeConfig(raw)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
|
}
|
||||||
|
provider := &ConsulCAProvider{
|
||||||
|
config: conf,
|
||||||
|
srv: srv,
|
||||||
|
id: fmt.Sprintf("%s,%s", conf.PrivateKey, conf.RootCert),
|
||||||
}
|
}
|
||||||
|
|
||||||
c.config = conf
|
// Check if this configuration of the provider has already been
|
||||||
return nil
|
// initialized in the state store.
|
||||||
|
state := srv.fsm.State()
|
||||||
|
_, providerState, err := state.CAProviderState(provider.id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit early if the state store has already been populated for this config.
|
||||||
|
if providerState != nil {
|
||||||
|
return provider, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
newState := structs.CAConsulProviderState{
|
||||||
|
ID: provider.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the initial provider state to get the index to use for the
|
||||||
|
// CA serial number.
|
||||||
|
{
|
||||||
|
args := &structs.CARequest{
|
||||||
|
Op: structs.CAOpSetProviderState,
|
||||||
|
ProviderState: &newState,
|
||||||
|
}
|
||||||
|
resp, err := srv.raftApply(structs.ConnectCARequestType, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if respErr, ok := resp.(error); ok {
|
||||||
|
return nil, respErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idx, _, err := state.CAProviderState(provider.id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a private key if needed
|
||||||
|
if conf.PrivateKey == "" {
|
||||||
|
pk, err := generatePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
newState.PrivateKey = pk
|
||||||
|
} else {
|
||||||
|
newState.PrivateKey = conf.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the root CA
|
||||||
|
ca, err := provider.generateCA(newState.PrivateKey, conf.RootCert, idx+1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error generating CA: %v", err)
|
||||||
|
}
|
||||||
|
newState.CARoot = ca
|
||||||
|
|
||||||
|
// Write the provider state
|
||||||
|
args := &structs.CARequest{
|
||||||
|
Op: structs.CAOpSetProviderState,
|
||||||
|
ProviderState: &newState,
|
||||||
|
}
|
||||||
|
resp, err := srv.raftApply(structs.ConnectCARequestType, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if respErr, ok := resp.(error); ok {
|
||||||
|
return nil, respErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeConfig(raw map[string]interface{}) (*ConsulCAProviderConfig, error) {
|
func decodeConfig(raw map[string]interface{}) (*ConsulCAProviderConfig, error) {
|
||||||
|
@ -59,59 +125,22 @@ func decodeConfig(raw map[string]interface{}) (*ConsulCAProviderConfig, error) {
|
||||||
return nil, fmt.Errorf("error decoding config: %s", err)
|
return nil, fmt.Errorf("error decoding config: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.PrivateKey == "" && config.RootCert != "" {
|
||||||
|
return nil, fmt.Errorf("must provide a private key when providing a root cert")
|
||||||
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the active root CA and generate a new one if needed
|
// Return the active root CA and generate a new one if needed
|
||||||
func (c *ConsulCAProvider) ActiveRoot() (*structs.CARoot, error) {
|
func (c *ConsulCAProvider) ActiveRoot() (*structs.CARoot, error) {
|
||||||
state := c.srv.fsm.State()
|
state := c.srv.fsm.State()
|
||||||
_, providerState, err := state.CAProviderState()
|
_, providerState, err := state.CAProviderState(c.id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var update bool
|
return providerState.CARoot, nil
|
||||||
var newState structs.CAConsulProviderState
|
|
||||||
if providerState != nil {
|
|
||||||
newState = *providerState
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a private key if needed
|
|
||||||
if providerState == nil || providerState.PrivateKey == "" {
|
|
||||||
pk, err := generatePrivateKey()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
newState.PrivateKey = pk
|
|
||||||
update = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a root CA if needed
|
|
||||||
if providerState == nil || providerState.CARoot == nil {
|
|
||||||
ca, err := c.generateCA(newState.PrivateKey, newState.RootIndex+1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
newState.CARoot = ca
|
|
||||||
newState.RootIndex += 1
|
|
||||||
update = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the provider state if we generated a new private key/cert
|
|
||||||
if update {
|
|
||||||
args := &structs.CARequest{
|
|
||||||
Op: structs.CAOpSetProviderState,
|
|
||||||
ProviderState: &newState,
|
|
||||||
}
|
|
||||||
resp, err := c.srv.raftApply(structs.ConnectCARequestType, args)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if respErr, ok := resp.(error); ok {
|
|
||||||
return nil, respErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newState.CARoot, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConsulCAProvider) ActiveIntermediate() (*structs.CARoot, error) {
|
func (c *ConsulCAProvider) ActiveIntermediate() (*structs.CARoot, error) {
|
||||||
|
@ -120,15 +149,12 @@ func (c *ConsulCAProvider) ActiveIntermediate() (*structs.CARoot, error) {
|
||||||
|
|
||||||
func (c *ConsulCAProvider) GenerateIntermediate() (*structs.CARoot, error) {
|
func (c *ConsulCAProvider) GenerateIntermediate() (*structs.CARoot, error) {
|
||||||
state := c.srv.fsm.State()
|
state := c.srv.fsm.State()
|
||||||
_, providerState, err := state.CAProviderState()
|
idx, providerState, err := state.CAProviderState(c.id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if providerState == nil {
|
|
||||||
return nil, fmt.Errorf("CA provider not yet initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
ca, err := c.generateCA(providerState.PrivateKey, providerState.RootIndex+1)
|
ca, err := c.generateCA(providerState.PrivateKey, "", idx+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -136,12 +162,34 @@ func (c *ConsulCAProvider) GenerateIntermediate() (*structs.CARoot, error) {
|
||||||
return ca, nil
|
return ca, nil
|
||||||
}
|
}
|
||||||
|