Cilium leaks sensitive information in cilium-bugtool
Description
Cilium is a networking, observability, and security solution with an eBPF-based dataplane. Starting in version 1.13.0 and prior to versions 1.13.7, 1.14.12, and 1.15.6, the output of cilium-bugtool can contain sensitive data when the tool is run (with the --envoy-dump flag set) against Cilium deployments with the Envoy proxy enabled. Users of the TLS inspection, Ingress with TLS termination, Gateway API with TLS termination, and Kafka network policies with API key filtering features are affected. The sensitive data includes the CA certificate, certificate chain, and private key used by Cilium HTTP Network Policies, and when using Ingress/Gateway API and the API keys used in Kafka-related network policy. cilium-bugtool is a debugging tool that is typically invoked manually and does not run during the normal operation of a Cilium cluster. This issue has been patched in Cilium v1.15.6, v1.14.12, and v1.13.17. There is no workaround to this issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/cilium/ciliumGo | >= 1.13.0, < 1.13.17 | 1.13.17 |
github.com/cilium/ciliumGo | >= 1.14.0, < 1.14.12 | 1.14.12 |
github.com/cilium/ciliumGo | >= 1.15.0, < 1.15.6 | 1.15.6 |
Affected products
1Patches
6958d7b77274bbugtool: Add json masking function
3 files changed · +258 −0
bugtool/cmd/mask.go+64 −0 added@@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package cmd + +import ( + "encoding/json" +) + +const ( + redacted = "[redacted]" + ident = "\t" +) + +// jsonFieldMaskPostProcess returns a postProcessFunc that masks the specified field names. +// The input byte slice is expected to be a JSON object. +func jsonFieldMaskPostProcess(fieldNames []string) postProcessFunc { + return func(b []byte) ([]byte, error) { + return maskFields(b, fieldNames) + } +} + +func maskFields(b []byte, fieldNames []string) ([]byte, error) { + var data map[string]interface{} + + if err := json.Unmarshal(b, &data); err != nil { + return nil, err + } + + mask(data, fieldNames) + + // MarshalIndent is used to make the output more readable. + return json.MarshalIndent(data, "", ident) +} + +func contains(names []string, name string) bool { + for _, n := range names { + if n == name { + return true + } + } + return false +} + +func mask(data map[string]interface{}, fieldNames []string) { + for k, v := range data { + if contains(fieldNames, k) { + data[k] = redacted + continue + } + + switch t := v.(type) { + case map[string]interface{}: + mask(t, fieldNames) + case []interface{}: + for i, item := range t { + if subData, ok := item.(map[string]interface{}); ok { + mask(subData, fieldNames) + t[i] = subData + } + } + } + } +}
bugtool/cmd/mask_test.go+192 −0 added@@ -0,0 +1,192 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_jsonFieldMaskPostProcess(t *testing.T) { + type args struct { + input []byte + fieldNames []string + } + tests := []struct { + name string + args args + want []byte + wantErr bool + }{ + { + name: "simple struct", + args: args{ + input: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com" + }`), + fieldNames: []string{"password"}, + }, + want: []byte(`{ + "username": "user1", + "password": "[redacted]", + "email": "user1@example.com" + }`), + }, + { + name: "array struct", + args: args{ + input: []byte(`{ + "username": "user1", + "secrets": [ + { + "password": "mypassword" + }, + { + "password": "anotherone" + } + ], + "password": "mypassword", + "email": "user1@example.com" + }`), + fieldNames: []string{"password"}, + }, + want: []byte(`{ + "username": "user1", + "secrets": [ + { + "password": "[redacted]" + }, + { + "password": "[redacted]" + } + ], + "password": "[redacted]", + "email": "user1@example.com" + }`), + }, + { + name: "nested struct", + args: args{ + input: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com", + "complex": { + "password": "anotherpassword" + } + }`), + fieldNames: []string{"password"}, + }, + want: []byte(`{ + "username": "user1", + "password": "[redacted]", + "email": "user1@example.com", + "complex": { + "password": "[redacted]" + } + }`), + }, + { + name: "nested array struct", + args: args{ + input: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com", + "complex": { + "secrets": [ + { + "password": "mypassword" + }, + { + "password": "anotherpassword" + } + ] + } + }`), + fieldNames: []string{"password"}, + }, + want: []byte(`{ + "username": "user1", + "password": "[redacted]", + "email": "user1@example.com", + "complex": { + "secrets": [ + { + "password": "[redacted]" + }, + { + "password": "[redacted]" + } + ] + } + }`), + }, + { + name: "no masked field", + args: args{ + input: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com", + "complex": { + "password": "anotherpassword" + } + }`), + fieldNames: []string{"no-such-field"}, + }, + want: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com", + "complex": { + "password": "anotherpassword" + } + }`), + }, + { + name: "mask object field", + args: args{ + input: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com", + "complex": { + "password": "anotherpassword" + } + }`), + fieldNames: []string{"complex"}, + }, + want: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com", + "complex": "[redacted]" + }`), + }, + { + name: "invalid input", + args: args{ + input: []byte(`{"username": "user1",}`), + fieldNames: []string{"password"}, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := jsonFieldMaskPostProcess(tt.args.fieldNames)(tt.args.input) + require.Equal(t, tt.wantErr, err != nil) + // only assert the output if there is no error + // as JSONEq func is used to compare the output + if !tt.wantErr { + require.JSONEq(t, string(tt.want), string(got)) + } + }) + } +}
bugtool/cmd/root.go+2 −0 modified@@ -172,6 +172,8 @@ func isValidArchiveType(archiveType string) bool { return false } +type postProcessFunc func(output []byte) ([]byte, error) + func runTool() { // Validate archive type if !isValidArchiveType(archiveType) {
224e288a5bf4bugtool: Add json masking function
3 files changed · +250 −0
bugtool/cmd/mask.go+56 −0 added@@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package cmd + +import ( + "encoding/json" + "slices" +) + +const ( + redacted = "[redacted]" + ident = "\t" +) + +// jsonFieldMaskPostProcess returns a postProcessFunc that masks the specified field names. +// The input byte slice is expected to be a JSON object. +func jsonFieldMaskPostProcess(fieldNames []string) postProcessFunc { + return func(b []byte) ([]byte, error) { + return maskFields(b, fieldNames) + } +} + +func maskFields(b []byte, fieldNames []string) ([]byte, error) { + var data map[string]interface{} + + if err := json.Unmarshal(b, &data); err != nil { + return nil, err + } + + mask(data, fieldNames) + + // MarshalIndent is used to make the output more readable. + return json.MarshalIndent(data, "", ident) +} + +func mask(data map[string]interface{}, fieldNames []string) { + for k, v := range data { + if slices.Contains(fieldNames, k) { + data[k] = redacted + continue + } + + switch t := v.(type) { + case map[string]interface{}: + mask(t, fieldNames) + case []interface{}: + for i, item := range t { + if subData, ok := item.(map[string]interface{}); ok { + mask(subData, fieldNames) + t[i] = subData + } + } + } + } +}
bugtool/cmd/mask_test.go+192 −0 added@@ -0,0 +1,192 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_jsonFieldMaskPostProcess(t *testing.T) { + type args struct { + input []byte + fieldNames []string + } + tests := []struct { + name string + args args + want []byte + wantErr bool + }{ + { + name: "simple struct", + args: args{ + input: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com" + }`), + fieldNames: []string{"password"}, + }, + want: []byte(`{ + "username": "user1", + "password": "[redacted]", + "email": "user1@example.com" + }`), + }, + { + name: "array struct", + args: args{ + input: []byte(`{ + "username": "user1", + "secrets": [ + { + "password": "mypassword" + }, + { + "password": "anotherone" + } + ], + "password": "mypassword", + "email": "user1@example.com" + }`), + fieldNames: []string{"password"}, + }, + want: []byte(`{ + "username": "user1", + "secrets": [ + { + "password": "[redacted]" + }, + { + "password": "[redacted]" + } + ], + "password": "[redacted]", + "email": "user1@example.com" + }`), + }, + { + name: "nested struct", + args: args{ + input: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com", + "complex": { + "password": "anotherpassword" + } + }`), + fieldNames: []string{"password"}, + }, + want: []byte(`{ + "username": "user1", + "password": "[redacted]", + "email": "user1@example.com", + "complex": { + "password": "[redacted]" + } + }`), + }, + { + name: "nested array struct", + args: args{ + input: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com", + "complex": { + "secrets": [ + { + "password": "mypassword" + }, + { + "password": "anotherpassword" + } + ] + } + }`), + fieldNames: []string{"password"}, + }, + want: []byte(`{ + "username": "user1", + "password": "[redacted]", + "email": "user1@example.com", + "complex": { + "secrets": [ + { + "password": "[redacted]" + }, + { + "password": "[redacted]" + } + ] + } + }`), + }, + { + name: "no masked field", + args: args{ + input: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com", + "complex": { + "password": "anotherpassword" + } + }`), + fieldNames: []string{"no-such-field"}, + }, + want: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com", + "complex": { + "password": "anotherpassword" + } + }`), + }, + { + name: "mask object field", + args: args{ + input: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com", + "complex": { + "password": "anotherpassword" + } + }`), + fieldNames: []string{"complex"}, + }, + want: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com", + "complex": "[redacted]" + }`), + }, + { + name: "invalid input", + args: args{ + input: []byte(`{"username": "user1",}`), + fieldNames: []string{"password"}, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := jsonFieldMaskPostProcess(tt.args.fieldNames)(tt.args.input) + require.Equal(t, tt.wantErr, err != nil) + // only assert the output if there is no error + // as JSONEq func is used to compare the output + if !tt.wantErr { + require.JSONEq(t, string(tt.want), string(got)) + } + }) + } +}
bugtool/cmd/root.go+2 −0 modified@@ -168,6 +168,8 @@ func isValidArchiveType(archiveType string) bool { return false } +type postProcessFunc func(output []byte) ([]byte, error) + func runTool() { // Validate archive type if !isValidArchiveType(archiveType) {
0191b1ebcfddbugtool: Add json masking function
3 files changed · +250 −0
bugtool/cmd/mask.go+56 −0 added@@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package cmd + +import ( + "encoding/json" + "slices" +) + +const ( + redacted = "[redacted]" + ident = "\t" +) + +// jsonFieldMaskPostProcess returns a postProcessFunc that masks the specified field names. +// The input byte slice is expected to be a JSON object. +func jsonFieldMaskPostProcess(fieldNames []string) postProcessFunc { + return func(b []byte) ([]byte, error) { + return maskFields(b, fieldNames) + } +} + +func maskFields(b []byte, fieldNames []string) ([]byte, error) { + var data map[string]interface{} + + if err := json.Unmarshal(b, &data); err != nil { + return nil, err + } + + mask(data, fieldNames) + + // MarshalIndent is used to make the output more readable. + return json.MarshalIndent(data, "", ident) +} + +func mask(data map[string]interface{}, fieldNames []string) { + for k, v := range data { + if slices.Contains(fieldNames, k) { + data[k] = redacted + continue + } + + switch t := v.(type) { + case map[string]interface{}: + mask(t, fieldNames) + case []interface{}: + for i, item := range t { + if subData, ok := item.(map[string]interface{}); ok { + mask(subData, fieldNames) + t[i] = subData + } + } + } + } +}
bugtool/cmd/mask_test.go+192 −0 added@@ -0,0 +1,192 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_jsonFieldMaskPostProcess(t *testing.T) { + type args struct { + input []byte + fieldNames []string + } + tests := []struct { + name string + args args + want []byte + wantErr bool + }{ + { + name: "simple struct", + args: args{ + input: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com" + }`), + fieldNames: []string{"password"}, + }, + want: []byte(`{ + "username": "user1", + "password": "[redacted]", + "email": "user1@example.com" + }`), + }, + { + name: "array struct", + args: args{ + input: []byte(`{ + "username": "user1", + "secrets": [ + { + "password": "mypassword" + }, + { + "password": "anotherone" + } + ], + "password": "mypassword", + "email": "user1@example.com" + }`), + fieldNames: []string{"password"}, + }, + want: []byte(`{ + "username": "user1", + "secrets": [ + { + "password": "[redacted]" + }, + { + "password": "[redacted]" + } + ], + "password": "[redacted]", + "email": "user1@example.com" + }`), + }, + { + name: "nested struct", + args: args{ + input: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com", + "complex": { + "password": "anotherpassword" + } + }`), + fieldNames: []string{"password"}, + }, + want: []byte(`{ + "username": "user1", + "password": "[redacted]", + "email": "user1@example.com", + "complex": { + "password": "[redacted]" + } + }`), + }, + { + name: "nested array struct", + args: args{ + input: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com", + "complex": { + "secrets": [ + { + "password": "mypassword" + }, + { + "password": "anotherpassword" + } + ] + } + }`), + fieldNames: []string{"password"}, + }, + want: []byte(`{ + "username": "user1", + "password": "[redacted]", + "email": "user1@example.com", + "complex": { + "secrets": [ + { + "password": "[redacted]" + }, + { + "password": "[redacted]" + } + ] + } + }`), + }, + { + name: "no masked field", + args: args{ + input: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com", + "complex": { + "password": "anotherpassword" + } + }`), + fieldNames: []string{"no-such-field"}, + }, + want: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com", + "complex": { + "password": "anotherpassword" + } + }`), + }, + { + name: "mask object field", + args: args{ + input: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com", + "complex": { + "password": "anotherpassword" + } + }`), + fieldNames: []string{"complex"}, + }, + want: []byte(`{ + "username": "user1", + "password": "mypassword", + "email": "user1@example.com", + "complex": "[redacted]" + }`), + }, + { + name: "invalid input", + args: args{ + input: []byte(`{"username": "user1",}`), + fieldNames: []string{"password"}, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := jsonFieldMaskPostProcess(tt.args.fieldNames)(tt.args.input) + require.Equal(t, tt.wantErr, err != nil) + // only assert the output if there is no error + // as JSONEq func is used to compare the output + if !tt.wantErr { + require.JSONEq(t, string(tt.want), string(got)) + } + }) + } +}
bugtool/cmd/root.go+2 −0 modified@@ -168,6 +168,8 @@ func isValidArchiveType(archiveType string) bool { return false } +type postProcessFunc func(output []byte) ([]byte, error) + func runTool() { // Validate archive type if !isValidArchiveType(archiveType) {
9eb25ba40391bugtool: Add post-processing masking function for Envoy
1 file changed · +46 −4
bugtool/cmd/root.go+46 −4 modified@@ -27,6 +27,7 @@ import ( "github.com/cilium/cilium/pkg/components" "github.com/cilium/cilium/pkg/defaults" "github.com/cilium/cilium/pkg/option" + "github.com/cilium/cilium/pkg/safeio" ) // BugtoolRootCmd is the top level command for the bugtool. @@ -170,6 +171,18 @@ func isValidArchiveType(archiveType string) bool { type postProcessFunc func(output []byte) ([]byte, error) +var envoySecretMask = jsonFieldMaskPostProcess([]string{ + // Cilium LogEntry -> KafkaLogEntry{l7} -> KafkaLogEntry{api_key} + "api_key", + // This could be from one of the following: + // - Cilium NetworkPolicy -> PortNetworkPolicy{ingress_per_port_policies, egress_per_port_policies} + // -> PortNetworkPolicyRule{rules} -> TLSContext{downstream_tls_context, upstream_tls_context} + // - Upstream Envoy tls_certificate + "trusted_ca", + "certificate_chain", + "private_key", +}) + func runTool() { // Validate archive type if !isValidArchiveType(archiveType) { @@ -216,13 +229,13 @@ func runTool() { } } else { if envoyDump { - if err := dumpEnvoy(cmdDir, "http://admin/config_dump?include_eds", "envoy-config.json"); err != nil { + if err := dumpEnvoy(cmdDir, "http://admin/config_dump?include_eds", "envoy-config.json", envoySecretMask); err != nil { fmt.Fprintf(os.Stderr, "Unable to dump envoy config: %s\n", err) } } if envoyMetrics { - if err := dumpEnvoy(cmdDir, "http://admin/stats/prometheus", "envoy-metrics.txt"); err != nil { + if err := dumpEnvoy(cmdDir, "http://admin/stats/prometheus", "envoy-metrics.txt", nil); err != nil { fmt.Fprintf(os.Stderr, "Unable to retrieve envoy prometheus metrics: %s\n", err) } } @@ -516,7 +529,7 @@ func dumpHubbleMetrics(rootDir string) error { return downloadToFile(httpClient, url, filepath.Join(rootDir, "hubble-metrics.txt")) } -func dumpEnvoy(rootDir string, resource string, fileName string) error { +func dumpEnvoy(rootDir string, resource string, fileName string, postProcess postProcessFunc) error { // curl --unix-socket /var/run/cilium/envoy/sockets/admin.sock http:/admin/config_dump\?include_eds > dump.json c := &http.Client{ Transport: &http.Transport{ @@ -525,7 +538,11 @@ func dumpEnvoy(rootDir string, resource string, fileName string) error { }, }, } - return downloadToFile(c, resource, filepath.Join(rootDir, fileName)) + + if postProcess == nil { + return downloadToFile(c, resource, filepath.Join(rootDir, fileName)) + } + return downloadToFileWithPostProcess(c, resource, filepath.Join(rootDir, fileName), postProcess) } func pprofTraces(rootDir string, pprofDebug int) error { @@ -589,3 +606,28 @@ func downloadToFile(client *http.Client, url, file string) error { _, err = io.Copy(out, resp.Body) return err } + +// downloadToFileWithPostProcess downloads the content from the given URL and writes it to the given file. +// The content is then post-processed using the given postProcess function before being written to the file. +// Note: Please use downloadToFile instead of this function if no post-processing is required. +func downloadToFileWithPostProcess(client *http.Client, url, file string, postProcess postProcessFunc) error { + resp, err := client.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("bad status: %s", resp.Status) + } + + b, err := safeio.ReadAllLimit(resp.Body, safeio.MB) + if err != nil { + return err + } + + b, err = postProcess(b) + if err != nil { + return err + } + return os.WriteFile(file, b, 0644) +}
9299c0fd0024bugtool: Add post-processing masking function for Envoy
1 file changed · +46 −4
bugtool/cmd/root.go+46 −4 modified@@ -27,6 +27,7 @@ import ( "github.com/cilium/cilium/pkg/components" "github.com/cilium/cilium/pkg/defaults" "github.com/cilium/cilium/pkg/option" + "github.com/cilium/cilium/pkg/safeio" ) // BugtoolRootCmd is the top level command for the bugtool. @@ -170,6 +171,18 @@ func isValidArchiveType(archiveType string) bool { type postProcessFunc func(output []byte) ([]byte, error) +var envoySecretMask = jsonFieldMaskPostProcess([]string{ + // Cilium LogEntry -> KafkaLogEntry{l7} -> KafkaLogEntry{api_key} + "api_key", + // This could be from one of the following: + // - Cilium NetworkPolicy -> PortNetworkPolicy{ingress_per_port_policies, egress_per_port_policies} + // -> PortNetworkPolicyRule{rules} -> TLSContext{downstream_tls_context, upstream_tls_context} + // - Upstream Envoy tls_certificate + "trusted_ca", + "certificate_chain", + "private_key", +}) + func runTool() { // Validate archive type if !isValidArchiveType(archiveType) { @@ -216,13 +229,13 @@ func runTool() { } } else { if envoyDump { - if err := dumpEnvoy(cmdDir, "http://admin/config_dump?include_eds", "envoy-config.json"); err != nil { + if err := dumpEnvoy(cmdDir, "http://admin/config_dump?include_eds", "envoy-config.json", envoySecretMask); err != nil { fmt.Fprintf(os.Stderr, "Unable to dump envoy config: %s\n", err) } } if envoyMetrics { - if err := dumpEnvoy(cmdDir, "http://admin/stats/prometheus", "envoy-metrics.txt"); err != nil { + if err := dumpEnvoy(cmdDir, "http://admin/stats/prometheus", "envoy-metrics.txt", nil); err != nil { fmt.Fprintf(os.Stderr, "Unable to retrieve envoy prometheus metrics: %s\n", err) } } @@ -516,7 +529,7 @@ func dumpHubbleMetrics(rootDir string) error { return downloadToFile(httpClient, url, filepath.Join(rootDir, "hubble-metrics.txt")) } -func dumpEnvoy(rootDir string, resource string, fileName string) error { +func dumpEnvoy(rootDir string, resource string, fileName string, postProcess postProcessFunc) error { // curl --unix-socket /var/run/cilium/envoy/sockets/admin.sock http:/admin/config_dump\?include_eds > dump.json c := &http.Client{ Transport: &http.Transport{ @@ -525,7 +538,11 @@ func dumpEnvoy(rootDir string, resource string, fileName string) error { }, }, } - return downloadToFile(c, resource, filepath.Join(rootDir, fileName)) + + if postProcess == nil { + return downloadToFile(c, resource, filepath.Join(rootDir, fileName)) + } + return downloadToFileWithPostProcess(c, resource, filepath.Join(rootDir, fileName), postProcess) } func pprofTraces(rootDir string, pprofDebug int) error { @@ -589,3 +606,28 @@ func downloadToFile(client *http.Client, url, file string) error { _, err = io.Copy(out, resp.Body) return err } + +// downloadToFileWithPostProcess downloads the content from the given URL and writes it to the given file. +// The content is then post-processed using the given postProcess function before being written to the file. +// Note: Please use downloadToFile instead of this function if no post-processing is required. +func downloadToFileWithPostProcess(client *http.Client, url, file string, postProcess postProcessFunc) error { + resp, err := client.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("bad status: %s", resp.Status) + } + + b, err := safeio.ReadAllLimit(resp.Body, safeio.MB) + if err != nil { + return err + } + + b, err = postProcess(b) + if err != nil { + return err + } + return os.WriteFile(file, b, 0644) +}
bf9a1ae1b2d2bugtool: Add post-processing masking function for Envoy
1 file changed · +45 −4
bugtool/cmd/root.go+45 −4 modified@@ -174,6 +174,18 @@ func isValidArchiveType(archiveType string) bool { type postProcessFunc func(output []byte) ([]byte, error) +var envoySecretMask = jsonFieldMaskPostProcess([]string{ + // Cilium LogEntry -> KafkaLogEntry{l7} -> KafkaLogEntry{api_key} + "api_key", + // This could be from one of the following: + // - Cilium NetworkPolicy -> PortNetworkPolicy{ingress_per_port_policies, egress_per_port_policies} + // -> PortNetworkPolicyRule{rules} -> TLSContext{downstream_tls_context, upstream_tls_context} + // - Upstream Envoy tls_certificate + "trusted_ca", + "certificate_chain", + "private_key", +}) + func runTool() { // Validate archive type if !isValidArchiveType(archiveType) { @@ -220,13 +232,13 @@ func runTool() { } } else { if envoyDump { - if err := dumpEnvoy(cmdDir, "http://admin/config_dump?include_eds", "envoy-config.json"); err != nil { + if err := dumpEnvoy(cmdDir, "http://admin/config_dump?include_eds", "envoy-config.json", envoySecretMask); err != nil { fmt.Fprintf(os.Stderr, "Unable to dump envoy config: %s\n", err) } } if envoyMetrics { - if err := dumpEnvoy(cmdDir, "http://admin/stats/prometheus", "envoy-metrics.txt"); err != nil { + if err := dumpEnvoy(cmdDir, "http://admin/stats/prometheus", "envoy-metrics.txt", nil); err != nil { fmt.Fprintf(os.Stderr, "Unable to retrieve envoy prometheus metrics: %s\n", err) } } @@ -520,7 +532,7 @@ func dumpHubbleMetrics(rootDir string) error { return downloadToFile(httpClient, url, filepath.Join(rootDir, "hubble-metrics.txt")) } -func dumpEnvoy(rootDir string, resource string, fileName string) error { +func dumpEnvoy(rootDir string, resource string, fileName string, postProcess postProcessFunc) error { // curl --unix-socket /var/run/cilium/envoy-admin.sock http:/admin/config_dump\?include_eds > dump.json c := &http.Client{ Transport: &http.Transport{ @@ -529,7 +541,11 @@ func dumpEnvoy(rootDir string, resource string, fileName string) error { }, }, } - return downloadToFile(c, resource, filepath.Join(rootDir, fileName)) + + if postProcess == nil { + return downloadToFile(c, resource, filepath.Join(rootDir, fileName)) + } + return downloadToFileWithPostProcess(c, resource, filepath.Join(rootDir, fileName), postProcess) } func pprofTraces(rootDir string, pprofDebug int) error { @@ -593,3 +609,28 @@ func downloadToFile(client *http.Client, url, file string) error { _, err = io.Copy(out, resp.Body) return err } + +// downloadToFileWithPostProcess downloads the content from the given URL and writes it to the given file. +// The content is then post-processed using the given postProcess function before being written to the file. +// Note: Please use downloadToFile instead of this function if no post-processing is required. +func downloadToFileWithPostProcess(client *http.Client, url, file string, postProcess postProcessFunc) error { + resp, err := client.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("bad status: %s", resp.Status) + } + + b, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + b, err = postProcess(b) + if err != nil { + return err + } + return os.WriteFile(file, b, 0644) +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
10- github.com/advisories/GHSA-wh78-7948-358jghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-37307ghsaADVISORY
- github.com/cilium/cilium/commit/0191b1ebcfdd61cefd06da0315a0e7d504167407ghsax_refsource_MISCWEB
- github.com/cilium/cilium/commit/224e288a5bf40d0bb0f16c9413693b319633431aghsax_refsource_MISCWEB
- github.com/cilium/cilium/commit/9299c0fd0024e33397cffc666ff851e82af28741ghsax_refsource_MISCWEB
- github.com/cilium/cilium/commit/958d7b77274bf2c272d8cdfd812631d644250653ghsax_refsource_MISCWEB
- github.com/cilium/cilium/commit/9eb25ba40391a9b035d7e66401b862818f4aac4bghsax_refsource_MISCWEB
- github.com/cilium/cilium/commit/bf9a1ae1b2d2b2c9cca329d7aa96aa4858032a61ghsax_refsource_MISCWEB
- github.com/cilium/cilium/security/advisories/GHSA-wh78-7948-358jghsax_refsource_CONFIRMWEB
- pkg.go.dev/vuln/GO-2024-2922ghsaWEB
News mentions
0No linked articles in our index yet.