Skip to content

Commit e4dc4ac

Browse files
committed
Implement client certificate subject validation
1 parent c0288b7 commit e4dc4ac

File tree

6 files changed

+194
-24
lines changed

6 files changed

+194
-24
lines changed

cmd/kafka-proxy/server.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package server
22

33
import (
44
"fmt"
5+
56
"github.com/grepplabs/kafka-proxy/config"
67
"github.com/grepplabs/kafka-proxy/proxy"
78
"github.com/oklog/run"
@@ -20,13 +21,14 @@ import (
2021
"time"
2122

2223
"errors"
24+
"strings"
25+
2326
"github.com/grepplabs/kafka-proxy/pkg/apis"
2427
localauth "github.com/grepplabs/kafka-proxy/plugin/local-auth/shared"
2528
tokeninfo "github.com/grepplabs/kafka-proxy/plugin/token-info/shared"
2629
tokenprovider "github.com/grepplabs/kafka-proxy/plugin/token-provider/shared"
2730
"github.com/hashicorp/go-hclog"
2831
"github.com/hashicorp/go-plugin"
29-
"strings"
3032

3133
"github.com/grepplabs/kafka-proxy/pkg/registry"
3234
// built-in plugins
@@ -105,6 +107,14 @@ func initFlags() {
105107
Server.Flags().StringSliceVar(&c.Proxy.TLS.ListenerCipherSuites, "proxy-listener-cipher-suites", []string{}, "List of supported cipher suites")
106108
Server.Flags().StringSliceVar(&c.Proxy.TLS.ListenerCurvePreferences, "proxy-listener-curve-preferences", []string{}, "List of curve preferences")
107109

110+
Server.Flags().BoolVar(&c.Proxy.TLS.ClientCert.ValidateSubject, "proxy-listener-tls-client-cert-validate-subject", false, "Whether to validate client certificate subject")
111+
Server.Flags().StringVar(&c.Proxy.TLS.ClientCert.Subject.CommonName, "proxy-listener-tls-client-cert-subject-common-name", "", "Required client certificate subject common name")
112+
Server.Flags().StringSliceVar(&c.Proxy.TLS.ClientCert.Subject.Country, "proxy-listener-tls-required-client-subject-country", []string{}, "Required client certificate subject country")
113+
Server.Flags().StringSliceVar(&c.Proxy.TLS.ClientCert.Subject.Province, "proxy-listener-tls-required-client-subject-province", []string{}, "Required client certificate subject province")
114+
Server.Flags().StringSliceVar(&c.Proxy.TLS.ClientCert.Subject.Locality, "proxy-listener-tls-required-client-subject-locality", []string{}, "Required client certificate subject locality")
115+
Server.Flags().StringSliceVar(&c.Proxy.TLS.ClientCert.Subject.Organization, "proxy-listener-tls-required-client-subject-organization", []string{}, "Required client certificate subject organization")
116+
Server.Flags().StringSliceVar(&c.Proxy.TLS.ClientCert.Subject.OrganizationalUnit, "proxy-listener-tls-required-client-subject-organizational-unit", []string{}, "Required client certificate subject organizational unit")
117+
108118
// local authentication plugin
109119
Server.Flags().BoolVar(&c.Auth.Local.Enable, "auth-local-enable", false, "Enable local SASL/PLAIN authentication performed by listener - SASL handshake will not be passed to kafka brokers")
110120
Server.Flags().StringVar(&c.Auth.Local.Command, "auth-local-command", "", "Path to authentication plugin binary")

config/config.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ package config
22

33
import (
44
"fmt"
5-
"github.com/grepplabs/kafka-proxy/pkg/libs/util"
6-
"github.com/pkg/errors"
75
"net"
86
"net/url"
97
"strings"
108
"time"
9+
10+
"github.com/grepplabs/kafka-proxy/pkg/libs/util"
11+
"github.com/pkg/errors"
1112
)
1213

1314
const defaultClientID = "kafka-proxy"
@@ -70,6 +71,17 @@ type Config struct {
7071
CAChainCertFile string
7172
ListenerCipherSuites []string
7273
ListenerCurvePreferences []string
74+
ClientCert struct {
75+
ValidateSubject bool
76+
Subject struct {
77+
CommonName string
78+
Country []string
79+
Province []string
80+
Locality []string
81+
Organization []string
82+
OrganizationalUnit []string
83+
}
84+
}
7385
}
7486
}
7587
Auth struct {

proxy/proxy.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ package proxy
33
import (
44
"crypto/tls"
55
"fmt"
6+
"net"
7+
"sync"
8+
69
"github.com/grepplabs/kafka-proxy/config"
710
"github.com/grepplabs/kafka-proxy/pkg/libs/util"
811
"github.com/sirupsen/logrus"
9-
"net"
10-
"sync"
1112
)
1213

1314
type ListenFunc func(cfg config.ListenerConfig) (l net.Listener, err error)

proxy/tls.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"crypto/tls"
55
"crypto/x509"
66
"encoding/pem"
7+
"fmt"
78
"io/ioutil"
89
"net"
910
"strings"
@@ -120,6 +121,35 @@ func newTLSListenerConfig(conf *config.Config) (*tls.Config, error) {
120121
cfg.ClientCAs = clientCAs
121122
cfg.ClientAuth = tls.RequireAndVerifyClientCert
122123
}
124+
125+
cfg.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
126+
if conf.Proxy.TLS.ClientCert.ValidateSubject {
127+
expected := fmt.Sprintf("s:/CN=%s/C=%v/S=%v/L=%v/O=%v/OU=%v",
128+
conf.Proxy.TLS.ClientCert.Subject.CommonName,
129+
conf.Proxy.TLS.ClientCert.Subject.Country,
130+
conf.Proxy.TLS.ClientCert.Subject.Province,
131+
conf.Proxy.TLS.ClientCert.Subject.Locality,
132+
conf.Proxy.TLS.ClientCert.Subject.Organization,
133+
conf.Proxy.TLS.ClientCert.Subject.OrganizationalUnit)
134+
for _, chain := range verifiedChains {
135+
for _, cert := range chain {
136+
current := fmt.Sprintf("s:/CN=%s/C=%v/S=%v/L=%v/O=%v/OU=%v",
137+
cert.Subject.CommonName,
138+
cert.Subject.Country,
139+
cert.Subject.Province,
140+
cert.Subject.Locality,
141+
cert.Subject.Organization,
142+
cert.Subject.OrganizationalUnit)
143+
if current == expected {
144+
return nil
145+
}
146+
}
147+
}
148+
return fmt.Errorf("tls: no client certificate presented required subject '%s'", expected)
149+
}
150+
return nil
151+
}
152+
123153
return cfg, nil
124154
}
125155

proxy/tls_test.go

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@ package proxy
33
import (
44
"bytes"
55
"crypto/x509"
6-
"github.com/armon/go-socks5"
7-
"github.com/grepplabs/kafka-proxy/config"
8-
"github.com/pkg/errors"
9-
"github.com/stretchr/testify/assert"
6+
"crypto/x509/pkix"
107
"io"
118
"net"
129
"os"
1310
"strings"
1411
"testing"
1512
"time"
13+
14+
"github.com/armon/go-socks5"
15+
"github.com/grepplabs/kafka-proxy/config"
16+
"github.com/pkg/errors"
17+
"github.com/stretchr/testify/assert"
1618
)
1719

1820
func TestDefaultCipherSuites(t *testing.T) {
@@ -51,6 +53,69 @@ func TestEnabledCipherSuites(t *testing.T) {
5153
a.Equal(1, len(serverConfig.CurvePreferences))
5254
}
5355

56+
func TestValidEnabledClientCertSubjectValidate(t *testing.T) {
57+
a := assert.New(t)
58+
testSubject := pkix.Name{
59+
CommonName: "integration-test",
60+
Country: []string{"DE"},
61+
Locality: []string{"test-file"},
62+
Organization: []string{"integration-test"},
63+
OrganizationalUnit: []string{"invalid-OrganizationalUnit"},
64+
}
65+
bundle := NewCertsBundleWithSubject(testSubject)
66+
defer bundle.Close()
67+
c := new(config.Config)
68+
c.Proxy.TLS.ClientCert.ValidateSubject = true
69+
c.Proxy.TLS.ClientCert.Subject.CommonName = testSubject.CommonName
70+
c.Proxy.TLS.ClientCert.Subject.Country = testSubject.Country
71+
c.Proxy.TLS.ClientCert.Subject.Locality = testSubject.Locality
72+
c.Proxy.TLS.ClientCert.Subject.Organization = testSubject.Organization
73+
c.Proxy.TLS.ClientCert.Subject.OrganizationalUnit = testSubject.OrganizationalUnit
74+
c.Proxy.TLS.ListenerCertFile = bundle.ServerCert.Name()
75+
c.Proxy.TLS.ListenerKeyFile = bundle.ServerKey.Name()
76+
c.Proxy.TLS.CAChainCertFile = bundle.CACert.Name()
77+
78+
c.Kafka.TLS.CAChainCertFile = bundle.CACert.Name()
79+
c.Kafka.TLS.ClientCertFile = bundle.ClientCert.Name()
80+
c.Kafka.TLS.ClientKeyFile = bundle.ClientKey.Name()
81+
82+
_, _, _, err := makeTLSPipe(c, nil)
83+
84+
a.Nil(err)
85+
}
86+
87+
func TestInvalidEnabledClientCertSubjectValidate(t *testing.T) {
88+
a := assert.New(t)
89+
testSubject := pkix.Name{
90+
CommonName: "integration-test",
91+
Country: []string{"DE"},
92+
Locality: []string{"test-file"},
93+
Organization: []string{"integration-test"},
94+
OrganizationalUnit: []string{"invalid-OrganizationalUnit"},
95+
}
96+
bundle := NewCertsBundleWithSubject(testSubject)
97+
defer bundle.Close()
98+
c := new(config.Config)
99+
c.Proxy.TLS.ClientCert.ValidateSubject = true
100+
c.Proxy.TLS.ClientCert.Subject.CommonName = testSubject.CommonName
101+
c.Proxy.TLS.ClientCert.Subject.Country = testSubject.Country
102+
c.Proxy.TLS.ClientCert.Subject.Locality = testSubject.Locality
103+
c.Proxy.TLS.ClientCert.Subject.Organization = testSubject.Organization
104+
c.Proxy.TLS.ClientCert.Subject.OrganizationalUnit = []string{"expected-OrganizationalUnit"}
105+
c.Proxy.TLS.ListenerCertFile = bundle.ServerCert.Name()
106+
c.Proxy.TLS.ListenerKeyFile = bundle.ServerKey.Name()
107+
c.Proxy.TLS.CAChainCertFile = bundle.CACert.Name()
108+
109+
c.Kafka.TLS.CAChainCertFile = bundle.CACert.Name()
110+
c.Kafka.TLS.ClientCertFile = bundle.ClientCert.Name()
111+
c.Kafka.TLS.ClientKeyFile = bundle.ClientKey.Name()
112+
113+
_, _, _, err := makeTLSPipe(c, nil)
114+
115+
a.NotNil(err)
116+
a.Contains(err.Error(), "tls: no client certificate presented required subject 's:/CN=integration-test/C=[DE]/S=[]/L=[test-file]/O=[integration-test]/OU=[expected-OrganizationalUnit]'")
117+
}
118+
54119
func TestTLSUnknownAuthorityNoCAChainCert(t *testing.T) {
55120
a := assert.New(t)
56121

proxy/util_test.go

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,19 @@ import (
77
"crypto/x509"
88
"crypto/x509/pkix"
99
"encoding/pem"
10-
"github.com/armon/go-socks5"
11-
"github.com/elazarl/goproxy"
12-
"github.com/elazarl/goproxy/ext/auth"
13-
"github.com/grepplabs/kafka-proxy/config"
14-
"github.com/pkg/errors"
15-
"golang.org/x/net/proxy"
1610
"io/ioutil"
1711
"math/big"
1812
"net"
1913
"net/http"
2014
"os"
2115
"time"
16+
17+
"github.com/armon/go-socks5"
18+
"github.com/elazarl/goproxy"
19+
"github.com/elazarl/goproxy/ext/auth"
20+
"github.com/grepplabs/kafka-proxy/config"
21+
"github.com/pkg/errors"
22+
"golang.org/x/net/proxy"
2223
)
2324

2425
type testAcceptResult struct {
@@ -380,18 +381,23 @@ func makeHttpProxyPipe() (net.Conn, net.Conn, func(), error) {
380381
}
381382

382383
func generateCert(catls *tls.Certificate, certFile *os.File, keyFile *os.File) error {
384+
return generateCertWithSubject(catls, certFile, keyFile, pkix.Name{
385+
Organization: []string{"ORGANIZATION_NAME"},
386+
OrganizationalUnit: []string{"ORGANIZATIONAL_UNIT"},
387+
Country: []string{"COUNTRY_CODE"},
388+
Province: []string{"PROVINCE"},
389+
Locality: []string{"CITY"},
390+
StreetAddress: []string{"ADDRESS"},
391+
PostalCode: []string{"POSTAL_CODE"},
392+
CommonName: "localhost",
393+
})
394+
}
395+
396+
func generateCertWithSubject(catls *tls.Certificate, certFile *os.File, keyFile *os.File, subject pkix.Name) error {
383397
// Prepare certificate
384398
cert := &x509.Certificate{
385399
SerialNumber: big.NewInt(1),
386-
Subject: pkix.Name{
387-
Organization: []string{"ORGANIZATION_NAME"},
388-
Country: []string{"COUNTRY_CODE"},
389-
Province: []string{"PROVINCE"},
390-
Locality: []string{"CITY"},
391-
StreetAddress: []string{"ADDRESS"},
392-
PostalCode: []string{"POSTAL_CODE"},
393-
CommonName: "localhost",
394-
},
400+
Subject: subject,
395401
NotBefore: time.Now(),
396402
NotAfter: time.Now().AddDate(10, 0, 0),
397403
SubjectKeyId: []byte{1, 2, 3, 4, 6},
@@ -539,6 +545,52 @@ func NewCertsBundle() *CertsBundle {
539545
return bundle
540546
}
541547

548+
func NewCertsBundleWithSubject(subject pkix.Name) *CertsBundle {
549+
bundle := &CertsBundle{}
550+
dirName, err := ioutil.TempDir("", "tls-test")
551+
if err != nil {
552+
panic(err)
553+
}
554+
bundle.CACert, err = ioutil.TempFile(dirName, "ca-cert-")
555+
if err != nil {
556+
panic(err)
557+
}
558+
bundle.CAKey, err = ioutil.TempFile(dirName, "ca-key-")
559+
if err != nil {
560+
panic(err)
561+
}
562+
bundle.ServerCert, err = ioutil.TempFile(dirName, "server-cert-")
563+
if err != nil {
564+
panic(err)
565+
}
566+
bundle.ServerKey, err = ioutil.TempFile(dirName, "server-key-")
567+
if err != nil {
568+
panic(err)
569+
}
570+
bundle.ClientCert, err = ioutil.TempFile(dirName, "client-cert-")
571+
if err != nil {
572+
panic(err)
573+
}
574+
bundle.ClientKey, err = ioutil.TempFile("", "client-key-")
575+
if err != nil {
576+
panic(err)
577+
}
578+
// generate certs
579+
catls, err := generateCA(bundle.CACert, bundle.CAKey)
580+
if err != nil {
581+
panic(err)
582+
}
583+
err = generateCert(catls, bundle.ServerCert, bundle.ServerKey)
584+
if err != nil {
585+
panic(err)
586+
}
587+
err = generateCertWithSubject(catls, bundle.ClientCert, bundle.ClientKey, subject)
588+
if err != nil {
589+
panic(err)
590+
}
591+
return bundle
592+
}
593+
542594
func (bundle *CertsBundle) Close() {
543595
_ = os.Remove(bundle.CACert.Name())
544596
_ = os.Remove(bundle.CAKey.Name())

0 commit comments

Comments
 (0)