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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ slash) and `log.iostream` (`stdout` or `stderr`). Kubernetes records carry
`k8s.container.name` and `k8s.pod.name`, including init containers. Direct, command,
hook, and worker logs do not carry container attributes.

Before export, the worker redacts known environment values supplied to the task
Before export, the worker redacts known secret environment values supplied to the task
or its hooks. Local stdout/stderr is not scrubbed. Secrets created inside tasks
or resolved through Kubernetes `valueFrom`/`envFrom` are not known to the worker
and cannot be redacted. Setup hooks must write generated credentials to the
Expand Down
35 changes: 35 additions & 0 deletions internal/tasktelemetry/log/redaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"net/url"
"sort"
"strconv"
"strings"
"sync"
)
Expand Down Expand Up @@ -42,6 +43,40 @@ func (b *redactionBuffer) WriteString(s string) (int, error) {
return b.Builder.WriteString(s)
}

func (r *redactor) addEnv(name, value string) {
name = strings.ToUpper(name)
for _, marker := range []string{"KEY", "TOKEN", "SECRET", "PASSWORD", "PASSWD", "AUTH", "CREDENTIAL", "PRIVATE"} {
if strings.Contains(name, marker) {
r.add(value)
return
}
}
// When adding the task environment, do not mask flag values or known non-sensitive variables.
// Because the redactor matches on values alone, over-redacting makes the log output unreadable.
// For example, we shouldn't redact every occurrence of "0".
if value == "true" || value == "false" {
return
}
if _, err := strconv.ParseFloat(value, 64); err == nil {
return
}
switch name {
case "TASK_ID", "PATH", "HOME", "SHELL", "USER", "LOGNAME", "TERM", "TMPDIR", "DISPLAY":
return
}
if strings.HasPrefix(name, "OZ_") || strings.HasPrefix(name, "WARP_") {
switch name[strings.IndexByte(name, '_')+1:] {
case "RUN_ID", "EXECUTION_ID", "WORKER_BACKEND", "WORKSPACE_ROOT", "ENVIRONMENT_FILE", "DOCKER_IMAGE":
return
case "SERVER_ROOT_URL":
if endpoint, err := url.Parse(value); err == nil && endpoint.User == nil && endpoint.RawQuery == "" && endpoint.Fragment == "" {
return
}
}
}
r.add(value)
}

func (r *redactor) add(value string) {
if value == "" {
return
Expand Down
47 changes: 47 additions & 0 deletions internal/tasktelemetry/log/redaction_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package tasklogs

import "testing"

func TestEnvironmentRedaction(t *testing.T) {
for _, list := range []bool{false, true} {
name := "map"
if list {
name = "list"
}
t.Run(name, func(t *testing.T) {
for _, tc := range []struct {
name, value, message, want string
}{
{"ENABLED", "1", "2026-10-18T19:58:17Z libapparmor1", "2026-10-18T19:58:17Z libapparmor1"},
{"DISABLED", "0", "\x1b[0m version 4.0.1", "\x1b[0m version 4.0.1"},
{"DEBUG", "true", "debug=true", "debug=true"},
{"RETRY_DELAY", "0.5", "delay=0.5", "delay=0.5"},
{"TASK_ID", "run-id", "task run-id", "task run-id"},
{"OZ_RUN_ID", "run-id", "oz-task-run-id", "oz-task-run-id"},
{"WARP_EXECUTION_ID", "execution-id", "execution-id", "execution-id"},
{"WARP_SERVER_ROOT_URL", "https://staging.warp.dev", "https://staging.warp.dev", "https://staging.warp.dev"},
{"PATH", "/usr/bin", "using /usr/bin", "using /usr/bin"},
{"WARP_API_KEY", "1234", "key=1234", "key=[REDACTED]"},
{"password", "false", "password=false", "password=[REDACTED]"},
{"AUTH_TOKEN", "x", "token=x", "token=[REDACTED]"},
{"CUSTOM", "custom-value", "custom-value", "[REDACTED]"},
{"WARP_SERVER_ROOT_URL", "https://user:password@example.com", "https://user:password@example.com", "[REDACTED]"},
} {
t.Run(tc.name, func(t *testing.T) {
r := &Reporter{}
if list {
r.AddEnvList([]string{tc.name + "=" + tc.value})
} else {
r.AddEnv(map[string]string{tc.name: tc.value})
}
if got := r.redactor.redact(tc.message); got != tc.want {
t.Errorf("structured redaction = %q, want %q", got, tc.want)
}
if got := r.redactor.redactLine(tc.message); got != tc.want {
t.Errorf("raw redaction = %q, want %q", got, tc.want)
}
})
}
})
}
}
10 changes: 5 additions & 5 deletions internal/tasktelemetry/log/reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,14 @@ func (r *Reporter) Context() context.Context {
return r.ctx
}

// AddEnv registers nonempty environment values for task-local credential redaction.
// AddEnv registers potentially sensitive environment values for task-local redaction.
// Structured events match complete values; raw lines also match newline fragments.
func (r *Reporter) AddEnv(env map[string]string) {
if r == nil {
return
}
for _, value := range env {
r.redactor.add(value)
for name, value := range env {
r.redactor.addEnv(name, value)
}
}

Expand All @@ -89,8 +89,8 @@ func (r *Reporter) AddEnvList(env []string) {
return
}
for _, entry := range env {
_, value, _ := strings.Cut(entry, "=")
r.redactor.add(value)
name, value, _ := strings.Cut(entry, "=")
r.redactor.addEnv(name, value)
}
}

Expand Down
Loading