diff --git a/test/e2e/argo_server_test.go b/test/e2e/argo_server_test.go index 561abdc9d42c..645b0d5dd74f 100644 --- a/test/e2e/argo_server_test.go +++ b/test/e2e/argo_server_test.go @@ -1975,3 +1975,52 @@ func (s *ArgoServerSuite) TestRateLimitHeader() { func TestArgoServerSuite(t *testing.T) { suite.Run(t, new(ArgoServerSuite)) } + +func (s *ArgoServerSuite) TestWorkflowLogRedaction() { + nsName := fixtures.Namespace + // create secret if not present + secretName := "test-secret" + secretData := map[string][]byte{ + "testpassword": []byte("S00perS3cretPa55word"), + } + secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: secretName}, Data: secretData} + ctx := context.Background() + s.Run("CreateSecret", func() { + _, e := s.KubeClient.CoreV1().Secrets(nsName).Create(ctx, secret, metav1.CreateOptions{}) + assert.NoError(s.T(), e) + }) + defer func() { + // Clean up created secret + _ = s.KubeClient.CoreV1().Secrets(nsName).Delete(ctx, secretName, metav1.DeleteOptions{}) + }() + + var name string + s.Given(). + Workflow("@smoke/workflow-with-secrets.yaml"). + When(). + SubmitWorkflow(). + WaitForWorkflow(fixtures.ToStart). + Then(). + ExpectWorkflow(func(t *testing.T, metadata *metav1.ObjectMeta, status *wfv1.WorkflowStatus) { + name = metadata.Name + }) + + // lets check the logs + for _, tt := range []struct { + name string + path string + }{ + {"PodLogs", "/" + name + "/log?logOptions.container=main"}, + {"WorkflowLogs", "/log?podName=" + name + "&logOptions.container=main"}, + } { + s.Run(tt.name, func() { + s.stream("/api/v1/workflows/argo/"+name+tt.path, func(t *testing.T, line string) (done bool) { + if strings.Contains(line, "data: ") { + assert.Contains(t, line, "secret from env: [*********]") + return true + } + return false + }) + }) + } +} diff --git a/test/e2e/smoke/workflow-with-secrets.yaml b/test/e2e/smoke/workflow-with-secrets.yaml new file mode 100644 index 000000000000..1b1dc5fe9fbd --- /dev/null +++ b/test/e2e/smoke/workflow-with-secrets.yaml @@ -0,0 +1,17 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: secrets- +spec: + entrypoint: print-secret + templates: + - name: print-secret + container: + image: argoproj/argosay:v2 + args: [echo, "secret from env: $MYSECRETPASSWORD"] + env: + - name: MYSECRETPASSWORD + valueFrom: + secretKeyRef: + name: test-secret + key: testpassword \ No newline at end of file diff --git a/util/logs/workflow-logger.go b/util/logs/workflow-logger.go index 753f378082e2..976751e7bd2b 100644 --- a/util/logs/workflow-logger.go +++ b/util/logs/workflow-logger.go @@ -80,6 +80,12 @@ func WorkflowLogs(ctx context.Context, wfClient versioned.Interface, kubeClient var podListOptions metav1.ListOptions + // get secrets for redaction + secrets, err := kubeClient.CoreV1().Secrets(req.GetNamespace()).List(ctx, metav1.ListOptions{}) + if err != nil { + logCtx.WithField("err", err).Debugln("error in listing secrets") + } + // we add selector if cli specify the pod selector when using logs if req.GetSelector() != "" { podListOptions = metav1.ListOptions{LabelSelector: common.LabelKeyWorkflow + "=" + req.GetName() + "," + req.GetSelector()} @@ -165,6 +171,17 @@ func WorkflowLogs(ctx context.Context, wfClient versioned.Interface, kubeClient } if rx.MatchString(content) { // this means we filter the lines in the server, but will still incur the cost of retrieving them from Kubernetes logCtx.WithFields(log.Fields{"timestamp": timestamp, "content": content}).Debug("Log line") + + // log redaction for secrets + if secrets != nil { + for _, s := range secrets.Items { + for _, v := range s.Data { + if strings.Contains(content, string(v)) { + content = strings.Replace(content, string(v), "[*********]", -1) + } + } + } + } unsortedEntries <- logEntry{podName: podName, content: content, timestamp: timestamp} } }