Skip to content

Commit 0e2e0cd

Browse files
authored
fix(runpod): strip pod spec before saving collected output (#2105)
1 parent 651ec1b commit 0e2e0cd

2 files changed

Lines changed: 105 additions & 3 deletions

File tree

pkg/collect/run_pod.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func (c *CollectRunPod) Collect(progressChan chan<- interface{}) (result Collect
6969
if err != nil {
7070
return
7171
}
72-
result, err = savePodDetails(ctx, client, result, c.BundlePath, c.ClientConfig, pod, c.Collector)
72+
result, err = savePodDetails(ctx, client, result, c.BundlePath, pod, c.Collector)
7373
if err != nil {
7474
klog.Errorf("failed to save pod details: %v", err)
7575
}
@@ -407,7 +407,7 @@ func RunPodLogsWithOptions(ctx context.Context, client v1.CoreV1Interface, podSp
407407
return io.ReadAll(logs)
408408
}
409409

410-
func savePodDetails(ctx context.Context, client *kubernetes.Clientset, output CollectorResult, bundlePath string, clientConfig *rest.Config, pod *corev1.Pod, runPodCollector *troubleshootv1beta2.RunPod) (CollectorResult, error) {
410+
func savePodDetails(ctx context.Context, client kubernetes.Interface, output CollectorResult, bundlePath string, pod *corev1.Pod, runPodCollector *troubleshootv1beta2.RunPod) (CollectorResult, error) {
411411
podStatus, err := client.CoreV1().Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{})
412412
if err != nil {
413413
return nil, errors.Wrap(err, "failed to get pod")
@@ -418,7 +418,12 @@ func savePodDetails(ctx context.Context, client *kubernetes.Clientset, output Co
418418
return nil, errors.Wrap(err, "failed to get pod events")
419419
}
420420

421-
podBytes, err := json.MarshalIndent(podStatus, "", " ")
421+
// The full pod Spec can contain sensitive data such as env vars, commands, args,
422+
// volumes, and image pull secrets. Strip it before saving the pod to the bundle.
423+
sanitizedPod := *podStatus
424+
sanitizedPod.Spec = corev1.PodSpec{}
425+
426+
podBytes, err := json.MarshalIndent(sanitizedPod, "", " ")
422427
if err != nil {
423428
return nil, errors.Wrap(err, "failed to marshal pod status")
424429
}

pkg/collect/run_pod_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package collect
22

33
import (
44
"context"
5+
"encoding/json"
56
"testing"
67

78
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
@@ -239,3 +240,99 @@ func Test_deleteImagePullSecret(t *testing.T) {
239240
})
240241
}
241242
}
243+
244+
func TestSavePodDetails_StripsPodSpec(t *testing.T) {
245+
pod := &corev1.Pod{
246+
ObjectMeta: metav1.ObjectMeta{
247+
Name: "test-run-pod",
248+
Namespace: "default",
249+
Labels: map[string]string{
250+
"troubleshoot-role": "run-collector",
251+
},
252+
},
253+
Spec: corev1.PodSpec{
254+
NodeName: "test-node",
255+
Containers: []corev1.Container{
256+
{
257+
Name: "collector",
258+
Image: "busybox",
259+
Command: []string{"sh", "-c", "echo secret-command"},
260+
Args: []string{"--token", "super-secret-token"},
261+
Env: []corev1.EnvVar{
262+
{Name: "PASSWORD", Value: "hunter2"},
263+
{
264+
Name: "API_KEY",
265+
ValueFrom: &corev1.EnvVarSource{
266+
SecretKeyRef: &corev1.SecretKeySelector{
267+
LocalObjectReference: corev1.LocalObjectReference{Name: "my-secret"},
268+
Key: "api-key",
269+
},
270+
},
271+
},
272+
},
273+
},
274+
},
275+
ImagePullSecrets: []corev1.LocalObjectReference{{Name: "my-pull-secret"}},
276+
Volumes: []corev1.Volume{
277+
{
278+
Name: "secret-vol",
279+
VolumeSource: corev1.VolumeSource{
280+
Secret: &corev1.SecretVolumeSource{SecretName: "my-secret"},
281+
},
282+
},
283+
},
284+
},
285+
Status: corev1.PodStatus{
286+
Phase: corev1.PodSucceeded,
287+
ContainerStatuses: []corev1.ContainerStatus{
288+
{
289+
Name: "collector",
290+
State: corev1.ContainerState{
291+
Terminated: &corev1.ContainerStateTerminated{ExitCode: 0},
292+
},
293+
},
294+
},
295+
},
296+
}
297+
298+
client := fake.NewSimpleClientset(pod)
299+
collector := &troubleshootv1beta2.RunPod{
300+
Name: "test-collector",
301+
Namespace: "default",
302+
}
303+
304+
result, err := savePodDetails(context.Background(), client, NewResult(), "", pod, collector)
305+
require.NoError(t, err)
306+
307+
podKey := "test-collector/test-collector.json"
308+
require.Contains(t, result, podKey)
309+
310+
saved := string(result[podKey])
311+
312+
// Debugging info must still be present
313+
assert.Contains(t, saved, `"name": "test-run-pod"`)
314+
assert.Contains(t, saved, `"phase": "Succeeded"`)
315+
assert.Contains(t, saved, `"exitCode": 0`)
316+
317+
// Sensitive spec data must not be present
318+
assert.NotContains(t, saved, "hunter2")
319+
assert.NotContains(t, saved, "super-secret-token")
320+
assert.NotContains(t, saved, "secret-command")
321+
assert.NotContains(t, saved, "my-secret")
322+
assert.NotContains(t, saved, "my-pull-secret")
323+
assert.NotContains(t, saved, `"command":`)
324+
assert.NotContains(t, saved, `"args":`)
325+
assert.NotContains(t, saved, `"env":`)
326+
327+
// The saved JSON must still unmarshal into a Pod so downstream consumers
328+
// (e.g. goldpinger) can read Name and Status.ContainerStatuses.
329+
var savedPod corev1.Pod
330+
require.NoError(t, json.Unmarshal(result[podKey], &savedPod))
331+
assert.Equal(t, "test-run-pod", savedPod.Name)
332+
assert.Equal(t, corev1.PodSucceeded, savedPod.Status.Phase)
333+
assert.Len(t, savedPod.Status.ContainerStatuses, 1)
334+
assert.Equal(t, int32(0), savedPod.Status.ContainerStatuses[0].State.Terminated.ExitCode)
335+
assert.Empty(t, savedPod.Spec.Containers)
336+
assert.Empty(t, savedPod.Spec.Volumes)
337+
assert.Empty(t, savedPod.Spec.ImagePullSecrets)
338+
}

0 commit comments

Comments
 (0)