Skip to content

Commit

Permalink
client/pkg/transport: Support SAN URIs in TLS peer verification
Browse files Browse the repository at this point in the history
Cherry-pick etcd-io#13445 manually because
the remote repository has been deleted, and add support for multiple
values for allowed client and peer URIs

Signed-off-by: Ayaz Badouraly <[email protected]>
  • Loading branch information
badouralix authored and nyodas committed Jun 10, 2024
1 parent 20e1551 commit 54e885a
Show file tree
Hide file tree
Showing 33 changed files with 685 additions and 450 deletions.
31 changes: 26 additions & 5 deletions client/pkg/transport/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ type TLSInfo struct {
// TLS certificate provided by a client.
AllowedHostnames []string

// AllowedURIs is a list of acceptable subjective alternative name URIs that must match the
// TLS certificate provided by a client.
AllowedURIs []string

// Logger logs TLS errors.
// If nil, all logs are discarded.
Logger *zap.Logger
Expand Down Expand Up @@ -407,10 +411,17 @@ func (info TLSInfo) baseConfig() (*tls.Config, error) {
// Client certificates may be verified by either an exact match on the CN,
// or a more general check of the CN and SANs.
var verifyCertificate func(*x509.Certificate) bool
if len(info.AllowedCNs) > 0 {
if len(info.AllowedHostnames) > 0 {
return nil, fmt.Errorf("AllowedCNs and AllowedHostnames are mutually exclusive (cn=%q, hostname=%q)", info.AllowedCNs, info.AllowedHostnames)
var definedRestrictions int
for _, restriction := range []int{len(info.AllowedCNs), len(info.AllowedHostnames), len(info.AllowedURIs)} {
if restriction > 0 {
definedRestrictions++
if definedRestrictions > 1 {
return nil, errors.New("exactly one of AllowedCNs, AllowedHostnames, or AllowedURIs can be defined")
}
}
}
switch {
case len(info.AllowedCNs) > 0:
verifyCertificate = func(cert *x509.Certificate) bool {
for _, allowedCN := range info.AllowedCNs {
if allowedCN == cert.Subject.CommonName {
Expand All @@ -419,8 +430,7 @@ func (info TLSInfo) baseConfig() (*tls.Config, error) {
}
return false
}
}
if len(info.AllowedHostnames) > 0 {
case len(info.AllowedHostnames) > 0:
verifyCertificate = func(cert *x509.Certificate) bool {
for _, allowedHostname := range info.AllowedHostnames {
if cert.VerifyHostname(allowedHostname) == nil {
Expand All @@ -429,6 +439,17 @@ func (info TLSInfo) baseConfig() (*tls.Config, error) {
}
return false
}
case len(info.AllowedURIs) > 0:
verifyCertificate = func(cert *x509.Certificate) bool {
for _, allowedURI := range info.AllowedURIs {
for _, uri := range cert.URIs {
if allowedURI == uri.String() {
return true
}
}
}
return false
}
}
if verifyCertificate != nil {
cfg.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
Expand Down
4 changes: 4 additions & 0 deletions server/etcdmain/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ func newConfig() *config {
fs.BoolVar(&cfg.ec.ClientTLSInfo.ClientCertAuth, "client-cert-auth", false, "Enable client cert authentication.")
fs.StringVar(&cfg.ec.ClientTLSInfo.CRLFile, "client-crl-file", "", "Path to the client certificate revocation list file.")
fs.Var(flags.NewStringsValue(""), "client-cert-allowed-hostname", "Comma-delimited SAN hostnames for client cert authentication.")
fs.Var(flags.NewStringsValue(""), "client-cert-allowed-uri", "Comma-delimited SAN URIs for client cert authentication.")
fs.StringVar(&cfg.ec.ClientTLSInfo.TrustedCAFile, "trusted-ca-file", "", "Path to the client server TLS trusted CA cert file.")
fs.BoolVar(&cfg.ec.ClientAutoTLS, "auto-tls", false, "Client TLS using generated certificates")
fs.StringVar(&cfg.ec.PeerTLSInfo.CertFile, "peer-cert-file", "", "Path to the peer server TLS cert file.")
Expand All @@ -240,6 +241,7 @@ func newConfig() *config {
fs.StringVar(&cfg.ec.PeerTLSInfo.CRLFile, "peer-crl-file", "", "Path to the peer certificate revocation list file.")
fs.Var(flags.NewStringsValue(""), "peer-cert-allowed-cn", "Comma-separated list of allowed CNs for inter-peer TLS authentication.")
fs.Var(flags.NewStringsValue(""), "peer-cert-allowed-hostname", "Comma-separated list of allowed SAN hostnames for inter-peer TLS authentication.")
fs.Var(flags.NewStringsValue(""), "peer-cert-allowed-uri", "Comma-separated list of allowed SAN URIs for inter-peer TLS authentication.")
fs.Var(flags.NewStringsValue(""), "cipher-suites", "Comma-separated list of supported TLS cipher suites between client/server and peers (empty will be auto-populated by Go).")
fs.BoolVar(&cfg.ec.PeerTLSInfo.SkipClientSANVerify, "experimental-peer-skip-client-san-verification", false, "Skip verification of SAN field in client certificate for peer connections.")
fs.StringVar(&cfg.ec.TlsMinVersion, "tls-min-version", string(tlsutil.TLSVersion12), "Minimum TLS version supported by etcd. Possible values: TLS1.2, TLS1.3.")
Expand Down Expand Up @@ -410,8 +412,10 @@ func (cfg *config) configFromCmdLine() error {
cfg.ec.HostWhitelist = flags.UniqueStringsMapFromFlag(cfg.cf.flagSet, "host-whitelist")

cfg.ec.ClientTLSInfo.AllowedHostnames = flags.StringsFromFlag(cfg.cf.flagSet, "client-cert-allowed-hostname")
cfg.ec.ClientTLSInfo.AllowedURIs = flags.StringsFromFlag(cfg.cf.flagSet, "client-cert-allowed-uri")
cfg.ec.PeerTLSInfo.AllowedCNs = flags.StringsFromFlag(cfg.cf.flagSet, "peer-cert-allowed-cn")
cfg.ec.PeerTLSInfo.AllowedHostnames = flags.StringsFromFlag(cfg.cf.flagSet, "peer-cert-allowed-hostname")
cfg.ec.PeerTLSInfo.AllowedURIs = flags.StringsFromFlag(cfg.cf.flagSet, "peer-cert-allowed-uri")

cfg.ec.CipherSuites = flags.StringsFromFlag(cfg.cf.flagSet, "cipher-suites")

Expand Down
4 changes: 4 additions & 0 deletions server/etcdmain/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ Security:
Path to the client certificate revocation list file.
--client-cert-allowed-hostname ''
Comma-delimited SAN hostnames for client cert authentication.
--client-cert-allowed-uri ''
Comma-delimited SAN URIs for client cert authentication.
--trusted-ca-file ''
Path to the client server TLS trusted CA cert file.
--auto-tls 'false'
Expand All @@ -176,6 +178,8 @@ Security:
Comma-separated list of allowed CNs for inter-peer TLS authentication.
--peer-cert-allowed-hostname ''
Comma-separated list of allowed SAN hostnames for inter-peer TLS authentication.
--peer-cert-allowed-uri ''
Comma-separated list of allowed SAN URIs for inter-peer TLS authentication.
--peer-auto-tls 'false'
Peer TLS using self-generated certificates if --peer-key-file and --peer-cert-file are not provided.
--peer-client-cert-file ''
Expand Down
80 changes: 79 additions & 1 deletion tests/e2e/etcd_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,85 @@ func TestEtcdPeerNameAuth(t *testing.T) {
}
}

// TestEtcdPeerURIAuth checks that the inter peer auth based on SAN URI validation is working correctly.
func TestEtcdPeerURIAuth(t *testing.T) {
skipInShortMode(t)

peers, tmpdirs := make([]string, 3), make([]string, 3)
for i := range peers {
peers[i] = fmt.Sprintf("e%d=https://127.0.0.1:%d", i, etcdProcessBasePort+i)
d, err := os.MkdirTemp("", fmt.Sprintf("e%d.etcd", i))
if err != nil {
t.Fatal(err)
}
tmpdirs[i] = d
}
ic := strings.Join(peers, ",")

procs := make([]*expect.ExpectProcess, len(peers))
defer func() {
for i := range procs {
if procs[i] != nil {
procs[i].Stop()
}
os.RemoveAll(tmpdirs[i])
}
}()

// node 0 and 1 have a cert with the correct certificate name, node 2 doesn't
for i := range procs {
commonArgs := []string{
binDir + "/etcd",
"--name", fmt.Sprintf("e%d", i),
"--listen-client-urls", "http://0.0.0.0:0",
"--data-dir", tmpdirs[i],
"--advertise-client-urls", "http://0.0.0.0:0",
"--listen-peer-urls", fmt.Sprintf("https://127.0.0.1:%d,https://127.0.0.1:%d", etcdProcessBasePort+i, etcdProcessBasePort+len(peers)+i),
"--initial-advertise-peer-urls", fmt.Sprintf("https://127.0.0.1:%d", etcdProcessBasePort+i),
"--initial-cluster", ic,
}

var args []string
if i <= 1 {
args = []string{
"--peer-cert-file", certPath4,
"--peer-key-file", privateKeyPath4,
"--peer-trusted-ca-file", caPath,
"--peer-client-cert-auth",
"--peer-cert-allowed-uri", "spiffe://example4.com/service",
}
} else {
args = []string{
"--peer-cert-file", certPath4,
"--peer-key-file", privateKeyPath4,
"--peer-trusted-ca-file", caPath,
"--peer-client-cert-auth",
"--peer-cert-allowed-uri", "spiffe://example.com/service",
}
}

commonArgs = append(commonArgs, args...)

p, err := spawnCmd(commonArgs, nil)
if err != nil {
t.Fatal(err)
}
procs[i] = p
}

for i, p := range procs {
var expect []string
if i <= 1 {
expect = etcdServerReadyLines
} else {
expect = []string{"client certificate authentication failed"}
}
if err := waitReadyExpectProc(p, expect); err != nil {
t.Fatal(err)
}
}
}

func TestGrpcproxyAndCommonName(t *testing.T) {
e2e.SkipInShortMode(t)

Expand Down Expand Up @@ -499,5 +578,4 @@ func TestEtcdTLSVersion(t *testing.T) {
assert.NoError(t, err)
assert.NoError(t, e2e.WaitReadyExpectProc(proc, e2e.EtcdServerReadyLines), "did not receive expected output from etcd process")
assert.NoError(t, proc.Stop())

}
56 changes: 54 additions & 2 deletions tests/e2e/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,67 @@
package e2e

import (
"flag"
"os"
"runtime"
"testing"

"go.etcd.io/etcd/client/pkg/v3/testutil"
"go.etcd.io/etcd/tests/v3/framework/e2e"
"go.etcd.io/etcd/tests/v3/integration"
)

var (
binDir string
certDir string

certPath string
privateKeyPath string
caPath string

certPath2 string
privateKeyPath2 string

certPath3 string
privateKeyPath3 string

certPath4 string
privateKeyPath4 string

crlPath string
revokedCertPath string
revokedPrivateKeyPath string
)

func TestMain(m *testing.M) {
e2e.InitFlags()
os.Setenv("ETCD_UNSUPPORTED_ARCH", runtime.GOARCH)
os.Unsetenv("ETCDCTL_API")

binDirDef := integration.MustAbsPath("../../bin")
certDirDef := fixturesDir

flag.StringVar(&binDir, "bin-dir", binDirDef, "The directory for store etcd and etcdctl binaries.")
flag.StringVar(&certDir, "cert-dir", certDirDef, "The directory for store certificate files.")
flag.Parse()

binPath = binDir + "/etcd"
ctlBinPath = binDir + "/etcdctl"
utlBinPath = binDir + "/etcdutl"
certPath = certDir + "/server.crt"
privateKeyPath = certDir + "/server.key.insecure"
caPath = certDir + "/ca.crt"
revokedCertPath = certDir + "/server-revoked.crt"
revokedPrivateKeyPath = certDir + "/server-revoked.key.insecure"
crlPath = certDir + "/revoke.crl"

certPath2 = certDir + "/server2.crt"
privateKeyPath2 = certDir + "/server2.key.insecure"

certPath3 = certDir + "/server3.crt"
privateKeyPath3 = certDir + "/server3.key.insecure"

certPath4 = certDir + "/server4.crt"
privateKeyPath4 = certDir + "/server4.key.insecure"

v := m.Run()
if v == 0 && testutil.CheckLeakedGoroutine() {
os.Exit(1)
Expand Down
30 changes: 15 additions & 15 deletions tests/fixtures/ca.crt
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
-----BEGIN CERTIFICATE-----
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
-----END CERTIFICATE-----
32 changes: 16 additions & 16 deletions tests/fixtures/client-clientusage.crt
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIECDCCAvCgAwIBAgIULbzkAv8zbkJzZIRDPnBwXl0/BH0wDQYJKoZIhvcNAQEL
MIIECDCCAvCgAwIBAgIUT0SMH+iXjxDhuUVSxiCnvxQApQowDQYJKoZIhvcNAQEL
BQAwbzEMMAoGA1UEBhMDVVNBMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH
Ew1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKEwRldGNkMRYwFAYDVQQLEw1ldGNkIFNl
Y3VyaXR5MQswCQYDVQQDEwJjYTAeFw0yMTAyMjgxMDQ4MDBaFw0zMTAyMjYxMDQ4
Y3VyaXR5MQswCQYDVQQDEwJjYTAeFw0yMTEwMjcwMjIxMDBaFw0zMTEwMjUwMjIx
MDBaMHgxDDAKBgNVBAYTA1VTQTETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UE
BxMNU2FuIEZyYW5jaXNjbzENMAsGA1UEChMEZXRjZDEWMBQGA1UECxMNZXRjZCBT
ZWN1cml0eTEUMBIGA1UEAxMLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA
A4IBDwAwggEKAoIBAQDWBNo9tYRoQKv76xabz0EPXGJKHIrUjf0NbXz3d9jbP2sH
3hutXr/A221pULfZYIZdaUtmEuEr1905nYwJ2gnO9Y/iSc6fQ/4EjoT+VZLdINQw
I1dG2rtv2ZuYL5oYfgCjLkV1LzYuyfY/zJ93WoJW0YA0t50MEQNGEqD7pYlhsPej
iGyjagSi7zsoAkAagNprULH6RyAqDG7db+MfJOUzHUv4PWGBXPb0PHY3xA+WayFB
nP5AZO16oDh/UnzvfEAJULXeIOLs4eOmtzKMwZwrWzgCB+jBeVlc1FOwXQcmBamN
eYUs75GoO9aSSLROvnQiw2P0z0xVNmDokDXGsSRxAgMBAAGjgZIwgY8wDgYDVR0P
A4IBDwAwggEKAoIBAQCkSw/bIc0SbAMQ8WjvDe9MO3a4tas4kgJENkgmlNCIVbQD
vSmIHenTPinqqlUvp/Xeu4dReN+gapkRPI2ulzQCVcxWdtZimRgBZysYXkYTAB2A
cku6z7ctcrxbHxbed6nIfj+tGdJRSLr+gHOMl2zE3DBsriZEdkqjYPytJOYGBuda
/Dp/BBoZZx7b/v9LU/pQ6VK1EU+wd95YWOP6rXjiCshT8/ERsz/RStq7SxYGXF7X
mvFfKGcinYi72VTExQIMoM3/3rmzcCVPXYIWu2e4xK7HOyfFp91UyZ7LbZNpejKu
x7rLuwj+zye5xZhzJlHLi+r3Il5IY+UltE/ImcHdAgMBAAGjgZIwgY8wDgYDVR0P
AQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYD
VR0OBBYEFCB4ysDF81d6lkKIvebj08BcRWNoMB8GA1UdIwQYMBaAFCkmK/Tt2Rj1
YHTOAHrA5v8cyLygMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG
9w0BAQsFAAOCAQEAo2B+piCBTjdpCLFj/kc+A0alZTbNdr0+BTsN+5aBE9k4JlZS
smkIQL0vyzjKw/W/o2EyPVcVKJX52/GQsC3bQrBb2lH1jRYgt5pRo24kKHy4Nlc3
IaYg++ssfT2ZdpYiL3lzLyOHEumcynz3nI5M81e5CCIdEennxaM8FuiYN5OXDOR3
j+bCYHLYPaWYZopfiSrnq+Z4gRUS2sMI1yqtiPSUdIJLnTfyEEdexvs/KUtFWvFO
4AcecKvT6HA8oNDiWfE6e854uDLTkbXW1rK+FWPU9pv5NR50+GBCvxvmDGtGXxQu
yu+kOsx2gfgNc4idIv1pjZF/1YzrrKGAhChN2A==
VR0OBBYEFER4ehcmQZCf/z7/UZ90WYpPeWF0MB8GA1UdIwQYMBaAFInfVDONBAn8
7EZryCTQW88atoC/MBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG
9w0BAQsFAAOCAQEAJZWcnjo4i7bcPYDykoDet5MppeeBk1YL4700lpD+B3Fpthu8
9gCdLDkx4iPSkrQpsDm2FQwwbKQc11nQt2KbDJw6HgLvQN8SFzSBRAkXOQOno+x9
aq7NuhwR45Cb9HR5PhBS9QRZeKi8a1IKL15641/KO1lzfsyCc+17AcNhVkUa73+C
nroaBxP8H2JrdfNpBQXJ1YHnTB7gF4FSJ3hXBNrX187ogeuwtfi3bzeNNDqC6dUX
c7OlR8Y9WsPYlQoNzpZmYAeoPCyYZx8onasDHu7KuRReMBQCBCjTLQa2/drYix+u
bsGnaQl1/cvfuw0N7h/DNesjN9GwPr6QQ+KgZA==
-----END CERTIFICATE-----
50 changes: 25 additions & 25 deletions tests/fixtures/client-clientusage.key.insecure
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----
Loading

0 comments on commit 54e885a

Please sign in to comment.