Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(token): implement vault token precendence logic #306

Merged
merged 1 commit into from
Nov 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .golang-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,7 @@ linters:
- gofumpt
- revive
- depguard
- tagalign
- tagalign
- copyloopvar
- intrange
- execinquery
2 changes: 1 addition & 1 deletion cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func (s *VaultSuite) TestMode() {

func TestVaultSuite(t *testing.T) {
// github actions doesn't offer the docker socket, which we need to run this test suite
if runtime.GOOS == "linux" {
if runtime.GOOS != "windows" {
suite.Run(t, new(VaultSuite))
}
}
28 changes: 25 additions & 3 deletions docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

`vkv` supports all of Vaults [environment variables](https://www.vaultproject.io/docs/commands#environment-variables) as well as any configured [Token helpers](https://developer.hashicorp.com/vault/docs/commands/token-helper).

In order to authenticate you will have to set at least `VAULT_ADDR` and `VAULT_TOKEN`.
In order to authenticate you will have to set at least one of the `VAULT_ADDR` or `VKV_LOGIN_COMMAND` and `VAULT_TOKEN` env vars.

## MacOS/Linux
```
Expand All @@ -20,11 +20,33 @@ vkv.exe export --path <KVv2-path>

## Special Env Var `VKV_LOGIN_COMMAND`
For advanced use cases, you can set `VKV_LOGIN_COMMAND`, that way `vkv` will first execute the specified command and use the output of the command as the token.
This is way you dont have to hardcode and set `VAULT_TOKEN`, this is especially useful when using `vkv` in CI. (See Gitlab Integration):
This is way you don't have to hardcode and set `VAULT_TOKEN`, this is especially useful when using `vkv` in CI. (See Gitlab Integration):

Example:

```bash
export VKV_LOGIN_COMMAND="vault write -field=token auth/jwt/login jwt=${CI_JOB_JWT_V2}"
vkv export -p
```
```

## Token Precedence
The following token precedence is applied (from highest to lowest):

1. `VKV_TOKEN`
2. `VKV_LOGIN_COMMAND`
3. [Vault Token Helper](https://developer.hashicorp.com/vault/docs/commands/token-helper), where the token will be written to `~/.vault-token`.

If `vkv` detects **more than one possible token source**, warnings are shown as the following, indicating which token source will be used:

```bash
$> vkv export -p secret
[WARN] More than one token source configured (either VAULT_TOKEN, VKV_LOGIN_COMMAND or ~/.vault-token).
[WARN] See https://falcosuessgott.github.io/vkv/authentication/#token-precedence for vkv's token precedence logic. Disable these warnings with VKV_DISABLE_WARNING.
[INFO] Using VAULT_TOKEN.

secret/ [desc=key/value secret storage] [type=kv2]
└── secret [v=1]
└── key=*****
```

As described, one can disable these warning by setting `VKV_DISABLE_WARNING` to any value.
2 changes: 1 addition & 1 deletion pkg/testutils/testutils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (s *VaultSuite) TestVaultConnection() {

func TestVaultSuite(t *testing.T) {
// github actions doesn't offer the docker socket, which we need to run this test suite
if runtime.GOOS == "linux" {
if runtime.GOOS != "windows" {
suite.Run(t, new(VaultSuite))
}
}
134 changes: 103 additions & 31 deletions pkg/vault/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,66 +18,138 @@

// NewDefaultClient returns a new vault client wrapper.
func NewDefaultClient() (*Vault, error) {
token, err := getToken()
if err != nil {
return nil, err
}

// create vault client using defaults (recommended)
c, err := api.NewClient(nil)
if err != nil {
return nil, err
}

// use tokenhelper if available
c.SetToken(token)

// self lookup current auth for verification
if _, err := c.Auth().Token().LookupSelf(); err != nil {
return nil, fmt.Errorf("not authenticated, perhaps not a valid token: %w", err)
}

return &Vault{Client: c}, nil
}

// NewClient returns a new vault client wrapper.
func NewClient(addr, token string) (*Vault, error) {
cfg := &api.Config{
Address: addr,
}

c, err := api.NewClient(cfg)
if err != nil {
return nil, err
}

Check warning on line 51 in pkg/vault/client.go

View check run for this annotation

Codecov / codecov/patch

pkg/vault/client.go#L50-L51

Added lines #L50 - L51 were not covered by tests

c.SetToken(token)

return &Vault{Client: c}, nil
}

// getToken finds the token configured by the user via env vars or token helpers
// Precedence: 1. VAULT_TOKEN, 2. VKV_LOGIN_COMMAND, 3. Vault Token Helper.
//
//nolint:cyclop
func getToken() (string, error) {
// warn user if more than one is configured
envToken, envTokenOk := os.LookupEnv("VAULT_TOKEN")
tokenCommand, tokenCommandOk := os.LookupEnv("VKV_LOGIN_COMMAND")

th, err := tokenhelper.NewInternalTokenHelper()
if err != nil {
return nil, fmt.Errorf("error creating default token helper: %w", err)
return "", fmt.Errorf("error creating default token helper: %w", err)

Check warning on line 69 in pkg/vault/client.go

View check run for this annotation

Codecov / codecov/patch

pkg/vault/client.go#L69

Added line #L69 was not covered by tests
}

token, err := th.Get()
thToken, err := th.Get()
if err != nil {
return nil, fmt.Errorf("error getting token from default token helper: %w", err)
return "", fmt.Errorf("error getting token from default token helper: %w", err)
}

Check warning on line 75 in pkg/vault/client.go

View check run for this annotation

Codecov / codecov/patch

pkg/vault/client.go#L74-L75

Added lines #L74 - L75 were not covered by tests

var (
// number of tokens configured
tokenSources int

// if we issue a warning to the user, we also want to inform with what token option we went
warn bool
)

if envTokenOk {
tokenSources++
}

if tokenCommandOk {
tokenSources++
}

if token != "" {
c.SetToken(token)
if thToken != "" {
tokenSources++

Check warning on line 94 in pkg/vault/client.go

View check run for this annotation

Codecov / codecov/patch

pkg/vault/client.go#L94

Added line #L94 was not covered by tests
}

// custom: if VKV_LOGIN_COMMAND is set, execute it and set the output as token
cmd, ok := os.LookupEnv("VKV_LOGIN_COMMAND")
if ok && cmd != "" {
cmdParts := strings.Split(cmd, " ")
// check whether user disabled warnings
_, disableWarn := os.LookupEnv("VKV_DISABLE_WARNING")

if tokenSources > 1 {
warn = true

Check warning on line 101 in pkg/vault/client.go

View check run for this annotation

Codecov / codecov/patch

pkg/vault/client.go#L101

Added line #L101 was not covered by tests

token, err := exec.Run(cmdParts)
if err != nil {
return nil, fmt.Errorf("error running VKV_LOGIN_CMD (%s): %w", cmd, err)
if !disableWarn {
fmt.Println("[WARN] More than one token source configured (either VAULT_TOKEN, VKV_LOGIN_COMMAND or ~/.vault-token).")
fmt.Println("[WARN] See https://falcosuessgott.github.io/vkv/authentication/token-precedence for vkv's token precedence logic. Disable these warnings with VKV_DISABLE_WARNING.")

Check warning on line 105 in pkg/vault/client.go

View check run for this annotation

Codecov / codecov/patch

pkg/vault/client.go#L103-L105

Added lines #L103 - L105 were not covered by tests
}
}

vaultToken := strings.TrimSpace(string(token))
if vaultToken == "" {
return nil, errors.New("VKV_LOGIN_COMMAND required but not set")
// if VAULT_TOKEN is set - return it
if envToken != "" {
if warn && !disableWarn {
fmt.Println("[INFO] Using VAULT_TOKEN.")
fmt.Println()

Check warning on line 113 in pkg/vault/client.go

View check run for this annotation

Codecov / codecov/patch

pkg/vault/client.go#L112-L113

Added lines #L112 - L113 were not covered by tests
}

// set token
c.SetToken(vaultToken)
return envToken, nil
}

// self lookup current auth for verification
if _, err := c.Auth().Token().LookupSelf(); err != nil {
return nil, fmt.Errorf("not authenticated, perhaps not a valid token: %w", err)
// if VKV_LOGIN_COMMAND
if tokenCommand != "" {
if warn && !disableWarn {
fmt.Println("[INFO] Using VKV_LOGIN_COMMAND.")
fmt.Println()
}

Check warning on line 124 in pkg/vault/client.go

View check run for this annotation

Codecov / codecov/patch

pkg/vault/client.go#L122-L124

Added lines #L122 - L124 were not covered by tests

return runVaultTokenCommand(tokenCommand)
}

return &Vault{Client: c}, nil
}
if thToken != "" {
if warn && !disableWarn {
fmt.Println("[INFO] Using ~/.vault-token.")
fmt.Println()
}

Check warning on line 133 in pkg/vault/client.go

View check run for this annotation

Codecov / codecov/patch

pkg/vault/client.go#L130-L133

Added lines #L130 - L133 were not covered by tests

// NewClient returns a new vault client wrapper.
func NewClient(addr, token string) (*Vault, error) {
cfg := &api.Config{
Address: addr,
return thToken, nil

Check warning on line 135 in pkg/vault/client.go

View check run for this annotation

Codecov / codecov/patch

pkg/vault/client.go#L135

Added line #L135 was not covered by tests
}

c, err := api.NewClient(cfg)
return "", errors.New("no token provided")
}

func runVaultTokenCommand(cmd string) (string, error) {
cmdParts := strings.Split(cmd, " ")

token, err := exec.Run(cmdParts)
if err != nil {
return nil, err
return "", fmt.Errorf("error running VKV_LOGIN_CMD (%s): %w", cmd, err)

Check warning on line 146 in pkg/vault/client.go

View check run for this annotation

Codecov / codecov/patch

pkg/vault/client.go#L146

Added line #L146 was not covered by tests
}

c.SetToken(token)
vaultToken := strings.TrimSpace(string(token))
if vaultToken == "" {
return "", errors.New("VKV_LOGIN_COMMAND required but not set")
}

Check warning on line 152 in pkg/vault/client.go

View check run for this annotation

Codecov / codecov/patch

pkg/vault/client.go#L151-L152

Added lines #L151 - L152 were not covered by tests

return &Vault{Client: c}, nil
return vaultToken, nil
}
53 changes: 52 additions & 1 deletion pkg/vault/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,57 @@ func (s *VaultSuite) SetupSubTest() {
s.client = v
}

func (s *VaultSuite) TestGetToken() {
testCases := []struct {
name string
envVars map[string]string
expToken string
err bool
}{
{
name: "vault token",
expToken: "token",
envVars: map[string]string{
"VAULT_TOKEN": "token",
},
},
{
name: "vkv login command",
expToken: "testtoken",
envVars: map[string]string{
"VKV_LOGIN_COMMAND": "echo testtoken",
},
},
{
name: "none",
err: true,
},
}

for _, tc := range testCases {
s.Run(tc.name, func() {
// unsetting any local VAULT_TOKEN env var
os.Unsetenv("VAULT_TOKEN")

// set env vars
for k, v := range tc.envVars {
s.T().Setenv(k, v)
}

// invoke token
t, err := getToken()

// assert
if tc.err {
s.Require().Error(err, tc.name)
} else {
s.Require().NoError(err, tc.name)
s.Require().Equal(tc.expToken, t, tc.name)
}
})
}
}

func (s *VaultSuite) TestNewClient() {
testCases := []struct {
name string
Expand Down Expand Up @@ -120,7 +171,7 @@ func (s *VaultSuite) TestNewClient() {
func TestVaultSuite(t *testing.T) {
// github actions doenst offer the docker sock, which we need
// to run this test suite
if runtime.GOOS == "linux" {
if runtime.GOOS != "windows" {
suite.Run(t, new(VaultSuite))
}
}
Loading