Skip to content

Commit

Permalink
fix(token): implement vault token precendence logic
Browse files Browse the repository at this point in the history
  • Loading branch information
FalcoSuessgott committed Nov 3, 2024
1 parent dcb33e2 commit 367b5ad
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 38 deletions.
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 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 @@ type Vault struct {

// 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
}

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)
}

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)
}

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++
}

// 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

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/ for vkv's token precedence logic. Disable these warnings with VKV_DISABLE_WARNING.")
}
}

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()
}

// 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()
}

return runVaultTokenCommand(tokenCommand)
}

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

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

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)
}

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

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))
}
}

0 comments on commit 367b5ad

Please sign in to comment.