Skip to content

Commit 9aef236

Browse files
authored
Implement secrets management (#10)
Implements a set of secrets management commands, along with a flag for `run` which allows secrets to be injected as environment variables. Note that this implementation stores the secrets to an unencrypted file on disk. This will be changed in an upcoming PR. Example of use: ``` $ bin/vt secret list Available secrets: - fizz - foo $ bin/vt run --secret=foo,target=FOO --secret=fizz,target=FIZZ alpine -- env Logging to: /tmp/vibetool-alpine.log MCP server alpine-1742982443 is running in the background (PID: 18425) Use 'vibetool stop alpine-1742982443' to stop the server $ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ed1bda951b03 alpine "env" About a minute ago Exited (0) About a minute ago alpine-1742982443 $ docker logs ed1bda951b03 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=ed1bda951b03 FOO=bar FIZZ=buzz MCP_TRANSPORT=stdio HOME=/root ```
1 parent 622b722 commit 9aef236

File tree

10 files changed

+421
-0
lines changed

10 files changed

+421
-0
lines changed

Taskfile.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ tasks:
77
- golangci-lint run ./...
88
- go vet ./...
99

10+
lint-fix:
11+
desc: Run linting tools, and apply fixes.
12+
cmds:
13+
- golangci-lint run --fix ./...
14+
1015
test:
1116
desc: Run tests
1217
cmds:

cmd/vt/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ func init() {
4343
rootCmd.AddCommand(rmCmd)
4444
rootCmd.AddCommand(proxyCmd)
4545
rootCmd.AddCommand(versionCmd)
46+
rootCmd.AddCommand(newSecretCommand())
4647
}
4748

4849
func main() {

cmd/vt/run.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var (
2525
runNoClientConfig bool
2626
runForeground bool
2727
runVolumes []string
28+
runSecrets []string
2829
)
2930

3031
func init() {
@@ -59,6 +60,12 @@ func init() {
5960
[]string{},
6061
"Mount a volume into the container (format: host-path:container-path[:ro])",
6162
)
63+
runCmd.Flags().StringArrayVar(
64+
&runSecrets,
65+
"secret",
66+
[]string{},
67+
"Specify a secret to be fetched from the secrets manager and set as an environment variable (format: NAME,target=TARGET)",
68+
)
6269

6370
// Add OIDC validation flags
6471
AddOIDCFlags(runCmd)
@@ -101,6 +108,7 @@ func runCmdFunc(cmd *cobra.Command, args []string) error {
101108
OIDCClientID: oidcClientID,
102109
Debug: debugMode,
103110
Volumes: runVolumes,
111+
Secrets: runSecrets,
104112
}
105113

106114
// Run the MCP server

cmd/vt/run_common.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/stacklok/vibetool/pkg/networking"
2121
"github.com/stacklok/vibetool/pkg/permissions"
2222
"github.com/stacklok/vibetool/pkg/process"
23+
"github.com/stacklok/vibetool/pkg/secrets"
2324
"github.com/stacklok/vibetool/pkg/transport"
2425
)
2526

@@ -73,6 +74,10 @@ type RunOptions struct {
7374
// Volumes are the directory mounts to pass to the container
7475
// Format: "host-path:container-path[:ro]"
7576
Volumes []string
77+
78+
// Secrets are the secret parameters to pass to the container
79+
// Format: "<secret name>,target=<target environment variable>"
80+
Secrets []string
7681
}
7782

7883
// RunMCPServer runs an MCP server with the specified options
@@ -106,6 +111,22 @@ func RunMCPServer(ctx context.Context, cmd *cobra.Command, options RunOptions) e
106111
// Generate a container name if not provided
107112
containerName, baseName := container.GetOrGenerateContainerName(options.Name, options.Image)
108113

114+
// If any secrets are specified, attempt to load them, and add to list of environment variables.
115+
if len(runSecrets) > 0 {
116+
secretManager, err := secrets.CreateDefaultSecretsManager()
117+
if err != nil {
118+
return fmt.Errorf("error instantiating secret manager %v", err)
119+
}
120+
secretVariables, err := environment.ParseSecretParameters(runSecrets, secretManager)
121+
if err != nil {
122+
return fmt.Errorf("failed to get secrets: %v", err)
123+
}
124+
125+
for key, value := range secretVariables {
126+
runEnv = append(runEnv, fmt.Sprintf("%s=%s", key, value))
127+
}
128+
}
129+
109130
// Parse transport mode
110131
transportType, err := transport.ParseTransportType(options.Transport)
111132
if err != nil {

cmd/vt/secret.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package main
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/stacklok/vibetool/pkg/secrets"
7+
)
8+
9+
func newSecretCommand() *cobra.Command {
10+
cmd := &cobra.Command{
11+
Use: "secret",
12+
Short: "Manage secrets",
13+
Long: "The secret command provides subcommands to set, get, delete, and list secrets.",
14+
}
15+
16+
cmd.AddCommand(
17+
newSecretSetCommand(),
18+
newSecretGetCommand(),
19+
newSecretDeleteCommand(),
20+
newSecretListCommand(),
21+
)
22+
23+
return cmd
24+
}
25+
26+
func newSecretSetCommand() *cobra.Command {
27+
return &cobra.Command{
28+
Use: "set <name> <value>",
29+
Short: "Set a secret",
30+
Args: cobra.ExactArgs(2),
31+
Run: func(cmd *cobra.Command, args []string) {
32+
name, value := args[0], args[1]
33+
34+
// Validate input
35+
if name == "" {
36+
cmd.Println("Error: Secret name cannot be empty")
37+
return
38+
}
39+
40+
manager, err := secrets.CreateDefaultSecretsManager()
41+
if err != nil {
42+
cmd.Printf("Failed to create secrets manager: %v\n", err)
43+
return
44+
}
45+
46+
err = manager.SetSecret(name, value)
47+
if err != nil {
48+
cmd.Printf("Failed to set secret %s: %v\n", name, err)
49+
return
50+
}
51+
cmd.Printf("Secret %s set successfully\n", name)
52+
},
53+
}
54+
}
55+
56+
func newSecretGetCommand() *cobra.Command {
57+
return &cobra.Command{
58+
Use: "get <name>",
59+
Short: "Get a secret",
60+
Args: cobra.ExactArgs(1),
61+
Run: func(cmd *cobra.Command, args []string) {
62+
name := args[0]
63+
64+
// Validate input
65+
if name == "" {
66+
cmd.Println("Error: Secret name cannot be empty")
67+
return
68+
}
69+
70+
manager, err := secrets.CreateDefaultSecretsManager()
71+
if err != nil {
72+
cmd.Printf("Failed to create secrets manager: %v\n", err)
73+
return
74+
}
75+
76+
value, err := manager.GetSecret(name)
77+
if err != nil {
78+
cmd.Printf("Failed to get secret %s: %v\n", name, err)
79+
return
80+
}
81+
cmd.Printf("Secret %s: %s\n", name, value)
82+
},
83+
}
84+
}
85+
86+
func newSecretDeleteCommand() *cobra.Command {
87+
return &cobra.Command{
88+
Use: "delete <name>",
89+
Short: "Delete a secret",
90+
Args: cobra.ExactArgs(1),
91+
Run: func(cmd *cobra.Command, args []string) {
92+
name := args[0]
93+
94+
// Validate input
95+
if name == "" {
96+
cmd.Println("Error: Secret name cannot be empty")
97+
return
98+
}
99+
100+
manager, err := secrets.CreateDefaultSecretsManager()
101+
if err != nil {
102+
cmd.Printf("Failed to create secrets manager: %v\n", err)
103+
return
104+
}
105+
106+
err = manager.DeleteSecret(name)
107+
if err != nil {
108+
cmd.Printf("Failed to delete secret %s: %v\n", name, err)
109+
return
110+
}
111+
cmd.Printf("Secret %s deleted successfully\n", name)
112+
},
113+
}
114+
}
115+
116+
func newSecretListCommand() *cobra.Command {
117+
return &cobra.Command{
118+
Use: "list",
119+
Short: "List all available secrets",
120+
Args: cobra.NoArgs,
121+
Run: func(cmd *cobra.Command, _ []string) {
122+
manager, err := secrets.CreateDefaultSecretsManager()
123+
if err != nil {
124+
cmd.Printf("Failed to create secrets manager: %v\n", err)
125+
return
126+
}
127+
128+
secretNames, err := manager.ListSecrets()
129+
if err != nil {
130+
cmd.Printf("Failed to list secrets: %v\n", err)
131+
return
132+
}
133+
134+
if len(secretNames) == 0 {
135+
cmd.Println("No secrets found")
136+
return
137+
}
138+
139+
cmd.Println("Available secrets:")
140+
for _, name := range secretNames {
141+
cmd.Printf(" - %s\n", name)
142+
}
143+
},
144+
}
145+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313

1414
require (
1515
github.com/Microsoft/go-winio v0.4.14 // indirect
16+
github.com/adrg/xdg v0.5.3 // indirect
1617
github.com/davecgh/go-spew v1.1.1 // indirect
1718
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
1819
github.com/distribution/reference v0.6.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
22
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
3+
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
4+
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
35
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
46
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
57
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=

pkg/environment/environment.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,33 @@ package environment
55
import (
66
"fmt"
77
"strings"
8+
9+
"github.com/stacklok/vibetool/pkg/secrets"
810
)
911

12+
// ParseSecretParameters parses the secret parameters from the command line,
13+
// fetches them from the secrets manager, and returns a map of secrets and
14+
// their environment variable names.
15+
func ParseSecretParameters(parameters []string, secretsManager secrets.Manager) (map[string]string, error) {
16+
secretVariables := make(map[string]string, len(parameters))
17+
18+
for _, param := range parameters {
19+
parameter, err := secrets.ParseSecretParameter(param)
20+
if err != nil {
21+
return nil, err
22+
}
23+
24+
secret, err := secretsManager.GetSecret(parameter.Name)
25+
if err != nil {
26+
return nil, err
27+
}
28+
29+
secretVariables[parameter.Target] = secret
30+
}
31+
32+
return secretVariables, nil
33+
}
34+
1035
// ParseEnvironmentVariables parses environment variables from a slice of strings
1136
// in the format KEY=VALUE
1237
func ParseEnvironmentVariables(envVars []string) (map[string]string, error) {

0 commit comments

Comments
 (0)