diff --git a/pkg/collect/run_pod.go b/pkg/collect/run_pod.go index 126c2d28..74a49d78 100644 --- a/pkg/collect/run_pod.go +++ b/pkg/collect/run_pod.go @@ -69,7 +69,7 @@ func (c *CollectRunPod) Collect(progressChan chan<- interface{}) (result Collect if err != nil { return } - result, err = savePodDetails(ctx, client, result, c.BundlePath, c.ClientConfig, pod, c.Collector) + result, err = savePodDetails(ctx, client, result, c.BundlePath, pod, c.Collector) if err != nil { klog.Errorf("failed to save pod details: %v", err) } @@ -407,7 +407,7 @@ func RunPodLogsWithOptions(ctx context.Context, client v1.CoreV1Interface, podSp return io.ReadAll(logs) } -func savePodDetails(ctx context.Context, client *kubernetes.Clientset, output CollectorResult, bundlePath string, clientConfig *rest.Config, pod *corev1.Pod, runPodCollector *troubleshootv1beta2.RunPod) (CollectorResult, error) { +func savePodDetails(ctx context.Context, client kubernetes.Interface, output CollectorResult, bundlePath string, pod *corev1.Pod, runPodCollector *troubleshootv1beta2.RunPod) (CollectorResult, error) { podStatus, err := client.CoreV1().Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{}) if err != nil { return nil, errors.Wrap(err, "failed to get pod") @@ -418,7 +418,12 @@ func savePodDetails(ctx context.Context, client *kubernetes.Clientset, output Co return nil, errors.Wrap(err, "failed to get pod events") } - podBytes, err := json.MarshalIndent(podStatus, "", " ") + // The full pod Spec can contain sensitive data such as env vars, commands, args, + // volumes, and image pull secrets. Strip it before saving the pod to the bundle. + sanitizedPod := *podStatus + sanitizedPod.Spec = corev1.PodSpec{} + + podBytes, err := json.MarshalIndent(sanitizedPod, "", " ") if err != nil { return nil, errors.Wrap(err, "failed to marshal pod status") } diff --git a/pkg/collect/run_pod_test.go b/pkg/collect/run_pod_test.go index 7acdc112..6ddd3b6e 100644 --- a/pkg/collect/run_pod_test.go +++ b/pkg/collect/run_pod_test.go @@ -2,6 +2,7 @@ package collect import ( "context" + "encoding/json" "testing" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" @@ -239,3 +240,99 @@ func Test_deleteImagePullSecret(t *testing.T) { }) } } + +func TestSavePodDetails_StripsPodSpec(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-run-pod", + Namespace: "default", + Labels: map[string]string{ + "troubleshoot-role": "run-collector", + }, + }, + Spec: corev1.PodSpec{ + NodeName: "test-node", + Containers: []corev1.Container{ + { + Name: "collector", + Image: "busybox", + Command: []string{"sh", "-c", "echo secret-command"}, + Args: []string{"--token", "super-secret-token"}, + Env: []corev1.EnvVar{ + {Name: "PASSWORD", Value: "hunter2"}, + { + Name: "API_KEY", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "my-secret"}, + Key: "api-key", + }, + }, + }, + }, + }, + }, + ImagePullSecrets: []corev1.LocalObjectReference{{Name: "my-pull-secret"}}, + Volumes: []corev1.Volume{ + { + Name: "secret-vol", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{SecretName: "my-secret"}, + }, + }, + }, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodSucceeded, + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: "collector", + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}, + }, + }, + }, + }, + } + + client := fake.NewSimpleClientset(pod) + collector := &troubleshootv1beta2.RunPod{ + Name: "test-collector", + Namespace: "default", + } + + result, err := savePodDetails(context.Background(), client, NewResult(), "", pod, collector) + require.NoError(t, err) + + podKey := "test-collector/test-collector.json" + require.Contains(t, result, podKey) + + saved := string(result[podKey]) + + // Debugging info must still be present + assert.Contains(t, saved, `"name": "test-run-pod"`) + assert.Contains(t, saved, `"phase": "Succeeded"`) + assert.Contains(t, saved, `"exitCode": 0`) + + // Sensitive spec data must not be present + assert.NotContains(t, saved, "hunter2") + assert.NotContains(t, saved, "super-secret-token") + assert.NotContains(t, saved, "secret-command") + assert.NotContains(t, saved, "my-secret") + assert.NotContains(t, saved, "my-pull-secret") + assert.NotContains(t, saved, `"command":`) + assert.NotContains(t, saved, `"args":`) + assert.NotContains(t, saved, `"env":`) + + // The saved JSON must still unmarshal into a Pod so downstream consumers + // (e.g. goldpinger) can read Name and Status.ContainerStatuses. + var savedPod corev1.Pod + require.NoError(t, json.Unmarshal(result[podKey], &savedPod)) + assert.Equal(t, "test-run-pod", savedPod.Name) + assert.Equal(t, corev1.PodSucceeded, savedPod.Status.Phase) + assert.Len(t, savedPod.Status.ContainerStatuses, 1) + assert.Equal(t, int32(0), savedPod.Status.ContainerStatuses[0].State.Terminated.ExitCode) + assert.Empty(t, savedPod.Spec.Containers) + assert.Empty(t, savedPod.Spec.Volumes) + assert.Empty(t, savedPod.Spec.ImagePullSecrets) +}