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/spool_test.go +81

@@ -4,6 +4,8 @@ import (
4 4 "os"
5 5 "strings"
6 6 "testing"
7
8 "github.com/rodent-software/vole/ci/redact"
7 9 )
8 10
9 11 func TestSpoolKeepsWhatFits(t *testing.T) {
@@ -118,3 +120,82 @@ func TestCheckArtifactLimits(t *testing.T) {
118 120 t.Fatal("an oversized artifact set was accepted")
119 121 }
120 122 }
123
124 // A secret is removed before the bytes reach the file, so the streamer and the
125 // instance's copy are masked by construction rather than by a later pass.
126 func TestSpoolMasksSecretValues(t *testing.T) {
127 t.Parallel()
128 s, err := newSpool(1024)
129 if err != nil {
130 t.Fatal(err)
131 }
132 defer s.Close()
133 s.SetMask(redact.New([]string{"s3cret-token"}))
134
135 // Two writes, because a pipe splits wherever it likes.
136 if _, err := s.Write([]byte("token=s3cret-")); err != nil {
137 t.Fatal(err)
138 }
139 if _, err := s.Write([]byte("token\n")); err != nil {
140 t.Fatal(err)
141 }
142 if err := s.seal(); err != nil {
143 t.Fatal(err)
144 }
145 raw, err := os.ReadFile(s.Path())
146 if err != nil {
147 t.Fatal(err)
148 }
149 if string(raw) != "token="+redact.Placeholder+"\n" {
150 t.Fatalf("log = %q", raw)
151 }
152 if s.Len() != int64(len(raw)) {
153 t.Fatalf("Len = %d, file = %d bytes; step offsets must match the file", s.Len(), len(raw))
154 }
155 }
156
157 // The held-back tail is flushed when the log is sealed, so a value at the very end
158 // is masked rather than missing.
159 func TestSpoolFlushesTheMaskedTail(t *testing.T) {
160 t.Parallel()
161 s, err := newSpool(1024)
162 if err != nil {
163 t.Fatal(err)
164 }
165 defer s.Close()
166 s.SetMask(redact.New([]string{"s3cret-token"}))
167 if _, err := s.Write([]byte("done s3cret-token")); err != nil {
168 t.Fatal(err)
169 }
170 if err := s.seal(); err != nil {
171 t.Fatal(err)
172 }
173 raw, err := os.ReadFile(s.Path())
174 if err != nil {
175 t.Fatal(err)
176 }
177 if string(raw) != "done "+redact.Placeholder {
178 t.Fatalf("log = %q", raw)
179 }
180 }
181
182 // The runner flushes at the end of each step, so a few held-back bytes do not make
183 // the next step's range start early.
184 func TestSpoolFlushEndsAStepRange(t *testing.T) {
185 t.Parallel()
186 s, err := newSpool(1024)
187 if err != nil {
188 t.Fatal(err)
189 }
190 defer s.Close()
191 s.SetMask(redact.New([]string{"s3cret-token"}))
192 start := s.Len()
193 if _, err := s.Write([]byte("step one s3cret-token")); err != nil {
194 t.Fatal(err)
195 }
196 s.Flush()
197 want := int64(len("step one ") + len(redact.Placeholder))
198 if end := s.Len(); end != start+want {
199 t.Fatalf("step range = %d bytes, want %d", end-start, want)
200 }
201 }