From 1197b43c7bcce0bd3ce0270fbf7dc1aa891aed52 Mon Sep 17 00:00:00 2001 From: Paul Banks Date: Mon, 11 Nov 2019 21:36:22 +0000 Subject: [PATCH] Support Connect CAs that can't cross sign (#6726) * Support Connect CAs that can't cross sign * revert spurios mod changes from make tools * Add log warning when forcing CA rotation * Fixup SupportsCrossSigning to report errors and work with Plugin interface (fixes tests) * Fix failing snake_case test * Remove misleading comment * Revert "Remove misleading comment" This reverts commit bc4db9cabed8ad5d0e39b30e1fe79196d248349c. * Remove misleading comment * Regen proto files messed up by rebase --- agent/connect/ca/mock_Provider.go | 21 ++ agent/connect/ca/plugin/provider.pb.binary.go | 10 + agent/connect/ca/plugin/provider.pb.go | 278 +++++++++++++++--- agent/connect/ca/plugin/provider.proto | 5 + agent/connect/ca/plugin/transport_grpc.go | 10 + agent/connect/ca/plugin/transport_netrpc.go | 6 + agent/connect/ca/provider.go | 14 +- agent/connect/ca/provider_consul.go | 9 + agent/connect/ca/provider_vault.go | 5 + agent/connect_ca_endpoint.go | 6 +- agent/connect_ca_endpoint_test.go | 160 ++++++---- agent/consul/connect_ca_endpoint.go | 40 ++- agent/consul/connect_ca_endpoint_test.go | 106 +++++++ agent/structs/connect_ca.go | 34 +++ website/source/api/connect/ca.html.md | 10 +- .../docs/commands/connect/ca.html.md.erb | 2 + website/source/docs/connect/ca.html.md | 60 +++- 17 files changed, 657 insertions(+), 119 deletions(-) diff --git a/agent/connect/ca/mock_Provider.go b/agent/connect/ca/mock_Provider.go index ba42e0452..3bc5f4c46 100644 --- a/agent/connect/ca/mock_Provider.go +++ b/agent/connect/ca/mock_Provider.go @@ -238,3 +238,24 @@ func (_m *MockProvider) State() (map[string]string, error) { return r0, r1 } + +// SupportsCrossSigning provides a mock function with given fields: +func (_m *MockProvider) SupportsCrossSigning() (bool, error) { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/agent/connect/ca/plugin/provider.pb.binary.go b/agent/connect/ca/plugin/provider.pb.binary.go index 20e951bd8..d188bff36 100644 --- a/agent/connect/ca/plugin/provider.pb.binary.go +++ b/agent/connect/ca/plugin/provider.pb.binary.go @@ -137,6 +137,16 @@ func (msg *CrossSignCAResponse) UnmarshalBinary(b []byte) error { return proto.Unmarshal(b, msg) } +// MarshalBinary implements encoding.BinaryMarshaler +func (msg *BoolResponse) MarshalBinary() ([]byte, error) { + return proto.Marshal(msg) +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler +func (msg *BoolResponse) UnmarshalBinary(b []byte) error { + return proto.Unmarshal(b, msg) +} + // MarshalBinary implements encoding.BinaryMarshaler func (msg *Empty) MarshalBinary() ([]byte, error) { return proto.Marshal(msg) diff --git a/agent/connect/ca/plugin/provider.pb.go b/agent/connect/ca/plugin/provider.pb.go index 100b03502..0bd4fcb4c 100644 --- a/agent/connect/ca/plugin/provider.pb.go +++ b/agent/connect/ca/plugin/provider.pb.go @@ -666,6 +666,53 @@ func (m *CrossSignCAResponse) GetCrtPem() string { return "" } +type BoolResponse struct { + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *BoolResponse) Reset() { *m = BoolResponse{} } +func (m *BoolResponse) String() string { return proto.CompactTextString(m) } +func (*BoolResponse) ProtoMessage() {} +func (*BoolResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_c6a9f3c02af3d1c8, []int{13} +} +func (m *BoolResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *BoolResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_BoolResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalTo(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *BoolResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_BoolResponse.Merge(m, src) +} +func (m *BoolResponse) XXX_Size() int { + return m.Size() +} +func (m *BoolResponse) XXX_DiscardUnknown() { + xxx_messageInfo_BoolResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_BoolResponse proto.InternalMessageInfo + +func (m *BoolResponse) GetOk() bool { + if m != nil { + return m.Ok + } + return false +} + // Protobufs doesn't allow no req/resp so in the cases where there are // no arguments we use the Empty message. type Empty struct { @@ -678,7 +725,7 @@ func (m *Empty) Reset() { *m = Empty{} } func (m *Empty) String() string { return proto.CompactTextString(m) } func (*Empty) ProtoMessage() {} func (*Empty) Descriptor() ([]byte, []int) { - return fileDescriptor_c6a9f3c02af3d1c8, []int{13} + return fileDescriptor_c6a9f3c02af3d1c8, []int{14} } func (m *Empty) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -721,48 +768,52 @@ func init() { proto.RegisterType((*SignResponse)(nil), "plugin.SignResponse") proto.RegisterType((*SignIntermediateResponse)(nil), "plugin.SignIntermediateResponse") proto.RegisterType((*CrossSignCAResponse)(nil), "plugin.CrossSignCAResponse") + proto.RegisterType((*BoolResponse)(nil), "plugin.BoolResponse") proto.RegisterType((*Empty)(nil), "plugin.Empty") } func init() { proto.RegisterFile("provider.proto", fileDescriptor_c6a9f3c02af3d1c8) } var fileDescriptor_c6a9f3c02af3d1c8 = []byte{ - // 560 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x55, 0xc1, 0x6e, 0xd3, 0x40, - 0x10, 0xc5, 0x69, 0x93, 0x34, 0xd3, 0x94, 0x5a, 0xdb, 0xd0, 0x18, 0x03, 0x4e, 0x64, 0x01, 0x09, - 0x82, 0x46, 0x82, 0x82, 0x2a, 0x71, 0x22, 0x58, 0x50, 0x55, 0x5c, 0x8a, 0x23, 0xae, 0x44, 0xc1, - 0x59, 0xa2, 0x95, 0x62, 0xaf, 0xd9, 0x5d, 0x57, 0xf0, 0x27, 0x7c, 0x12, 0x47, 0x3e, 0x01, 0x85, - 0x7f, 0xe0, 0x8c, 0xbc, 0xb1, 0x1d, 0x7b, 0xe3, 0xd6, 0xb7, 0xcc, 0xf8, 0xcd, 0xdb, 0x79, 0xb3, - 0x6f, 0x36, 0x70, 0x3b, 0x64, 0xf4, 0x8a, 0xcc, 0x31, 0x1b, 0x85, 0x8c, 0x0a, 0x8a, 0x1a, 0xe1, - 0x32, 0x5a, 0x90, 0xc0, 0xfe, 0x0e, 0xba, 0x43, 0x83, 0xaf, 0x64, 0x11, 0x31, 0xec, 0xe2, 0x6f, - 0x11, 0xe6, 0x02, 0x3d, 0x00, 0xf0, 0x96, 0x11, 0x17, 0x98, 0x4d, 0xc9, 0xdc, 0xd0, 0xfa, 0xda, - 0xb0, 0xe5, 0xb6, 0x92, 0xcc, 0xc5, 0x1c, 0x75, 0xa1, 0x49, 0xf8, 0x94, 0x51, 0x2a, 0x8c, 0x5a, - 0x5f, 0x1b, 0xee, 0xb9, 0x0d, 0xc2, 0x5d, 0x4a, 0x05, 0x3a, 0x86, 0x86, 0x27, 0xb9, 0x8c, 0x9d, - 0xbe, 0x36, 0x6c, 0xbb, 0x49, 0x84, 0x3a, 0x50, 0xe7, 0x62, 0x26, 0xb0, 0xb1, 0x2b, 0xd3, 0xeb, - 0xc0, 0xfe, 0x0c, 0xc7, 0x13, 0x2c, 0x2e, 0x02, 0x81, 0x99, 0x8f, 0xe7, 0x64, 0x26, 0xb2, 0xf3, - 0x9f, 0x80, 0x4e, 0x72, 0xe9, 0x69, 0x88, 0xfd, 0xa4, 0x8b, 0xc3, 0x7c, 0xfe, 0x12, 0xfb, 0xe8, - 0x2e, 0xec, 0xc5, 0x8d, 0x48, 0x48, 0x4d, 0x42, 0x9a, 0x71, 0x7c, 0x89, 0x7d, 0xbb, 0x07, 0xfb, - 0x13, 0xb2, 0x08, 0x52, 0x52, 0x1d, 0x76, 0x3c, 0xce, 0x24, 0x4f, 0xdb, 0x8d, 0x7f, 0xda, 0x4f, - 0xa1, 0x1b, 0x03, 0xca, 0x3a, 0xd8, 0x06, 0x3f, 0x06, 0xe4, 0x30, 0xca, 0x79, 0x5c, 0xe1, 0x8c, - 0xf3, 0x38, 0x26, 0x32, 0x1c, 0x13, 0xf6, 0x23, 0x38, 0x98, 0x08, 0xc9, 0xc4, 0x43, 0x1a, 0x70, - 0xbc, 0x11, 0xaf, 0xe5, 0xc5, 0x9f, 0x00, 0x1a, 0x7b, 0x82, 0x5c, 0xe1, 0x78, 0x70, 0x19, 0xb6, - 0x0b, 0x4d, 0x8f, 0x89, 0x9c, 0xde, 0x86, 0xc7, 0xa4, 0x96, 0xd7, 0xd0, 0x3b, 0xc7, 0x01, 0x66, - 0x33, 0x81, 0xf3, 0xed, 0x3a, 0x13, 0xb7, 0x50, 0xcb, 0x59, 0xa1, 0x96, 0xb3, 0xb8, 0xf6, 0x15, - 0x98, 0xeb, 0xa3, 0x8a, 0x42, 0xab, 0x8e, 0x3c, 0x83, 0xfb, 0x65, 0x47, 0x56, 0x17, 0x0e, 0xa0, - 0xbd, 0x9e, 0x7b, 0x15, 0xf0, 0x14, 0x8c, 0xed, 0xf9, 0x57, 0x15, 0x8d, 0xe0, 0xa8, 0x70, 0x0f, - 0x55, 0xf8, 0x26, 0xd4, 0xdf, 0xf9, 0xa1, 0xf8, 0xf1, 0xe2, 0x5f, 0x1d, 0x6a, 0xce, 0x18, 0xbd, - 0x84, 0x56, 0xe6, 0x77, 0x64, 0x8c, 0xd6, 0x5b, 0x30, 0x52, 0x57, 0xc0, 0x3c, 0x48, 0xbf, 0xc8, - 0x62, 0x74, 0x02, 0x75, 0x79, 0xab, 0xa8, 0x98, 0x37, 0xef, 0xa4, 0x61, 0xf1, 0xce, 0x9f, 0x41, - 0x3b, 0x9d, 0x9d, 0x5c, 0x0c, 0xa5, 0x4a, 0x21, 0x3f, 0x03, 0xd8, 0x78, 0x41, 0xc5, 0x9a, 0x69, - 0x58, 0x62, 0x97, 0x8f, 0xd0, 0xbd, 0xc6, 0x15, 0x2a, 0xcb, 0x20, 0x0d, 0xab, 0x5c, 0xf4, 0x06, - 0x0e, 0x95, 0xa5, 0x44, 0x56, 0xa6, 0xb1, 0x74, 0x5b, 0x55, 0x35, 0xe7, 0xa9, 0xb3, 0x0b, 0x24, - 0x4a, 0x3f, 0x76, 0x51, 0x55, 0xa9, 0x05, 0x3e, 0x40, 0xa7, 0xac, 0x5b, 0x95, 0xea, 0xe1, 0x4d, - 0xd2, 0x32, 0xb2, 0xe7, 0xb0, 0x1b, 0x3b, 0x06, 0x1d, 0x65, 0x62, 0x36, 0x4f, 0x83, 0xd9, 0x29, - 0x26, 0x93, 0x92, 0x4f, 0xa0, 0xab, 0xf6, 0x44, 0xbd, 0x3c, 0xb2, 0x6c, 0x18, 0xfd, 0xeb, 0x01, - 0x09, 0xed, 0x7b, 0xd8, 0xcf, 0x19, 0x18, 0x65, 0xf7, 0xbb, 0xfd, 0xba, 0x98, 0xf7, 0x4a, 0xbf, - 0x25, 0x3c, 0x03, 0x68, 0x3a, 0x4b, 0x3c, 0x0b, 0xa2, 0xf0, 0x66, 0x7b, 0xbd, 0xd5, 0x7f, 0xad, - 0x2c, 0xed, 0xf7, 0xca, 0xd2, 0xfe, 0xac, 0x2c, 0xed, 0xe7, 0x5f, 0xeb, 0xd6, 0x97, 0x86, 0xfc, - 0x0b, 0x38, 0xfd, 0x1f, 0x00, 0x00, 0xff, 0xff, 0xd1, 0xcf, 0x1a, 0x9a, 0x14, 0x06, 0x00, 0x00, + // 599 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x55, 0xd1, 0x6e, 0xd3, 0x4a, + 0x10, 0xbd, 0x4e, 0x5b, 0x27, 0x99, 0xa6, 0x6d, 0xb4, 0xcd, 0x6d, 0x8c, 0x01, 0x27, 0xb2, 0x80, + 0x04, 0x41, 0x23, 0x41, 0x41, 0x95, 0xe0, 0x85, 0xd4, 0x82, 0xaa, 0xe2, 0xa5, 0x38, 0xe2, 0x95, + 0x28, 0x38, 0x4b, 0x64, 0x35, 0xf1, 0x9a, 0xdd, 0x75, 0x05, 0x7f, 0xc2, 0x7f, 0xf0, 0x13, 0x3c, + 0xf2, 0x09, 0x28, 0xfc, 0x08, 0xf2, 0xc6, 0xde, 0xd8, 0x1b, 0xb7, 0x7e, 0xcb, 0xcc, 0x9e, 0x39, + 0x3b, 0x67, 0xf6, 0x8c, 0x03, 0xfb, 0x21, 0x25, 0xd7, 0xfe, 0x14, 0xd3, 0x41, 0x48, 0x09, 0x27, + 0x48, 0x0f, 0xe7, 0xd1, 0xcc, 0x0f, 0xec, 0x6f, 0xd0, 0x74, 0x48, 0xf0, 0xc5, 0x9f, 0x45, 0x14, + 0xbb, 0xf8, 0x6b, 0x84, 0x19, 0x47, 0xf7, 0x01, 0xbc, 0x79, 0xc4, 0x38, 0xa6, 0x63, 0x7f, 0x6a, + 0x68, 0x5d, 0xad, 0x5f, 0x77, 0xeb, 0x49, 0xe6, 0x62, 0x8a, 0xda, 0x50, 0xf5, 0xd9, 0x98, 0x12, + 0xc2, 0x8d, 0x4a, 0x57, 0xeb, 0xd7, 0x5c, 0xdd, 0x67, 0x2e, 0x21, 0x1c, 0x1d, 0x81, 0xee, 0x09, + 0x2e, 0x63, 0xab, 0xab, 0xf5, 0x1b, 0x6e, 0x12, 0xa1, 0x16, 0xec, 0x30, 0x3e, 0xe1, 0xd8, 0xd8, + 0x16, 0xe9, 0x55, 0x60, 0x7f, 0x82, 0xa3, 0x11, 0xe6, 0x17, 0x01, 0xc7, 0x74, 0x81, 0xa7, 0xfe, + 0x84, 0xcb, 0xfb, 0x1f, 0x43, 0xd3, 0xcf, 0xa4, 0xc7, 0x21, 0x5e, 0x24, 0x5d, 0x1c, 0x64, 0xf3, + 0x97, 0x78, 0x81, 0xee, 0x40, 0x2d, 0x6e, 0x44, 0x40, 0x2a, 0x02, 0x52, 0x8d, 0xe3, 0x4b, 0xbc, + 0xb0, 0x3b, 0xb0, 0x3b, 0xf2, 0x67, 0x41, 0x4a, 0xda, 0x84, 0x2d, 0x8f, 0x51, 0xc1, 0xd3, 0x70, + 0xe3, 0x9f, 0xf6, 0x13, 0x68, 0xc7, 0x80, 0xa2, 0x0e, 0x36, 0xc1, 0x8f, 0x00, 0x39, 0x94, 0x30, + 0x16, 0x57, 0x38, 0xc3, 0x2c, 0x8e, 0x72, 0x89, 0xa3, 0xdc, 0x7e, 0x08, 0x7b, 0x23, 0x2e, 0x98, + 0x58, 0x48, 0x02, 0x86, 0xd7, 0xe2, 0xb5, 0xac, 0xf8, 0x63, 0x40, 0x43, 0x8f, 0xfb, 0xd7, 0x38, + 0x1e, 0x9c, 0xc4, 0xb6, 0xa1, 0xea, 0x51, 0x9e, 0xd1, 0xab, 0x7b, 0x54, 0x68, 0x79, 0x05, 0x9d, + 0x73, 0x1c, 0x60, 0x3a, 0xe1, 0x38, 0xdb, 0xae, 0x33, 0x72, 0x73, 0xb5, 0x8c, 0xe6, 0x6a, 0x19, + 0x8d, 0x6b, 0x5f, 0x82, 0xb9, 0xba, 0x2a, 0x2f, 0xb4, 0xec, 0xca, 0x53, 0xb8, 0x57, 0x74, 0x65, + 0x79, 0x61, 0x0f, 0x1a, 0xab, 0xb9, 0x97, 0x01, 0x4f, 0xc0, 0xd8, 0x9c, 0x7f, 0x59, 0xd1, 0x00, + 0x0e, 0x73, 0xef, 0x50, 0x86, 0xb7, 0xa0, 0x71, 0x46, 0xc8, 0x5c, 0x02, 0xf7, 0xa1, 0x42, 0xae, + 0x04, 0xa6, 0xe6, 0x56, 0xc8, 0x95, 0x5d, 0x85, 0x9d, 0xb7, 0x8b, 0x90, 0x7f, 0x7f, 0xfe, 0x53, + 0x87, 0x8a, 0x33, 0x44, 0x2f, 0xa0, 0x2e, 0xf7, 0x01, 0x19, 0x83, 0xd5, 0x96, 0x0c, 0xd4, 0x15, + 0x31, 0xf7, 0xd2, 0x13, 0x51, 0x8c, 0x8e, 0x61, 0x47, 0xbc, 0x3a, 0xca, 0xe7, 0xcd, 0xff, 0xd3, + 0x30, 0xef, 0x89, 0xa7, 0xd0, 0x48, 0x67, 0x2b, 0x16, 0x47, 0xa9, 0x52, 0xc8, 0x4f, 0x01, 0xd6, + 0x5e, 0x51, 0xb1, 0x66, 0x1a, 0x16, 0xd8, 0xe9, 0x03, 0xb4, 0x6f, 0x70, 0x8d, 0xca, 0xd2, 0x4b, + 0xc3, 0x32, 0x97, 0xbd, 0x81, 0x03, 0x65, 0x69, 0x91, 0x25, 0x35, 0x16, 0x6e, 0xb3, 0xaa, 0xe6, + 0x3c, 0x75, 0x7e, 0x8e, 0x44, 0xe9, 0xc7, 0xce, 0xab, 0x2a, 0xb4, 0xc8, 0x7b, 0x68, 0x15, 0x75, + 0xab, 0x52, 0x3d, 0xb8, 0x4d, 0x9a, 0x24, 0x7b, 0x06, 0xdb, 0xb1, 0xa3, 0xd0, 0xa1, 0x14, 0xb3, + 0xfe, 0x74, 0x98, 0xad, 0x7c, 0x32, 0x29, 0xf9, 0x08, 0x4d, 0xd5, 0xbe, 0xa8, 0x93, 0x45, 0x16, + 0x0d, 0xa3, 0x7b, 0x33, 0x20, 0xa1, 0x7d, 0x07, 0xbb, 0x19, 0x83, 0x23, 0xf9, 0xbe, 0x9b, 0x5f, + 0x1f, 0xf3, 0x6e, 0xe1, 0x59, 0xc2, 0xf3, 0x1a, 0x5a, 0xa3, 0x28, 0x0c, 0x09, 0xe5, 0x4c, 0x1e, + 0xfb, 0xc1, 0x4c, 0x1d, 0x8f, 0xd4, 0x96, 0xdb, 0x92, 0x1e, 0x54, 0x9d, 0x39, 0x9e, 0x04, 0x51, + 0x78, 0xbb, 0x37, 0xcf, 0x9a, 0xbf, 0x96, 0x96, 0xf6, 0x7b, 0x69, 0x69, 0x7f, 0x96, 0x96, 0xf6, + 0xe3, 0xaf, 0xf5, 0xdf, 0x67, 0x5d, 0xfc, 0xbf, 0x9c, 0xfc, 0x0b, 0x00, 0x00, 0xff, 0xff, 0x9e, + 0xa1, 0xdc, 0xbb, 0x71, 0x06, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -788,6 +839,7 @@ type CAClient interface { Sign(ctx context.Context, in *SignRequest, opts ...grpc.CallOption) (*SignResponse, error) SignIntermediate(ctx context.Context, in *SignIntermediateRequest, opts ...grpc.CallOption) (*SignIntermediateResponse, error) CrossSignCA(ctx context.Context, in *CrossSignCARequest, opts ...grpc.CallOption) (*CrossSignCAResponse, error) + SupportsCrossSigning(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*BoolResponse, error) Cleanup(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) } @@ -898,6 +950,15 @@ func (c *cAClient) CrossSignCA(ctx context.Context, in *CrossSignCARequest, opts return out, nil } +func (c *cAClient) SupportsCrossSigning(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*BoolResponse, error) { + out := new(BoolResponse) + err := c.cc.Invoke(ctx, "/plugin.CA/SupportsCrossSigning", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *cAClient) Cleanup(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) { out := new(Empty) err := c.cc.Invoke(ctx, "/plugin.CA/Cleanup", in, out, opts...) @@ -920,6 +981,7 @@ type CAServer interface { Sign(context.Context, *SignRequest) (*SignResponse, error) SignIntermediate(context.Context, *SignIntermediateRequest) (*SignIntermediateResponse, error) CrossSignCA(context.Context, *CrossSignCARequest) (*CrossSignCAResponse, error) + SupportsCrossSigning(context.Context, *Empty) (*BoolResponse, error) Cleanup(context.Context, *Empty) (*Empty, error) } @@ -1125,6 +1187,24 @@ func _CA_CrossSignCA_Handler(srv interface{}, ctx context.Context, dec func(inte return interceptor(ctx, in, info, handler) } +func _CA_SupportsCrossSigning_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CAServer).SupportsCrossSigning(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/plugin.CA/SupportsCrossSigning", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CAServer).SupportsCrossSigning(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + func _CA_Cleanup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(Empty) if err := dec(in); err != nil { @@ -1191,6 +1271,10 @@ var _CA_serviceDesc = grpc.ServiceDesc{ MethodName: "CrossSignCA", Handler: _CA_CrossSignCA_Handler, }, + { + MethodName: "SupportsCrossSigning", + Handler: _CA_SupportsCrossSigning_Handler, + }, { MethodName: "Cleanup", Handler: _CA_Cleanup_Handler, @@ -1579,6 +1663,37 @@ func (m *CrossSignCAResponse) MarshalTo(dAtA []byte) (int, error) { return i, nil } +func (m *BoolResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *BoolResponse) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if m.Ok { + dAtA[i] = 0x8 + i++ + if m.Ok { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i++ + } + if m.XXX_unrecognized != nil { + i += copy(dAtA[i:], m.XXX_unrecognized) + } + return i, nil +} + func (m *Empty) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -1832,6 +1947,21 @@ func (m *CrossSignCAResponse) Size() (n int) { return n } +func (m *BoolResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Ok { + n += 2 + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + func (m *Empty) Size() (n int) { if m == nil { return 0 @@ -3103,6 +3233,80 @@ func (m *CrossSignCAResponse) Unmarshal(dAtA []byte) error { } return nil } +func (m *BoolResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProvider + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: BoolResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: BoolResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Ok", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProvider + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Ok = bool(v != 0) + default: + iNdEx = preIndex + skippy, err := skipProvider(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthProvider + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthProvider + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *Empty) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 diff --git a/agent/connect/ca/plugin/provider.proto b/agent/connect/ca/plugin/provider.proto index de15778fe..f51a5c040 100644 --- a/agent/connect/ca/plugin/provider.proto +++ b/agent/connect/ca/plugin/provider.proto @@ -24,6 +24,7 @@ service CA { rpc Sign(SignRequest) returns (SignResponse); rpc SignIntermediate(SignIntermediateRequest) returns (SignIntermediateResponse); rpc CrossSignCA(CrossSignCARequest) returns (CrossSignCAResponse); + rpc SupportsCrossSigning(Empty) returns (BoolResponse); rpc Cleanup(Empty) returns (Empty); } @@ -83,6 +84,10 @@ message CrossSignCAResponse { string crt_pem = 1; } +message BoolResponse { + bool ok = 1; +} + // Protobufs doesn't allow no req/resp so in the cases where there are // no arguments we use the Empty message. message Empty {} diff --git a/agent/connect/ca/plugin/transport_grpc.go b/agent/connect/ca/plugin/transport_grpc.go index 09ff0957c..5aeb72a49 100644 --- a/agent/connect/ca/plugin/transport_grpc.go +++ b/agent/connect/ca/plugin/transport_grpc.go @@ -97,6 +97,11 @@ func (p *providerPluginGRPCServer) CrossSignCA(_ context.Context, req *CrossSign return &CrossSignCAResponse{CrtPem: crtPEM}, err } +func (p *providerPluginGRPCServer) SupportsCrossSigning(context.Context, *Empty) (*BoolResponse, error) { + ok, err := p.impl.SupportsCrossSigning() + return &BoolResponse{Ok: ok}, err +} + func (p *providerPluginGRPCServer) Cleanup(context.Context, *Empty) (*Empty, error) { return &Empty{}, p.impl.Cleanup() } @@ -230,6 +235,11 @@ func (p *providerPluginGRPCClient) CrossSignCA(crt *x509.Certificate) (string, e return resp.CrtPem, nil } +func (p *providerPluginGRPCClient) SupportsCrossSigning() (bool, error) { + resp, err := p.client.SupportsCrossSigning(p.doneCtx, &Empty{}) + return resp.Ok, err +} + func (p *providerPluginGRPCClient) Cleanup() error { _, err := p.client.Cleanup(p.doneCtx, &Empty{}) return p.err(err) diff --git a/agent/connect/ca/plugin/transport_netrpc.go b/agent/connect/ca/plugin/transport_netrpc.go index 335facdd9..8de564bb4 100644 --- a/agent/connect/ca/plugin/transport_netrpc.go +++ b/agent/connect/ca/plugin/transport_netrpc.go @@ -192,6 +192,12 @@ func (p *providerPluginRPCClient) CrossSignCA(crt *x509.Certificate) (string, er return resp.CrtPem, err } +func (p *providerPluginRPCClient) SupportsCrossSigning() (bool, error) { + var out BoolResponse + err := p.client.Call("Plugin.SupportsCrossSigning", struct{}{}, &out) + return out.Ok, err +} + func (p *providerPluginRPCClient) Cleanup() error { return p.client.Call("Plugin.Cleanup", struct{}{}, &struct{}{}) } diff --git a/agent/connect/ca/provider.go b/agent/connect/ca/provider.go index ff1463599..a789869c5 100644 --- a/agent/connect/ca/provider.go +++ b/agent/connect/ca/provider.go @@ -84,16 +84,26 @@ type Provider interface { // of 0 to ensure that the certificate cannot be used to generate further CA certs. SignIntermediate(*x509.CertificateRequest) (string, error) - // CrossSignCA must accept a CA certificate from another CA provider - // and cross sign it exactly as it is such that it forms a chain back the the + // CrossSignCA must accept a CA certificate from another CA provider and cross + // sign it exactly as it is such that it forms a chain back the the // CAProvider's current root. Specifically, the Distinguished Name, Subject // Alternative Name, SubjectKeyID and other relevant extensions must be kept. // The resulting certificate must have a distinct Serial Number and the // AuthorityKeyID set to the CAProvider's current signing key as well as the // Issuer related fields changed as necessary. The resulting certificate is // returned as a PEM formatted string. + // + // If the CA provider does not support this operation, it may return an error + // provided `SupportsCrossSigning` also returns false. CrossSignCA(*x509.Certificate) (string, error) + // SupportsCrossSigning should indicate whether the CA provider supports + // cross-signing an external root to provide a seamless rotation. If the CA + // does not support this, the user will have to force an upgrade when that CA + // provider is the current CA as the upgrade may cause interruptions to + // connectivity during the rollout. + SupportsCrossSigning() (bool, error) + // Cleanup performs any necessary cleanup that should happen when the provider // is shut down permanently, such as removing a temporary PKI backend in Vault // created for an intermediate CA. diff --git a/agent/connect/ca/provider_consul.go b/agent/connect/ca/provider_consul.go index 8ebaaae4e..2975bb561 100644 --- a/agent/connect/ca/provider_consul.go +++ b/agent/connect/ca/provider_consul.go @@ -508,6 +508,10 @@ func (c *ConsulProvider) CrossSignCA(cert *x509.Certificate) (string, error) { c.Lock() defer c.Unlock() + if c.config.DisableCrossSigning { + return "", errors.New("cross-signing disabled") + } + // Get the provider state idx, providerState, err := c.getState() if err != nil { @@ -568,6 +572,11 @@ func (c *ConsulProvider) CrossSignCA(cert *x509.Certificate) (string, error) { return buf.String(), nil } +// SupportsCrossSigning implements Provider +func (c *ConsulProvider) SupportsCrossSigning() (bool, error) { + return !c.config.DisableCrossSigning, nil +} + // getState returns the current provider state from the state delegate, and returns // ErrNotInitialized if no entry is found. func (c *ConsulProvider) getState() (uint64, *structs.CAConsulProviderState, error) { diff --git a/agent/connect/ca/provider_vault.go b/agent/connect/ca/provider_vault.go index a19310937..10573c35d 100644 --- a/agent/connect/ca/provider_vault.go +++ b/agent/connect/ca/provider_vault.go @@ -389,6 +389,11 @@ func (v *VaultProvider) CrossSignCA(cert *x509.Certificate) (string, error) { return xcCert, nil } +// SupportsCrossSigning implements Provider +func (c *VaultProvider) SupportsCrossSigning() (bool, error) { + return true, nil +} + // Cleanup unmounts the configured intermediate PKI backend. It's fine to tear // this down and recreate it on small config changes because the intermediate // certs get bundled with the leaf certs, so there's no cost to the CA changing. diff --git a/agent/connect_ca_endpoint.go b/agent/connect_ca_endpoint.go index 7f245f171..a13efb4cd 100644 --- a/agent/connect_ca_endpoint.go +++ b/agent/connect_ca_endpoint.go @@ -62,9 +62,9 @@ func (s *HTTPServer) ConnectCAConfigurationSet(resp http.ResponseWriter, req *ht s.parseDC(req, &args.Datacenter) s.parseToken(req, &args.Token) if err := decodeBody(req.Body, &args.Config); err != nil { - resp.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(resp, "Request decode failed: %v", err) - return nil, nil + return nil, BadRequestError{ + Reason: fmt.Sprintf("Request decode failed: %v", err), + } } var reply interface{} diff --git a/agent/connect_ca_endpoint_test.go b/agent/connect_ca_endpoint_test.go index e979efeb7..f73313146 100644 --- a/agent/connect_ca_endpoint_test.go +++ b/agent/connect_ca_endpoint_test.go @@ -5,14 +5,12 @@ import ( "net/http" "net/http/httptest" "testing" - "time" "github.com/hashicorp/consul/testrpc" "github.com/stretchr/testify/require" "github.com/hashicorp/consul/agent/connect" - ca "github.com/hashicorp/consul/agent/connect/ca" "github.com/hashicorp/consul/agent/structs" "github.com/stretchr/testify/assert" ) @@ -64,61 +62,117 @@ func TestConnectCARoots_list(t *testing.T) { func TestConnectCAConfig(t *testing.T) { t.Parallel() - assert := assert.New(t) - a := NewTestAgent(t, t.Name(), "") - defer a.Shutdown() - testrpc.WaitForTestAgent(t, a.RPC, "dc1") - - expected := &structs.ConsulCAProviderConfig{ - RotationPeriod: 90 * 24 * time.Hour, - } - expected.LeafCertTTL = 72 * time.Hour - expected.PrivateKeyType = connect.DefaultPrivateKeyType - expected.PrivateKeyBits = connect.DefaultPrivateKeyBits - - // Get the initial config. - { - req, _ := http.NewRequest("GET", "/v1/connect/ca/configuration", nil) - resp := httptest.NewRecorder() - obj, err := a.srv.ConnectCAConfiguration(resp, req) - assert.NoError(err) - - value := obj.(structs.CAConfiguration) - parsed, err := ca.ParseConsulCAConfig(value.Config) - assert.NoError(err) - assert.Equal("consul", value.Provider) - assert.Equal(expected, parsed) - } - - // Set the config. - { - body := bytes.NewBuffer([]byte(` + tests := []struct { + name string + body string + wantErr bool + wantCfg structs.CAConfiguration + }{ { - "Provider": "consul", - "Config": { - "LeafCertTTL": "72h", - "RotationPeriod": "1h" - } - }`)) - req, _ := http.NewRequest("PUT", "/v1/connect/ca/configuration", body) - resp := httptest.NewRecorder() - _, err := a.srv.ConnectCAConfiguration(resp, req) - assert.NoError(err) + name: "basic", + body: ` + { + "Provider": "consul", + "Config": { + "LeafCertTTL": "72h", + "RotationPeriod": "1h" + } + }`, + wantErr: false, + wantCfg: structs.CAConfiguration{ + Provider: "consul", + ClusterID: connect.TestClusterID, + Config: map[string]interface{}{ + "LeafCertTTL": "72h", + "RotationPeriod": "1h", + }, + }, + }, + { + name: "force without cross sign CamelCase", + body: ` + { + "Provider": "consul", + "Config": { + "LeafCertTTL": "72h", + "RotationPeriod": "1h" + }, + "ForceWithoutCrossSigning": true + }`, + wantErr: false, + wantCfg: structs.CAConfiguration{ + Provider: "consul", + ClusterID: connect.TestClusterID, + Config: map[string]interface{}{ + "LeafCertTTL": "72h", + "RotationPeriod": "1h", + }, + ForceWithoutCrossSigning: true, + }, + }, + { + name: "force without cross sign snake_case", + // Note that config is still CamelCase. We don't currently support snake + // case config in the API only in config files for this. Arguably that's a + // bug but it's unrelated to the force options being tested here so we'll + // only test the new behaviour here rather than scope creep to refactoring + // all the CA config handling. + body: ` + { + "provider": "consul", + "config": { + "LeafCertTTL": "72h", + "RotationPeriod": "1h" + }, + "force_without_cross_signing": true + }`, + wantErr: false, + wantCfg: structs.CAConfiguration{ + Provider: "consul", + ClusterID: connect.TestClusterID, + Config: map[string]interface{}{ + "LeafCertTTL": "72h", + "RotationPeriod": "1h", + }, + ForceWithoutCrossSigning: true, + }, + }, } - // The config should be updated now. - { - expected.RotationPeriod = time.Hour + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + a := NewTestAgent(t, t.Name(), "") + defer a.Shutdown() + testrpc.WaitForTestAgent(t, a.RPC, "dc1") - req, _ := http.NewRequest("GET", "/v1/connect/ca/configuration", nil) - resp := httptest.NewRecorder() - obj, err := a.srv.ConnectCAConfiguration(resp, req) - assert.NoError(err) + // Set the config. + { + body := bytes.NewBuffer([]byte(tc.body)) + req, _ := http.NewRequest("PUT", "/v1/connect/ca/configuration", body) + resp := httptest.NewRecorder() + _, err := a.srv.ConnectCAConfiguration(resp, req) + if tc.wantErr { + require.Error(err) + return + } + require.NoError(err) + } - value := obj.(structs.CAConfiguration) - parsed, err := ca.ParseConsulCAConfig(value.Config) - assert.NoError(err) - assert.Equal("consul", value.Provider) - assert.Equal(expected, parsed) + // The config should be updated now. + { + req, _ := http.NewRequest("GET", "/v1/connect/ca/configuration", nil) + resp := httptest.NewRecorder() + obj, err := a.srv.ConnectCAConfiguration(resp, req) + require.NoError(err) + + got := obj.(structs.CAConfiguration) + // Reset Raft indexes to make it non flaky + got.CreateIndex = 0 + got.ModifyIndex = 0 + require.Equal(tc.wantCfg, got) + } + }) } } diff --git a/agent/consul/connect_ca_endpoint.go b/agent/consul/connect_ca_endpoint.go index 9ee2a8677..fb00c0ff2 100644 --- a/agent/consul/connect_ca_endpoint.go +++ b/agent/consul/connect_ca_endpoint.go @@ -244,6 +244,26 @@ func (s *ConnectCA) ConfigurationSet( // either by swapping the provider type or changing the provider's config // to use a different root certificate. + // First up, sanity check that the current provider actually supports + // cross-signing. + oldProvider, _ := s.srv.getCAProvider() + if oldProvider == nil { + return fmt.Errorf("internal error: CA provider is nil") + } + canXSign, err := oldProvider.SupportsCrossSigning() + if err != nil { + return fmt.Errorf("CA provider error: %s", err) + } + if !canXSign && !args.Config.ForceWithoutCrossSigning { + return errors.New("The current CA Provider does not support cross-signing. " + + "You can try again with ForceWithoutCrossSigningSet but this may cause " + + "disruption - see documentation for more.") + } + if !canXSign && args.Config.ForceWithoutCrossSigning { + s.srv.logger.Println("[WARN] current CA doesn't support cross signing but " + + "CA reconfiguration forced anyway with ForceWithoutCrossSigning") + } + // If it's a config change that would trigger a rotation (different provider/root): // 1. Get the root from the new provider. // 2. Call CrossSignCA on the old provider to sign the new root with the old one to @@ -255,18 +275,18 @@ func (s *ConnectCA) ConfigurationSet( return err } - // Have the old provider cross-sign the new intermediate - oldProvider, _ := s.srv.getCAProvider() - if oldProvider == nil { - return fmt.Errorf("internal error: CA provider is nil") - } - xcCert, err := oldProvider.CrossSignCA(newRoot) - if err != nil { - return err + if canXSign { + // Have the old provider cross-sign the new root + xcCert, err := oldProvider.CrossSignCA(newRoot) + if err != nil { + return err + } + + // Add the cross signed cert to the new CA's intermediates (to be attached + // to leaf certs). + newActiveRoot.IntermediateCerts = []string{xcCert} } - // Add the cross signed cert to the new root's intermediates. - newActiveRoot.IntermediateCerts = []string{xcCert} intermediate, err := newProvider.GenerateIntermediate() if err != nil { return err diff --git a/agent/consul/connect_ca_endpoint_test.go b/agent/consul/connect_ca_endpoint_test.go index 669370398..d55c7e4b6 100644 --- a/agent/consul/connect_ca_endpoint_test.go +++ b/agent/consul/connect_ca_endpoint_test.go @@ -144,6 +144,112 @@ func TestConnectCAConfig_GetSet(t *testing.T) { } } +// This test case tests that the logic around forcing a rotation without cross +// signing works when requested (and is denied when not requested). This occurs +// if the current CA is not able to cross sign external CA certificates. +func TestConnectCAConfig_GetSetForceNoCrossSigning(t *testing.T) { + t.Parallel() + + require := require.New(t) + // Setup a server with a built-in CA that as artificially disabled cross + // signing. This is simpler than running tests with external CA dependencies. + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.CAConfig.Config["DisableCrossSigning"] = true + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + codec := rpcClient(t, s1) + defer codec.Close() + + testrpc.WaitForTestAgent(t, s1.RPC, "dc1") + + // Store the current root + rootReq := &structs.DCSpecificRequest{ + Datacenter: "dc1", + } + var rootList structs.IndexedCARoots + require.NoError(msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", rootReq, &rootList)) + require.Len(rootList.Roots, 1) + oldRoot := rootList.Roots[0] + + // Get the starting config + { + args := &structs.DCSpecificRequest{ + Datacenter: "dc1", + } + var reply structs.CAConfiguration + require.NoError(msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationGet", args, &reply)) + + actual, err := ca.ParseConsulCAConfig(reply.Config) + require.NoError(err) + expected, err := ca.ParseConsulCAConfig(s1.config.CAConfig.Config) + require.NoError(err) + require.Equal(reply.Provider, s1.config.CAConfig.Provider) + require.Equal(actual, expected) + } + + // Update to a new CA with different key. This should fail since the existing + // CA doesn't support cross signing so can't rotate safely. + _, newKey, err := connect.GeneratePrivateKey() + require.NoError(err) + newConfig := &structs.CAConfiguration{ + Provider: "consul", + Config: map[string]interface{}{ + "PrivateKey": newKey, + }, + } + { + args := &structs.CARequest{ + Datacenter: "dc1", + Config: newConfig, + } + var reply interface{} + err := msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", args, &reply) + require.EqualError(err, "The current CA Provider does not support cross-signing. "+ + "You can try again with ForceWithoutCrossSigningSet but this may cause disruption"+ + " - see documentation for more.") + } + + // Now try again with the force flag set and it should work + { + newConfig.ForceWithoutCrossSigning = true + args := &structs.CARequest{ + Datacenter: "dc1", + Config: newConfig, + } + var reply interface{} + err := msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", args, &reply) + require.NoError(err) + } + + // Make sure the new root has been added but with no cross-signed intermediate + { + args := &structs.DCSpecificRequest{ + Datacenter: "dc1", + } + var reply structs.IndexedCARoots + require.NoError(msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", args, &reply)) + require.Len(reply.Roots, 2) + + for _, r := range reply.Roots { + if r.ID == oldRoot.ID { + // The old root should no longer be marked as the active root, + // and none of its other fields should have changed. + require.False(r.Active) + require.Equal(r.Name, oldRoot.Name) + require.Equal(r.RootCert, oldRoot.RootCert) + require.Equal(r.SigningCert, oldRoot.SigningCert) + require.Equal(r.IntermediateCerts, oldRoot.IntermediateCerts) + } else { + // The new root should NOT have a valid cross-signed cert from the old + // root as an intermediate. + require.True(r.Active) + require.Empty(r.IntermediateCerts) + } + } + } +} + func TestConnectCAConfig_TriggerRotation(t *testing.T) { t.Parallel() diff --git a/agent/structs/connect_ca.go b/agent/structs/connect_ca.go index 5fc7c96f1..a31f4315f 100644 --- a/agent/structs/connect_ca.go +++ b/agent/structs/connect_ca.go @@ -1,6 +1,7 @@ package structs import ( + "encoding/json" "fmt" "reflect" "time" @@ -244,6 +245,14 @@ type CAConfiguration struct { // identifiers anyway so this is simpler. State map[string]string + // ForceWithoutCrossSigning indicates that the CA reconfiguration should go + // ahead even if the current CA is unable to cross sign certificates. This + // risks temporary connection failures during the rollout as new leafs will be + // rejected by proxies that have not yet observed the new root cert but is the + // only option if a CA that doesn't support cross signing needs to be + // reconfigured or mirated away from. + ForceWithoutCrossSigning bool + RaftIndex } @@ -287,6 +296,25 @@ func (c *CAConfiguration) UnmarshalBinary(data []byte) error { if err != nil { return err } + return nil +} + +func (c *CAConfiguration) UnmarshalJSON(data []byte) (err error) { + type Alias CAConfiguration + + aux := &struct { + ForceWithoutCrossSigningSnake bool `json:"force_without_cross_signing"` + + *Alias + }{ + Alias: (*Alias)(c), + } + if err = json.Unmarshal(data, &aux); err != nil { + return err + } + if aux.ForceWithoutCrossSigningSnake { + c.ForceWithoutCrossSigning = aux.ForceWithoutCrossSigningSnake + } return nil } @@ -398,6 +426,12 @@ type ConsulCAProviderConfig struct { PrivateKey string RootCert string RotationPeriod 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 + // cross sign. We don't document this config field publicly or make any + // attempt to parse it from snake case unlike other fields here. + DisableCrossSigning bool } // CAConsulProviderState is used to track the built-in Consul CA provider's state. diff --git a/website/source/api/connect/ca.html.md b/website/source/api/connect/ca.html.md index 43d9dd848..de137ad58 100644 --- a/website/source/api/connect/ca.html.md +++ b/website/source/api/connect/ca.html.md @@ -132,6 +132,13 @@ The table below shows this endpoint's support for for the chosen provider. For more information on configuring the Connect CA providers, see [Provider Config](/docs/connect/ca.html). +- `ForceWithoutCrossSigning` `(bool: )` - Indicates that the CA change + should be force to complete even if the current CA doesn't support cross + signing. Changing root without cross-signing may cause temporary connection + failures until the rollout completes. See [Forced Rotation Without + Cross-Signing](/docs/connect/ca.html#forced-rotation-without-cross-signing) + for more detail. + ### Sample Payload ```json @@ -142,7 +149,8 @@ providers, see [Provider Config](/docs/connect/ca.html). "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----...", "RootCert": "-----BEGIN CERTIFICATE-----...", "RotationPeriod": "2160h" - } + }, + "ForceWithoutCrossSigning": false } ``` diff --git a/website/source/docs/commands/connect/ca.html.md.erb b/website/source/docs/commands/connect/ca.html.md.erb index 08874de05..296cacc4e 100644 --- a/website/source/docs/commands/connect/ca.html.md.erb +++ b/website/source/docs/commands/connect/ca.html.md.erb @@ -77,6 +77,8 @@ Usage: `consul connect ca set-config [options]` #### Command Options * `-config-file` - (required) Specifies a JSON-formatted file to use for the new configuration. + The format of this config file matches the request payload documented in the + [Update CA Configuration API](/api/connect/ca.html#update-ca-configuration). The output looks like this: diff --git a/website/source/docs/connect/ca.html.md b/website/source/docs/connect/ca.html.md index 41c9e31f7..28cc86921 100644 --- a/website/source/docs/connect/ca.html.md +++ b/website/source/docs/connect/ca.html.md @@ -107,30 +107,37 @@ CA provider documentation in the sidebar to the left. ## Root Certificate Rotation Whenever the CA's configuration is updated in a way that causes the root key to -change, a special rotation process will be triggered in order to smoothly transition to -the new certificate. This rotation is automatically orchestrated by Consul. +change, a special rotation process will be triggered in order to smoothly +transition to the new certificate. This rotation is automatically orchestrated +by Consul. + +~> If the current CA Provider doesn't support cross-signing, this process can't +be followed. See [Forced Rotation Without +Cross-Signing](#forced-rotation-without-cross-signing). This also automatically occurs when a completely different CA provider is configured (since this changes the root key). Therefore, this automatic rotation process can also be used to cleanly transition between CA providers. For example, updating Connect to use Vault instead of the built-in CA. -During rotation, an intermediate CA certificate is requested from the new root, which is then -cross-signed by the old root. This cross-signed certificate is then distributed -alongside any newly-generated leaf certificates used by the proxies once the new root -becomes active, and provides a chain of trust back to the old root certificate in the -event that a certificate signed by the new root is presented to a proxy that has not yet -updated its bundle of trusted root CA certificates to include the new root. +During rotation, an intermediate CA certificate is requested from the new root, +which is then cross-signed by the old root. This cross-signed certificate is +then distributed alongside any newly-generated leaf certificates used by the +proxies once the new root becomes active, and provides a chain of trust back to +the old root certificate in the event that a certificate signed by the new root +is presented to a proxy that has not yet updated its bundle of trusted root CA +certificates to include the new root. After the cross-signed certificate has been successfully generated and the new root certificate or CA provider has been set up, the new root becomes the active one and is immediately used for signing any new incoming certificate requests. -If we check the [list CA roots endpoint](/api/connect/ca.html#list-ca-root-certificates) -after updating the configuration with a new root certificate, we can see both the old and new root -certificates are present, and the currently active root has an intermediate certificate -which has been generated and cross-signed automatically by the old root during the -rotation process: +If we check the [list CA roots +endpoint](/api/connect/ca.html#list-ca-root-certificates) after updating the +configuration with a new root certificate, we can see both the old and new root +certificates are present, and the currently active root has an intermediate +certificate which has been generated and cross-signed automatically by the old +root during the rotation process: ```bash $ curl localhost:8500/v1/connect/ca/roots @@ -178,3 +185,30 @@ $ curl localhost:8500/v1/connect/ca/roots The old root certificate will be automatically removed once enough time has elapsed for any leaf certificates signed by it to expire. + +### Forced Rotation Without Cross-Signing + +If the CA provider that is currently in use does not support cross-signing, then +attempts to change the root key or CA provider will fail. This is to ensure +operators don't make the change without understanding that there is additional +risk involved. + +It is possible to force the change to happen anyway by setting the +`ForceWithoutCrossSigning` field in the CA configuration to `true`. + +The downside is that all new certificates will immediately start being signed +with the new root key, but it will take some time for agents throughout the +cluster to observe the root CA change and reconfigure applications and proxies +to accept certificates signed by this new root. This will mean connections made +with a new certificate may fail for a short period after the CA change. + +Typically all connected agents will have observed the new roots within seconds +even in a large deployment so the impact should be contained. But it is possible +for a disconnected, overloaded or misconfigured agent to not see the new root +for an unbounded amount of time during which new connections to services on that +host will fail. The issue will resolve as soon as the agent can reconnect to +servers. + +Currently both Consul and Vault CA providers _do_ support cross signing. As more +providers are added this documentation will list any that this section applies +to. \ No newline at end of file