1
0
Fork 0

Expand Client Auth Type configuration

This commit is contained in:
Jean-Baptiste Doumenjou 2019-07-12 17:50:04 +02:00 committed by Traefiker Bot
parent 7a4b4c941c
commit 2c7cfd1c68
31 changed files with 304 additions and 151 deletions

View file

@ -4,21 +4,22 @@ const certificateHeader = "-----BEGIN CERTIFICATE-----\n"
// +k8s:deepcopy-gen=true
// ClientCA defines traefik CA files for a entryPoint
// and it indicates if they are mandatory or have just to be analyzed if provided.
type ClientCA struct {
Files []FileOrContent `json:"files,omitempty" toml:"files,omitempty" yaml:"files,omitempty"`
Optional bool `json:"optional,omitempty" toml:"optional,omitempty" yaml:"optional,omitempty"`
// ClientAuth defines the parameters of the client authentication part of the TLS connection, if any.
type ClientAuth struct {
CAFiles []FileOrContent `json:"caFiles,omitempty" toml:"caFiles,omitempty" yaml:"caFiles,omitempty"`
// ClientAuthType defines the client authentication type to apply.
// The available values are: "NoClientCert", "RequestClientCert", "VerifyClientCertIfGiven" and "RequireAndVerifyClientCert".
ClientAuthType string `json:"clientAuthType,omitempty" toml:"clientAuthType,omitempty" yaml:"clientAuthType,omitempty"`
}
// +k8s:deepcopy-gen=true
// Options configures TLS for an entry point
type Options struct {
MinVersion string `json:"minVersion,omitempty" toml:"minVersion,omitempty" yaml:"minVersion,omitempty" export:"true"`
CipherSuites []string `json:"cipherSuites,omitempty" toml:"cipherSuites,omitempty" yaml:"cipherSuites,omitempty"`
ClientCA ClientCA `json:"clientCA,omitempty" toml:"clientCA,omitempty" yaml:"clientCA,omitempty"`
SniStrict bool `json:"sniStrict,omitempty" toml:"sniStrict,omitempty" yaml:"sniStrict,omitempty" export:"true"`
MinVersion string `json:"minVersion,omitempty" toml:"minVersion,omitempty" yaml:"minVersion,omitempty" export:"true"`
CipherSuites []string `json:"cipherSuites,omitempty" toml:"cipherSuites,omitempty" yaml:"cipherSuites,omitempty"`
ClientAuth ClientAuth `json:"clientAuth,omitempty" toml:"clientAuth,omitempty" yaml:"clientAuth,omitempty"`
SniStrict bool `json:"sniStrict,omitempty" toml:"sniStrict,omitempty" yaml:"sniStrict,omitempty" export:"true"`
}
// +k8s:deepcopy-gen=true

View file

@ -3,6 +3,7 @@ package tls
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"sync"
@ -159,23 +160,45 @@ func buildTLSConfig(tlsOption Options) (*tls.Config, error) {
// ensure http2 enabled
conf.NextProtos = []string{"h2", "http/1.1", tlsalpn01.ACMETLS1Protocol}
if len(tlsOption.ClientCA.Files) > 0 {
if len(tlsOption.ClientAuth.CAFiles) > 0 {
pool := x509.NewCertPool()
for _, caFile := range tlsOption.ClientCA.Files {
for _, caFile := range tlsOption.ClientAuth.CAFiles {
data, err := caFile.Read()
if err != nil {
return nil, err
}
ok := pool.AppendCertsFromPEM(data)
if !ok {
return nil, fmt.Errorf("invalid certificate(s) in %s", caFile)
if caFile.IsPath() {
return nil, fmt.Errorf("invalid certificate(s) in %s", caFile)
}
return nil, errors.New("invalid certificate(s) content")
}
}
conf.ClientCAs = pool
if tlsOption.ClientCA.Optional {
conf.ClientAuth = tls.RequireAndVerifyClientCert
}
clientAuthType := tlsOption.ClientAuth.ClientAuthType
if len(clientAuthType) > 0 {
if conf.ClientCAs == nil && (clientAuthType == "VerifyClientCertIfGiven" ||
clientAuthType == "RequireAndVerifyClientCert") {
return nil, fmt.Errorf("invalid clientAuthType: %s, CAFiles is required", clientAuthType)
}
switch clientAuthType {
case "NoClientCert":
conf.ClientAuth = tls.NoClientCert
case "RequestClientCert":
conf.ClientAuth = tls.RequestClientCert
case "RequireAnyClientCert":
conf.ClientAuth = tls.RequireAnyClientCert
case "VerifyClientCertIfGiven":
conf.ClientAuth = tls.VerifyClientCertIfGiven
} else {
case "RequireAndVerifyClientCert":
conf.ClientAuth = tls.RequireAndVerifyClientCert
default:
return nil, fmt.Errorf("unknown client auth type %q", clientAuthType)
}
}

View file

@ -2,9 +2,12 @@ package tls
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// LocalhostCert is a PEM-encoded TLS cert with SAN IPs
@ -146,3 +149,125 @@ func TestManager_Get(t *testing.T) {
})
}
}
func TestClientAuth(t *testing.T) {
tlsConfigs := map[string]Options{
"eca": {ClientAuth: ClientAuth{}},
"ecat": {ClientAuth: ClientAuth{ClientAuthType: ""}},
"ncc": {ClientAuth: ClientAuth{ClientAuthType: "NoClientCert"}},
"rcc": {ClientAuth: ClientAuth{ClientAuthType: "RequestClientCert"}},
"racc": {ClientAuth: ClientAuth{ClientAuthType: "RequireAnyClientCert"}},
"vccig": {
ClientAuth: ClientAuth{
CAFiles: []FileOrContent{localhostCert},
ClientAuthType: "VerifyClientCertIfGiven",
},
},
"vccigwca": {
ClientAuth: ClientAuth{ClientAuthType: "VerifyClientCertIfGiven"},
},
"ravcc": {ClientAuth: ClientAuth{ClientAuthType: "RequireAndVerifyClientCert"}},
"ravccwca": {
ClientAuth: ClientAuth{
CAFiles: []FileOrContent{localhostCert},
ClientAuthType: "RequireAndVerifyClientCert",
},
},
"ravccwbca": {
ClientAuth: ClientAuth{
CAFiles: []FileOrContent{"Bad content"},
ClientAuthType: "RequireAndVerifyClientCert",
},
},
"ucat": {ClientAuth: ClientAuth{ClientAuthType: "Unknown"}},
}
block, _ := pem.Decode([]byte(localhostCert))
cert, err := x509.ParseCertificate(block.Bytes)
require.NoError(t, err)
testCases := []struct {
desc string
tlsOptionsName string
expectedClientAuth tls.ClientAuthType
expectedRawSubject []byte
}{
{
desc: "Empty ClientAuth option should get a tls.NoClientCert (default value)",
tlsOptionsName: "eca",
expectedClientAuth: tls.NoClientCert,
},
{
desc: "Empty ClientAuthType option should get a tls.NoClientCert (default value)",
tlsOptionsName: "ecat",
expectedClientAuth: tls.NoClientCert,
},
{
desc: "NoClientCert option should get a tls.NoClientCert as ClientAuthType",
tlsOptionsName: "ncc",
expectedClientAuth: tls.NoClientCert,
},
{
desc: "RequestClientCert option should get a tls.RequestClientCert as ClientAuthType",
tlsOptionsName: "rcc",
expectedClientAuth: tls.RequestClientCert,
},
{
desc: "RequireAnyClientCert option should get a tls.RequireAnyClientCert as ClientAuthType",
tlsOptionsName: "racc",
expectedClientAuth: tls.RequireAnyClientCert,
},
{
desc: "VerifyClientCertIfGiven option should get a tls.VerifyClientCertIfGiven as ClientAuthType",
tlsOptionsName: "vccig",
expectedClientAuth: tls.VerifyClientCertIfGiven,
},
{
desc: "VerifyClientCertIfGiven option without CAFiles yields a default ClientAuthType (NoClientCert)",
tlsOptionsName: "vccigwca",
expectedClientAuth: tls.NoClientCert,
},
{
desc: "RequireAndVerifyClientCert option without CAFiles yields a default ClientAuthType (NoClientCert)",
tlsOptionsName: "ravcc",
expectedClientAuth: tls.NoClientCert,
},
{
desc: "RequireAndVerifyClientCert option should get a tls.RequireAndVerifyClientCert as ClientAuthType with CA files",
tlsOptionsName: "ravccwca",
expectedClientAuth: tls.RequireAndVerifyClientCert,
expectedRawSubject: cert.RawSubject,
},
{
desc: "Unknown option yields a default ClientAuthType (NoClientCert)",
tlsOptionsName: "ucat",
expectedClientAuth: tls.NoClientCert,
},
{
desc: "Bad CA certificate content yields a default ClientAuthType (NoClientCert)",
tlsOptionsName: "ravccwbca",
expectedClientAuth: tls.NoClientCert,
},
}
tlsManager := NewManager()
tlsManager.UpdateConfigs(nil, tlsConfigs, nil)
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
config, err := tlsManager.Get("default", test.tlsOptionsName)
assert.NoError(t, err)
if test.expectedRawSubject != nil {
subjects := config.ClientCAs.Subjects()
assert.Len(t, subjects, 1)
assert.Equal(t, subjects[0], test.expectedRawSubject)
}
assert.Equal(t, config.ClientAuth, test.expectedClientAuth)
})
}
}

View file

@ -51,22 +51,22 @@ func (in *CertAndStores) DeepCopy() *CertAndStores {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClientCA) DeepCopyInto(out *ClientCA) {
func (in *ClientAuth) DeepCopyInto(out *ClientAuth) {
*out = *in
if in.Files != nil {
in, out := &in.Files, &out.Files
if in.CAFiles != nil {
in, out := &in.CAFiles, &out.CAFiles
*out = make([]FileOrContent, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientCA.
func (in *ClientCA) DeepCopy() *ClientCA {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientAuth.
func (in *ClientAuth) DeepCopy() *ClientAuth {
if in == nil {
return nil
}
out := new(ClientCA)
out := new(ClientAuth)
in.DeepCopyInto(out)
return out
}
@ -79,7 +79,7 @@ func (in *Options) DeepCopyInto(out *Options) {
*out = make([]string, len(*in))
copy(*out, *in)
}
in.ClientCA.DeepCopyInto(&out.ClientCA)
in.ClientAuth.DeepCopyInto(&out.ClientAuth)
return
}