Skip to content

Commit 8a1d490

Browse files
authored
feat: support setting multiple TLS certs for different domains on the interceptor proxy (#1116)
Signed-off-by: Jan Wozniak <wozniak.jan@gmail.com>
1 parent 2601d92 commit 8a1d490

File tree

10 files changed

+346
-28
lines changed

10 files changed

+346
-28
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ This changelog keeps track of work items that have been completed and are ready
2323

2424
### New
2525

26+
- **General**: Support setting multiple TLS certs for different domains on the interceptor proxy ([#1116](https://github.com/kedacore/http-add-on/issues/1116))
2627
- **General**: TODO ([#TODO](https://github.com/kedacore/http-add-on/issues/TODO))
2728

2829
### Improvements

Makefile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ DNS.3 = *.interceptor-tls-test-ns
4545
endef
4646
export DOMAINS
4747

48+
define ABC_DOMAINS
49+
basicConstraints=CA:FALSE
50+
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
51+
subjectAltName = @alt_names
52+
[alt_names]
53+
DNS.1 = abc
54+
endef
55+
export ABC_DOMAINS
56+
4857
# Build targets
4958

5059
build-operator:
@@ -68,6 +77,9 @@ test-certs: rootca-test-certs
6877
echo "$$DOMAINS" > certs/domains.ext
6978
openssl req -new -nodes -newkey rsa:2048 -keyout certs/tls.key -out certs/tls.csr -subj "/C=US/ST=KedaState/L=KedaCity/O=Keda-Certificates/CN=keda.local"
7079
openssl x509 -req -sha256 -days 1024 -in certs/tls.csr -CA certs/RootCA.pem -CAkey certs/RootCA.key -CAcreateserial -extfile certs/domains.ext -out certs/tls.crt
80+
echo "$$ABC_DOMAINS" > certs/abc_domains.ext
81+
openssl req -new -nodes -newkey rsa:2048 -keyout certs/abc.tls.key -out certs/abc.tls.csr -subj "/C=US/ST=KedaState/L=KedaCity/O=Keda-Certificates/CN=abc"
82+
openssl x509 -req -sha256 -days 1024 -in certs/abc.tls.csr -CA certs/RootCA.pem -CAkey certs/RootCA.key -CAcreateserial -extfile certs/abc_domains.ext -out certs/abc.tls.crt
7183

7284
clean-test-certs:
7385
rm -r certs || true

config/interceptor/e2e-test/tls/deployment.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,21 @@ spec:
1818
value: "/certs/tls.crt"
1919
- name: KEDA_HTTP_PROXY_TLS_KEY_PATH
2020
value: "/certs/tls.key"
21+
- name: KEDA_HTTP_PROXY_TLS_CERT_STORE_PATHS
22+
value: "/additional-certs"
2123
- name: KEDA_HTTP_PROXY_TLS_PORT
2224
value: "8443"
2325
volumeMounts:
2426
- readOnly: true
2527
mountPath: "/certs"
2628
name: certs
29+
- readOnly: true
30+
mountPath: "/additional-certs/abc-certs"
31+
name: abc-certs
2732
volumes:
2833
- name: certs
2934
secret:
3035
secretName: keda-tls
36+
- name: abc-certs
37+
secret:
38+
secretName: abc-certs

docs/operate.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,9 @@ If you need to provide any headers such as authentication details in order to ut
2323
The interceptor proxy has the ability to run both a HTTP and HTTPS server simultaneously to allow you to scale workloads that use either protocol. By default, the interceptor proxy will only serve over HTTP, but this behavior can be changed by configuring the appropriate environment variables on the deployment.
2424

2525
The TLS server can be enabled by setting the environment variable `KEDA_HTTP_PROXY_TLS_ENABLED` to `true` on the interceptor deployment (`false` by default). The TLS server will start on port `8443` by default, but this can be configured by setting `KEDA_HTTP_PROXY_TLS_PORT` to your desired port number. The TLS server will require valid TLS certificates to start, the path to the certificates can be configured via the `KEDA_HTTP_PROXY_TLS_CERT_PATH` and `KEDA_HTTP_PROXY_TLS_KEY_PATH` environment variables (`/certs/tls.crt` and `/certs/tls.key` by default).
26+
27+
For setting multiple TLS certs, set `KEDA_HTTP_PROXY_TLS_CERT_STORE_PATHS` with comma-separated list of directories that will be recursively searched for any valid cert/key pairs. Currently, two naming patterns are supported
28+
* `XYZ.crt` + `XYZ.key` - this is a convention when using Kubernetes Secrets of type tls
29+
* `XYZ.pem` + `XYZ-key.pem`
30+
31+
The matching between certs and requests is performed during the TLS ClientHelo message, where the SNI service name is compared to SANs provided in each cert and the first matching cert will be used for the rest of the TLS handshake.

interceptor/config/serving.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ type Serving struct {
4141
TLSCertPath string `envconfig:"KEDA_HTTP_PROXY_TLS_CERT_PATH" default:"/certs/tls.crt"`
4242
// TLSKeyPath is the path to read the private key file from for the TLS server
4343
TLSKeyPath string `envconfig:"KEDA_HTTP_PROXY_TLS_KEY_PATH" default:"/certs/tls.key"`
44+
// TLSCertStorePaths is a comma separated list of paths to read the certificate/key pairs for the TLS server
45+
TLSCertStorePaths string `envconfig:"KEDA_HTTP_PROXY_TLS_CERT_STORE_PATHS" default:""`
4446
// TLSPort is the port that the server should serve on if TLS is enabled
4547
TLSPort int `envconfig:"KEDA_HTTP_PROXY_TLS_PORT" default:"8443"`
4648
}

interceptor/main.go

Lines changed: 149 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ import (
99
"fmt"
1010
"net/http"
1111
"os"
12+
"path/filepath"
13+
"strings"
1214
"time"
1315

1416
"github.com/go-logr/logr"
1517
"github.com/prometheus/client_golang/prometheus/promhttp"
18+
"golang.org/x/exp/maps"
1619
"golang.org/x/sync/errgroup"
1720
"k8s.io/client-go/kubernetes"
1821
ctrl "sigs.k8s.io/controller-runtime"
@@ -168,7 +171,7 @@ func main() {
168171
// start a proxy server with TLS
169172
if proxyTLSEnabled {
170173
eg.Go(func() error {
171-
proxyTLSConfig := map[string]string{"certificatePath": servingCfg.TLSCertPath, "keyPath": servingCfg.TLSKeyPath}
174+
proxyTLSConfig := map[string]string{"certificatePath": servingCfg.TLSCertPath, "keyPath": servingCfg.TLSKeyPath, "certstorePaths": servingCfg.TLSCertStorePaths}
172175
proxyTLSPort := servingCfg.TLSPort
173176

174177
setupLog.Info("starting the proxy server with TLS enabled", "port", proxyTLSPort)
@@ -219,7 +222,7 @@ func runAdminServer(
219222

220223
addr := fmt.Sprintf("0.0.0.0:%d", port)
221224
lggr.Info("admin server starting", "address", addr)
222-
return kedahttp.ServeContext(ctx, addr, adminServer, false, nil)
225+
return kedahttp.ServeContext(ctx, addr, adminServer, nil)
223226
}
224227

225228
func runMetricsServer(
@@ -229,7 +232,135 @@ func runMetricsServer(
229232
) error {
230233
lggr.Info("starting the prometheus metrics server", "port", metricsCfg.OtelPrometheusExporterPort, "path", "/metrics")
231234
addr := fmt.Sprintf("0.0.0.0:%d", metricsCfg.OtelPrometheusExporterPort)
232-
return kedahttp.ServeContext(ctx, addr, promhttp.Handler(), false, nil)
235+
return kedahttp.ServeContext(ctx, addr, promhttp.Handler(), nil)
236+
}
237+
238+
// addCert adds a certificate to the map of certificates based on the certificate's SANs
239+
func addCert(m map[string]tls.Certificate, certPath, keyPath string, logger logr.Logger) (*tls.Certificate, error) {
240+
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
241+
if err != nil {
242+
return nil, fmt.Errorf("error loading certificate and key: %w", err)
243+
}
244+
if cert.Leaf == nil {
245+
if len(cert.Certificate) == 0 {
246+
return nil, fmt.Errorf("no certificate found in certificate chain")
247+
}
248+
cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
249+
if err != nil {
250+
return nil, fmt.Errorf("error parsing certificate: %w", err)
251+
}
252+
}
253+
for _, d := range cert.Leaf.DNSNames {
254+
logger.Info("adding certificate", "dns", d)
255+
m[d] = cert
256+
}
257+
for _, ip := range cert.Leaf.IPAddresses {
258+
logger.Info("adding certificate", "ip", ip.String())
259+
m[ip.String()] = cert
260+
}
261+
for _, uri := range cert.Leaf.URIs {
262+
logger.Info("adding certificate", "uri", uri.String())
263+
m[uri.String()] = cert
264+
}
265+
return &cert, nil
266+
}
267+
268+
func defaultCertPool(logger logr.Logger) *x509.CertPool {
269+
systemCAs, err := x509.SystemCertPool()
270+
if err == nil {
271+
return systemCAs
272+
}
273+
274+
logger.Info("error loading system CA pool, using empty pool", "error", err)
275+
return x509.NewCertPool()
276+
}
277+
278+
// getTLSConfig creates a TLS config from KEDA_HTTP_PROXY_TLS_CERT_PATH, KEDA_HTTP_PROXY_TLS_KEY_PATH and KEDA_HTTP_PROXY_TLS_CERTSTORE_PATHS
279+
// The matching between request and certificate is performed by comparing TLS/SNI server name with x509 SANs
280+
func getTLSConfig(tlsConfig map[string]string, logger logr.Logger) (*tls.Config, error) {
281+
certPath := tlsConfig["certificatePath"]
282+
keyPath := tlsConfig["keyPath"]
283+
certStorePaths := tlsConfig["certstorePaths"]
284+
servingTLS := &tls.Config{
285+
RootCAs: defaultCertPool(logger),
286+
}
287+
var defaultCert *tls.Certificate
288+
289+
uriDomainsToCerts := make(map[string]tls.Certificate)
290+
if certPath != "" && keyPath != "" {
291+
cert, err := addCert(uriDomainsToCerts, certPath, keyPath, logger)
292+
if err != nil {
293+
return servingTLS, fmt.Errorf("error adding certificate and key: %w", err)
294+
}
295+
defaultCert = cert
296+
rawCert, err := os.ReadFile(certPath)
297+
if err != nil {
298+
return servingTLS, fmt.Errorf("error reading certificate: %w", err)
299+
}
300+
servingTLS.RootCAs.AppendCertsFromPEM(rawCert)
301+
}
302+
303+
if certStorePaths != "" {
304+
certFiles := make(map[string]string)
305+
keyFiles := make(map[string]string)
306+
dirPaths := strings.Split(certStorePaths, ",")
307+
for _, dir := range dirPaths {
308+
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
309+
if err != nil {
310+
return err
311+
}
312+
if info.IsDir() {
313+
return nil
314+
}
315+
switch {
316+
case strings.HasSuffix(path, "-key.pem"):
317+
certID := path[:len(path)-8]
318+
keyFiles[certID] = path
319+
case strings.HasSuffix(path, ".pem"):
320+
certID := path[:len(path)-4]
321+
certFiles[certID] = path
322+
case strings.HasSuffix(path, ".key"):
323+
certID := path[:len(path)-4]
324+
keyFiles[certID] = path
325+
case strings.HasSuffix(path, ".crt"):
326+
certID := path[:len(path)-4]
327+
certFiles[certID] = path
328+
}
329+
return nil
330+
})
331+
if err != nil {
332+
return servingTLS, fmt.Errorf("error walking certificate store: %w", err)
333+
}
334+
}
335+
336+
for certID, certPath := range certFiles {
337+
logger.Info("adding certificate", "certID", certID, "certPath", certPath)
338+
keyPath, ok := keyFiles[certID]
339+
if !ok {
340+
return servingTLS, fmt.Errorf("no key found for certificate %s", certPath)
341+
}
342+
if _, err := addCert(uriDomainsToCerts, certPath, keyPath, logger); err != nil {
343+
return servingTLS, fmt.Errorf("error adding certificate %s: %w", certPath, err)
344+
}
345+
rawCert, err := os.ReadFile(certPath)
346+
if err != nil {
347+
return servingTLS, fmt.Errorf("error reading certificate: %w", err)
348+
}
349+
servingTLS.RootCAs.AppendCertsFromPEM(rawCert)
350+
}
351+
}
352+
353+
servingTLS.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
354+
if cert, ok := uriDomainsToCerts[hello.ServerName]; ok {
355+
return &cert, nil
356+
}
357+
if defaultCert != nil {
358+
return defaultCert, nil
359+
}
360+
return nil, fmt.Errorf("no certificate found for %s", hello.ServerName)
361+
}
362+
servingTLS.Certificates = maps.Values(uriDomainsToCerts)
363+
return servingTLS, nil
233364
}
234365

235366
func runProxyServer(
@@ -251,33 +382,29 @@ func runProxyServer(
251382
})
252383
go probeHandler.Start(ctx)
253384

254-
tlsCfg := tls.Config{}
385+
var tlsCfg *tls.Config
255386
if tlsEnabled {
256-
caCert, err := os.ReadFile(tlsConfig["certificatePath"])
257-
if err != nil {
258-
logger.Error(fmt.Errorf("error reading file from TLSCertPath"), "error", err)
259-
os.Exit(1)
260-
}
261-
caCertPool := x509.NewCertPool()
262-
caCertPool.AppendCertsFromPEM(caCert)
263-
cert, err := tls.LoadX509KeyPair(tlsConfig["certificatePath"], tlsConfig["keyPath"])
264-
387+
cfg, err := getTLSConfig(tlsConfig, logger)
265388
if err != nil {
266-
logger.Error(fmt.Errorf("error creating TLS configuration for proxy server"), "error", err)
389+
logger.Error(fmt.Errorf("error creating certGetter for proxy server"), "error", err)
267390
os.Exit(1)
268391
}
269-
270-
tlsCfg.RootCAs = caCertPool
271-
tlsCfg.Certificates = []tls.Certificate{cert}
392+
tlsCfg = cfg
272393
}
273394

274395
var upstreamHandler http.Handler
396+
forwardingTLSCfg := &tls.Config{}
397+
if tlsCfg != nil {
398+
forwardingTLSCfg.RootCAs = tlsCfg.RootCAs
399+
forwardingTLSCfg.Certificates = tlsCfg.Certificates
400+
}
401+
275402
upstreamHandler = newForwardingHandler(
276403
logger,
277404
dialContextFunc,
278405
waitFunc,
279406
newForwardingConfigFromTimeouts(timeouts),
280-
&tlsCfg,
407+
forwardingTLSCfg,
281408
)
282409
upstreamHandler = middleware.NewCountingMiddleware(
283410
q,
@@ -302,5 +429,8 @@ func runProxyServer(
302429

303430
addr := fmt.Sprintf("0.0.0.0:%d", port)
304431
logger.Info("proxy server starting", "address", addr)
305-
return kedahttp.ServeContext(ctx, addr, rootHandler, tlsEnabled, tlsConfig)
432+
if tlsEnabled {
433+
return kedahttp.ServeContext(ctx, addr, rootHandler, tlsCfg)
434+
}
435+
return kedahttp.ServeContext(ctx, addr, rootHandler, nil)
306436
}

0 commit comments

Comments
 (0)