From ce3f635c8681849e047a20f7869036fd72b2812c Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Fri, 9 Jan 2026 11:39:17 +0800 Subject: [PATCH 1/2] mask secrets Signed-off-by: Huabing Zhao --- internal/admin/console/api.go | 49 +++++++++++++++++- internal/admin/console/api_test.go | 80 ++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/internal/admin/console/api.go b/internal/admin/console/api.go index fa2d4edba1..61ab8de0e4 100644 --- a/internal/admin/console/api.go +++ b/internal/admin/console/api.go @@ -11,10 +11,12 @@ import ( "runtime" "time" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/envoyproxy/gateway/internal/cmd/version" + "github.com/envoyproxy/gateway/internal/gatewayapi/resource" ) // SystemInfo represents basic system information @@ -206,7 +208,7 @@ func (h *Handler) handleCompleteConfigDump(w http.ResponseWriter, _ *http.Reques } // Get the actual resources using GetResources() method - resources := h.providerResources.GetResources() + resources := redactSecretData(h.providerResources.GetResources()) // Create a structured response with the actual resource data response := map[string]interface{}{ @@ -221,6 +223,51 @@ func (h *Handler) handleCompleteConfigDump(w http.ResponseWriter, _ *http.Reques } } +func redactSecretData(resources []*resource.Resources) []*resource.Resources { + redactedResources := make([]*resource.Resources, len(resources)) + for i, res := range resources { + // Copy the resource to avoid modifying the original. + // A shallow copy is sufficient here since we only modify the secrets array + // and want to avoid unnecessary memory allocations. + copyRes := *res + copyRes.Secrets = redactSecrets(res.Secrets) + redactedResources[i] = ©Res + } + + return redactedResources +} + +func redactSecrets(secrets []*corev1.Secret) []*corev1.Secret { + redacted := make([]*corev1.Secret, len(secrets)) + for i, secret := range secrets { + objectMeta := secret.ObjectMeta + // ManagedFields and Annotations can also have sensitive information, so remove them. + objectMeta.ManagedFields = nil + objectMeta.Annotations = nil + + redacted[i] = &corev1.Secret{ + TypeMeta: secret.TypeMeta, + ObjectMeta: objectMeta, + Type: secret.Type, + Immutable: secret.Immutable, + } + if len(secret.Data) > 0 { + redacted[i].Data = make(map[string][]byte, len(secret.Data)) + for key := range secret.Data { + redacted[i].Data[key] = []byte("") + } + } + if len(secret.StringData) > 0 { + redacted[i].StringData = make(map[string]string, len(secret.StringData)) + for key := range secret.StringData { + redacted[i].StringData[key] = "" + } + } + } + + return redacted +} + // loadConfigDump loads configuration data from provider resources func (h *Handler) loadConfigDump() ConfigDumpInfo { configDump := ConfigDumpInfo{ diff --git a/internal/admin/console/api_test.go b/internal/admin/console/api_test.go index ee353c339d..5a3ffc5acf 100644 --- a/internal/admin/console/api_test.go +++ b/internal/admin/console/api_test.go @@ -6,6 +6,7 @@ package console import ( + "context" "encoding/json" "net/http" "net/http/httptest" @@ -15,6 +16,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/telepresenceio/watchable" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -360,6 +362,84 @@ func TestHandleAPIConfigDumpWithResourceAll(t *testing.T) { assert.Equal(t, float64(0), totalCount) } +func TestHandleAPIConfigDumpWithResourceAllRedactsSecrets(t *testing.T) { + cfg := &config.Server{ + Logger: logging.NewLogger(os.Stdout, egv1a1.DefaultEnvoyGatewayLogging()), + } + + providerRes := &message.ProviderResources{} + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "default", + Annotations: map[string]string{ + corev1.LastAppliedConfigAnnotation: "{\"data\":{\"token\":\"c3VwZXJzZWNyZXQ=\"}}", + "example.com/foo": "value", + }, + ManagedFields: []metav1.ManagedFieldsEntry{ + { + Manager: "kubectl", + Operation: metav1.ManagedFieldsOperationApply, + }, + }, + }, + Data: map[string][]byte{ + "token": []byte("supersecret"), + }, + StringData: map[string]string{ + "token": "supersecret", + }, + } + controllerResources := resource.ControllerResources{ + &resource.Resources{ + Secrets: []*corev1.Secret{secret}, + }, + } + providerRes.GatewayAPIResources.Store("test", &resource.ControllerResourcesContext{ + Resources: &controllerResources, + Context: context.Background(), + }) + + handler := NewHandler(cfg, providerRes) + + req := httptest.NewRequest(http.MethodGet, "/api/config_dump?resource=all", nil) + resp := httptest.NewRecorder() + + handler.handleAPIConfigDump(resp, req) + + assert.Equal(t, http.StatusOK, resp.Code) + + var result struct { + Resources []*resource.Resources `json:"resources"` + } + resultErr := json.Unmarshal(resp.Body.Bytes(), &result) + require.NoError(t, resultErr) + + require.Len(t, result.Resources, 1) + require.Len(t, result.Resources[0].Secrets, 1) + masked := result.Resources[0].Secrets[0] + // Ensure the masked secret is redacted + assert.Contains(t, masked.Data, "token") + assert.Equal(t, []byte{}, masked.Data["token"]) + assert.Empty(t, masked.StringData["token"]) + assert.Equal(t, "test-secret", masked.Name) + assert.Empty(t, masked.Annotations) + assert.Empty(t, masked.ManagedFields) + // Ensure the original secret is not modified + assert.Equal(t, []byte("supersecret"), secret.Data["token"]) + assert.Equal(t, "supersecret", secret.StringData["token"]) + assert.Equal(t, map[string]string{ + corev1.LastAppliedConfigAnnotation: "{\"data\":{\"token\":\"c3VwZXJzZWNyZXQ=\"}}", + "example.com/foo": "value", + }, secret.Annotations) + assert.Equal(t, []metav1.ManagedFieldsEntry{ + { + Manager: "kubectl", + Operation: metav1.ManagedFieldsOperationApply, + }, + }, secret.ManagedFields) +} + func TestHandleAPIConfigDumpWithResourceAllMethodNotAllowed(t *testing.T) { cfg := &config.Server{ Logger: logging.NewLogger(os.Stdout, egv1a1.DefaultEnvoyGatewayLogging()), From 8a6d425e4a57e3d584143972ce51332875114500 Mon Sep 17 00:00:00 2001 From: "Huabing (Robin) Zhao" Date: Thu, 15 Jan 2026 14:21:17 +0800 Subject: [PATCH 2/2] address comments Signed-off-by: Huabing (Robin) Zhao --- internal/admin/console/api.go | 8 ++++++-- internal/admin/console/api_test.go | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/admin/console/api.go b/internal/admin/console/api.go index 61ab8de0e4..2bf309e0a6 100644 --- a/internal/admin/console/api.go +++ b/internal/admin/console/api.go @@ -90,6 +90,10 @@ type ResourceSummary struct { var startTime = time.Now() +const redactedSecretValue = "[redacted]" + +var redactedSecretValueBytes = []byte(redactedSecretValue) + // handleAPIInfo returns basic system information func (h *Handler) handleAPIInfo(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { @@ -254,13 +258,13 @@ func redactSecrets(secrets []*corev1.Secret) []*corev1.Secret { if len(secret.Data) > 0 { redacted[i].Data = make(map[string][]byte, len(secret.Data)) for key := range secret.Data { - redacted[i].Data[key] = []byte("") + redacted[i].Data[key] = redactedSecretValueBytes } } if len(secret.StringData) > 0 { redacted[i].StringData = make(map[string]string, len(secret.StringData)) for key := range secret.StringData { - redacted[i].StringData[key] = "" + redacted[i].StringData[key] = redactedSecretValue } } } diff --git a/internal/admin/console/api_test.go b/internal/admin/console/api_test.go index 5a3ffc5acf..e43a599d57 100644 --- a/internal/admin/console/api_test.go +++ b/internal/admin/console/api_test.go @@ -420,8 +420,8 @@ func TestHandleAPIConfigDumpWithResourceAllRedactsSecrets(t *testing.T) { masked := result.Resources[0].Secrets[0] // Ensure the masked secret is redacted assert.Contains(t, masked.Data, "token") - assert.Equal(t, []byte{}, masked.Data["token"]) - assert.Empty(t, masked.StringData["token"]) + assert.Equal(t, redactedSecretValueBytes, masked.Data["token"]) + assert.Equal(t, redactedSecretValue, masked.StringData["token"]) assert.Equal(t, "test-secret", masked.Name) assert.Empty(t, masked.Annotations) assert.Empty(t, masked.ManagedFields)