Implement the insecure version of the Cluster.AutoConfig RPC endpoint

Right now this is only hooked into the insecure RPC server and requires JWT authorization. If no JWT authorizer is setup in the configuration then we inject a disabled “authorizer” to always report that JWT authorization is disabled.
This commit is contained in:
Matt Keeler 2020-06-05 15:56:19 -04:00
parent 1dba94311a
commit 9b01f9423c
No known key found for this signature in database
GPG Key ID: 04DBAE1857E0081B
16 changed files with 5331 additions and 10 deletions

View File

@ -1403,6 +1403,18 @@ func (a *Agent) consulConfig() (*consul.Config, error) {
} }
} }
// copy over auto config settings
base.AutoConfigEnabled = a.config.AutoConfig.Enabled
base.AutoConfigIntroToken = a.config.AutoConfig.IntroToken
base.AutoConfigIntroTokenFile = a.config.AutoConfig.IntroTokenFile
base.AutoConfigServerAddresses = a.config.AutoConfig.ServerAddresses
base.AutoConfigDNSSANs = a.config.AutoConfig.DNSSANs
base.AutoConfigIPSANs = a.config.AutoConfig.IPSANs
base.AutoConfigAuthzEnabled = a.config.AutoConfig.Authorizer.Enabled
base.AutoConfigAuthzAuthMethod = a.config.AutoConfig.Authorizer.AuthMethod
base.AutoConfigAuthzClaimAssertions = a.config.AutoConfig.Authorizer.ClaimAssertions
base.AutoConfigAuthzAllowReuse = a.config.AutoConfig.Authorizer.AllowReuse
// Setup the user event callback // Setup the user event callback
base.UserEventHandler = func(e serf.UserEvent) { base.UserEventHandler = func(e serf.UserEvent) {
select { select {

View File

@ -0,0 +1,21 @@
package agentpb
func (req *AutoConfigRequest) RequestDatacenter() string {
return req.Datacenter
}
func (req *AutoConfigRequest) IsRead() bool {
return false
}
func (req *AutoConfigRequest) AllowStaleRead() bool {
return false
}
func (req *AutoConfigRequest) TokenSecret() string {
return req.ConsulToken
}
func (req *AutoConfigRequest) SetTokenSecret(token string) {
req.ConsulToken = token
}

View File

@ -0,0 +1,28 @@
// Code generated by protoc-gen-go-binary. DO NOT EDIT.
// source: agent/agentpb/auto_config.proto
package agentpb
import (
"github.com/golang/protobuf/proto"
)
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *AutoConfigRequest) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *AutoConfigRequest) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *AutoConfigResponse) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *AutoConfigResponse) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}

View File

@ -0,0 +1,757 @@
// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: agent/agentpb/auto_config.proto
package agentpb
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
config "github.com/hashicorp/consul/agent/agentpb/config"
io "io"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
// AutoConfigRequest is the data structure to be sent along with the
// Cluster.AutoConfig RPC
type AutoConfigRequest struct {
// Datacenter is the local datacenter name. This wont actually be set by clients
// but rather will be set by the servers to allow for forwarding to
// the leader. If it ever happens to be set and differs from the local datacenters
// name then an error should be returned.
Datacenter string `protobuf:"bytes,1,opt,name=Datacenter,proto3" json:"Datacenter,omitempty"`
// Node is the node name that the requester would like to assume
// the identity of.
Node string `protobuf:"bytes,2,opt,name=Node,proto3" json:"Node,omitempty"`
// Segment is the network segment that the requester would like to join
Segment string `protobuf:"bytes,4,opt,name=Segment,proto3" json:"Segment,omitempty"`
// JWT is a signed JSON Web Token used to authorize the request
JWT string `protobuf:"bytes,5,opt,name=JWT,proto3" json:"JWT,omitempty"`
// ConsulToken is a Consul ACL token that the agent requesting the
// configuration already has.
ConsulToken string `protobuf:"bytes,6,opt,name=ConsulToken,proto3" json:"ConsulToken,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *AutoConfigRequest) Reset() { *m = AutoConfigRequest{} }
func (m *AutoConfigRequest) String() string { return proto.CompactTextString(m) }
func (*AutoConfigRequest) ProtoMessage() {}
func (*AutoConfigRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_c842365210d144b0, []int{0}
}
func (m *AutoConfigRequest) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *AutoConfigRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
if deterministic {
return xxx_messageInfo_AutoConfigRequest.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 *AutoConfigRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_AutoConfigRequest.Merge(m, src)
}
func (m *AutoConfigRequest) XXX_Size() int {
return m.Size()
}
func (m *AutoConfigRequest) XXX_DiscardUnknown() {
xxx_messageInfo_AutoConfigRequest.DiscardUnknown(m)
}
var xxx_messageInfo_AutoConfigRequest proto.InternalMessageInfo
func (m *AutoConfigRequest) GetDatacenter() string {
if m != nil {
return m.Datacenter
}
return ""
}
func (m *AutoConfigRequest) GetNode() string {
if m != nil {
return m.Node
}
return ""
}
func (m *AutoConfigRequest) GetSegment() string {
if m != nil {
return m.Segment
}
return ""
}
func (m *AutoConfigRequest) GetJWT() string {
if m != nil {
return m.JWT
}
return ""
}
func (m *AutoConfigRequest) GetConsulToken() string {
if m != nil {
return m.ConsulToken
}
return ""
}
// AutoConfigResponse is the data structure sent in response to a Cluster.AutoConfig request
type AutoConfigResponse struct {
Config *config.Config `protobuf:"bytes,1,opt,name=Config,proto3" json:"Config,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *AutoConfigResponse) Reset() { *m = AutoConfigResponse{} }
func (m *AutoConfigResponse) String() string { return proto.CompactTextString(m) }
func (*AutoConfigResponse) ProtoMessage() {}
func (*AutoConfigResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_c842365210d144b0, []int{1}
}
func (m *AutoConfigResponse) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *AutoConfigResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
if deterministic {
return xxx_messageInfo_AutoConfigResponse.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 *AutoConfigResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_AutoConfigResponse.Merge(m, src)
}
func (m *AutoConfigResponse) XXX_Size() int {
return m.Size()
}
func (m *AutoConfigResponse) XXX_DiscardUnknown() {
xxx_messageInfo_AutoConfigResponse.DiscardUnknown(m)
}
var xxx_messageInfo_AutoConfigResponse proto.InternalMessageInfo
func (m *AutoConfigResponse) GetConfig() *config.Config {
if m != nil {
return m.Config
}
return nil
}
func init() {
proto.RegisterType((*AutoConfigRequest)(nil), "agentpb.AutoConfigRequest")
proto.RegisterType((*AutoConfigResponse)(nil), "agentpb.AutoConfigResponse")
}
func init() { proto.RegisterFile("agent/agentpb/auto_config.proto", fileDescriptor_c842365210d144b0) }
var fileDescriptor_c842365210d144b0 = []byte{
// 258 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4f, 0x4c, 0x4f, 0xcd,
0x2b, 0xd1, 0x07, 0x93, 0x05, 0x49, 0xfa, 0x89, 0xa5, 0x25, 0xf9, 0xf1, 0xc9, 0xf9, 0x79, 0x69,
0x99, 0xe9, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0xec, 0x50, 0x29, 0x29, 0x45, 0x54, 0x95,
0x10, 0x45, 0xfa, 0xc8, 0x6a, 0x95, 0xa6, 0x32, 0x72, 0x09, 0x3a, 0x96, 0x96, 0xe4, 0x3b, 0x83,
0x05, 0x83, 0x52, 0x0b, 0x4b, 0x53, 0x8b, 0x4b, 0x84, 0xe4, 0xb8, 0xb8, 0x5c, 0x12, 0x4b, 0x12,
0x93, 0x53, 0xf3, 0x4a, 0x52, 0x8b, 0x24, 0x18, 0x15, 0x18, 0x35, 0x38, 0x83, 0x90, 0x44, 0x84,
0x84, 0xb8, 0x58, 0xfc, 0xf2, 0x53, 0x52, 0x25, 0x98, 0xc0, 0x32, 0x60, 0xb6, 0x90, 0x04, 0x17,
0x7b, 0x70, 0x6a, 0x7a, 0x6e, 0x6a, 0x5e, 0x89, 0x04, 0x0b, 0x58, 0x18, 0xc6, 0x15, 0x12, 0xe0,
0x62, 0xf6, 0x0a, 0x0f, 0x91, 0x60, 0x05, 0x8b, 0x82, 0x98, 0x42, 0x0a, 0x5c, 0xdc, 0xce, 0xf9,
0x79, 0xc5, 0xa5, 0x39, 0x21, 0xf9, 0xd9, 0xa9, 0x79, 0x12, 0x6c, 0x60, 0x19, 0x64, 0x21, 0x25,
0x1b, 0x2e, 0x21, 0x64, 0x67, 0x15, 0x17, 0xe4, 0xe7, 0x15, 0xa7, 0x0a, 0xa9, 0x71, 0xb1, 0x41,
0x44, 0xc0, 0x6e, 0xe2, 0x36, 0xe2, 0xd3, 0x83, 0x7a, 0x06, 0xaa, 0x0e, 0x2a, 0xeb, 0x64, 0x7d,
0xe2, 0x91, 0x1c, 0xe3, 0x85, 0x47, 0x72, 0x8c, 0x0f, 0x1e, 0xc9, 0x31, 0xce, 0x78, 0x2c, 0xc7,
0x10, 0xa5, 0x99, 0x9e, 0x59, 0x92, 0x51, 0x9a, 0xa4, 0x97, 0x9c, 0x9f, 0xab, 0x9f, 0x91, 0x58,
0x9c, 0x91, 0x99, 0x9c, 0x5f, 0x54, 0x00, 0x0a, 0x8a, 0xe2, 0xd2, 0x1c, 0x7d, 0x94, 0x60, 0x4a,
0x62, 0x03, 0x87, 0x8c, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0x3b, 0x8c, 0xf1, 0x75, 0x68, 0x01,
0x00, 0x00,
}
func (m *AutoConfigRequest) 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 *AutoConfigRequest) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if len(m.Datacenter) > 0 {
dAtA[i] = 0xa
i++
i = encodeVarintAutoConfig(dAtA, i, uint64(len(m.Datacenter)))
i += copy(dAtA[i:], m.Datacenter)
}
if len(m.Node) > 0 {
dAtA[i] = 0x12
i++
i = encodeVarintAutoConfig(dAtA, i, uint64(len(m.Node)))
i += copy(dAtA[i:], m.Node)
}
if len(m.Segment) > 0 {
dAtA[i] = 0x22
i++
i = encodeVarintAutoConfig(dAtA, i, uint64(len(m.Segment)))
i += copy(dAtA[i:], m.Segment)
}
if len(m.JWT) > 0 {
dAtA[i] = 0x2a
i++
i = encodeVarintAutoConfig(dAtA, i, uint64(len(m.JWT)))
i += copy(dAtA[i:], m.JWT)
}
if len(m.ConsulToken) > 0 {
dAtA[i] = 0x32
i++
i = encodeVarintAutoConfig(dAtA, i, uint64(len(m.ConsulToken)))
i += copy(dAtA[i:], m.ConsulToken)
}
if m.XXX_unrecognized != nil {
i += copy(dAtA[i:], m.XXX_unrecognized)
}
return i, nil
}
func (m *AutoConfigResponse) 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 *AutoConfigResponse) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if m.Config != nil {
dAtA[i] = 0xa
i++
i = encodeVarintAutoConfig(dAtA, i, uint64(m.Config.Size()))
n1, err := m.Config.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n1
}
if m.XXX_unrecognized != nil {
i += copy(dAtA[i:], m.XXX_unrecognized)
}
return i, nil
}
func encodeVarintAutoConfig(dAtA []byte, offset int, v uint64) int {
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return offset + 1
}
func (m *AutoConfigRequest) Size() (n int) {
if m == nil {
return 0
}
var l int
_ = l
l = len(m.Datacenter)
if l > 0 {
n += 1 + l + sovAutoConfig(uint64(l))
}
l = len(m.Node)
if l > 0 {
n += 1 + l + sovAutoConfig(uint64(l))
}
l = len(m.Segment)
if l > 0 {
n += 1 + l + sovAutoConfig(uint64(l))
}
l = len(m.JWT)
if l > 0 {
n += 1 + l + sovAutoConfig(uint64(l))
}
l = len(m.ConsulToken)
if l > 0 {
n += 1 + l + sovAutoConfig(uint64(l))
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
return n
}
func (m *AutoConfigResponse) Size() (n int) {
if m == nil {
return 0
}
var l int
_ = l
if m.Config != nil {
l = m.Config.Size()
n += 1 + l + sovAutoConfig(uint64(l))
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
return n
}
func sovAutoConfig(x uint64) (n int) {
for {
n++
x >>= 7
if x == 0 {
break
}
}
return n
}
func sozAutoConfig(x uint64) (n int) {
return sovAutoConfig(uint64((x << 1) ^ uint64((int64(x) >> 63))))
}
func (m *AutoConfigRequest) 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 ErrIntOverflowAutoConfig
}
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: AutoConfigRequest: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: AutoConfigRequest: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Datacenter", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAutoConfig
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthAutoConfig
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthAutoConfig
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Datacenter = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Node", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAutoConfig
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthAutoConfig
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthAutoConfig
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Node = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 4:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Segment", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAutoConfig
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthAutoConfig
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthAutoConfig
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Segment = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 5:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field JWT", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAutoConfig
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthAutoConfig
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthAutoConfig
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.JWT = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 6:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field ConsulToken", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAutoConfig
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthAutoConfig
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthAutoConfig
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.ConsulToken = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipAutoConfig(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthAutoConfig
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthAutoConfig
}
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 *AutoConfigResponse) 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 ErrIntOverflowAutoConfig
}
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: AutoConfigResponse: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: AutoConfigResponse: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Config", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAutoConfig
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthAutoConfig
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthAutoConfig
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.Config == nil {
m.Config = &config.Config{}
}
if err := m.Config.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipAutoConfig(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthAutoConfig
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthAutoConfig
}
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 skipAutoConfig(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowAutoConfig
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
wireType := int(wire & 0x7)
switch wireType {
case 0:
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowAutoConfig
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
iNdEx++
if dAtA[iNdEx-1] < 0x80 {
break
}
}
return iNdEx, nil
case 1:
iNdEx += 8
return iNdEx, nil
case 2:
var length int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowAutoConfig
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
length |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if length < 0 {
return 0, ErrInvalidLengthAutoConfig
}
iNdEx += length
if iNdEx < 0 {
return 0, ErrInvalidLengthAutoConfig
}
return iNdEx, nil
case 3:
for {
var innerWire uint64
var start int = iNdEx
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowAutoConfig
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
innerWire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
innerWireType := int(innerWire & 0x7)
if innerWireType == 4 {
break
}
next, err := skipAutoConfig(dAtA[start:])
if err != nil {
return 0, err
}
iNdEx = start + next
if iNdEx < 0 {
return 0, ErrInvalidLengthAutoConfig
}
}
return iNdEx, nil
case 4:
return iNdEx, nil
case 5:
iNdEx += 4
return iNdEx, nil
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
}
panic("unreachable")
}
var (
ErrInvalidLengthAutoConfig = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowAutoConfig = fmt.Errorf("proto: integer overflow")
)

View File

@ -0,0 +1,36 @@
syntax = "proto3";
package agentpb;
option go_package = "github.com/hashicorp/consul/agent/agentpb";
import "agent/agentpb/config/config.proto";
// AutoConfigRequest is the data structure to be sent along with the
// Cluster.AutoConfig RPC
message AutoConfigRequest {
// Datacenter is the local datacenter name. This wont actually be set by clients
// but rather will be set by the servers to allow for forwarding to
// the leader. If it ever happens to be set and differs from the local datacenters
// name then an error should be returned.
string Datacenter = 1;
// Node is the node name that the requester would like to assume
// the identity of.
string Node = 2;
// Segment is the network segment that the requester would like to join
string Segment = 4;
// JWT is a signed JSON Web Token used to authorize the request
string JWT = 5;
// ConsulToken is a Consul ACL token that the agent requesting the
// configuration already has.
string ConsulToken = 6;
}
// AutoConfigResponse is the data structure sent in response to a Cluster.AutoConfig request
message AutoConfigResponse {
config.Config Config = 1;
}

View File

@ -0,0 +1,88 @@
// Code generated by protoc-gen-go-binary. DO NOT EDIT.
// source: agent/agentpb/config/config.proto
package config
import (
"github.com/golang/protobuf/proto"
)
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *Config) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *Config) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *Gossip) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *Gossip) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *GossipEncryption) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *GossipEncryption) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *TLS) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *TLS) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *ACL) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *ACL) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *ACLTokens) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *ACLTokens) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *ACLServiceProviderToken) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *ACLServiceProviderToken) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *AutoEncrypt) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *AutoEncrypt) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,70 @@
syntax = "proto3";
package config;
option go_package = "github.com/hashicorp/consul/agent/agentpb/config";
message Config {
string Datacenter = 1;
string PrimaryDatacenter = 2;
string NodeName = 3;
string SegmentName = 4;
ACL ACL = 5;
AutoEncrypt AutoEncrypt = 6;
Gossip Gossip = 7;
TLS TLS = 8;
}
message Gossip {
GossipEncryption Encryption = 1;
repeated string RetryJoinLAN = 2;
}
message GossipEncryption {
string Key = 1;
bool VerifyIncoming = 2;
bool VerifyOutgoing = 3;
}
message TLS {
bool VerifyOutgoing = 1;
bool VerifyServerHostname = 2;
string CipherSuites = 3;
string MinVersion = 4;
bool PreferServerCipherSuites = 5;
}
message ACL {
bool Enabled = 1;
string PolicyTTL = 2;
string RoleTTL = 3;
string TokenTTL = 4;
string DownPolicy = 5;
string DefaultPolicy = 6;
bool EnableKeyListPolicy = 7;
ACLTokens Tokens = 8;
string DisabledTTL = 9;
bool EnableTokenPersistence = 10;
bool MSPDisableBootstrap = 11;
}
message ACLTokens {
string Master = 1;
string Replication = 2;
string AgentMaster = 3;
string Default = 4;
string Agent = 5;
repeated ACLServiceProviderToken ManagedServiceProvider = 6;
}
message ACLServiceProviderToken {
string AccessorID = 1;
string SecretID = 2;
}
message AutoEncrypt {
bool TLS = 1;
repeated string DNSSAN = 2;
repeated string IPSAN = 3;
bool AllowTLS = 4;
}

View File

@ -0,0 +1,312 @@
package consul
import (
"context"
"encoding/base64"
"fmt"
"net"
"time"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/agentpb"
"github.com/hashicorp/consul/agent/agentpb/config"
"github.com/hashicorp/consul/agent/consul/authmethod/ssoauth"
"github.com/hashicorp/consul/agent/metadata"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/lib/template"
"github.com/hashicorp/consul/tlsutil"
bexpr "github.com/hashicorp/go-bexpr"
)
type AutoConfigOptions struct {
NodeName string
SegmentName string
}
type AutoConfigAuthorizer interface {
// Authorizes the request and returns a struct containing the various
// options for how to generate the configuration.
Authorize(*agentpb.AutoConfigRequest) (AutoConfigOptions, error)
}
type disabledAuthorizer struct{}
func (_ *disabledAuthorizer) Authorize(_ *agentpb.AutoConfigRequest) (AutoConfigOptions, error) {
return AutoConfigOptions{}, fmt.Errorf("Auto Config is disabled")
}
type jwtAuthorizer struct {
validator *ssoauth.Validator
allowReuse bool
claimAssertions []string
}
func (a *jwtAuthorizer) Authorize(req *agentpb.AutoConfigRequest) (AutoConfigOptions, error) {
// perform basic JWT Authorization
identity, err := a.validator.ValidateLogin(context.Background(), req.JWT)
if err != nil {
// TODO (autoconf) maybe we should add a more generic permission denied error not tied to the ACL package?
return AutoConfigOptions{}, acl.PermissionDenied("Failed JWT authorization: %v", err)
}
varMap := map[string]string{
"node": req.Node,
"segment": req.Segment,
}
// TODO (autoconf) check for JWT reuse if configured to do so.
for _, raw := range a.claimAssertions {
// validate and fill any HIL
filled, err := template.InterpolateHIL(raw, varMap, true)
if err != nil {
return AutoConfigOptions{}, fmt.Errorf("Failed to render claim assertion template %q: %w", raw, err)
}
evaluator, err := bexpr.CreateEvaluatorForType(filled, nil, identity.SelectableFields)
if err != nil {
return AutoConfigOptions{}, fmt.Errorf("Failed to create evaluator for claim assertion %q: %w", filled, err)
}
ok, err := evaluator.Evaluate(identity.SelectableFields)
if err != nil {
return AutoConfigOptions{}, fmt.Errorf("Failed to execute claim assertion %q: %w", filled, err)
}
if !ok {
return AutoConfigOptions{}, acl.PermissionDenied("Failed JWT claim assertion")
}
}
return AutoConfigOptions{
NodeName: req.Node,
SegmentName: req.Segment,
}, nil
}
// Cluster endpoint is used for cluster configuration operations
type Cluster struct {
srv *Server
authorizer AutoConfigAuthorizer
}
// updateTLSCertificatesInConfig will ensure that the TLS settings regarding how an agent is
// made aware of its certificates are populated. This will only work if connect is enabled and
// in some cases only if auto_encrypt is enabled on the servers. This endpoint has the option
// to configure auto_encrypt or potentially in the future to generate the certificates inline.
func (c *Cluster) updateTLSCertificatesInConfig(opts AutoConfigOptions, conf *config.Config) error {
if c.srv.config.AutoEncryptAllowTLS {
conf.AutoEncrypt = &config.AutoEncrypt{TLS: true}
} else {
conf.AutoEncrypt = &config.AutoEncrypt{TLS: false}
}
return nil
}
// updateACLtokensInConfig will configure all of the agents ACL settings and will populate
// the configuration with an agent token usable for all default agent operations.
func (c *Cluster) updateACLsInConfig(opts AutoConfigOptions, conf *config.Config) error {
acl := &config.ACL{
Enabled: c.srv.config.ACLsEnabled,
PolicyTTL: c.srv.config.ACLPolicyTTL.String(),
RoleTTL: c.srv.config.ACLRoleTTL.String(),
TokenTTL: c.srv.config.ACLTokenTTL.String(),
DisabledTTL: c.srv.config.ACLDisabledTTL.String(),
DownPolicy: c.srv.config.ACLDownPolicy,
DefaultPolicy: c.srv.config.ACLDefaultPolicy,
EnableKeyListPolicy: c.srv.config.ACLEnableKeyListPolicy,
}
// when ACLs are enabled we want to create a local token with a node identity
if c.srv.config.ACLsEnabled {
// we have to require local tokens or else it would require having these servers use a token with acl:write to make a
// token create RPC to the servers in the primary DC.
if !c.srv.LocalTokensEnabled() {
return fmt.Errorf("Agent Auto Configuration requires local token usage to be enabled in this datacenter: %s", c.srv.config.Datacenter)
}
// generate the accessor id
accessor, err := lib.GenerateUUID(c.srv.checkTokenUUID)
if err != nil {
return err
}
// generate the secret id
secret, err := lib.GenerateUUID(c.srv.checkTokenUUID)
if err != nil {
return err
}
// set up the token
token := structs.ACLToken{
AccessorID: accessor,
SecretID: secret,
Description: fmt.Sprintf("Auto Config Token for Node %q", opts.NodeName),
CreateTime: time.Now(),
Local: true,
NodeIdentities: []*structs.ACLNodeIdentity{
{
NodeName: opts.NodeName,
Datacenter: c.srv.config.Datacenter,
},
},
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
}
req := structs.ACLTokenBatchSetRequest{
Tokens: structs.ACLTokens{&token},
CAS: false,
}
// perform the request to mint the new token
if _, err := c.srv.raftApplyMsgpack(structs.ACLTokenSetRequestType, &req); err != nil {
return err
}
acl.Tokens = &config.ACLTokens{Agent: secret}
}
conf.ACL = acl
return nil
}
// updateJoinAddressesInConfig determines the correct gossip endpoints that clients should
// be connecting to for joining the cluster based on the segment given in the opts parameter.
func (c *Cluster) updateJoinAddressesInConfig(opts AutoConfigOptions, conf *config.Config) error {
members, err := c.srv.LANSegmentMembers(opts.SegmentName)
if err != nil {
return err
}
var joinAddrs []string
for _, m := range members {
if ok, _ := metadata.IsConsulServer(m); ok {
serfAddr := net.TCPAddr{IP: m.Addr, Port: int(m.Port)}
joinAddrs = append(joinAddrs, serfAddr.String())
}
}
if conf.Gossip == nil {
conf.Gossip = &config.Gossip{}
}
conf.Gossip.RetryJoinLAN = joinAddrs
return nil
}
// updateGossipEncryptionInConfig will populate the gossip encryption configuration settings
func (c *Cluster) updateGossipEncryptionInConfig(_ AutoConfigOptions, conf *config.Config) error {
// Add gossip encryption settings if there is any key loaded
memberlistConfig := c.srv.config.SerfLANConfig.MemberlistConfig
if lanKeyring := memberlistConfig.Keyring; lanKeyring != nil {
if conf.Gossip == nil {
conf.Gossip = &config.Gossip{}
}
if conf.Gossip.Encryption == nil {
conf.Gossip.Encryption = &config.GossipEncryption{}
}
pk := lanKeyring.GetPrimaryKey()
if len(pk) > 0 {
conf.Gossip.Encryption.Key = base64.StdEncoding.EncodeToString(pk)
}
conf.Gossip.Encryption.VerifyIncoming = memberlistConfig.GossipVerifyIncoming
conf.Gossip.Encryption.VerifyOutgoing = memberlistConfig.GossipVerifyOutgoing
}
return nil
}
// updateTLSSettingsInConfig will populate the TLS configuration settings but will not
// populate leaf or ca certficiates.
func (c *Cluster) updateTLSSettingsInConfig(_ AutoConfigOptions, conf *config.Config) error {
// add in TLS configuration
if conf.TLS == nil {
conf.TLS = &config.TLS{}
}
conf.TLS.VerifyServerHostname = c.srv.tlsConfigurator.VerifyServerHostname()
base := c.srv.tlsConfigurator.Base()
conf.TLS.VerifyOutgoing = base.VerifyOutgoing
conf.TLS.MinVersion = base.TLSMinVersion
conf.TLS.PreferServerCipherSuites = base.PreferServerCipherSuites
var err error
conf.TLS.CipherSuites, err = tlsutil.CipherString(base.CipherSuites)
return err
}
// baseConfig will populate the configuration with some base settings such as the
// datacenter names, node name etc.
func (c *Cluster) baseConfig(opts AutoConfigOptions, conf *config.Config) error {
if opts.NodeName == "" {
return fmt.Errorf("Cannot generate auto config response without a node name")
}
conf.Datacenter = c.srv.config.Datacenter
conf.PrimaryDatacenter = c.srv.config.PrimaryDatacenter
conf.NodeName = opts.NodeName
conf.SegmentName = opts.SegmentName
return nil
}
type autoConfigUpdater func(c *Cluster, opts AutoConfigOptions, conf *config.Config) error
var (
// variable holding the list of config updating functions to execute when generating
// the auto config response. This will allow for more easily adding extra self-contained
// configurators here in the future.
autoConfigUpdaters []autoConfigUpdater = []autoConfigUpdater{
(*Cluster).baseConfig,
(*Cluster).updateJoinAddressesInConfig,
(*Cluster).updateGossipEncryptionInConfig,
(*Cluster).updateTLSSettingsInConfig,
(*Cluster).updateACLsInConfig,
(*Cluster).updateTLSCertificatesInConfig,
}
)
// AgentAutoConfig will authorize the incoming request and then generate the configuration
// to push down to the client
func (c *Cluster) AutoConfig(req *agentpb.AutoConfigRequest, resp *agentpb.AutoConfigResponse) error {
// default the datacenter to our datacenter - agents do not have to specify this as they may not
// yet know the datacenter name they are going to be in.
if req.Datacenter == "" {
req.Datacenter = c.srv.config.Datacenter
}
// TODO (autoconf) Is performing auto configuration over the WAN really a bad idea?
if req.Datacenter != c.srv.config.Datacenter {
return fmt.Errorf("invalid datacenter %q - agent auto configuration cannot target a remote datacenter", req.Datacenter)
}
// forward to the leader
if done, err := c.srv.forward("Cluster.AutoConfig", req, req, resp); done {
return err
}
// TODO (autoconf) maybe panic instead?
if c.authorizer == nil {
return fmt.Errorf("No Auto Config authorizer is configured")
}
// authorize the request with the configured authorizer
opts, err := c.authorizer.Authorize(req)
if err != nil {
return err
}
conf := &config.Config{}
// update all the configurations
for _, configFn := range autoConfigUpdaters {
if err := configFn(c, opts, conf); err != nil {
return err
}
}
resp.Config = conf
return nil
}

View File

@ -0,0 +1,617 @@
package consul
import (
"encoding/base64"
"fmt"
"io/ioutil"
"math/rand"
"net"
"os"
"path"
"testing"
"time"
"github.com/hashicorp/consul/agent/agentpb"
"github.com/hashicorp/consul/agent/agentpb/config"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/tlsutil"
"github.com/hashicorp/memberlist"
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
"github.com/stretchr/testify/require"
"gopkg.in/square/go-jose.v2/jwt"
)
func testJWTStandardClaims() jwt.Claims {
now := time.Now()
return jwt.Claims{
Subject: "consul",
Issuer: "consul",
Audience: jwt.Audience{"consul"},
NotBefore: jwt.NewNumericDate(now.Add(-1 * time.Second)),
Expiry: jwt.NewNumericDate(now.Add(10 * time.Minute)),
}
}
func signJWT(t *testing.T, privKey string, claims jwt.Claims, privateClaims interface{}) string {
t.Helper()
token, err := oidcauthtest.SignJWT(privKey, claims, privateClaims)
require.NoError(t, err)
return token
}
func signJWTWithStandardClaims(t *testing.T, privKey string, claims interface{}) string {
t.Helper()
return signJWT(t, privKey, testJWTStandardClaims(), claims)
}
// TestClusterAutoConfig is really an integration test of all the moving parts of the Cluster.AutoConfig RPC.
// Full testing of the individual parts will not be done in this test:
//
// * Any implementations of the AutoConfigAuthorizer interface (although these test do use the jwtAuthorizer)
// * Each of the individual config generation functions. These can be unit tested separately and many wont
// require a running test server.
func TestClusterAutoConfig(t *testing.T) {
type testCase struct {
request agentpb.AutoConfigRequest
expected agentpb.AutoConfigResponse
patchResponse func(t *testing.T, srv *Server, resp *agentpb.AutoConfigResponse)
err string
}
gossipKey := make([]byte, 32)
// this is not cryptographic randomness and is not secure but for the sake of this test its all we need.
n, err := rand.Read(gossipKey)
require.NoError(t, err)
require.Equal(t, 32, n)
gossipKeyEncoded := base64.StdEncoding.EncodeToString(gossipKey)
// generate a test certificate for the server serving out the insecure RPC
cert, key, cacert, err := testTLSCertificates("server.dc1.consul")
require.NoError(t, err)
// generate a JWT signer
pub, priv, err := oidcauthtest.GenerateKey()
require.NoError(t, err)
_, altpriv, err := oidcauthtest.GenerateKey()
require.NoError(t, err)
cases := map[string]testCase{
"wrong-datacenter": {
request: agentpb.AutoConfigRequest{
Datacenter: "no-such-dc",
},
err: `invalid datacenter "no-such-dc" - agent auto configuration cannot target a remote datacenter`,
},
"unverifiable": {
request: agentpb.AutoConfigRequest{
Node: "test-node",
// this is signed using an incorrect private key
JWT: signJWTWithStandardClaims(t, altpriv, map[string]interface{}{"consul_node_name": "test-node"}),
},
err: "Permission denied: Failed JWT authorization: no known key successfully validated the token signature",
},
"claim-assertion-failed": {
request: agentpb.AutoConfigRequest{
Node: "test-node",
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"wrong_claim": "test-node"}),
},
err: "Permission denied: Failed JWT claim assertion",
},
"good": {
request: agentpb.AutoConfigRequest{
Node: "test-node",
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
},
expected: agentpb.AutoConfigResponse{
Config: &config.Config{
Datacenter: "dc1",
PrimaryDatacenter: "dc1",
NodeName: "test-node",
AutoEncrypt: &config.AutoEncrypt{
TLS: true,
},
ACL: &config.ACL{
Enabled: true,
PolicyTTL: "30s",
TokenTTL: "30s",
RoleTTL: "30s",
DisabledTTL: "0s",
DownPolicy: "extend-cache",
DefaultPolicy: "deny",
Tokens: &config.ACLTokens{
Agent: "patched-secret",
},
},
Gossip: &config.Gossip{
Encryption: &config.GossipEncryption{
Key: gossipKeyEncoded,
VerifyIncoming: true,
VerifyOutgoing: true,
},
},
TLS: &config.TLS{
VerifyOutgoing: true,
VerifyServerHostname: true,
MinVersion: "tls12",
PreferServerCipherSuites: true,
},
},
},
patchResponse: func(t *testing.T, srv *Server, resp *agentpb.AutoConfigResponse) {
// we are expecting an ACL token but cannot check anything for equality
// so here we check that it was set and overwrite it
require.NotNil(t, resp.Config)
require.NotNil(t, resp.Config.ACL)
require.NotNil(t, resp.Config.ACL.Tokens)
require.NotEmpty(t, resp.Config.ACL.Tokens.Agent)
resp.Config.ACL.Tokens.Agent = "patched-secret"
// we don't know the expected join address until we start up the test server
joinAddr := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: srv.config.SerfLANConfig.MemberlistConfig.AdvertisePort}
require.NotNil(t, resp.Config.Gossip)
require.Equal(t, []string{joinAddr.String()}, resp.Config.Gossip.RetryJoinLAN)
resp.Config.Gossip.RetryJoinLAN = nil
},
},
}
_, s, _ := testACLServerWithConfig(t, func(c *Config) {
c.Domain = "consul"
c.AutoConfigAuthzEnabled = true
c.AutoConfigAuthzAuthMethod = structs.ACLAuthMethod{
Name: "Auth Config Authorizer",
Type: "jwt",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
Config: map[string]interface{}{
"BoundAudiences": []string{"consul"},
"BoundIssuer": "consul",
"JWTValidationPubKeys": []string{pub},
"ClaimMappings": map[string]string{
"consul_node_name": "node",
},
},
}
c.AutoConfigAuthzClaimAssertions = []string{
`value.node == "${node}"`,
}
c.AutoConfigAuthzAllowReuse = true
cafile := path.Join(c.DataDir, "cacert.pem")
err := ioutil.WriteFile(cafile, []byte(cacert), 0600)
require.NoError(t, err)
certfile := path.Join(c.DataDir, "cert.pem")
err = ioutil.WriteFile(certfile, []byte(cert), 0600)
require.NoError(t, err)
keyfile := path.Join(c.DataDir, "key.pem")
err = ioutil.WriteFile(keyfile, []byte(key), 0600)
require.NoError(t, err)
c.CAFile = cafile
c.CertFile = certfile
c.KeyFile = keyfile
c.VerifyOutgoing = true
c.VerifyIncoming = true
c.VerifyServerHostname = true
c.TLSMinVersion = "tls12"
c.TLSPreferServerCipherSuites = true
c.ConnectEnabled = true
c.AutoEncryptAllowTLS = true
c.SerfLANConfig.MemberlistConfig.GossipVerifyIncoming = true
c.SerfLANConfig.MemberlistConfig.GossipVerifyOutgoing = true
keyring, err := memberlist.NewKeyring(nil, gossipKey)
require.NoError(t, err)
c.SerfLANConfig.MemberlistConfig.Keyring = keyring
}, false)
conf := tlsutil.Config{
CAFile: s.config.CAFile,
VerifyServerHostname: s.config.VerifyServerHostname,
VerifyOutgoing: s.config.VerifyOutgoing,
Domain: s.config.Domain,
}
codec, err := insecureRPCClient(s, conf)
require.NoError(t, err)
waitForLeaderEstablishment(t, s)
for testName, tcase := range cases {
t.Run(testName, func(t *testing.T) {
var reply agentpb.AutoConfigResponse
err := msgpackrpc.CallWithCodec(codec, "Cluster.AutoConfig", &tcase.request, &reply)
if tcase.err != "" {
testutil.RequireErrorContains(t, err, tcase.err)
} else {
require.NoError(t, err)
if tcase.patchResponse != nil {
tcase.patchResponse(t, s, &reply)
}
require.Equal(t, tcase.expected, reply)
}
})
}
}
func TestClusterAutoConfig_baseConfig(t *testing.T) {
type testCase struct {
serverConfig Config
opts AutoConfigOptions
expected config.Config
err string
}
cases := map[string]testCase{
"ok": {
serverConfig: Config{
Datacenter: "oSWzfhnU",
PrimaryDatacenter: "53XO9mx4",
},
opts: AutoConfigOptions{
NodeName: "lBdc0lsH",
SegmentName: "HZiwlWpi",
},
expected: config.Config{
Datacenter: "oSWzfhnU",
PrimaryDatacenter: "53XO9mx4",
NodeName: "lBdc0lsH",
SegmentName: "HZiwlWpi",
},
},
"no-node-name": {
serverConfig: Config{
Datacenter: "oSWzfhnU",
PrimaryDatacenter: "53XO9mx4",
},
err: "Cannot generate auto config response without a node name",
},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
cluster := Cluster{
srv: &Server{
config: &tcase.serverConfig,
},
}
var actual config.Config
err := cluster.baseConfig(tcase.opts, &actual)
if tcase.err == "" {
require.NoError(t, err)
require.Equal(t, tcase.expected, actual)
} else {
testutil.RequireErrorContains(t, err, tcase.err)
}
})
}
}
func TestClusterAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
_, _, cacert, err := testTLSCertificates("server.dc1.consul")
require.NoError(t, err)
dir := testutil.TempDir(t, "auto-config-tls-settings")
t.Cleanup(func() { os.RemoveAll(dir) })
cafile := path.Join(dir, "cacert.pem")
err = ioutil.WriteFile(cafile, []byte(cacert), 0600)
require.NoError(t, err)
parseCiphers := func(t *testing.T, cipherStr string) []uint16 {
t.Helper()
ciphers, err := tlsutil.ParseCiphers(cipherStr)
require.NoError(t, err)
return ciphers
}
type testCase struct {
tlsConfig tlsutil.Config
expected config.Config
}
cases := map[string]testCase{
"secure": {
tlsConfig: tlsutil.Config{
VerifyOutgoing: true,
VerifyServerHostname: true,
TLSMinVersion: "tls12",
PreferServerCipherSuites: true,
CAFile: cafile,
CipherSuites: parseCiphers(t, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"),
},
expected: config.Config{
TLS: &config.TLS{
VerifyOutgoing: true,
VerifyServerHostname: true,
MinVersion: "tls12",
PreferServerCipherSuites: true,
CipherSuites: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
},
},
},
"less-secure": {
tlsConfig: tlsutil.Config{
VerifyOutgoing: true,
VerifyServerHostname: false,
TLSMinVersion: "tls10",
PreferServerCipherSuites: false,
CAFile: cafile,
CipherSuites: parseCiphers(t, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"),
},
expected: config.Config{
TLS: &config.TLS{
VerifyOutgoing: true,
VerifyServerHostname: false,
MinVersion: "tls10",
PreferServerCipherSuites: false,
CipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
},
},
},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
logger := testutil.Logger(t)
configurator, err := tlsutil.NewConfigurator(tcase.tlsConfig, logger)
require.NoError(t, err)
cluster := &Cluster{
srv: &Server{
tlsConfigurator: configurator,
},
}
var actual config.Config
err = cluster.updateTLSSettingsInConfig(AutoConfigOptions{}, &actual)
require.NoError(t, err)
require.Equal(t, tcase.expected, actual)
})
}
}
func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
type testCase struct {
conf memberlist.Config
expected config.Config
}
gossipKey := make([]byte, 32)
// this is not cryptographic randomness and is not secure but for the sake of this test its all we need.
n, err := rand.Read(gossipKey)
require.NoError(t, err)
require.Equal(t, 32, n)
gossipKeyEncoded := base64.StdEncoding.EncodeToString(gossipKey)
keyring, err := memberlist.NewKeyring(nil, gossipKey)
require.NoError(t, err)
cases := map[string]testCase{
"encryption-required": {
conf: memberlist.Config{
Keyring: keyring,
GossipVerifyIncoming: true,
GossipVerifyOutgoing: true,
},
expected: config.Config{
Gossip: &config.Gossip{
Encryption: &config.GossipEncryption{
Key: gossipKeyEncoded,
VerifyIncoming: true,
VerifyOutgoing: true,
},
},
},
},
"encryption-allowed": {
conf: memberlist.Config{
Keyring: keyring,
GossipVerifyIncoming: false,
GossipVerifyOutgoing: false,
},
expected: config.Config{
Gossip: &config.Gossip{
Encryption: &config.GossipEncryption{
Key: gossipKeyEncoded,
VerifyIncoming: false,
VerifyOutgoing: false,
},
},
},
},
"encryption-disabled": {
// zero values all around - if no keyring is configured then the gossip
// encryption settings should not be set.
},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
cluster := Cluster{
srv: &Server{
config: DefaultConfig(),
},
}
cluster.srv.config.SerfLANConfig.MemberlistConfig = &tcase.conf
var actual config.Config
err := cluster.updateGossipEncryptionInConfig(AutoConfigOptions{}, &actual)
require.NoError(t, err)
require.Equal(t, tcase.expected, actual)
})
}
}
func TestAutoConfig_updateTLSCertificatesInConfig(t *testing.T) {
type testCase struct {
serverConfig Config
expected config.Config
}
cases := map[string]testCase{
"auto_encrypt-enabled": {
serverConfig: Config{
ConnectEnabled: true,
AutoEncryptAllowTLS: true,
},
expected: config.Config{
AutoEncrypt: &config.AutoEncrypt{TLS: true},
},
},
"auto_encrypt-disabled": {
serverConfig: Config{
ConnectEnabled: true,
AutoEncryptAllowTLS: false,
},
expected: config.Config{
AutoEncrypt: &config.AutoEncrypt{TLS: false},
},
},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
cluster := Cluster{
srv: &Server{
config: &tcase.serverConfig,
},
}
var actual config.Config
err := cluster.updateTLSCertificatesInConfig(AutoConfigOptions{}, &actual)
require.NoError(t, err)
require.Equal(t, tcase.expected, actual)
})
}
}
func TestAutoConfig_updateACLsInConfig(t *testing.T) {
type testCase struct {
patch func(c *Config)
expected config.Config
verify func(t *testing.T, c *config.Config)
err string
}
cases := map[string]testCase{
"enabled": {
patch: func(c *Config) {
c.ACLsEnabled = true
c.ACLPolicyTTL = 7 * time.Second
c.ACLRoleTTL = 10 * time.Second
c.ACLTokenTTL = 12 * time.Second
c.ACLDisabledTTL = 31 * time.Second
c.ACLDefaultPolicy = "allow"
c.ACLDownPolicy = "deny"
c.ACLEnableKeyListPolicy = true
},
expected: config.Config{
ACL: &config.ACL{
Enabled: true,
PolicyTTL: "7s",
RoleTTL: "10s",
TokenTTL: "12s",
DisabledTTL: "31s",
DownPolicy: "deny",
DefaultPolicy: "allow",
EnableKeyListPolicy: true,
Tokens: &config.ACLTokens{Agent: "verified"},
},
},
verify: func(t *testing.T, c *config.Config) {
t.Helper()
// the agent token secret is non-deterministically generated
// So we want to validate that one was set and overwrite with
// a value that the expected configurate wants.
require.NotNil(t, c)
require.NotNil(t, c.ACL)
require.NotNil(t, c.ACL.Tokens)
require.NotEmpty(t, c.ACL.Tokens.Agent)
c.ACL.Tokens.Agent = "verified"
},
},
"disabled": {
patch: func(c *Config) {
c.ACLsEnabled = false
c.ACLPolicyTTL = 7 * time.Second
c.ACLRoleTTL = 10 * time.Second
c.ACLTokenTTL = 12 * time.Second
c.ACLDisabledTTL = 31 * time.Second
c.ACLDefaultPolicy = "allow"
c.ACLDownPolicy = "deny"
c.ACLEnableKeyListPolicy = true
},
expected: config.Config{
ACL: &config.ACL{
Enabled: false,
PolicyTTL: "7s",
RoleTTL: "10s",
TokenTTL: "12s",
DisabledTTL: "31s",
DownPolicy: "deny",
DefaultPolicy: "allow",
EnableKeyListPolicy: true,
},
},
},
"local-tokens-disabled": {
patch: func(c *Config) {
c.PrimaryDatacenter = "somewhere else"
},
err: "Agent Auto Configuration requires local token usage to be enabled in this datacenter",
},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
_, s, _ := testACLServerWithConfig(t, tcase.patch, false)
waitForLeaderEstablishment(t, s)
cluster := Cluster{srv: s}
var actual config.Config
err := cluster.updateACLsInConfig(AutoConfigOptions{NodeName: "something"}, &actual)
if tcase.err != "" {
testutil.RequireErrorContains(t, err, tcase.err)
} else {
require.NoError(t, err)
if tcase.verify != nil {
tcase.verify(t, &actual)
}
require.Equal(t, tcase.expected, actual)
}
})
}
}
func TestAutoConfig_updateJoinAddressesInConfig(t *testing.T) {
conf := testClusterConfig{
Datacenter: "primary",
Servers: 3,
}
nodes := newTestCluster(t, &conf)
cluster := Cluster{srv: nodes.Servers[0]}
var actual config.Config
err := cluster.updateJoinAddressesInConfig(AutoConfigOptions{}, &actual)
require.NoError(t, err)
var expected []string
for _, srv := range nodes.Servers {
expected = append(expected, fmt.Sprintf("127.0.0.1:%d", srv.config.SerfLANConfig.MemberlistConfig.BindPort))
}
require.NotNil(t, actual.Gossip)
require.ElementsMatch(t, expected, actual.Gossip.RetryJoinLAN)
}

View File

@ -305,6 +305,17 @@ type Config struct {
// by default in Consul 1.0 and later. // by default in Consul 1.0 and later.
ACLEnableKeyListPolicy bool ACLEnableKeyListPolicy bool
AutoConfigEnabled bool
AutoConfigIntroToken string
AutoConfigIntroTokenFile string
AutoConfigServerAddresses []string
AutoConfigDNSSANs []string
AutoConfigIPSANs []net.IP
AutoConfigAuthzEnabled bool
AutoConfigAuthzAuthMethod structs.ACLAuthMethod
AutoConfigAuthzClaimAssertions []string
AutoConfigAuthzAllowReuse bool
// TombstoneTTL is used to control how long KV tombstones are retained. // TombstoneTTL is used to control how long KV tombstones are retained.
// This provides a window of time where the X-Consul-Index is monotonic. // This provides a window of time where the X-Consul-Index is monotonic.
// Outside this window, the index may not be monotonic. This is a result // Outside this window, the index may not be monotonic. This is a result

View File

@ -20,6 +20,7 @@ import (
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
ca "github.com/hashicorp/consul/agent/connect/ca" ca "github.com/hashicorp/consul/agent/connect/ca"
"github.com/hashicorp/consul/agent/consul/authmethod" "github.com/hashicorp/consul/agent/consul/authmethod"
"github.com/hashicorp/consul/agent/consul/authmethod/ssoauth"
"github.com/hashicorp/consul/agent/consul/autopilot" "github.com/hashicorp/consul/agent/consul/autopilot"
"github.com/hashicorp/consul/agent/consul/fsm" "github.com/hashicorp/consul/agent/consul/fsm"
"github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/consul/state"
@ -834,6 +835,27 @@ func (s *Server) setupRPC() error {
// been configured. // been configured.
s.insecureRPCServer.Register(&AutoEncrypt{srv: s}) s.insecureRPCServer.Register(&AutoEncrypt{srv: s})
// Setup the AutoConfig JWT Authorizer
var authz AutoConfigAuthorizer
if s.config.AutoConfigAuthzEnabled {
// create the auto config authorizer from the JWT authmethod
validator, err := ssoauth.NewValidator(s.logger, &s.config.AutoConfigAuthzAuthMethod)
if err != nil {
return fmt.Errorf("Failed to initialize JWT Auto Config Authorizer: %w", err)
}
authz = &jwtAuthorizer{
validator: validator,
allowReuse: s.config.AutoConfigAuthzAllowReuse,
claimAssertions: s.config.AutoConfigAuthzClaimAssertions,
}
} else {
// This authorizer always returns that the endpoint is disabled
authz = &disabledAuthorizer{}
}
// now register with the insecure RPC server
s.insecureRPCServer.Register(&Cluster{srv: s, authorizer: authz})
ln, err := net.ListenTCP("tcp", s.config.RPCAddr) ln, err := net.ListenTCP("tcp", s.config.RPCAddr)
if err != nil { if err != nil {
return err return err

View File

@ -2,6 +2,10 @@ package consul
import ( import (
"bytes" "bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"fmt" "fmt"
"net" "net"
"net/rpc" "net/rpc"
@ -38,6 +42,46 @@ const (
TestDefaultMasterToken = "d9f05e83-a7ae-47ce-839e-c0d53a68c00a" TestDefaultMasterToken = "d9f05e83-a7ae-47ce-839e-c0d53a68c00a"
) )
// testTLSCertificates Generates a TLS CA and server key/cert and returns them
// in PEM encoded form.
func testTLSCertificates(serverName string) (cert string, key string, cacert string, err error) {
// generate CA
serial, err := tlsutil.GenerateSerialNumber()
if err != nil {
return "", "", "", err
}
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return "", "", "", err
}
ca, err := tlsutil.GenerateCA(signer, serial, 365, nil)
if err != nil {
return "", "", "", err
}
// generate leaf
serial, err = tlsutil.GenerateSerialNumber()
if err != nil {
return "", "", "", err
}
cert, privateKey, err := tlsutil.GenerateCert(
signer,
ca,
serial,
"Test Cert Name",
365,
[]string{serverName},
nil,
[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
)
if err != nil {
return "", "", "", err
}
return cert, privateKey, ca, nil
}
// testServerACLConfig wraps another arbitrary Config altering callback // testServerACLConfig wraps another arbitrary Config altering callback
// to setup some common ACL configurations. A new callback func will // to setup some common ACL configurations. A new callback func will
// be returned that has the original callback invoked after setting // be returned that has the original callback invoked after setting

View File

@ -80,17 +80,15 @@ function main {
local proto_go_path=${proto_path%%.proto}.pb.go local proto_go_path=${proto_path%%.proto}.pb.go
local proto_go_bin_path=${proto_path%%.proto}.pb.binary.go local proto_go_bin_path=${proto_path%%.proto}.pb.binary.go
local go_proto_out="" local go_proto_out="paths=source_relative"
local sep=""
if is_set "${grpc}" if is_set "${grpc}"
then then
go_proto_out="plugins=grpc" go_proto_out="${go_proto_out},plugins=grpc"
sep=","
fi fi
if is_set "${imp_replace}" if is_set "${imp_replace}"
then then
go_proto_out="${go_proto_out}${sep}${gogo_proto_imp_replace}" go_proto_out="${go_proto_out},${gogo_proto_imp_replace}"
fi fi
if test -n "${go_proto_out}" if test -n "${go_proto_out}"
@ -98,15 +96,19 @@ function main {
go_proto_out="${go_proto_out}:" go_proto_out="${go_proto_out}:"
fi fi
# How we run protoc probably needs some documentation.
#
# This is the path to where
# -I="${gogo_proto_path}/protobuf" \
local -i ret=0 local -i ret=0
status_stage "Generating ${proto_path} into ${proto_go_path} and ${proto_go_bin_path}" status_stage "Generating ${proto_path} into ${proto_go_path} and ${proto_go_bin_path}"
debug_run protoc \ debug_run protoc \
-I="$(dirname ${proto_path})" \
-I="${gogo_proto_path}/protobuf" \ -I="${gogo_proto_path}/protobuf" \
-I="${gogo_proto_path}" \ -I="${gogo_proto_path}" \
-I="${gogo_proto_mod_path}" \ -I="${gogo_proto_mod_path}" \
--gofast_out="${go_proto_out}$(dirname ${proto_path})" \ -I="${SOURCE_DIR}" \
--go-binary_out="$(dirname ${proto_path})" \ --gofast_out="${go_proto_out}${SOURCE_DIR}" \
--go-binary_out="${SOURCE_DIR}" \
"${proto_path}" "${proto_path}"
if test $? -ne 0 if test $? -ne 0
then then

View File

@ -490,13 +490,13 @@ func init() {
// test. These are cached between runs but do not persist between restarts // test. These are cached between runs but do not persist between restarts
// of the test binary. // of the test binary.
var err error var err error
ecdsaPublicKey, ecdsaPrivateKey, err = generateKey() ecdsaPublicKey, ecdsaPrivateKey, err = GenerateKey()
if err != nil { if err != nil {
panic(err) panic(err)
} }
} }
func generateKey() (pub, priv string, err error) { func GenerateKey() (pub, priv string, err error) {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil { if err != nil {
return "", "", fmt.Errorf("error generating private key: %v", err) return "", "", fmt.Errorf("error generating private key: %v", err)

View File

@ -936,6 +936,7 @@ func ParseCiphers(cipherStr string) ([]uint16, error) {
} }
ciphers := strings.Split(cipherStr, ",") ciphers := strings.Split(cipherStr, ",")
// Note: this needs to be kept up to date with the cipherMap in CipherString
cipherMap := map[string]uint16{ cipherMap := map[string]uint16{
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
@ -958,3 +959,31 @@ func ParseCiphers(cipherStr string) ([]uint16, error) {
return suites, nil return suites, nil
} }
// CipherString performs the inverse operation of ParseCiphers
func CipherString(ciphers []uint16) (string, error) {
// Note: this needs to be kept up to date with the cipherMap in ParseCiphers
cipherMap := map[uint16]string{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
}
cipherStrings := make([]string, len(ciphers))
for i, cipher := range ciphers {
if v, ok := cipherMap[cipher]; ok {
cipherStrings[i] = v
} else {
return "", fmt.Errorf("unsupported cipher %d", cipher)
}
}
return strings.Join(cipherStrings, ","), nil
}