From a3c4ad2ca3159318bf2f9a1fbd829c7f97a8cf0b Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 9 Apr 2026 17:54:46 +0000 Subject: [PATCH] types: omit secret fields from JSON marshalling Add json:"-" to PostgresConfig.Pass, OIDCConfig.ClientSecret, and CLIConfig.APIKey so they are excluded from json.Marshal output (e.g. the /debug/config endpoint). --- hscontrol/types/config.go | 6 +++--- hscontrol/types/config_test.go | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index bf50d308..73d5a4dd 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -148,7 +148,7 @@ type PostgresConfig struct { Port int Name string User string - Pass string + Pass string `json:"-"` // never serialise the database password Ssl string MaxOpenConnections int MaxIdleConnections int @@ -198,7 +198,7 @@ type OIDCConfig struct { OnlyStartIfOIDCIsAvailable bool Issuer string ClientID string - ClientSecret string + ClientSecret string `json:"-"` // never serialise the OIDC client secret Scope []string ExtraParams map[string]string AllowedDomains []string @@ -237,7 +237,7 @@ type TaildropConfig struct { type CLIConfig struct { Address string - APIKey string + APIKey string `json:"-"` // never serialise the headscale admin API key Timeout time.Duration Insecure bool } diff --git a/hscontrol/types/config_test.go b/hscontrol/types/config_test.go index b281cb9d..d3b23a2d 100644 --- a/hscontrol/types/config_test.go +++ b/hscontrol/types/config_test.go @@ -1,6 +1,7 @@ package types import ( + "encoding/json" "fmt" "os" "path/filepath" @@ -472,3 +473,40 @@ func TestSafeServerURL(t *testing.T) { }) } } + +// TestConfigJSONOmitsSecrets verifies that marshalling a Config to JSON +// (as /debug/config does via state.DebugConfig) does not leak the +// Postgres password, the OIDC client secret, or the headscale admin +// API key. Operators who widen metrics_listen_addr to 0.0.0.0 should +// not be able to read these back via debug endpoints reachable over +// CGNAT/loopback. +func TestConfigJSONOmitsSecrets(t *testing.T) { + const ( + secretPostgresPass = "p0stgres-secret-marker" + secretClientSecret = "oidc-client-secret-marker" //nolint:gosec // test marker, not a real credential + secretAPIKey = "headscale-cli-api-key-marker" //nolint:gosec // test marker, not a real credential + ) + + cfg := &Config{ + Database: DatabaseConfig{ + Postgres: PostgresConfig{ + Pass: secretPostgresPass, + }, + }, + OIDC: OIDCConfig{ + ClientSecret: secretClientSecret, + }, + CLI: CLIConfig{ + APIKey: secretAPIKey, + }, + } + + out, err := json.Marshal(cfg) + require.NoError(t, err) + + body := string(out) + for _, secret := range []string{secretPostgresPass, secretClientSecret, secretAPIKey} { + assert.NotContains(t, body, secret, + "marshalled Config must not contain secret %q", secret) + } +}