Skip to content

Commit f5a8ba6

Browse files
committed
integrate cert-source lib, add listener CLR file and cert rotation
1 parent adfee6a commit f5a8ba6

33 files changed

+1949
-137
lines changed

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ You can launch a kafka-proxy container with auth-ldap plugin for trying it out w
140140
--debug-enable Enable Debug endpoint
141141
--debug-listen-address string Debug listen address (default "0.0.0.0:6060")
142142
--default-listener-ip string Default listener IP (default "0.0.0.0")
143+
--deterministic-listeners Enable deterministic listeners (listener port = min port + broker id).
143144
--dial-address-mapping stringArray Mapping of target broker address to new one (host:port,host:port). The mapping is performed during connection establishment
144145
--dynamic-advertised-listener string Advertised address for dynamic listeners. If empty, default-listener-ip is used
145146
--dynamic-listeners-disable Disable dynamic listeners.
@@ -178,12 +179,14 @@ You can launch a kafka-proxy container with auth-ldap plugin for trying it out w
178179
--proxy-listener-ca-chain-cert-file string PEM encoded CA's certificate file. If provided, client certificate is required and verified
179180
--proxy-listener-cert-file string PEM encoded file with server certificate
180181
--proxy-listener-cipher-suites strings List of supported cipher suites
182+
--proxy-listener-crl-file string PEM encoded X509 CRLs file
181183
--proxy-listener-curve-preferences strings List of curve preferences
182184
--proxy-listener-keep-alive duration Keep alive period for an active network connection. If zero, keep-alives are disabled (default 1m0s)
183185
--proxy-listener-key-file string PEM encoded file with private key for the server certificate
184186
--proxy-listener-key-password string Password to decrypt rsa private key
185187
--proxy-listener-read-buffer-size int Size of the operating system's receive buffer associated with the connection. If zero, system default is used
186188
--proxy-listener-tls-enable Whether or not to use TLS listener
189+
--proxy-listener-tls-refresh duration Interval for refreshing server TLS certificates. If set to zero, the refresh watch is disabled
187190
--proxy-listener-tls-required-client-subject strings Required client certificate subject common name; example; s:/CN=[value]/C=[state]/C=[DE,PL] or r:/CN=[^val.{2}$]/C=[state]/C=[DE,PL]; check manual for more details
188191
--proxy-listener-write-buffer-size int Sets the size of the operating system's transmit buffer associated with the connection. If zero, system default is used
189192
--proxy-request-buffer-size int Request buffer size pro tcp connection (default 4096)
@@ -207,7 +210,9 @@ You can launch a kafka-proxy container with auth-ldap plugin for trying it out w
207210
--tls-client-key-password string Password to decrypt rsa private key
208211
--tls-enable Whether or not to use TLS when connecting to the broker
209212
--tls-insecure-skip-verify It controls whether a client verifies the server's certificate chain and host name
213+
--tls-refresh duration Interval for refreshing client TLS certificates. If set to zero, the refresh watch is disabled
210214
--tls-same-client-cert-enable Use only when mutual TLS is enabled on proxy and broker. It controls whether a proxy validates if proxy client certificate exactly matches brokers client cert (tls-client-cert-file)
215+
--tls-system-cert-pool Use system pool for root CAs
211216
212217
### Usage example
213218

cmd/kafka-proxy/server.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,12 @@ func initFlags() {
103103
Server.Flags().DurationVar(&c.Proxy.ListenerKeepAlive, "proxy-listener-keep-alive", 60*time.Second, "Keep alive period for an active network connection. If zero, keep-alives are disabled")
104104

105105
Server.Flags().BoolVar(&c.Proxy.TLS.Enable, "proxy-listener-tls-enable", false, "Whether or not to use TLS listener")
106+
Server.Flags().DurationVar(&c.Proxy.TLS.Refresh, "proxy-listener-tls-refresh", 0*time.Second, "Interval for refreshing server TLS certificates. If set to zero, the refresh watch is disabled")
106107
Server.Flags().StringVar(&c.Proxy.TLS.ListenerCertFile, "proxy-listener-cert-file", "", "PEM encoded file with server certificate")
107108
Server.Flags().StringVar(&c.Proxy.TLS.ListenerKeyFile, "proxy-listener-key-file", "", "PEM encoded file with private key for the server certificate")
108109
Server.Flags().StringVar(&c.Proxy.TLS.ListenerKeyPassword, "proxy-listener-key-password", os.Getenv("PROXY_LISTENER_KEY_PASSWORD"), "Password to decrypt rsa private key")
109-
Server.Flags().StringVar(&c.Proxy.TLS.CAChainCertFile, "proxy-listener-ca-chain-cert-file", "", "PEM encoded CA's certificate file. If provided, client certificate is required and verified")
110+
Server.Flags().StringVar(&c.Proxy.TLS.ListenerCAChainCertFile, "proxy-listener-ca-chain-cert-file", "", "PEM encoded CA's certificate file. If provided, client certificate is required and verified")
111+
Server.Flags().StringVar(&c.Proxy.TLS.ListenerCRLFile, "proxy-listener-crl-file", "", "PEM encoded X509 CRLs file")
110112
Server.Flags().StringSliceVar(&c.Proxy.TLS.ListenerCipherSuites, "proxy-listener-cipher-suites", []string{}, "List of supported cipher suites")
111113
Server.Flags().StringSliceVar(&c.Proxy.TLS.ListenerCurvePreferences, "proxy-listener-curve-preferences", []string{}, "List of curve preferences")
112114

@@ -153,11 +155,13 @@ func initFlags() {
153155

154156
// TLS
155157
Server.Flags().BoolVar(&c.Kafka.TLS.Enable, "tls-enable", false, "Whether or not to use TLS when connecting to the broker")
158+
Server.Flags().DurationVar(&c.Kafka.TLS.Refresh, "tls-refresh", 0*time.Second, "Interval for refreshing client TLS certificates. If set to zero, the refresh watch is disabled")
156159
Server.Flags().BoolVar(&c.Kafka.TLS.InsecureSkipVerify, "tls-insecure-skip-verify", false, "It controls whether a client verifies the server's certificate chain and host name")
157160
Server.Flags().StringVar(&c.Kafka.TLS.ClientCertFile, "tls-client-cert-file", "", "PEM encoded file with client certificate")
158161
Server.Flags().StringVar(&c.Kafka.TLS.ClientKeyFile, "tls-client-key-file", "", "PEM encoded file with private key for the client certificate")
159162
Server.Flags().StringVar(&c.Kafka.TLS.ClientKeyPassword, "tls-client-key-password", os.Getenv("TLS_CLIENT_KEY_PASSWORD"), "Password to decrypt rsa private key")
160163
Server.Flags().StringVar(&c.Kafka.TLS.CAChainCertFile, "tls-ca-chain-cert-file", "", "PEM encoded CA's certificate file")
164+
Server.Flags().BoolVar(&c.Kafka.TLS.SystemCertPool, "tls-system-cert-pool", false, "Use system pool for root CAs")
161165

162166
//Same TLS client cert tls-same-client-cert-enable
163167
Server.Flags().BoolVar(&c.Kafka.TLS.SameClientCertEnable, "tls-same-client-cert-enable", false, "Use only when mutual TLS is enabled on proxy and broker. It controls whether a proxy validates if proxy client certificate exactly matches brokers client cert (tls-client-cert-file)")

config/config.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,12 @@ type Config struct {
8888

8989
TLS struct {
9090
Enable bool
91+
Refresh time.Duration
9192
ListenerCertFile string
9293
ListenerKeyFile string
9394
ListenerKeyPassword string
94-
CAChainCertFile string
95+
ListenerCAChainCertFile string
96+
ListenerCRLFile string
9597
ListenerCipherSuites []string
9698
ListenerCurvePreferences []string
9799
ClientCert struct {
@@ -145,11 +147,13 @@ type Config struct {
145147

146148
TLS struct {
147149
Enable bool
150+
Refresh time.Duration
148151
InsecureSkipVerify bool
149152
ClientCertFile string
150153
ClientKeyFile string
151154
ClientKeyPassword string
152155
CAChainCertFile string
156+
SystemCertPool bool
153157
SameClientCertEnable bool
154158
}
155159

go.mod

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/fsnotify/fsnotify v1.4.9
1313
github.com/go-ldap/ldap/v3 v3.2.3
1414
github.com/google/uuid v1.6.0
15+
github.com/grepplabs/cert-source v0.0.8
1516
github.com/hashicorp/go-hclog v1.6.3
1617
github.com/hashicorp/go-multierror v0.0.0-20171204182908-b7773ae21874
1718
github.com/hashicorp/go-plugin v1.6.3
@@ -26,7 +27,7 @@ require (
2627
github.com/spf13/viper v1.0.2
2728
github.com/stretchr/testify v1.10.0
2829
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c
29-
github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76
30+
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78
3031
golang.org/x/net v0.34.0
3132
golang.org/x/oauth2 v0.24.0
3233
google.golang.org/api v0.126.0

go.sum

+4-2
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cU
137137
github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI=
138138
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
139139
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
140+
github.com/grepplabs/cert-source v0.0.8 h1:rcZeipbbljq46mMvw9yVF4FX/1zzLVfyenV3C07XS8g=
141+
github.com/grepplabs/cert-source v0.0.8/go.mod h1:gs3IoykME1cFfZ6/h6hch8yg8ktUInsR9OY2xSHA2r4=
140142
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
141143
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce h1:prjrVgOk2Yg6w+PflHoszQNLTUh4kaByUcEWM/9uin4=
142144
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -281,8 +283,8 @@ github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV
281283
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
282284
github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0=
283285
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
284-
github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 h1:tBiBTKHnIjovYoLX/TPkcf+OjqqKGQrPtGT3Foz+Pgo=
285-
github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76/go.mod h1:SQliXeA7Dhkt//vS29v3zpbEwoa+zb2Cn5xj5uO4K5U=
286+
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
287+
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
286288
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
287289
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
288290
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=

proxy/client.go

+8-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package proxy
22

33
import (
4-
"crypto/tls"
54
"crypto/x509"
65
"fmt"
76
"net"
@@ -46,7 +45,7 @@ type Client struct {
4645
}
4746

4847
func NewClient(conns *ConnSet, c *config.Config, netAddressMappingFunc config.NetAddressMappingFunc, localPasswordAuthenticator apis.PasswordAuthenticator, localTokenAuthenticator apis.TokenInfo, saslTokenProvider apis.TokenProvider, gatewayTokenProvider apis.TokenProvider, gatewayTokenInfo apis.TokenInfo) (*Client, error) {
49-
tlsConfig, err := newTLSClientConfig(c)
48+
tlsConfigFunc, err := newTLSClientConfig(c)
5049
if err != nil {
5150
return nil, err
5251
}
@@ -59,7 +58,7 @@ func NewClient(conns *ConnSet, c *config.Config, netAddressMappingFunc config.Ne
5958
}
6059
}
6160

62-
dialer, err := newDialer(c, tlsConfig)
61+
dialer, err := newDialer(c, tlsConfigFunc)
6362
if err != nil {
6463
return nil, err
6564
}
@@ -195,7 +194,7 @@ func getAddressToDialAddressMapping(cfg *config.Config) (map[string]config.DialA
195194
return addressToDialAddressMapping, nil
196195
}
197196

198-
func newDialer(c *config.Config, tlsConfig *tls.Config) (Dialer, error) {
197+
func newDialer(c *config.Config, tlsConfigFunc TLSConfigFunc) (Dialer, error) {
199198
directDialer := directDialer{
200199
dialTimeout: c.Kafka.DialTimeout,
201200
keepAlive: c.Kafka.KeepAlive,
@@ -230,13 +229,13 @@ func newDialer(c *config.Config, tlsConfig *tls.Config) (Dialer, error) {
230229
rawDialer = directDialer
231230
}
232231
if c.Kafka.TLS.Enable {
233-
if tlsConfig == nil {
234-
return nil, errors.New("tlsConfig must not be nil")
232+
if tlsConfigFunc == nil || tlsConfigFunc() == nil {
233+
return nil, errors.New("tlsConfigFunc must not be nil")
235234
}
236235
tlsDialer := tlsDialer{
237-
timeout: c.Kafka.DialTimeout,
238-
rawDialer: rawDialer,
239-
config: tlsConfig,
236+
timeout: c.Kafka.DialTimeout,
237+
rawDialer: rawDialer,
238+
configFunc: tlsConfigFunc,
240239
}
241240
return tlsDialer, nil
242241
}

proxy/dial.go

+5-6
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,15 @@ func (d socks5Dialer) Dial(network, addr string) (net.Conn, error) {
6969
}
7070

7171
type tlsDialer struct {
72-
timeout time.Duration
73-
rawDialer Dialer
74-
config *tls.Config
72+
timeout time.Duration
73+
rawDialer Dialer
74+
configFunc TLSConfigFunc
7575
}
7676

7777
// see tls.DialWithDialer
7878
func (d tlsDialer) Dial(network, addr string) (net.Conn, error) {
79-
if d.config == nil {
79+
config := d.configFunc()
80+
if config == nil {
8081
return nil, errors.New("tlsConfig must not be nil")
8182
}
8283
if d.rawDialer == nil {
@@ -106,8 +107,6 @@ func (d tlsDialer) Dial(network, addr string) (net.Conn, error) {
106107
}
107108
hostname := addr[:colonPos]
108109

109-
config := d.config
110-
111110
// If no ServerName is set, infer the ServerName
112111
// from the hostname we're connecting to.
113112
if config.ServerName == "" {

proxy/tls.go

+42-80
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@ import (
88
"crypto/x509"
99
"encoding/pem"
1010
"fmt"
11+
"log/slog"
1112
"net"
1213
"os"
1314
"reflect"
1415
"strings"
1516
"time"
1617

18+
tlsconfig "github.com/grepplabs/cert-source/config"
19+
tlsclientconfig "github.com/grepplabs/cert-source/tls/client/config"
20+
tlsserver "github.com/grepplabs/cert-source/tls/server"
21+
tlsserverconfig "github.com/grepplabs/cert-source/tls/server/config"
1722
"github.com/grepplabs/kafka-proxy/config"
1823
"github.com/pkg/errors"
1924
"github.com/youmark/pkcs8"
@@ -59,25 +64,6 @@ var (
5964
func newTLSListenerConfig(conf *config.Config) (*tls.Config, error) {
6065
opts := conf.Proxy.TLS
6166

62-
if opts.ListenerKeyFile == "" || opts.ListenerCertFile == "" {
63-
return nil, errors.New("Listener key and cert files must not be empty")
64-
}
65-
certPEMBlock, err := os.ReadFile(opts.ListenerCertFile)
66-
if err != nil {
67-
return nil, err
68-
}
69-
keyPEMBlock, err := os.ReadFile(opts.ListenerKeyFile)
70-
if err != nil {
71-
return nil, err
72-
}
73-
keyPEMBlock, err = decryptPEM(keyPEMBlock, opts.ListenerKeyPassword)
74-
if err != nil {
75-
return nil, err
76-
}
77-
cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
78-
if err != nil {
79-
return nil, err
80-
}
8167
cipherSuites, err := getCipherSuites(opts.ListenerCipherSuites)
8268
if err != nil {
8369
return nil, err
@@ -86,35 +72,27 @@ func newTLSListenerConfig(conf *config.Config) (*tls.Config, error) {
8672
if err != nil {
8773
return nil, err
8874
}
89-
90-
cfg := &tls.Config{
91-
Certificates: []tls.Certificate{cert},
92-
ClientAuth: tls.NoClientCert,
93-
PreferServerCipherSuites: true,
94-
MinVersion: tls.VersionTLS12,
95-
CurvePreferences: curvePreferences,
96-
CipherSuites: cipherSuites,
97-
}
98-
if opts.CAChainCertFile != "" {
99-
caCertPEMBlock, err := os.ReadFile(opts.CAChainCertFile)
100-
if err != nil {
101-
return nil, err
102-
}
103-
clientCAs := x509.NewCertPool()
104-
if ok := clientCAs.AppendCertsFromPEM(caCertPEMBlock); !ok {
105-
return nil, errors.New("Failed to parse listener root certificate")
106-
}
107-
cfg.ClientCAs = clientCAs
108-
cfg.ClientAuth = tls.RequireAndVerifyClientCert
109-
}
110-
11175
tlsValidateFunc, err := tlsClientCertVerificationFunc(conf)
11276
if err != nil {
11377
return nil, err
11478
}
115-
cfg.VerifyPeerCertificate = tlsValidateFunc
116-
117-
return cfg, nil
79+
tlsConfig, err := tlsserverconfig.GetServerTLSConfig(slog.Default(),
80+
&tlsconfig.TLSServerConfig{
81+
Enable: true,
82+
Refresh: opts.Refresh,
83+
KeyPassword: opts.ListenerKeyPassword,
84+
File: tlsconfig.TLSServerFiles{
85+
Key: opts.ListenerKeyFile,
86+
Cert: opts.ListenerCertFile,
87+
ClientCAs: opts.ListenerCAChainCertFile,
88+
ClientCRL: opts.ListenerCRLFile,
89+
},
90+
},
91+
tlsserver.WithTLSServerVerifyPeerCertificate(tlsValidateFunc),
92+
tlsserver.WithTLSServerCipherSuites(cipherSuites),
93+
tlsserver.WithTLSServerCurvePreferences(curvePreferences),
94+
)
95+
return tlsConfig, nil
11896
}
11997

12098
func getCipherSuites(enabledCipherSuites []string) ([]uint16, error) {
@@ -147,45 +125,29 @@ func getCurvePreferences(enabledCurvePreferences []string) ([]tls.CurveID, error
147125
return curvePreferences, nil
148126
}
149127

150-
func newTLSClientConfig(conf *config.Config) (*tls.Config, error) {
128+
type TLSConfigFunc func() *tls.Config
129+
130+
func newTLSClientConfig(conf *config.Config) (TLSConfigFunc, error) {
151131
// https://blog.cloudflare.com/exposing-go-on-the-internet/
152132
opts := conf.Kafka.TLS
153-
154-
cfg := &tls.Config{InsecureSkipVerify: opts.InsecureSkipVerify}
155-
156-
if opts.ClientCertFile != "" && opts.ClientKeyFile != "" {
157-
certPEMBlock, err := os.ReadFile(opts.ClientCertFile)
158-
if err != nil {
159-
return nil, err
160-
}
161-
keyPEMBlock, err := os.ReadFile(opts.ClientKeyFile)
162-
if err != nil {
163-
return nil, err
164-
}
165-
keyPEMBlock, err = decryptPEM(keyPEMBlock, opts.ClientKeyPassword)
166-
if err != nil {
167-
return nil, err
168-
}
169-
cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
170-
if err != nil {
171-
return nil, err
172-
}
173-
cfg.Certificates = []tls.Certificate{cert}
174-
}
175-
176-
if opts.CAChainCertFile != "" {
177-
caCertPEMBlock, err := os.ReadFile(opts.CAChainCertFile)
178-
if err != nil {
179-
return nil, err
180-
}
181-
rootCAs := x509.NewCertPool()
182-
if ok := rootCAs.AppendCertsFromPEM(caCertPEMBlock); !ok {
183-
return nil, errors.New("Failed to parse client root certificate")
184-
}
185-
186-
cfg.RootCAs = rootCAs
133+
tlsConfigFunc, err := tlsclientconfig.GetTLSClientConfigFunc(slog.Default(), &tlsconfig.TLSClientConfig{
134+
Enable: true,
135+
Refresh: opts.Refresh,
136+
InsecureSkipVerify: opts.InsecureSkipVerify,
137+
KeyPassword: opts.ClientKeyPassword,
138+
UseSystemPool: opts.SystemCertPool,
139+
File: tlsconfig.TLSClientFiles{
140+
Key: opts.ClientKeyFile,
141+
Cert: opts.ClientCertFile,
142+
RootCAs: opts.CAChainCertFile,
143+
},
144+
})
145+
if err != nil {
146+
return nil, err
187147
}
188-
return cfg, nil
148+
return func() *tls.Config {
149+
return tlsConfigFunc()
150+
}, err
189151
}
190152

191153
func decryptPEM(pemData []byte, password string) ([]byte, error) {

0 commit comments

Comments
 (0)