Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ This changelog keeps track of work items that have been completed and are ready

### Improvements

- **General**: TODO ([#TODO](https://github.com/kedacore/http-add-on/issues/TODO))
- **Interceptor**: Document TLS SNI certificate selection and fallback behavior; add tests covering SNI-specific certificate preference over the default certificate ([#1600](https://github.com/kedacore/http-add-on/issues/1600))

### Fixes

Expand Down
10 changes: 10 additions & 0 deletions docs/developing.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,13 @@ make e2e-test E2E_ARGS="--dry-run"
The `PROFILE` variable selects a test profile directory under `test/e2e/` (e.g. `PROFILE=tls` runs `./test/e2e/tls/...`). Each subdirectory in `test/e2e/` is a profile.
The `RUN` variable filters tests by name using Go's `-run` flag (supports regex, e.g. `RUN=TestColdStart` or `RUN="TestHost|TestPath"`).
The `E2E_ARGS` variable passes flags to the [e2e-framework](https://github.com/kubernetes-sigs/e2e-framework) via `-args` (e.g. `--labels`, `--feature`, `--skip-labels`, `--dry-run`).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We moved the docs a few hours before you opened the PR here, could you move it there? https://keda.sh/http-add-on/0.14/operations/configure-tls/ (See the Suggest changes button)

### TLS SNI behavior

The interceptor can serve more than one certificate from the TLS listener by setting `KEDA_HTTP_PROXY_TLS_CERT_STORE_PATHS` to a comma-separated list of one or more directories that contain certificate/key pairs. During the TLS handshake it:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably include more details about the exact file names we expect/support.


1. Looks for an exact match between the client SNI value and a certificate SAN loaded from the configured certificate-store directories.
2. Falls back to the certificate from `KEDA_HTTP_PROXY_TLS_CERT_PATH` and `KEDA_HTTP_PROXY_TLS_KEY_PATH` when no SNI-specific certificate matches.
3. Fails the handshake when there is no matching SNI certificate and no default certificate is configured.

The existing `test/e2e/tls` profile covers successful TLS termination. The interceptor unit tests in `interceptor/tls_config_test.go` additionally cover the no-match fallback and no-default error paths.
5 changes: 4 additions & 1 deletion interceptor/config/serving.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ type Serving struct {
TLSCertPath string `env:"KEDA_HTTP_PROXY_TLS_CERT_PATH" envDefault:"/certs/tls.crt"`
// TLSKeyPath is the path to read the private key file from for the TLS server
TLSKeyPath string `env:"KEDA_HTTP_PROXY_TLS_KEY_PATH" envDefault:"/certs/tls.key"`
// TLSCertStorePaths is a comma separated list of paths to read the certificate/key pairs for the TLS server
// TLSCertStorePaths is a comma-separated list of directories containing additional
// certificate/key pairs for the TLS server. During the TLS handshake, the proxy
// first looks for an exact SNI/SAN match in these directories and falls back to
// TLSCertPath/TLSKeyPath when no matching certificate is found.
TLSCertStorePaths string `env:"KEDA_HTTP_PROXY_TLS_CERT_STORE_PATHS" envDefault:""`
// TLSSkipVerify is a boolean flag to specify whether the interceptor should skip TLS verification for upstreams
TLSSkipVerify bool `env:"KEDA_HTTP_PROXY_TLS_SKIP_VERIFY" envDefault:"false"`
Expand Down
32 changes: 32 additions & 0 deletions interceptor/tls_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,38 @@ func TestBuildTLSConfig_FallbackToDefault(t *testing.T) {
requireCertForHost(t, tlsCfg, "unknown.example.com")
}

func TestBuildTLSConfig_PrefersSNIMatchOverDefault(t *testing.T) {
baseDir := t.TempDir()
storeDir := filepath.Join(baseDir, "store")
if err := os.Mkdir(storeDir, 0o755); err != nil {
t.Fatalf("creating cert store dir: %v", err)
}
writeCert(t, baseDir, "default", "default.example.com")
writeCert(t, storeDir, "app", "app.example.com")

opts := TLSOptions{
CertificatePath: filepath.Join(baseDir, "default.crt"),
KeyPath: filepath.Join(baseDir, "default.key"),
CertStorePaths: storeDir,
}

tlsCfg, err := BuildTLSConfig(opts, logr.Discard())
if err != nil {
t.Fatalf("failed to build TLS config: %v", err)
}

cert, err := tlsCfg.GetCertificate(&tls.ClientHelloInfo{ServerName: "app.example.com"})
if err != nil {
t.Fatalf("expected SNI-matched certificate, got error: %v", err)
}
if cert == nil || cert.Leaf == nil {
t.Fatal("expected certificate leaf to be populated")
}
if got := cert.Leaf.DNSNames; len(got) != 1 || got[0] != "app.example.com" {
t.Fatalf("expected app.example.com certificate, got %v", got)
}
}

func TestBuildTLSConfig_NoDefaultCert(t *testing.T) {
opts := TLSOptions{}

Expand Down
Loading