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

feat: added credential_process option to config #17

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.0] - 2023-02-14

### Added

- Ability to specify `credential_process` option in the config to source Vault token from an external process

## [0.2.1] - 2020-07-28

### Changed
Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,9 @@ exclude_paths = []
kv_version = ""
# Enables or disables SSL Verification
insecure = false
# External process for sourcing Vault token
# If specified, `token` will be ignored
# credential_process = "get-vault-token -f credential-process"

[list]
# Default path to use for listing
Expand Down Expand Up @@ -327,6 +330,38 @@ As a last option, token can be retrieved from the `~/.vault-token` helper file.

NOTE: If the token are specified in both the vault token helper and configuration files the latter has higher precedence.

### Credential Process

You could also specify an external process for sourcing Vault token with the `credential_process` config option.

```toml
[global]
# Vault address
address = "http://127.0.0.1:8200"
# External process for sourcing Vault token
credential_process = "get-vault-token -f credential-process"
```

The mechanic works in the same way as the sourcing credentials with an external process
[work in AWS](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sourcing-external.html):

- The external process should print the token to stdout in the following format:

```json
{
"Version": "1",
"Token": "a Vault access token"
}
```

- **RVault** will run the process, parse the JSON from stdout, and add the token to the global configuration.
- While running an external process the `VAULT_ADDR` environment variable will be set to its actual value from the config.

The credentials gotten from the external process are not cacheable by design.
So, if you want to cache and renew the token later, you should implement such a logic in the external process.

NOTE: If the `credential_process` option is specified, it takes priority over all other token sources.

## Running in Docker

A Docker image is built with this repository and pushed to Docker Hub
Expand Down
13 changes: 13 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"flag"
"fmt"
"os"
"strings"

"rvault/internal/pkg/credential"

"github.com/spf13/cobra"
"github.com/spf13/pflag"
Expand Down Expand Up @@ -95,6 +98,16 @@ func initConfig() {
klog.V(1).Infof("Using config file: '%s'", viper.ConfigFileUsed())
}

// Try sourcing token with an external credential process
if viper.IsSet("global.credential_process") {
process := viper.GetString("global.credential_process")
token, err := credential.GetToken(strings.Fields(process))
if err != nil {
klog.Exitf("Can't execute credential process '%s': %v", process, err)
}
viper.Set("global.token", token)
}

// Read ~/.vault-token file
if !viper.IsSet("global.token") {
home, err := homedir.Dir()
Expand Down
60 changes: 60 additions & 0 deletions internal/pkg/credential/process.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package credential

import (
"encoding/json"
"fmt"
"os/exec"
"strings"

"github.com/spf13/viper"
)

const (
DefaultVersion = "1"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather unexport this one as it's not consumed outside of the package

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kir4h I see your concerns, but, as far as I can see, it's not violates any security aspects, but allow to refer this values from the other packages if it's required. On the other hand your proposal restricts the usage of this variables within the single package and will require additional efforts to to provides them for extarnal usage if it's required. Maybe I can't see some other rational in your proposal, so could you please elaborate a bit with your point of view.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not security related, more in line of YAGNI principle (the reason I noticed was the code inspector showing a warning for it)

Also In my view exporting a constant makes think that the constant is used elsewhere, and thus a refactor could be a breaking change where in reality that variable was only consumed internally and thus there is no breaking change.

And if at some point we want to export it it´s just a matter of a small refactor to do so, no big effort there at all.

)

var (
ErrNoCommandPassed = fmt.Errorf("expected at least a single command to execute")
)

type (
Data struct {
Version string
Token string
}
)

func GetToken(command []string) (string, error) {
if len(command) == 0 {
return "", ErrNoCommandPassed
}

stdout := new(strings.Builder)
stderr := new(strings.Builder)

cmd := exec.Command(command[0], command[1:]...)
kir4h marked this conversation as resolved.
Show resolved Hide resolved
cmd.Stdout = stdout
cmd.Stderr = stderr
cmd.Env = []string{
fmt.Sprintf("VAULT_ADDR=%s", viper.GetString("global.address")),
}
Comment on lines +38 to +40
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth documenting that this logic will inject VAULT_ADDR environment variable


err := cmd.Run()
if err != nil {
return "", fmt.Errorf("failed to exec process %v: %w, stdout: %s, stderr: %s", command, err, stdout.String(), stderr.String())
}

raw := stdout.String()

var data Data
err = json.Unmarshal([]byte(raw), &data)
if err != nil {
return "", fmt.Errorf("failed to parse process output %s: %w", raw, err)
}

if data.Version != DefaultVersion {
return "", fmt.Errorf("unexpected credentail process format version %s, expected %s", data.Version, DefaultVersion)
}

return data.Token, nil
}
31 changes: 31 additions & 0 deletions internal/pkg/credential/process_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package credential

import (
"errors"
"strings"
"testing"
)

func TestGetToken_SingleCommand(t *testing.T) {
_, err := GetToken([]string{"test"})
if !strings.Contains(err.Error(), "exit status 1") {
t.Errorf("GetToken got error = %v, want exit status 1", err)
}
}
kir4h marked this conversation as resolved.
Show resolved Hide resolved

func TestGetToken_NoCommand(t *testing.T) {
_, err := GetToken([]string{})
if !errors.Is(err, NoCommandPassed) {
t.Errorf("GetToken got error = %v, want NoCommandPassed", err)
}
}

func TestGetToken_HappyPath(t *testing.T) {
token, err := GetToken([]string{"sh", "-c", `echo '{"Version":"1","Token":"123"}'`})
if err != nil {
t.Errorf("GetToken got error = %v, want nil", err)
}
if token != "123" {
t.Errorf("GetToken got token = %v, want 123", token)
}
}