Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Mask secret values in the log as it is written

e82bbe7ddc584dc54ee57c7ce3b97f933c9e9886 · Keenan Nemetz · 7d ago · parent 4ac42680

Mask secret values in the log as it is written

A step that prints a secret by accident used to put it in the attempt log, and
the log is streamed to the instance and kept. The runner now replaces every
value it was given with *** before the bytes reach the log file, so the copy
the server stores never holds one and a value split across two writes is still
caught: the mask holds back the last longest-1 bytes and the runner flushes at
the end of each step.

ci/redact is the one implementation, so a local `vole workflow run` masks its
terminal output the same way. Its own error text goes through the same mask,
because "the runtime message does not contain a value" should be true rather
than assumed.

It is best effort, and the docs say where it stops: a re-encoded value does not
match, a value shorter than four bytes is left alone because masking every
occurrence of it would rewrite the log, and an artifact or a site file is
published exactly as the step wrote it.

Slice 3 of #154.

ci/agent/agent.go +25−1

@@ -31,6 +31,7 @@ import (
31 31 "github.com/rodent-software/vole/ci"
32 32 jobcache "github.com/rodent-software/vole/ci/cache"
33 33 "github.com/rodent-software/vole/ci/container"
34 "github.com/rodent-software/vole/ci/redact"
34 35 "github.com/rodent-software/vole/ci/runs"
35 36 "github.com/rodent-software/vole/client"
36 37 "github.com/rodent-software/vole/forge/access"
@@ -510,6 +511,13 @@ func (s *runnerSession) run(remote, cache string, job *runnerJob, lease time.Dur
510 511 return
511 512 }
512 513 defer log.Close()
514 // The values are masked as the log is written, so neither the file the streamer
515 // sends nor the instance's copy ever holds one. Errors the runner itself prints
516 // go through the same mask: an instance error or a runtime message is not
517 // supposed to contain a value, and this is what makes that true rather than
518 // assumed.
519 mask := redact.New(secretValues(job))
520 log.SetMask(mask)
513 521 s.stream = s.startLogStream(job, log)
514 522 defer func() {
515 523 s.stream.stop()
@@ -532,7 +540,7 @@ func (s *runnerSession) run(remote, cache string, job *runnerJob, lease time.Dur
532 540 return
533 541 }
534 542 fail := func(format string, args ...any) {
535 msg := fmt.Sprintf(format, args...)
543 msg := mask.String(fmt.Sprintf(format, args...))
536 544 fmt.Fprintf(os.Stderr, "vole runner: %s\n", msg)
537 545 fmt.Fprintf(log, "\n%s\n", msg)
538 546 s.finish(remote, co, job, log, 1, nil, nil, nil, box)
@@ -645,6 +653,9 @@ func (s *runnerSession) run(remote, cache string, job *runnerJob, lease time.Dur
645 653 started := time.Now().UTC()
646 654 fmt.Fprintf(log, "\n--- %s\n", step.Name)
647 655 code, lost := s.execStep(job.ID, sandbox, step, lease, log)
656 // The mask holds back a few bytes so a value split across writes is caught;
657 // flushing here keeps this step's byte range its own.
658 log.Flush()
648 659 result := runs.StepResult{
649 660 Name: step.Name, Started: started, Finished: time.Now().UTC(), Exit: code,
650 661 LogOffset: logStart, LogSize: log.Len() - logStart,
@@ -1124,6 +1135,19 @@ func (s *runnerSession) runCommand(ctx context.Context, cmd *exec.Cmd, runID str
1124 1135 }
1125 1136 }
1126 1137
1138 // secretValues is what the log has to redact: the values this job was actually
1139 // given, not the names it asked for.
1140 func secretValues(job *runnerJob) []string {
1141 if len(job.Secrets) == 0 {
1142 return nil
1143 }
1144 values := make([]string, 0, len(job.Secrets))
1145 for _, delivered := range job.Secrets {
1146 values = append(values, delivered.Value)
1147 }
1148 return values
1149 }
1150
1127 1151 // secretsScript renders a job's delivered values as the file every step sources.
1128 1152 // The rendering lives in ci/container because that is the package that sources it,
1129 1153 // so a local run writes exactly the same file a runner does.