Skip to content

Commit 9ad88fc

Browse files
authored
Merge pull request #472 from stbenjam/minimum-length
secretutil: implement configurable minimum secret length
2 parents 9b3f5fa + f2d77b1 commit 9ad88fc

File tree

9 files changed

+127
-5
lines changed

9 files changed

+127
-5
lines changed

config/prow/cluster/prowjob-crd/prowjob_customresourcedefinition.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ spec:
149149
items:
150150
type: string
151151
type: array
152+
minimum_secret_length:
153+
description: |-
154+
MinimumSecretLength is the minimum length a secret must have to be censored.
155+
Secrets shorter than this length will not be censored. If unset, defaults to 0
156+
(all secrets are censored regardless of length).
157+
type: integer
152158
type: object
153159
cookiefile_secret:
154160
description: |-

pkg/apis/prowjobs/v1/types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,11 @@ type CensoringOptions struct {
594594
// matches a glob in IncludeDirectories. Entries in this list are relative to $ARTIFACTS,
595595
// and are parsed with the go-zglob library, allowing for globbed matches.
596596
ExcludeDirectories []string `json:"exclude_directories,omitempty"`
597+
598+
// MinimumSecretLength is the minimum length a secret must have to be censored.
599+
// Secrets shorter than this length will not be censored. If unset, defaults to 0
600+
// (all secrets are censored regardless of length).
601+
MinimumSecretLength *int `json:"minimum_secret_length,omitempty"`
597602
}
598603

599604
type SchedulingOptions struct {
@@ -636,6 +641,10 @@ func (g *CensoringOptions) ApplyDefault(def *CensoringOptions) *CensoringOptions
636641
if merged.ExcludeDirectories == nil {
637642
merged.ExcludeDirectories = def.ExcludeDirectories
638643
}
644+
645+
if merged.MinimumSecretLength == nil {
646+
merged.MinimumSecretLength = def.MinimumSecretLength
647+
}
639648
return &merged
640649
}
641650

pkg/apis/prowjobs/v1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/config/prow-config-documented.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,10 @@ plank:
622622
# globbed matches.
623623
include_directories:
624624
- ""
625+
# MinimumSecretLength is the minimum length a secret must have to be censored.
626+
# Secrets shorter than this length will not be censored. If unset, defaults to 0
627+
# (all secrets are censored regardless of length).
628+
minimum_secret_length: 0
625629
# CookieFileSecret is the name of a kubernetes secret that contains
626630
# a git http.cookiefile, which should be used during the cloning process.
627631
cookiefile_secret: ""
@@ -975,6 +979,10 @@ plank:
975979
# globbed matches.
976980
include_directories:
977981
- ""
982+
# MinimumSecretLength is the minimum length a secret must have to be censored.
983+
# Secrets shorter than this length will not be censored. If unset, defaults to 0
984+
# (all secrets are censored regardless of length).
985+
minimum_secret_length: 0
978986
# CookieFileSecret is the name of a kubernetes secret that contains
979987
# a git http.cookiefile, which should be used during the cloning process.
980988
cookiefile_secret: ""

pkg/secretutil/censor.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,22 @@ type Censorer interface {
3434
}
3535

3636
func NewCensorer() *ReloadingCensorer {
37+
return NewCensorerWithMinLength(0)
38+
}
39+
40+
func NewCensorerWithMinLength(minLength int) *ReloadingCensorer {
3741
return &ReloadingCensorer{
38-
RWMutex: &sync.RWMutex{},
39-
Replacer: bytereplacer.New(),
42+
RWMutex: &sync.RWMutex{},
43+
Replacer: bytereplacer.New(),
44+
minimumSecretLength: minLength,
4045
}
4146
}
4247

4348
type ReloadingCensorer struct {
4449
*sync.RWMutex
4550
*bytereplacer.Replacer
46-
largestSecret int
51+
largestSecret int
52+
minimumSecretLength int
4753
}
4854

4955
var _ Censorer = &ReloadingCensorer{}
@@ -108,6 +114,10 @@ func (c *ReloadingCensorer) Refresh(secrets ...string) {
108114
// to justify secret booleans, since all values are known and replacing them carries a high price.
109115
continue
110116
}
117+
if len(secret) < c.minimumSecretLength {
118+
// Skip secrets that are shorter than the minimum length
119+
continue
120+
}
111121

112122
addReplacement(secret)
113123
for _, item := range toEncode {

pkg/secretutil/censor_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,75 @@ func TestBooleanNotHidden(t *testing.T) {
150150
})
151151
}
152152
}
153+
154+
func TestMinimumSecretLength(t *testing.T) {
155+
var testCases = []struct {
156+
name string
157+
secrets []string
158+
minLength int
159+
input string
160+
expected string
161+
}{
162+
{
163+
name: "no minimum length - all secrets censored",
164+
secrets: []string{"a", "bb", "ccc", "dddd"},
165+
minLength: 0,
166+
input: "data: a bb ccc dddd",
167+
expected: "dXtX: X XX XXX XXXX",
168+
},
169+
{
170+
name: "minimum length 3 - short secrets not censored",
171+
secrets: []string{"a", "bb", "ccc", "dddd"},
172+
minLength: 3,
173+
input: "data: a bb ccc dddd",
174+
expected: "data: a bb XXX XXXX",
175+
},
176+
{
177+
name: "minimum length 5 - all secrets censored",
178+
secrets: []string{"short", "verylongsecret"},
179+
minLength: 5,
180+
input: "data: short verylongsecret",
181+
expected: "data: XXXXX XXXXXXXXXXXXXX",
182+
},
183+
{
184+
name: "minimum length 10 - no secrets censored",
185+
secrets: []string{"short", "medium"},
186+
minLength: 10,
187+
input: "data: short medium",
188+
expected: "data: short medium",
189+
},
190+
{
191+
name: "base64 encoding also respects minimum length",
192+
secrets: []string{"abc"},
193+
minLength: 5,
194+
input: "data: abc YWJj", // YWJj is base64 for "abc"
195+
expected: "data: abc YWJj", // both too short to censor
196+
},
197+
{
198+
name: "base64 encoding censored when long enough",
199+
secrets: []string{"secret"},
200+
minLength: 5,
201+
input: "data: secret c2VjcmV0", // c2VjcmV0 is base64 for "secret"
202+
expected: "data: XXXXXX XXXXXXXX",
203+
},
204+
{
205+
name: "mixed length secrets with minimum",
206+
secrets: []string{"x", "password123", "key"},
207+
minLength: 4,
208+
input: "config: x=1 password123=secret key=value",
209+
expected: "config: x=1 XXXXXXXXXXX=secret key=value",
210+
},
211+
}
212+
213+
for _, testCase := range testCases {
214+
t.Run(testCase.name, func(t *testing.T) {
215+
censorer := NewCensorerWithMinLength(testCase.minLength)
216+
censorer.Refresh(testCase.secrets...)
217+
input := []byte(testCase.input)
218+
censorer.Censor(&input)
219+
if diff := cmp.Diff(testCase.expected, string(input)); diff != "" {
220+
t.Errorf("%s: got incorrect text after censor: %v", testCase.name, diff)
221+
}
222+
})
223+
}
224+
}

pkg/sidecar/censor.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,14 @@ func (o Options) censor() error {
7777
return fmt.Errorf("could not load secrets: %w", err)
7878
}
7979
logrus.WithField("secrets", len(secrets)).Debug("Loaded secrets to censor.")
80-
censorer := secretutil.NewCensorer()
80+
81+
minLength := 0
82+
if o.CensoringOptions.MinimumSecretLength != nil {
83+
minLength = *o.CensoringOptions.MinimumSecretLength
84+
}
85+
logrus.WithField("minimum_secret_length", minLength).Debug("Using minimum secret length for censoring.")
86+
87+
censorer := secretutil.NewCensorerWithMinLength(minLength)
8188
censorer.RefreshBytes(secrets...)
8289

8390
bufferSize := defaultBufferSize

pkg/sidecar/options.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ type CensoringOptions struct {
125125
// IniFilenames are secret filenames that should be parsed as INI files in order to
126126
// censor the values in the key-value mapping as well as the full content of the file.
127127
IniFilenames []string `json:"ini_filenames,omitempty"`
128+
129+
// MinimumSecretLength is the minimum length a secret must have to be censored.
130+
// Secrets shorter than this length will not be censored. If unset, defaults to 0
131+
// (all secrets are censored regardless of length).
132+
MinimumSecretLength *int `json:"minimum_secret_length,omitempty"`
128133
}
129134

130135
func (o Options) entries() []wrapper.Options {

pkg/spyglass/lenses/common/bindata.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)