Skip to content

Commit 79a133f

Browse files
committed
Basic Secret Source Implementation
Add extensible Secret Sources that can be used to get secrets that will be redacted from logs. The in core secret sources are a file and mock based one. One is reading from key=value file, the other takes secrets as part of its argument. This likely will be extended in the future with more secure ways to do secrets. All secret values are being redacted from the logs of k6 before they go anywhere. Removing them from other places is not in scope. Closes #4139
1 parent 4f2acb9 commit 79a133f

File tree

16 files changed

+579
-4
lines changed

16 files changed

+579
-4
lines changed

cmd/state/state.go

+7
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import (
1414

1515
"go.k6.io/k6/internal/event"
1616
"go.k6.io/k6/internal/ui/console"
17+
"go.k6.io/k6/internal/usage"
1718
"go.k6.io/k6/lib/fsext"
19+
"go.k6.io/k6/secretsource"
1820
)
1921

2022
const defaultConfigFileName = "config.json"
@@ -55,6 +57,9 @@ type GlobalState struct {
5557

5658
Logger *logrus.Logger //nolint:forbidigo //TODO:change to FieldLogger
5759
FallbackLogger logrus.FieldLogger
60+
61+
SecretsManager *secretsource.SecretsManager
62+
Usage *usage.Usage
5863
}
5964

6065
// NewGlobalState returns a new GlobalState with the given ctx.
@@ -131,6 +136,7 @@ func NewGlobalState(ctx context.Context) *GlobalState {
131136
Hooks: make(logrus.LevelHooks),
132137
Level: logrus.InfoLevel,
133138
},
139+
Usage: usage.New(),
134140
}
135141
}
136142

@@ -142,6 +148,7 @@ type GlobalFlags struct {
142148
Address string
143149
ProfilingEnabled bool
144150
LogOutput string
151+
SecretSource []string
145152
LogFormat string
146153
Verbose bool
147154
}

ext/ext.go

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type ExtensionType uint8
2525
const (
2626
JSExtension ExtensionType = iota + 1
2727
OutputExtension
28+
SecretSourceExtension
2829
)
2930

3031
func (e ExtensionType) String() string {
@@ -34,6 +35,8 @@ func (e ExtensionType) String() string {
3435
s = "js"
3536
case OutputExtension:
3637
s = "output"
38+
case SecretSourceExtension:
39+
s = "secret-source"
3740
}
3841
return s
3942
}
@@ -157,4 +160,5 @@ func extractModuleInfo(mod interface{}) (path, version string) {
157160
func init() {
158161
extensions[JSExtension] = make(map[string]*Extension)
159162
extensions[OutputExtension] = make(map[string]*Extension)
163+
extensions[SecretSourceExtension] = make(map[string]*Extension)
160164
}

internal/cmd/root.go

+70
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ import (
1919
"go.k6.io/k6/cmd/state"
2020
"go.k6.io/k6/errext"
2121
"go.k6.io/k6/errext/exitcodes"
22+
"go.k6.io/k6/ext"
2223
"go.k6.io/k6/internal/log"
24+
"go.k6.io/k6/secretsource"
25+
26+
_ "go.k6.io/k6/internal/secretsource" // import it to register internal secret sources
2327
)
2428

2529
const waitLoggerCloseTimeout = time.Second * 5
@@ -162,6 +166,10 @@ func rootCmdPersistentFlagSet(gs *state.GlobalState) *pflag.FlagSet {
162166
// `gs.DefaultFlags.<value>`, so that the `k6 --help` message is
163167
// not messed up...
164168

169+
// TODO(@mstoykov): likely needs work - no env variables and such. No config.json.
170+
flags.StringArrayVar(&gs.Flags.SecretSource, "secret-source", gs.Flags.SecretSource,
171+
"setting secret sources for k6 file[=./path.fileformat],")
172+
165173
flags.StringVar(&gs.Flags.LogOutput, "log-output", gs.Flags.LogOutput,
166174
"change the output for k6 logs, possible values are stderr,stdout,none,loki[=host:port],file[=./path.fileformat]")
167175
flags.Lookup("log-output").DefValue = gs.DefaultFlags.LogOutput
@@ -257,6 +265,22 @@ func (c *rootCommand) setupLoggers(stop <-chan struct{}) error {
257265
c.globalState.Logger.Debug("Logger format: TEXT")
258266
}
259267

268+
secretsources, err := createSecretSources(c.globalState)
269+
if err != nil {
270+
return err
271+
}
272+
// it is important that we add this hook first as hooks are executed in order of addition
273+
// and this means no other hook will get secrets
274+
var secretsHook logrus.Hook
275+
c.globalState.SecretsManager, secretsHook, err = secretsource.NewSecretsManager(secretsources)
276+
if err != nil {
277+
return err
278+
}
279+
if len(secretsources) != 0 {
280+
// don't actually filter anything if there will be no secrets
281+
c.globalState.Logger.AddHook(secretsHook)
282+
}
283+
260284
cancel := func() {} // noop as default
261285
if hook != nil {
262286
ctx := context.Background()
@@ -289,3 +313,49 @@ func (c *rootCommand) setLoggerHook(ctx context.Context, h log.AsyncHook) {
289313
c.globalState.Logger.AddHook(h)
290314
c.globalState.Logger.SetOutput(io.Discard) // don't output to anywhere else
291315
}
316+
317+
func createSecretSources(gs *state.GlobalState) (map[string]secretsource.SecretSource, error) {
318+
baseParams := secretsource.Params{
319+
Logger: gs.Logger,
320+
Environment: gs.Env,
321+
FS: gs.FS,
322+
Usage: gs.Usage,
323+
}
324+
325+
result := make(map[string]secretsource.SecretSource)
326+
for _, line := range gs.Flags.SecretSource {
327+
t, config, ok := strings.Cut(line, "=")
328+
if !ok {
329+
return nil, fmt.Errorf("couldn't parse secret source configuration %q", line)
330+
}
331+
secretSources := ext.Get(ext.SecretSourceExtension)
332+
found, ok := secretSources[t]
333+
if !ok {
334+
return nil, fmt.Errorf("no secret source for type %q for configuration %q", t, line)
335+
}
336+
c := found.Module.(secretsource.Constructor) //nolint:forcetypeassert
337+
params := baseParams
338+
params.ConfigArgument = config
339+
// TODO(@mstoykov): make it not configurable just from cmd line
340+
// params.JSONConfig = test.derivedConfig.Collectors[outputType]
341+
342+
secretSource, err := c(params)
343+
if err != nil {
344+
return nil, err
345+
}
346+
name := secretSource.Name()
347+
_, alreadRegistered := result[name]
348+
if alreadRegistered {
349+
return nil, fmt.Errorf("secret source for name %q already registered before configuration %q", t, line)
350+
}
351+
result[name] = secretSource
352+
}
353+
354+
if len(result) == 1 {
355+
for _, l := range result {
356+
result["default"] = l
357+
}
358+
}
359+
360+
return result, nil
361+
}

internal/cmd/test_load.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"go.k6.io/k6/errext/exitcodes"
1919
"go.k6.io/k6/internal/js"
2020
"go.k6.io/k6/internal/loader"
21-
"go.k6.io/k6/internal/usage"
2221
"go.k6.io/k6/js/modules"
2322
"go.k6.io/k6/lib"
2423
"go.k6.io/k6/lib/fsext"
@@ -84,7 +83,8 @@ func loadLocalTest(gs *state.GlobalState, cmd *cobra.Command, args []string) (*l
8483
val, ok := gs.Env[key]
8584
return val, ok
8685
},
87-
Usage: usage.New(),
86+
Usage: gs.Usage,
87+
SecretsManager: gs.SecretsManager,
8888
}
8989

9090
test := &loadedTest{

internal/cmd/tests/test_state.go

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"go.k6.io/k6/internal/event"
1919
"go.k6.io/k6/internal/lib/testutils"
2020
"go.k6.io/k6/internal/ui/console"
21+
"go.k6.io/k6/internal/usage"
2122
"go.k6.io/k6/lib/fsext"
2223
)
2324

@@ -111,6 +112,7 @@ func NewGlobalTestState(tb testing.TB) *GlobalTestState {
111112
SignalStop: signal.Stop,
112113
Logger: logger,
113114
FallbackLogger: testutils.NewLogger(tb).WithField("fallback", true),
115+
Usage: usage.New(),
114116
}
115117

116118
return ts

internal/secretsource/file/file.go

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Package file implements secret source that reads the secrets from a file as key=value pairs one per line
2+
package file
3+
4+
import (
5+
"bufio"
6+
"errors"
7+
"fmt"
8+
"strings"
9+
10+
"go.k6.io/k6/secretsource"
11+
)
12+
13+
func init() {
14+
secretsource.RegisterExtension("file", func(params secretsource.Params) (secretsource.SecretSource, error) {
15+
fss := &fileSecretSource{}
16+
err := fss.parseArg(params.ConfigArgument)
17+
if err != nil {
18+
return nil, err
19+
}
20+
21+
f, err := params.FS.Open(fss.filename)
22+
if err != nil {
23+
return nil, err
24+
}
25+
scanner := bufio.NewScanner(f)
26+
27+
fss.internal = make(map[string]string)
28+
for scanner.Scan() {
29+
line := scanner.Text()
30+
k, v, ok := strings.Cut(line, "=")
31+
if !ok {
32+
return nil, fmt.Errorf("parsing %q, needs =", line)
33+
}
34+
35+
fss.internal[k] = v
36+
}
37+
return fss, nil
38+
})
39+
}
40+
41+
func (fss *fileSecretSource) parseArg(config string) error {
42+
list := strings.Split(config, ",")
43+
if len(list) >= 1 {
44+
for _, kv := range list {
45+
k, v, ok := strings.Cut(kv, "=")
46+
if !ok {
47+
fss.filename = kv
48+
}
49+
switch k {
50+
case "filename":
51+
fss.filename = v
52+
case "name":
53+
fss.name = v
54+
default:
55+
return fmt.Errorf("unknown configuration key for file secret source %q", k)
56+
}
57+
}
58+
}
59+
return nil
60+
}
61+
62+
type fileSecretSource struct {
63+
internal map[string]string
64+
name string
65+
filename string
66+
}
67+
68+
func (fss *fileSecretSource) Name() string {
69+
return fss.name
70+
}
71+
72+
func (fss *fileSecretSource) Description() string {
73+
return fmt.Sprintf("file source from %s", fss.filename)
74+
}
75+
76+
func (fss *fileSecretSource) Get(key string) (string, error) {
77+
v, ok := fss.internal[key]
78+
if !ok {
79+
return "", errors.New("no value")
80+
}
81+
return v, nil
82+
}

internal/secretsource/init.go

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Package secretsource registers all the internal secret sources when imported
2+
package secretsource
3+
4+
import (
5+
_ "go.k6.io/k6/internal/secretsource/file" // import them for init
6+
_ "go.k6.io/k6/internal/secretsource/mock" // import them for init
7+
)

internal/secretsource/mock/mock.go

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Package mock implements a secret source that is just taking secrets on the cli
2+
package mock
3+
4+
import (
5+
"errors"
6+
"fmt"
7+
"strings"
8+
9+
"go.k6.io/k6/secretsource"
10+
)
11+
12+
func init() {
13+
secretsource.RegisterExtension("mock", func(params secretsource.Params) (secretsource.SecretSource, error) {
14+
list := strings.Split(params.ConfigArgument, ",")
15+
secrets := make(map[string]string, len(list))
16+
name := "mock"
17+
for _, kv := range list {
18+
k, v, ok := strings.Cut(kv, "=")
19+
if !ok {
20+
return nil, fmt.Errorf("parsing %q, needs =", kv)
21+
}
22+
if k == "name" {
23+
name = k
24+
continue
25+
}
26+
27+
secrets[k] = v
28+
}
29+
return NewMockSecretSource(name, secrets), nil
30+
})
31+
}
32+
33+
// NewMockSecretSource returns a new secret source mock with the provided name and map of secrets
34+
func NewMockSecretSource(name string, secrets map[string]string) secretsource.SecretSource {
35+
return &mockSecretSource{
36+
internal: secrets,
37+
name: name,
38+
}
39+
}
40+
41+
type mockSecretSource struct {
42+
internal map[string]string
43+
name string
44+
}
45+
46+
func (mss *mockSecretSource) Name() string {
47+
return mss.name
48+
}
49+
50+
func (mss *mockSecretSource) Description() string {
51+
return "this is a mock secret source"
52+
}
53+
54+
func (mss *mockSecretSource) Get(key string) (string, error) {
55+
v, ok := mss.internal[key]
56+
if !ok {
57+
return "", errors.New("no value")
58+
}
59+
return v, nil
60+
}

0 commit comments

Comments
 (0)