Skip to content
Merged
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
53 changes: 52 additions & 1 deletion internal/admin/console/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -88,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 {
Expand Down Expand Up @@ -206,7 +212,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{}{
Expand All @@ -221,6 +227,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] = &copyRes
}

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 {
Comment thread
zhaohuabing marked this conversation as resolved.
redacted[i].Data = make(map[string][]byte, len(secret.Data))
for key := range secret.Data {
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] = redactedSecretValue
}
}
}

return redacted
}

// loadConfigDump loads configuration data from provider resources
func (h *Handler) loadConfigDump() ConfigDumpInfo {
configDump := ConfigDumpInfo{
Expand Down
80 changes: 80 additions & 0 deletions internal/admin/console/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package console

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
Expand All @@ -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"

Expand Down Expand Up @@ -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, 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)
// 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()),
Expand Down