Slice 3 of #154: mask the values a job was given in the log, and say where masking stops.
What it adds
ci/redact: one small package that replaces each delivered value with***, holding back the lastlongest-1bytes of a write so a value split across two writes is still caught. It has aMaskfor a stream the caller serialises and aWriterthat serialises for itself, because a command’s stdout and stderr are copied by two goroutines.- The attempt log masks at the point of writing (
spool.SetMask), so the file the streamer sends — and therefore the instance’s copy indata/logs/— never holds a value. The runner flushes at the end of each step, so a step’s byte range is still its own. - The runner’s own error text goes through the same mask, because “a runtime message cannot contain a value” should be true rather than assumed.
vole workflow runmasks its terminal output the same way, from the values it read with the developer’s key.
What it deliberately does not catch
Written down on the reference page and in docs/runner.md rather than left to be
discovered:
- a re-encoded value — base64, hex, a URL escape, one field of a connection string;
- a value shorter than 4 bytes, because replacing every occurrence of
1oryeswould rewrite the log and protect nothing worth calling a secret; - anything a step writes into an artifact, a site file or a release attachment: the mask covers the log, not the files the job chooses to publish.
Tests
ci/redact: a value split across three writes; longest-match where one value is a prefix of another; the 4-byte floor; a multi-line value; every write reported as taken so a program printing its own secret is not killed by a short write; concurrent writers.ci/agent: the spool masks before the file (so the streamer cannot miss it), the held-back tail is flushed at seal, and a flush at a step boundary ends the range where the output ended.- End to end: the job in
TestAJobReadsTheSecretsItDeclaresnow prints the value on purpose, and the log showsechoed=***— including a value with a line break in it — while the length and step-range assertions still hold.
Docs
docs/reference/workflow-reference.md loses the “masking is not supported” line and
gains the three limits above; docs/runner.md, docs/workflows.md and
docs/cli.md say what a reader gets.