Skip to content

Commit c87becc

Browse files
committed
TNZ-97934: Redact api_password and TLS private key from JSON-serialized config
Add MarshalJSON to API and SwitchboardApiTLS so that lager's JSON encoding of the config struct on validation failure (main.go:36) emits [REDACTED] instead of plaintext secrets. Certificate and all non-sensitive fields are preserved. Also adds unit tests for the redaction logic and an integration test that confirms the proxy binary does not leak the password or PEM private key in its fatal log output when startup validation fails. ai-assisted=yes [TNZ-97934](https://vmw-jira.broadcom.net/browse/TNZ-97934) Authored-by: Kim Bassett <kim.bassett@broadcom.com> Made-with: Claude Code
1 parent 87b1b9b commit c87becc

3 files changed

Lines changed: 178 additions & 0 deletions

File tree

src/github.com/cloudfoundry-incubator/switchboard/cmd/proxy/main_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,57 @@ var _ = Describe("Switchboard", func() {
356356
healthcheckWaitDuration = 3 * proxyConfig.HealthcheckTimeout()
357357
})
358358

359+
// TNZ-97934: validation failure must not leak api_password or TLS private key
360+
Context("when config validation fails", func() {
361+
It("does not log sensitive config values in the fatal error message", func() {
362+
const sensitivePassword = "SENTINEL-api-password-must-not-appear-in-logs"
363+
364+
// Build a config that will fail validation (bad GaleraAgentTLS CA)
365+
// while holding a distinctive password and a real TLS private key
366+
failConfig := config.Config{
367+
BindAddress: "127.0.0.1",
368+
Proxy: config.Proxy{
369+
Backends: backends,
370+
HealthcheckTimeoutMillis: 500,
371+
Port: proxyPort,
372+
},
373+
API: config.API{
374+
AggregatorPort: switchboardAPIAggregatorPort,
375+
Port: switchboardAPIPort,
376+
Username: "username",
377+
Password: sensitivePassword,
378+
TLS: config.SwitchboardApiTLS{
379+
Enabled: true,
380+
Certificate: string(testing.CertificatePEM(testCert.Certificate[0])),
381+
PrivateKey: string(testing.PrivateKeyPEM(testCert.PrivateKey)),
382+
},
383+
},
384+
HealthPort: switchboardHealthPort,
385+
StaticDir: staticDir,
386+
GaleraAgentTLS: config.GaleraAgentTLS{
387+
Enabled: true,
388+
CA: "not-a-valid-pem-certificate",
389+
},
390+
Metrics: config.Metrics{Port: metricsPort},
391+
}
392+
393+
configYAML, err := yaml.Marshal(failConfig)
394+
Expect(err).NotTo(HaveOccurred())
395+
396+
cmd := exec.Command(switchboardBinPath, fmt.Sprintf("-config=%s", string(configYAML)))
397+
output, _ := cmd.CombinedOutput()
398+
399+
// Sanity check: bad CA triggers Validate() → logger.Fatal → non-zero exit
400+
Expect(cmd.ProcessState.ExitCode()).NotTo(Equal(0))
401+
402+
outputStr := string(output)
403+
Expect(outputStr).NotTo(ContainSubstring(sensitivePassword),
404+
"api_password was leaked in validation-failure log output")
405+
Expect(outputStr).NotTo(ContainSubstring("PRIVATE KEY"),
406+
"TLS private key PEM was leaked in validation-failure log output")
407+
})
408+
})
409+
359410
Context("Non TLS for the API", func() {
360411

361412
When("apiConfig.TlS is provided", func() {

src/github.com/cloudfoundry-incubator/switchboard/config/config.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package config
33
import (
44
"crypto/tls"
55
"crypto/x509"
6+
"encoding/json"
67
"errors"
78
"flag"
89
"fmt"
@@ -51,6 +52,17 @@ type SwitchboardApiTLS struct {
5152
PrivateKey string `yaml:"PrivateKey"`
5253
}
5354

55+
func (t SwitchboardApiTLS) MarshalJSON() ([]byte, error) {
56+
type shadow SwitchboardApiTLS
57+
return json.Marshal(struct {
58+
shadow
59+
PrivateKey string `json:"PrivateKey"`
60+
}{
61+
shadow: shadow(t),
62+
PrivateKey: "[REDACTED]",
63+
})
64+
}
65+
5466
type Proxy struct {
5567
Port uint `yaml:"Port" validate:"nonzero"`
5668
InactiveMysqlPort uint `yaml:"InactiveMysqlPort"`
@@ -69,6 +81,17 @@ type API struct {
6981
TLS SwitchboardApiTLS `yaml:"TLS"`
7082
}
7183

84+
func (a API) MarshalJSON() ([]byte, error) {
85+
type shadow API
86+
return json.Marshal(struct {
87+
shadow
88+
Password string `json:"Password"`
89+
}{
90+
shadow: shadow(a),
91+
Password: "[REDACTED]",
92+
})
93+
}
94+
7295
type Backend struct {
7396
Host string `yaml:"Host" validate:"nonzero"`
7497
Port uint `yaml:"Port" validate:"nonzero"`

src/github.com/cloudfoundry-incubator/switchboard/config/config_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package config_test
22

33
import (
4+
"encoding/json"
45
"errors"
56
"fmt"
67
"net/http"
@@ -407,6 +408,109 @@ var _ = Describe("Config", func() {
407408
})
408409
})
409410

411+
Describe("JSON serialization (credential redaction)", func() {
412+
Describe("SwitchboardApiTLS", func() {
413+
It("redacts PrivateKey and preserves other fields", func() {
414+
tlsConfig := SwitchboardApiTLS{
415+
Enabled: true,
416+
Certificate: "-----BEGIN CERTIFICATE-----\nsome-cert\n-----END CERTIFICATE-----",
417+
PrivateKey: "-----BEGIN FAKE PRIVATE KEY-----\nsuper-secret-key\n-----END FAKE PRIVATE KEY-----",
418+
}
419+
420+
data, err := json.Marshal(tlsConfig)
421+
Expect(err).NotTo(HaveOccurred())
422+
423+
var result map[string]interface{}
424+
Expect(json.Unmarshal(data, &result)).To(Succeed())
425+
426+
Expect(result["PrivateKey"]).To(Equal("[REDACTED]"),
427+
"PrivateKey must be redacted in JSON output")
428+
Expect(result["Certificate"]).To(Equal(tlsConfig.Certificate),
429+
"Certificate (public) must be preserved in JSON output")
430+
Expect(result["Enabled"]).To(BeTrue(),
431+
"Enabled flag must be preserved in JSON output")
432+
})
433+
434+
It("redacts PrivateKey even when empty", func() {
435+
tlsConfig := SwitchboardApiTLS{Enabled: false}
436+
437+
data, err := json.Marshal(tlsConfig)
438+
Expect(err).NotTo(HaveOccurred())
439+
440+
var result map[string]interface{}
441+
Expect(json.Unmarshal(data, &result)).To(Succeed())
442+
443+
Expect(result["PrivateKey"]).To(Equal("[REDACTED]"))
444+
})
445+
})
446+
447+
Describe("API", func() {
448+
It("redacts Password and preserves other fields", func() {
449+
apiConfig := API{
450+
Port: 8080,
451+
AggregatorPort: 8081,
452+
Username: "admin",
453+
Password: "super-secret-password",
454+
ForceHttps: true,
455+
ProxyURIs: []string{"proxy1", "proxy2"},
456+
}
457+
458+
data, err := json.Marshal(apiConfig)
459+
Expect(err).NotTo(HaveOccurred())
460+
461+
var result map[string]interface{}
462+
Expect(json.Unmarshal(data, &result)).To(Succeed())
463+
464+
Expect(result["Password"]).To(Equal("[REDACTED]"),
465+
"Password must be redacted in JSON output")
466+
Expect(result["Username"]).To(Equal("admin"),
467+
"Username must be preserved in JSON output")
468+
Expect(result["Port"]).To(BeNumerically("==", 8080),
469+
"Port must be preserved in JSON output")
470+
Expect(result["ForceHttps"]).To(BeTrue(),
471+
"ForceHttps must be preserved in JSON output")
472+
})
473+
474+
It("also redacts the nested TLS PrivateKey", func() {
475+
apiConfig := API{
476+
Password: "secret-password",
477+
TLS: SwitchboardApiTLS{
478+
Enabled: true,
479+
Certificate: "some-cert",
480+
PrivateKey: "secret-private-key",
481+
},
482+
}
483+
484+
data, err := json.Marshal(apiConfig)
485+
Expect(err).NotTo(HaveOccurred())
486+
487+
var result map[string]interface{}
488+
Expect(json.Unmarshal(data, &result)).To(Succeed())
489+
490+
Expect(result["Password"]).To(Equal("[REDACTED]"))
491+
492+
tlsMap, ok := result["TLS"].(map[string]interface{})
493+
Expect(ok).To(BeTrue())
494+
Expect(tlsMap["PrivateKey"]).To(Equal("[REDACTED]"),
495+
"TLS.PrivateKey must be redacted via nested MarshalJSON")
496+
Expect(tlsMap["Certificate"]).To(Equal("some-cert"),
497+
"TLS.Certificate must be preserved")
498+
})
499+
500+
It("redacts Password even when empty", func() {
501+
apiConfig := API{}
502+
503+
data, err := json.Marshal(apiConfig)
504+
Expect(err).NotTo(HaveOccurred())
505+
506+
var result map[string]interface{}
507+
Expect(json.Unmarshal(data, &result)).To(Succeed())
508+
509+
Expect(result["Password"]).To(Equal("[REDACTED]"))
510+
})
511+
})
512+
})
513+
410514
Describe("Default values", func() {
411515
It("preserves defaults when empty config values are provided", func() {
412516
osArgs := []string{

0 commit comments

Comments
 (0)