mirror of
https://github.com/hashicorp/vault.git
synced 2026-06-09 08:55:13 -04:00
VSI (#4985)
This commit is contained in:
parent
8152811b38
commit
903ab7c485
41 changed files with 4287 additions and 1 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -43,6 +43,8 @@ Vagrantfile
|
|||
|
||||
# Configs
|
||||
*.hcl
|
||||
!command/agent/config/test-fixtures/config.hcl
|
||||
!command/agent/config/test-fixtures/config-embedded-type.hcl
|
||||
|
||||
.DS_Store
|
||||
.idea
|
||||
|
|
|
|||
384
command/agent.go
Normal file
384
command/agent.go
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
log "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/command/agent/auth"
|
||||
"github.com/hashicorp/vault/command/agent/auth/aws"
|
||||
"github.com/hashicorp/vault/command/agent/auth/azure"
|
||||
"github.com/hashicorp/vault/command/agent/auth/gcp"
|
||||
"github.com/hashicorp/vault/command/agent/auth/jwt"
|
||||
"github.com/hashicorp/vault/command/agent/auth/kubernetes"
|
||||
"github.com/hashicorp/vault/command/agent/config"
|
||||
"github.com/hashicorp/vault/command/agent/sink"
|
||||
"github.com/hashicorp/vault/command/agent/sink/file"
|
||||
"github.com/hashicorp/vault/helper/gated-writer"
|
||||
"github.com/hashicorp/vault/helper/logging"
|
||||
"github.com/hashicorp/vault/version"
|
||||
)
|
||||
|
||||
var _ cli.Command = (*AgentCommand)(nil)
|
||||
var _ cli.CommandAutocomplete = (*AgentCommand)(nil)
|
||||
|
||||
type AgentCommand struct {
|
||||
*BaseCommand
|
||||
|
||||
ShutdownCh chan struct{}
|
||||
SighupCh chan struct{}
|
||||
|
||||
logWriter io.Writer
|
||||
logGate *gatedwriter.Writer
|
||||
logger log.Logger
|
||||
|
||||
cleanupGuard sync.Once
|
||||
|
||||
startedCh chan (struct{}) // for tests
|
||||
|
||||
flagConfigs []string
|
||||
flagLogLevel string
|
||||
|
||||
flagTestVerifyOnly bool
|
||||
flagCombineLogs bool
|
||||
}
|
||||
|
||||
func (c *AgentCommand) Synopsis() string {
|
||||
return "Start a Vault agent"
|
||||
}
|
||||
|
||||
func (c *AgentCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: vault agent [options]
|
||||
|
||||
This command starts a Vault agent that can perform automatic authentication
|
||||
in certain environments.
|
||||
|
||||
Start an agent with a configuration file:
|
||||
|
||||
$ vault agent -config=/etc/vault/config.hcl
|
||||
|
||||
For a full list of examples, please see the documentation.
|
||||
|
||||
` + c.Flags().Help()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *AgentCommand) Flags() *FlagSets {
|
||||
set := c.flagSet(FlagSetHTTP)
|
||||
|
||||
f := set.NewFlagSet("Command Options")
|
||||
|
||||
f.StringSliceVar(&StringSliceVar{
|
||||
Name: "config",
|
||||
Target: &c.flagConfigs,
|
||||
Completion: complete.PredictOr(
|
||||
complete.PredictFiles("*.hcl"),
|
||||
complete.PredictFiles("*.json"),
|
||||
),
|
||||
Usage: "Path to a configuration file. This configuration file should " +
|
||||
"contain only agent directives.",
|
||||
})
|
||||
|
||||
f.StringVar(&StringVar{
|
||||
Name: "log-level",
|
||||
Target: &c.flagLogLevel,
|
||||
Default: "info",
|
||||
EnvVar: "VAULT_LOG_LEVEL",
|
||||
Completion: complete.PredictSet("trace", "debug", "info", "warn", "err"),
|
||||
Usage: "Log verbosity level. Supported values (in order of detail) are " +
|
||||
"\"trace\", \"debug\", \"info\", \"warn\", and \"err\".",
|
||||
})
|
||||
|
||||
// Internal-only flags to follow.
|
||||
//
|
||||
// Why hello there little source code reader! Welcome to the Vault source
|
||||
// code. The remaining options are intentionally undocumented and come with
|
||||
// no warranty or backwards-compatability promise. Do not use these flags
|
||||
// in production. Do not build automation using these flags. Unless you are
|
||||
// developing against Vault, you should not need any of these flags.
|
||||
|
||||
// TODO: should the below flags be public?
|
||||
f.BoolVar(&BoolVar{
|
||||
Name: "combine-logs",
|
||||
Target: &c.flagCombineLogs,
|
||||
Default: false,
|
||||
Hidden: true,
|
||||
})
|
||||
|
||||
f.BoolVar(&BoolVar{
|
||||
Name: "test-verify-only",
|
||||
Target: &c.flagTestVerifyOnly,
|
||||
Default: false,
|
||||
Hidden: true,
|
||||
})
|
||||
|
||||
// End internal-only flags.
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *AgentCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *AgentCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *AgentCommand) Run(args []string) int {
|
||||
f := c.Flags()
|
||||
|
||||
if err := f.Parse(args); err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Create a logger. We wrap it in a gated writer so that it doesn't
|
||||
// start logging too early.
|
||||
c.logGate = &gatedwriter.Writer{Writer: os.Stderr}
|
||||
c.logWriter = c.logGate
|
||||
if c.flagCombineLogs {
|
||||
c.logWriter = os.Stdout
|
||||
}
|
||||
var level log.Level
|
||||
c.flagLogLevel = strings.ToLower(strings.TrimSpace(c.flagLogLevel))
|
||||
switch c.flagLogLevel {
|
||||
case "trace":
|
||||
level = log.Trace
|
||||
case "debug":
|
||||
level = log.Debug
|
||||
case "notice", "info", "":
|
||||
level = log.Info
|
||||
case "warn", "warning":
|
||||
level = log.Warn
|
||||
case "err", "error":
|
||||
level = log.Error
|
||||
default:
|
||||
c.UI.Error(fmt.Sprintf("Unknown log level: %s", c.flagLogLevel))
|
||||
return 1
|
||||
}
|
||||
|
||||
c.logger = logging.NewVaultLoggerWithWriter(c.logWriter, level)
|
||||
|
||||
// Validation
|
||||
if len(c.flagConfigs) != 1 {
|
||||
c.UI.Error("Must specify exactly one config path using -config")
|
||||
return 1
|
||||
}
|
||||
|
||||
// Load the configuration
|
||||
config, err := config.LoadConfig(c.flagConfigs[0], c.logger)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error loading configuration from %s: %s", c.flagConfigs[0], err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Ensure at least one config was found.
|
||||
if config == nil {
|
||||
c.UI.Output(wrapAtLength(
|
||||
"No configuration read. Please provide the configuration with the " +
|
||||
"-config flag."))
|
||||
return 1
|
||||
}
|
||||
if config.AutoAuth == nil {
|
||||
c.UI.Error("No auto_auth block found in config file")
|
||||
return 1
|
||||
}
|
||||
|
||||
infoKeys := make([]string, 0, 10)
|
||||
info := make(map[string]string)
|
||||
info["log level"] = c.flagLogLevel
|
||||
infoKeys = append(infoKeys, "log level")
|
||||
|
||||
infoKeys = append(infoKeys, "version")
|
||||
verInfo := version.GetVersion()
|
||||
info["version"] = verInfo.FullVersionNumber(false)
|
||||
if verInfo.Revision != "" {
|
||||
info["version sha"] = strings.Trim(verInfo.Revision, "'")
|
||||
infoKeys = append(infoKeys, "version sha")
|
||||
}
|
||||
infoKeys = append(infoKeys, "cgo")
|
||||
info["cgo"] = "disabled"
|
||||
if version.CgoEnabled {
|
||||
info["cgo"] = "enabled"
|
||||
}
|
||||
|
||||
// Server configuration output
|
||||
padding := 24
|
||||
sort.Strings(infoKeys)
|
||||
c.UI.Output("==> Vault agent configuration:\n")
|
||||
for _, k := range infoKeys {
|
||||
c.UI.Output(fmt.Sprintf(
|
||||
"%s%s: %s",
|
||||
strings.Repeat(" ", padding-len(k)),
|
||||
strings.Title(k),
|
||||
info[k]))
|
||||
}
|
||||
c.UI.Output("")
|
||||
|
||||
// Tests might not want to start a vault server and just want to verify
|
||||
// the configuration.
|
||||
if c.flagTestVerifyOnly {
|
||||
if os.Getenv("VAULT_TEST_VERIFY_ONLY_DUMP_CONFIG") != "" {
|
||||
c.UI.Output(fmt.Sprintf(
|
||||
"\nConfiguration:\n%s\n",
|
||||
pretty.Sprint(*config)))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf(
|
||||
"Error fetching client: %v",
|
||||
err))
|
||||
return 1
|
||||
}
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
|
||||
var sinks []*sink.SinkConfig
|
||||
for _, sc := range config.AutoAuth.Sinks {
|
||||
switch sc.Type {
|
||||
case "file":
|
||||
config := &sink.SinkConfig{
|
||||
Logger: c.logger.Named("sink.file"),
|
||||
Config: sc.Config,
|
||||
Client: client,
|
||||
WrapTTL: sc.WrapTTL,
|
||||
DHType: sc.DHType,
|
||||
DHPath: sc.DHPath,
|
||||
AAD: sc.AAD,
|
||||
}
|
||||
s, err := file.NewFileSink(config)
|
||||
if err != nil {
|
||||
c.UI.Error(errwrap.Wrapf("Error creating file sink: {{err}}", err).Error())
|
||||
return 1
|
||||
}
|
||||
config.Sink = s
|
||||
sinks = append(sinks, config)
|
||||
default:
|
||||
c.UI.Error(fmt.Sprintf("Unknown sink type %q", sc.Type))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
var method auth.AuthMethod
|
||||
authConfig := &auth.AuthConfig{
|
||||
Logger: c.logger.Named(fmt.Sprintf("auth.%s", config.AutoAuth.Method.Type)),
|
||||
MountPath: config.AutoAuth.Method.MountPath,
|
||||
WrapTTL: config.AutoAuth.Method.WrapTTL,
|
||||
Config: config.AutoAuth.Method.Config,
|
||||
}
|
||||
switch config.AutoAuth.Method.Type {
|
||||
case "aws":
|
||||
method, err = aws.NewAWSAuthMethod(authConfig)
|
||||
case "azure":
|
||||
method, err = azure.NewAzureAuthMethod(authConfig)
|
||||
case "gcp":
|
||||
method, err = gcp.NewGCPAuthMethod(authConfig)
|
||||
case "jwt":
|
||||
method, err = jwt.NewJWTAuthMethod(authConfig)
|
||||
case "kubernetes":
|
||||
method, err = kubernetes.NewKubernetesAuthMethod(authConfig)
|
||||
default:
|
||||
c.UI.Error(fmt.Sprintf("Unknown auth method %q", config.AutoAuth.Method.Type))
|
||||
return 1
|
||||
}
|
||||
if err != nil {
|
||||
c.UI.Error(errwrap.Wrapf(fmt.Sprintf("Error creating %s auth method: {{err}}", config.AutoAuth.Method.Type), err).Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Output the header that the server has started
|
||||
if !c.flagCombineLogs {
|
||||
c.UI.Output("==> Vault server started! Log data will stream in below:\n")
|
||||
}
|
||||
|
||||
// Inform any tests that the server is ready
|
||||
select {
|
||||
case c.startedCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
|
||||
ss := sink.NewSinkServer(&sink.SinkServerConfig{
|
||||
Logger: c.logger.Named("sink.server"),
|
||||
Client: client,
|
||||
})
|
||||
|
||||
ah := auth.NewAuthHandler(&auth.AuthHandlerConfig{
|
||||
Logger: c.logger.Named("auth.handler"),
|
||||
Client: c.client,
|
||||
})
|
||||
|
||||
// Start things running
|
||||
go ah.Run(ctx, method)
|
||||
go ss.Run(ctx, ah.OutputCh, sinks)
|
||||
|
||||
// Release the log gate.
|
||||
c.logGate.Flush()
|
||||
|
||||
// Write out the PID to the file now that server has successfully started
|
||||
if err := c.storePidFile(config.PidFile); err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error storing PID: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := c.removePidFile(config.PidFile); err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error deleting the PID file: %s", err))
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-c.ShutdownCh:
|
||||
c.UI.Output("==> Vault agent shutdown triggered")
|
||||
cancelFunc()
|
||||
<-ah.DoneCh
|
||||
<-ss.DoneCh
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// storePidFile is used to write out our PID to a file if necessary
|
||||
func (c *AgentCommand) storePidFile(pidPath string) error {
|
||||
// Quit fast if no pidfile
|
||||
if pidPath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open the PID file
|
||||
pidFile, err := os.OpenFile(pidPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("could not open pid file: {{err}}", err)
|
||||
}
|
||||
defer pidFile.Close()
|
||||
|
||||
// Write out the PID
|
||||
pid := os.Getpid()
|
||||
_, err = pidFile.WriteString(fmt.Sprintf("%d", pid))
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("could not write to pid file: {{err}}", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// removePidFile is used to cleanup the PID file if necessary
|
||||
func (c *AgentCommand) removePidFile(pidPath string) error {
|
||||
if pidPath == "" {
|
||||
return nil
|
||||
}
|
||||
return os.Remove(pidPath)
|
||||
}
|
||||
215
command/agent/auth/auth.go
Normal file
215
command/agent/auth/auth.go
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/helper/jsonutil"
|
||||
)
|
||||
|
||||
type AuthMethod interface {
|
||||
Authenticate(context.Context, *api.Client) (string, map[string]interface{}, error)
|
||||
NewCreds() chan struct{}
|
||||
CredSuccess()
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
Logger hclog.Logger
|
||||
MountPath string
|
||||
WrapTTL time.Duration
|
||||
Config map[string]interface{}
|
||||
}
|
||||
|
||||
// AuthHandler is responsible for keeping a token alive and renewed and passing
|
||||
// new tokens to the sink server
|
||||
type AuthHandler struct {
|
||||
DoneCh chan struct{}
|
||||
OutputCh chan string
|
||||
logger hclog.Logger
|
||||
client *api.Client
|
||||
random *rand.Rand
|
||||
wrapTTL time.Duration
|
||||
}
|
||||
|
||||
type AuthHandlerConfig struct {
|
||||
Logger hclog.Logger
|
||||
Client *api.Client
|
||||
WrapTTL time.Duration
|
||||
}
|
||||
|
||||
func NewAuthHandler(conf *AuthHandlerConfig) *AuthHandler {
|
||||
ah := &AuthHandler{
|
||||
DoneCh: make(chan struct{}),
|
||||
OutputCh: make(chan string),
|
||||
logger: conf.Logger,
|
||||
client: conf.Client,
|
||||
random: rand.New(rand.NewSource(int64(time.Now().Nanosecond()))),
|
||||
wrapTTL: conf.WrapTTL,
|
||||
}
|
||||
|
||||
return ah
|
||||
}
|
||||
|
||||
func backoffOrQuit(ctx context.Context, backoff time.Duration) {
|
||||
select {
|
||||
case <-time.After(backoff):
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
|
||||
func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) {
|
||||
if am == nil {
|
||||
panic("nil auth method")
|
||||
}
|
||||
|
||||
ah.logger.Info("starting auth handler")
|
||||
defer func() {
|
||||
ah.logger.Info("auth handler stopped")
|
||||
close(ah.DoneCh)
|
||||
}()
|
||||
|
||||
credCh := am.NewCreds()
|
||||
if credCh == nil {
|
||||
credCh = make(chan struct{})
|
||||
}
|
||||
|
||||
var renewer *api.Renewer
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
am.Shutdown()
|
||||
return
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
// Create a fresh backoff value
|
||||
backoff := 2*time.Second + time.Duration(ah.random.Int63()%int64(time.Second*2)-int64(time.Second))
|
||||
|
||||
ah.logger.Info("authenticating")
|
||||
path, data, err := am.Authenticate(ctx, ah.client)
|
||||
if err != nil {
|
||||
ah.logger.Error("error getting path or data from method", "error", err, "backoff", backoff.Seconds())
|
||||
backoffOrQuit(ctx, backoff)
|
||||
continue
|
||||
}
|
||||
|
||||
clientToUse := ah.client
|
||||
if ah.wrapTTL > 0 {
|
||||
wrapClient, err := ah.client.Clone()
|
||||
if err != nil {
|
||||
ah.logger.Error("error creating client for wrapped call", "error", err, "backoff", backoff.Seconds())
|
||||
backoffOrQuit(ctx, backoff)
|
||||
continue
|
||||
}
|
||||
wrapClient.SetWrappingLookupFunc(func(string, string) string {
|
||||
return ah.wrapTTL.String()
|
||||
})
|
||||
clientToUse = wrapClient
|
||||
}
|
||||
|
||||
secret, err := clientToUse.Logical().Write(path, data)
|
||||
// Check errors/sanity
|
||||
if err != nil {
|
||||
ah.logger.Error("error authenticating", "error", err, "backoff", backoff.Seconds())
|
||||
backoffOrQuit(ctx, backoff)
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case ah.wrapTTL > 0:
|
||||
if secret.WrapInfo == nil {
|
||||
ah.logger.Error("authentication returned nil wrap info", "backoff", backoff.Seconds())
|
||||
backoffOrQuit(ctx, backoff)
|
||||
continue
|
||||
}
|
||||
if secret.WrapInfo.Token == "" {
|
||||
ah.logger.Error("authentication returned empty wrapped client token", "backoff", backoff.Seconds())
|
||||
backoffOrQuit(ctx, backoff)
|
||||
continue
|
||||
}
|
||||
wrappedResp, err := jsonutil.EncodeJSON(secret.WrapInfo)
|
||||
if err != nil {
|
||||
ah.logger.Error("failed to encode wrapinfo", "error", err, "backoff", backoff.Seconds())
|
||||
backoffOrQuit(ctx, backoff)
|
||||
continue
|
||||
}
|
||||
ah.logger.Info("authentication successful, sending wrapped token to sinks and pausing")
|
||||
ah.OutputCh <- string(wrappedResp)
|
||||
|
||||
am.CredSuccess()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
ah.logger.Info("shutdown triggered")
|
||||
return
|
||||
|
||||
case <-credCh:
|
||||
ah.logger.Info("auth method found new credentials, re-authenticating")
|
||||
continue
|
||||
}
|
||||
|
||||
default:
|
||||
if secret.Auth == nil {
|
||||
ah.logger.Error("authentication returned nil auth info", "backoff", backoff.Seconds())
|
||||
backoffOrQuit(ctx, backoff)
|
||||
continue
|
||||
}
|
||||
if secret.Auth.ClientToken == "" {
|
||||
ah.logger.Error("authentication returned empty client token", "backoff", backoff.Seconds())
|
||||
backoffOrQuit(ctx, backoff)
|
||||
continue
|
||||
}
|
||||
ah.logger.Info("authentication successful, sending token to sinks")
|
||||
ah.OutputCh <- secret.Auth.ClientToken
|
||||
|
||||
am.CredSuccess()
|
||||
}
|
||||
|
||||
if renewer != nil {
|
||||
renewer.Stop()
|
||||
}
|
||||
|
||||
renewer, err = ah.client.NewRenewer(&api.RenewerInput{
|
||||
Secret: secret,
|
||||
})
|
||||
if err != nil {
|
||||
ah.logger.Error("error creating renewer, backing off and retrying", "error", err, "backoff", backoff.Seconds())
|
||||
backoffOrQuit(ctx, backoff)
|
||||
continue
|
||||
}
|
||||
|
||||
// Start the renewal process
|
||||
ah.logger.Info("starting renewal process")
|
||||
go renewer.Renew()
|
||||
|
||||
RenewerLoop:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
ah.logger.Info("shutdown triggered, stopping renewer")
|
||||
renewer.Stop()
|
||||
break RenewerLoop
|
||||
|
||||
case err := <-renewer.DoneCh():
|
||||
ah.logger.Info("renewer done channel triggered")
|
||||
if err != nil {
|
||||
ah.logger.Error("error renewing token", "error", err)
|
||||
}
|
||||
break RenewerLoop
|
||||
|
||||
case <-renewer.RenewCh():
|
||||
ah.logger.Info("renewed auth token")
|
||||
|
||||
case <-credCh:
|
||||
ah.logger.Info("auth method found new credentials, re-authenticating")
|
||||
break RenewerLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
100
command/agent/auth/auth_test.go
Normal file
100
command/agent/auth/auth_test.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
hclog "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/builtin/credential/userpass"
|
||||
"github.com/hashicorp/vault/helper/logging"
|
||||
vaulthttp "github.com/hashicorp/vault/http"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
)
|
||||
|
||||
type userpassTestMethod struct{}
|
||||
|
||||
func newUserpassTestMethod(t *testing.T, client *api.Client) AuthMethod {
|
||||
err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{
|
||||
Type: "userpass",
|
||||
Config: api.AuthConfigInput{
|
||||
DefaultLeaseTTL: "1s",
|
||||
MaxLeaseTTL: "3s",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return &userpassTestMethod{}
|
||||
}
|
||||
|
||||
func (u *userpassTestMethod) Authenticate(_ context.Context, client *api.Client) (string, map[string]interface{}, error) {
|
||||
_, err := client.Logical().Write("auth/userpass/users/foo", map[string]interface{}{
|
||||
"password": "bar",
|
||||
})
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return "auth/userpass/login/foo", map[string]interface{}{
|
||||
"password": "bar",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u *userpassTestMethod) NewCreds() chan struct{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *userpassTestMethod) CredSuccess() {
|
||||
}
|
||||
|
||||
func (u *userpassTestMethod) Shutdown() {
|
||||
}
|
||||
|
||||
func TestAuthHandler(t *testing.T) {
|
||||
logger := logging.NewVaultLogger(hclog.Trace)
|
||||
coreConfig := &vault.CoreConfig{
|
||||
Logger: logger,
|
||||
CredentialBackends: map[string]logical.Factory{
|
||||
"userpass": userpass.Factory,
|
||||
},
|
||||
}
|
||||
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
|
||||
HandlerFunc: vaulthttp.Handler,
|
||||
})
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
|
||||
vault.TestWaitActive(t, cluster.Cores[0].Core)
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
|
||||
ah := NewAuthHandler(&AuthHandlerConfig{
|
||||
Logger: logger.Named("auth.handler"),
|
||||
Client: client,
|
||||
})
|
||||
|
||||
am := newUserpassTestMethod(t, client)
|
||||
go ah.Run(ctx, am)
|
||||
|
||||
// Consume tokens so we don't block
|
||||
stopTime := time.Now().Add(5 * time.Second)
|
||||
closed := false
|
||||
consumption:
|
||||
for {
|
||||
select {
|
||||
case <-ah.OutputCh:
|
||||
// Nothing
|
||||
case <-time.After(stopTime.Sub(time.Now())):
|
||||
if !closed {
|
||||
cancelFunc()
|
||||
closed = true
|
||||
}
|
||||
case <-ah.DoneCh:
|
||||
break consumption
|
||||
}
|
||||
}
|
||||
}
|
||||
207
command/agent/auth/aws/aws.go
Normal file
207
command/agent/auth/aws/aws.go
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
uuid "github.com/hashicorp/go-uuid"
|
||||
"github.com/hashicorp/vault/api"
|
||||
awsauth "github.com/hashicorp/vault/builtin/credential/aws"
|
||||
"github.com/hashicorp/vault/command/agent/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
typeEC2 = "ec2"
|
||||
typeIAM = "iam"
|
||||
identityEndpoint = "http://169.254.169.254/latest/dynamic/instance-identity"
|
||||
)
|
||||
|
||||
type awsMethod struct {
|
||||
logger hclog.Logger
|
||||
authType string
|
||||
nonce string
|
||||
mountPath string
|
||||
role string
|
||||
headerValue string
|
||||
accessKey string
|
||||
secretKey string
|
||||
sessionToken string
|
||||
}
|
||||
|
||||
func NewAWSAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
|
||||
if conf == nil {
|
||||
return nil, errors.New("empty config")
|
||||
}
|
||||
if conf.Config == nil {
|
||||
return nil, errors.New("empty config data")
|
||||
}
|
||||
|
||||
a := &awsMethod{
|
||||
logger: conf.Logger,
|
||||
mountPath: conf.MountPath,
|
||||
}
|
||||
|
||||
typeRaw, ok := conf.Config["type"]
|
||||
if !ok {
|
||||
return nil, errors.New("missing 'type' value")
|
||||
}
|
||||
a.authType, ok = typeRaw.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("could not convert 'type' config value to string")
|
||||
}
|
||||
|
||||
roleRaw, ok := conf.Config["role"]
|
||||
if !ok {
|
||||
return nil, errors.New("missing 'role' value")
|
||||
}
|
||||
a.role, ok = roleRaw.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("could not convert 'role' config value to string")
|
||||
}
|
||||
|
||||
switch {
|
||||
case a.role == "":
|
||||
return nil, errors.New("'role' value is empty")
|
||||
case a.authType == "":
|
||||
return nil, errors.New("'type' value is empty")
|
||||
case a.authType != typeEC2 && a.authType != typeIAM:
|
||||
return nil, errors.New("'type' value is invalid")
|
||||
}
|
||||
|
||||
accessKeyRaw, ok := conf.Config["access_key"]
|
||||
if ok {
|
||||
a.accessKey, ok = accessKeyRaw.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("could not convert 'access_key' value into string")
|
||||
}
|
||||
}
|
||||
|
||||
secretKeyRaw, ok := conf.Config["secret_key"]
|
||||
if ok {
|
||||
a.secretKey, ok = secretKeyRaw.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("could not convert 'secret_key' value into string")
|
||||
}
|
||||
}
|
||||
|
||||
sessionTokenRaw, ok := conf.Config["session_token"]
|
||||
if ok {
|
||||
a.sessionToken, ok = sessionTokenRaw.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("could not convert 'session_token' value into string")
|
||||
}
|
||||
}
|
||||
|
||||
headerValueRaw, ok := conf.Config["header_value"]
|
||||
if ok {
|
||||
a.headerValue, ok = headerValueRaw.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("could not convert 'header_value' value into string")
|
||||
}
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a *awsMethod) Authenticate(ctx context.Context, client *api.Client) (retToken string, retData map[string]interface{}, retErr error) {
|
||||
a.logger.Trace("beginning authentication")
|
||||
|
||||
data := make(map[string]interface{})
|
||||
|
||||
switch a.authType {
|
||||
case typeEC2:
|
||||
client := cleanhttp.DefaultClient()
|
||||
|
||||
// Fetch document
|
||||
{
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/document", identityEndpoint), nil)
|
||||
if err != nil {
|
||||
retErr = errwrap.Wrapf("error creating request: {{err}}", err)
|
||||
return
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
retErr = errwrap.Wrapf("error fetching instance document: {{err}}", err)
|
||||
return
|
||||
}
|
||||
if resp == nil {
|
||||
retErr = errors.New("empty response fetching instance document")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
doc, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
retErr = errwrap.Wrapf("error reading instance document response body: {{err}}", err)
|
||||
return
|
||||
}
|
||||
data["identity"] = base64.StdEncoding.EncodeToString(doc)
|
||||
}
|
||||
|
||||
// Fetch signature
|
||||
{
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/signature", identityEndpoint), nil)
|
||||
if err != nil {
|
||||
retErr = errwrap.Wrapf("error creating request: {{err}}", err)
|
||||
return
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
retErr = errwrap.Wrapf("error fetching instance document signature: {{err}}", err)
|
||||
return
|
||||
}
|
||||
if resp == nil {
|
||||
retErr = errors.New("empty response fetching instance document signature")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
sig, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
retErr = errwrap.Wrapf("error reading instance document signature response body: {{err}}", err)
|
||||
return
|
||||
}
|
||||
data["signature"] = string(sig)
|
||||
}
|
||||
|
||||
// Add the reauthentication value, if we have one
|
||||
if a.nonce == "" {
|
||||
uuid, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
retErr = errwrap.Wrapf("error generating uuid for reauthentication value: {{err}}", err)
|
||||
return
|
||||
}
|
||||
a.nonce = uuid
|
||||
}
|
||||
data["nonce"] = a.nonce
|
||||
|
||||
default:
|
||||
var err error
|
||||
data, err = awsauth.GenerateLoginData(a.accessKey, a.secretKey, a.sessionToken, a.headerValue)
|
||||
if err != nil {
|
||||
retErr = errwrap.Wrapf("error creating login value: {{err}}", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
data["role"] = a.role
|
||||
|
||||
return fmt.Sprintf("%s/login", a.mountPath), data, nil
|
||||
}
|
||||
|
||||
func (a *awsMethod) NewCreds() chan struct{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *awsMethod) CredSuccess() {
|
||||
}
|
||||
|
||||
func (a *awsMethod) Shutdown() {
|
||||
}
|
||||
180
command/agent/auth/azure/azure.go
Normal file
180
command/agent/auth/azure/azure.go
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/command/agent/auth"
|
||||
"github.com/hashicorp/vault/helper/jsonutil"
|
||||
"github.com/hashicorp/vault/helper/useragent"
|
||||
)
|
||||
|
||||
const (
|
||||
instanceEndpoint = "http://169.254.169.254/metadata/instance"
|
||||
identityEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
|
||||
|
||||
// minimum version 2018-02-01 needed for identity metadata
|
||||
// regional availability: https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service
|
||||
apiVersion = "2018-02-01"
|
||||
)
|
||||
|
||||
type azureMethod struct {
|
||||
logger hclog.Logger
|
||||
mountPath string
|
||||
|
||||
role string
|
||||
resource string
|
||||
}
|
||||
|
||||
func NewAzureAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
|
||||
if conf == nil {
|
||||
return nil, errors.New("empty config")
|
||||
}
|
||||
if conf.Config == nil {
|
||||
return nil, errors.New("empty config data")
|
||||
}
|
||||
|
||||
a := &azureMethod{
|
||||
logger: conf.Logger,
|
||||
mountPath: conf.MountPath,
|
||||
}
|
||||
|
||||
roleRaw, ok := conf.Config["role"]
|
||||
if !ok {
|
||||
return nil, errors.New("missing 'role' value")
|
||||
}
|
||||
a.role, ok = roleRaw.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("could not convert 'role' config value to string")
|
||||
}
|
||||
|
||||
resourceRaw, ok := conf.Config["resource"]
|
||||
if !ok {
|
||||
return nil, errors.New("missing 'resource' value")
|
||||
}
|
||||
a.resource, ok = resourceRaw.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("could not convert 'resource' config value to string")
|
||||
}
|
||||
|
||||
switch {
|
||||
case a.role == "":
|
||||
return nil, errors.New("'role' value is empty")
|
||||
case a.resource == "":
|
||||
return nil, errors.New("'resource' value is empty")
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a *azureMethod) Authenticate(ctx context.Context, client *api.Client) (retPath string, retData map[string]interface{}, retErr error) {
|
||||
a.logger.Trace("beginning authentication")
|
||||
|
||||
// Fetch instance data
|
||||
var instance struct {
|
||||
Compute struct {
|
||||
Name string
|
||||
ResourceGroupName string
|
||||
SubscriptionID string
|
||||
VMScaleSetName string
|
||||
}
|
||||
}
|
||||
|
||||
body, err := getMetadataInfo(ctx, instanceEndpoint, "")
|
||||
if err != nil {
|
||||
retErr = err
|
||||
return
|
||||
}
|
||||
|
||||
err = jsonutil.DecodeJSON(body, &instance)
|
||||
if err != nil {
|
||||
retErr = errwrap.Wrapf("error parsing instance metadata response: {{err}}", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch JWT
|
||||
var identity struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
body, err = getMetadataInfo(ctx, identityEndpoint, a.resource)
|
||||
if err != nil {
|
||||
retErr = err
|
||||
return
|
||||
}
|
||||
|
||||
err = jsonutil.DecodeJSON(body, &identity)
|
||||
if err != nil {
|
||||
retErr = errwrap.Wrapf("error parsing identity metadata response: {{err}}", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Attempt login
|
||||
data := map[string]interface{}{
|
||||
"role": a.role,
|
||||
"vm_name": instance.Compute.Name,
|
||||
"vmss_name": instance.Compute.VMScaleSetName,
|
||||
"resource_group_name": instance.Compute.ResourceGroupName,
|
||||
"subscription_id": instance.Compute.SubscriptionID,
|
||||
"jwt": identity.AccessToken,
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/login", a.mountPath), data, nil
|
||||
}
|
||||
|
||||
func (a *azureMethod) NewCreds() chan struct{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *azureMethod) CredSuccess() {
|
||||
}
|
||||
|
||||
func (a *azureMethod) Shutdown() {
|
||||
}
|
||||
|
||||
func getMetadataInfo(ctx context.Context, endpoint, resource string) ([]byte, error) {
|
||||
req, err := http.NewRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("api-version", apiVersion)
|
||||
if resource != "" {
|
||||
q.Add("resource", resource)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
req.Header.Set("Metadata", "true")
|
||||
req.Header.Set("User-Agent", useragent.String())
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
client := cleanhttp.DefaultClient()
|
||||
resp, err := client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(fmt.Sprintf("error fetching metadata from %s: {{err}}", endpoint), err)
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
return nil, fmt.Errorf("empty response fetching metadata from %s", endpoint)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(fmt.Sprintf("error reading metadata from %s: {{err}}", endpoint), err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("error response in metadata from %s: %s", endpoint, body)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
241
command/agent/auth/gcp/gcp.go
Normal file
241
command/agent/auth/gcp/gcp.go
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||
"github.com/hashicorp/go-gcp-common/gcputil"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/command/agent/auth"
|
||||
"github.com/hashicorp/vault/helper/parseutil"
|
||||
"golang.org/x/oauth2"
|
||||
iam "google.golang.org/api/iam/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
typeGCE = "gce"
|
||||
typeIAM = "iam"
|
||||
identityEndpoint = "http://metadata/computeMetadata/v1/instance/service-accounts/%s/identity"
|
||||
defaultIamMaxJwtExpMinutes = 15
|
||||
)
|
||||
|
||||
type gcpMethod struct {
|
||||
logger hclog.Logger
|
||||
authType string
|
||||
mountPath string
|
||||
role string
|
||||
credentials string
|
||||
serviceAccount string
|
||||
project string
|
||||
jwtExp int64
|
||||
}
|
||||
|
||||
func NewGCPAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
|
||||
if conf == nil {
|
||||
return nil, errors.New("empty config")
|
||||
}
|
||||
if conf.Config == nil {
|
||||
return nil, errors.New("empty config data")
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
g := &gcpMethod{
|
||||
logger: conf.Logger,
|
||||
mountPath: conf.MountPath,
|
||||
serviceAccount: "default",
|
||||
}
|
||||
|
||||
typeRaw, ok := conf.Config["type"]
|
||||
if !ok {
|
||||
return nil, errors.New("missing 'type' value")
|
||||
}
|
||||
g.authType, ok = typeRaw.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("could not convert 'type' config value to string")
|
||||
}
|
||||
|
||||
roleRaw, ok := conf.Config["role"]
|
||||
if !ok {
|
||||
return nil, errors.New("missing 'role' value")
|
||||
}
|
||||
g.role, ok = roleRaw.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("could not convert 'role' config value to string")
|
||||
}
|
||||
|
||||
switch {
|
||||
case g.role == "":
|
||||
return nil, errors.New("'role' value is empty")
|
||||
case g.authType == "":
|
||||
return nil, errors.New("'type' value is empty")
|
||||
case g.authType != typeGCE && g.authType != typeIAM:
|
||||
return nil, errors.New("'type' value is invalid")
|
||||
}
|
||||
|
||||
credentialsRaw, ok := conf.Config["credentials"]
|
||||
if ok {
|
||||
g.credentials, ok = credentialsRaw.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("could not convert 'credentials' value into string")
|
||||
}
|
||||
}
|
||||
|
||||
serviceAccountRaw, ok := conf.Config["service_account"]
|
||||
if ok {
|
||||
g.serviceAccount, ok = serviceAccountRaw.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("could not convert 'service_account' value into string")
|
||||
}
|
||||
}
|
||||
|
||||
projectRaw, ok := conf.Config["project"]
|
||||
if ok {
|
||||
g.project, ok = projectRaw.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("could not convert 'project' value into string")
|
||||
}
|
||||
}
|
||||
|
||||
jwtExpRaw, ok := conf.Config["jwt_exp"]
|
||||
if ok {
|
||||
g.jwtExp, err = parseutil.ParseInt(jwtExpRaw)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("error parsing 'jwt_raw' into integer: {{err}}", err)
|
||||
}
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (g *gcpMethod) Authenticate(ctx context.Context, client *api.Client) (retPath string, retData map[string]interface{}, retErr error) {
|
||||
g.logger.Trace("beginning authentication")
|
||||
|
||||
data := make(map[string]interface{})
|
||||
var jwt string
|
||||
|
||||
switch g.authType {
|
||||
case typeGCE:
|
||||
httpClient := cleanhttp.DefaultClient()
|
||||
|
||||
// Fetch token
|
||||
{
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf(identityEndpoint, g.serviceAccount), nil)
|
||||
if err != nil {
|
||||
retErr = errwrap.Wrapf("error creating request: {{err}}", err)
|
||||
return
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Add("Metadata-Flavor", "Google")
|
||||
q := req.URL.Query()
|
||||
q.Add("audience", fmt.Sprintf("%s/vault/%s", client.Address(), g.role))
|
||||
q.Add("format", "full")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
retErr = errwrap.Wrapf("error fetching instance token: {{err}}", err)
|
||||
return
|
||||
}
|
||||
if resp == nil {
|
||||
retErr = errors.New("empty response fetching instance toke")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
jwtBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
retErr = errwrap.Wrapf("error reading instance token response body: {{err}}", err)
|
||||
return
|
||||
}
|
||||
|
||||
jwt = string(jwtBytes)
|
||||
}
|
||||
|
||||
default:
|
||||
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, cleanhttp.DefaultClient())
|
||||
|
||||
credentials, tokenSource, err := gcputil.FindCredentials(g.credentials, ctx, iam.CloudPlatformScope)
|
||||
if err != nil {
|
||||
retErr = errwrap.Wrapf("could not obtain credentials: {{err}}", err)
|
||||
return
|
||||
}
|
||||
|
||||
httpClient := oauth2.NewClient(ctx, tokenSource)
|
||||
|
||||
var serviceAccount string
|
||||
if g.serviceAccount == "" && credentials != nil {
|
||||
serviceAccount = credentials.ClientEmail
|
||||
} else {
|
||||
serviceAccount = g.serviceAccount
|
||||
}
|
||||
if serviceAccount == "" {
|
||||
retErr = errors.New("could not obtain service account from credentials (possibly Application Default Credentials are being used); a service account to authenticate as must be provided")
|
||||
return
|
||||
}
|
||||
|
||||
project := "-"
|
||||
if g.project != "" {
|
||||
project = g.project
|
||||
} else if credentials != nil {
|
||||
project = credentials.ProjectId
|
||||
}
|
||||
|
||||
ttlMin := int64(defaultIamMaxJwtExpMinutes)
|
||||
if g.jwtExp != 0 {
|
||||
ttlMin = g.jwtExp
|
||||
}
|
||||
ttl := time.Minute * time.Duration(ttlMin)
|
||||
|
||||
jwtPayload := map[string]interface{}{
|
||||
"aud": fmt.Sprintf("http://vault/%s", g.role),
|
||||
"sub": serviceAccount,
|
||||
"exp": time.Now().Add(ttl).Unix(),
|
||||
}
|
||||
payloadBytes, err := json.Marshal(jwtPayload)
|
||||
if err != nil {
|
||||
retErr = errwrap.Wrapf("could not convert JWT payload to JSON string: {{err}}", err)
|
||||
return
|
||||
}
|
||||
|
||||
jwtReq := &iam.SignJwtRequest{
|
||||
Payload: string(payloadBytes),
|
||||
}
|
||||
|
||||
iamClient, err := iam.New(httpClient)
|
||||
if err != nil {
|
||||
retErr = errwrap.Wrapf("could not create IAM client: {{err}}", err)
|
||||
return
|
||||
}
|
||||
|
||||
resourceName := fmt.Sprintf("projects/%s/serviceAccounts/%s", project, serviceAccount)
|
||||
resp, err := iamClient.Projects.ServiceAccounts.SignJwt(resourceName, jwtReq).Do()
|
||||
if err != nil {
|
||||
retErr = errwrap.Wrapf(fmt.Sprintf("unable to sign JWT for %s using given Vault credentials: {{err}}", resourceName), err)
|
||||
return
|
||||
}
|
||||
|
||||
jwt = resp.SignedJwt
|
||||
}
|
||||
|
||||
data["role"] = g.role
|
||||
data["jwt"] = jwt
|
||||
|
||||
return fmt.Sprintf("%s/login", g.mountPath), data, nil
|
||||
}
|
||||
|
||||
func (g *gcpMethod) NewCreds() chan struct{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *gcpMethod) CredSuccess() {
|
||||
}
|
||||
|
||||
func (g *gcpMethod) Shutdown() {
|
||||
}
|
||||
184
command/agent/auth/jwt/jwt.go
Normal file
184
command/agent/auth/jwt/jwt.go
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/command/agent/auth"
|
||||
)
|
||||
|
||||
type jwtMethod struct {
|
||||
logger hclog.Logger
|
||||
path string
|
||||
mountPath string
|
||||
role string
|
||||
credsFound chan struct{}
|
||||
watchCh chan string
|
||||
stopCh chan struct{}
|
||||
doneCh chan struct{}
|
||||
credSuccessGate chan struct{}
|
||||
ticker *time.Ticker
|
||||
once *sync.Once
|
||||
latestToken *atomic.Value
|
||||
}
|
||||
|
||||
func NewJWTAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
|
||||
if conf == nil {
|
||||
return nil, errors.New("empty config")
|
||||
}
|
||||
if conf.Config == nil {
|
||||
return nil, errors.New("empty config data")
|
||||
}
|
||||
|
||||
j := &jwtMethod{
|
||||
logger: conf.Logger,
|
||||
mountPath: conf.MountPath,
|
||||
credsFound: make(chan struct{}),
|
||||
watchCh: make(chan string),
|
||||
stopCh: make(chan struct{}),
|
||||
doneCh: make(chan struct{}),
|
||||
credSuccessGate: make(chan struct{}),
|
||||
once: new(sync.Once),
|
||||
latestToken: new(atomic.Value),
|
||||
}
|
||||
j.latestToken.Store("")
|
||||
|
||||
pathRaw, ok := conf.Config["path"]
|
||||
if !ok {
|
||||
return nil, errors.New("missing 'path' value")
|
||||
}
|
||||
j.path, ok = pathRaw.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("could not convert 'path' config value to string")
|
||||
}
|
||||
|
||||
roleRaw, ok := conf.Config["role"]
|
||||
if !ok {
|
||||
return nil, errors.New("missing 'role' value")
|
||||
}
|
||||
j.role, ok = roleRaw.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("could not convert 'role' config value to string")
|
||||
}
|
||||
|
||||
switch {
|
||||
case j.path == "":
|
||||
return nil, errors.New("'path' value is empty")
|
||||
case j.role == "":
|
||||
return nil, errors.New("'role' value is empty")
|
||||
}
|
||||
|
||||
j.ticker = time.NewTicker(500 * time.Millisecond)
|
||||
|
||||
go j.runWatcher()
|
||||
|
||||
j.logger.Info("jwt auth method created", "path", j.path)
|
||||
|
||||
return j, nil
|
||||
}
|
||||
|
||||
func (j *jwtMethod) Authenticate(_ context.Context, client *api.Client) (string, map[string]interface{}, error) {
|
||||
j.logger.Trace("beginning authentication")
|
||||
|
||||
j.ingressToken()
|
||||
|
||||
latestToken := j.latestToken.Load().(string)
|
||||
if latestToken == "" {
|
||||
return "", nil, errors.New("latest known jwt is empty, cannot authenticate")
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/login", j.mountPath), map[string]interface{}{
|
||||
"role": j.role,
|
||||
"jwt": latestToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (j *jwtMethod) NewCreds() chan struct{} {
|
||||
return j.credsFound
|
||||
}
|
||||
|
||||
func (j *jwtMethod) CredSuccess() {
|
||||
j.once.Do(func() {
|
||||
close(j.credSuccessGate)
|
||||
})
|
||||
}
|
||||
|
||||
func (j *jwtMethod) Shutdown() {
|
||||
j.ticker.Stop()
|
||||
close(j.stopCh)
|
||||
<-j.doneCh
|
||||
}
|
||||
|
||||
func (j *jwtMethod) runWatcher() {
|
||||
defer close(j.doneCh)
|
||||
|
||||
select {
|
||||
case <-j.stopCh:
|
||||
return
|
||||
|
||||
case <-j.credSuccessGate:
|
||||
// We only start the next loop once we're initially successful,
|
||||
// since at startup Authenticate will be called and we don't want
|
||||
// to end up immediately reauthenticating by having found a new
|
||||
// value
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-j.stopCh:
|
||||
return
|
||||
|
||||
case <-j.ticker.C:
|
||||
latestToken := j.latestToken.Load().(string)
|
||||
j.ingressToken()
|
||||
newToken := j.latestToken.Load().(string)
|
||||
if newToken != latestToken {
|
||||
j.credsFound <- struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (j *jwtMethod) ingressToken() {
|
||||
fi, err := os.Lstat(j.path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
j.logger.Error("error encountered stat'ing jwt file", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
j.logger.Debug("new jwt file found")
|
||||
|
||||
if !fi.Mode().IsRegular() {
|
||||
j.logger.Error("jwt file is not a regular file")
|
||||
return
|
||||
}
|
||||
|
||||
token, err := ioutil.ReadFile(j.path)
|
||||
if err != nil {
|
||||
j.logger.Error("failed to read jwt file", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch len(token) {
|
||||
case 0:
|
||||
j.logger.Warn("empty jwt file read")
|
||||
|
||||
default:
|
||||
j.latestToken.Store(string(token))
|
||||
}
|
||||
|
||||
if err := os.Remove(j.path); err != nil {
|
||||
j.logger.Error("error removing jwt file", "error", err)
|
||||
}
|
||||
}
|
||||
76
command/agent/auth/kubernetes/kubernetes.go
Normal file
76
command/agent/auth/kubernetes/kubernetes.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/command/agent/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
serviceAccountFile = "var/run/secrets/kubernetes.io/serviceaccount/token"
|
||||
)
|
||||
|
||||
type kubernetesMethod struct {
|
||||
logger hclog.Logger
|
||||
mountPath string
|
||||
|
||||
role string
|
||||
}
|
||||
|
||||
func NewKubernetesAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
|
||||
if conf == nil {
|
||||
return nil, errors.New("empty config")
|
||||
}
|
||||
if conf.Config == nil {
|
||||
return nil, errors.New("empty config data")
|
||||
}
|
||||
|
||||
k := &kubernetesMethod{
|
||||
logger: conf.Logger,
|
||||
mountPath: conf.MountPath,
|
||||
}
|
||||
|
||||
roleRaw, ok := conf.Config["role"]
|
||||
if !ok {
|
||||
return nil, errors.New("missing 'role' value")
|
||||
}
|
||||
k.role, ok = roleRaw.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("could not convert 'role' config value to string")
|
||||
}
|
||||
if k.role == "" {
|
||||
return nil, errors.New("'role' value is empty")
|
||||
}
|
||||
|
||||
return k, nil
|
||||
}
|
||||
|
||||
func (k *kubernetesMethod) Authenticate(ctx context.Context, client *api.Client) (string, map[string]interface{}, error) {
|
||||
k.logger.Trace("beginning authentication")
|
||||
content, err := ioutil.ReadFile(serviceAccountFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/login", k.mountPath), map[string]interface{}{
|
||||
"role": k.role,
|
||||
"jwt": strings.TrimSpace(string(content)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *kubernetesMethod) NewCreds() chan struct{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *kubernetesMethod) CredSuccess() {
|
||||
}
|
||||
|
||||
func (k *kubernetesMethod) Shutdown() {
|
||||
}
|
||||
238
command/agent/config/config.go
Normal file
238
command/agent/config/config.go
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
log "github.com/hashicorp/go-hclog"
|
||||
multierror "github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/vault/helper/parseutil"
|
||||
|
||||
"github.com/hashicorp/hcl"
|
||||
"github.com/hashicorp/hcl/hcl/ast"
|
||||
)
|
||||
|
||||
// Config is the configuration for the vault server.
|
||||
type Config struct {
|
||||
AutoAuth *AutoAuth `hcl:"auto_auth"`
|
||||
PidFile string `hcl:"pid_file"`
|
||||
}
|
||||
|
||||
type AutoAuth struct {
|
||||
Method *Method `hcl:"-"`
|
||||
Sinks []*Sink `hcl:"sinks"`
|
||||
}
|
||||
|
||||
type Method struct {
|
||||
Type string
|
||||
MountPath string `hcl:"mount_path"`
|
||||
WrapTTLRaw interface{} `hcl:"wrap_ttl"`
|
||||
WrapTTL time.Duration `hcl:"-"`
|
||||
Config map[string]interface{}
|
||||
}
|
||||
|
||||
type Sink struct {
|
||||
Type string
|
||||
WrapTTLRaw interface{} `hcl:"wrap_ttl"`
|
||||
WrapTTL time.Duration `hcl:"-"`
|
||||
DHType string `hcl:"dh_type"`
|
||||
DHPath string `hcl:"dh_path"`
|
||||
AAD string `hcl:"aad"`
|
||||
AADEnvVar string `hcl:"aad_env_var"`
|
||||
Config map[string]interface{}
|
||||
}
|
||||
|
||||
// LoadConfig loads the configuration at the given path, regardless if
|
||||
// its a file or directory.
|
||||
func LoadConfig(path string, logger log.Logger) (*Config, error) {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
return nil, fmt.Errorf("location is a directory, not a file")
|
||||
}
|
||||
|
||||
// Read the file
|
||||
d, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse!
|
||||
obj, err := hcl.Parse(string(d))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Start building the result
|
||||
var result Config
|
||||
if err := hcl.DecodeObject(&result, obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list, ok := obj.Node.(*ast.ObjectList)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error parsing: file doesn't contain a root object")
|
||||
}
|
||||
|
||||
if err := parseAutoAuth(&result, list); err != nil {
|
||||
return nil, errwrap.Wrapf("error parsing 'auto_auth': {{err}}", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func parseAutoAuth(result *Config, list *ast.ObjectList) error {
|
||||
name := "auto_auth"
|
||||
|
||||
autoAuthList := list.Filter(name)
|
||||
if len(autoAuthList.Items) != 1 {
|
||||
return fmt.Errorf("one and only one %q block is required", name)
|
||||
}
|
||||
|
||||
// Get our item
|
||||
item := autoAuthList.Items[0]
|
||||
|
||||
var a AutoAuth
|
||||
if err := hcl.DecodeObject(&a, item.Val); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result.AutoAuth = &a
|
||||
|
||||
subs, ok := item.Val.(*ast.ObjectType)
|
||||
if !ok {
|
||||
return fmt.Errorf("could not parse %q as an object", name)
|
||||
}
|
||||
subList := subs.List
|
||||
|
||||
if err := parseMethod(result, subList); err != nil {
|
||||
return errwrap.Wrapf("error parsing 'method': {{err}}", err)
|
||||
}
|
||||
|
||||
if err := parseSinks(result, subList); err != nil {
|
||||
return errwrap.Wrapf("error parsing 'sink' stanzas: {{err}}", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case a.Method == nil:
|
||||
return fmt.Errorf("no 'method' block found")
|
||||
case len(a.Sinks) == 0:
|
||||
return fmt.Errorf("at least one 'sink' block must be provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseMethod(result *Config, list *ast.ObjectList) error {
|
||||
name := "method"
|
||||
|
||||
methodList := list.Filter(name)
|
||||
if len(methodList.Items) != 1 {
|
||||
return fmt.Errorf("one and only one %q block is required", name)
|
||||
}
|
||||
|
||||
// Get our item
|
||||
item := methodList.Items[0]
|
||||
|
||||
var m Method
|
||||
if err := hcl.DecodeObject(&m, item.Val); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if m.Type == "" {
|
||||
if len(item.Keys) == 1 {
|
||||
m.Type = strings.ToLower(item.Keys[0].Token.Value().(string))
|
||||
}
|
||||
if m.Type == "" {
|
||||
return errors.New("method type must be specified")
|
||||
}
|
||||
}
|
||||
|
||||
// Default to Vault's default
|
||||
if m.MountPath == "" {
|
||||
m.MountPath = fmt.Sprintf("auth/%s", m.Type)
|
||||
}
|
||||
// Standardize on no trailing slash
|
||||
m.MountPath = strings.TrimSuffix(m.MountPath, "/")
|
||||
|
||||
if m.WrapTTLRaw != nil {
|
||||
var err error
|
||||
if m.WrapTTL, err = parseutil.ParseDurationSecond(m.WrapTTLRaw); err != nil {
|
||||
return err
|
||||
}
|
||||
m.WrapTTLRaw = nil
|
||||
}
|
||||
|
||||
result.AutoAuth.Method = &m
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseSinks(result *Config, list *ast.ObjectList) error {
|
||||
name := "sink"
|
||||
|
||||
sinkList := list.Filter(name)
|
||||
if len(sinkList.Items) < 1 {
|
||||
return fmt.Errorf("at least one %q block is required", name)
|
||||
}
|
||||
|
||||
var ts []*Sink
|
||||
|
||||
for _, item := range sinkList.Items {
|
||||
var s Sink
|
||||
if err := hcl.DecodeObject(&s, item.Val); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Type == "" {
|
||||
if len(item.Keys) == 1 {
|
||||
s.Type = strings.ToLower(item.Keys[0].Token.Value().(string))
|
||||
}
|
||||
if s.Type == "" {
|
||||
return errors.New("sink type must be specified")
|
||||
}
|
||||
}
|
||||
|
||||
if s.WrapTTLRaw != nil {
|
||||
var err error
|
||||
if s.WrapTTL, err = parseutil.ParseDurationSecond(s.WrapTTLRaw); err != nil {
|
||||
return multierror.Prefix(err, fmt.Sprintf("sink.%s", s.Type))
|
||||
}
|
||||
s.WrapTTLRaw = nil
|
||||
}
|
||||
|
||||
switch s.DHType {
|
||||
case "":
|
||||
case "curve25519":
|
||||
default:
|
||||
return multierror.Prefix(errors.New("invalid value for 'dh_type'"), fmt.Sprintf("sink.%s", s.Type))
|
||||
}
|
||||
|
||||
if s.AADEnvVar != "" {
|
||||
s.AAD = os.Getenv(s.AADEnvVar)
|
||||
s.AADEnvVar = ""
|
||||
}
|
||||
|
||||
switch {
|
||||
case s.DHPath == "" && s.DHType == "":
|
||||
if s.AAD != "" {
|
||||
return multierror.Prefix(errors.New("specifying AAD data without 'dh_type' does not make sense"), fmt.Sprintf("sink.%s", s.Type))
|
||||
}
|
||||
case s.DHPath != "" && s.DHType != "":
|
||||
default:
|
||||
return multierror.Prefix(errors.New("'dh_type' and 'dh_path' must be specified together"), fmt.Sprintf("sink.%s", s.Type))
|
||||
}
|
||||
|
||||
ts = append(ts, &s)
|
||||
}
|
||||
|
||||
result.AutoAuth.Sinks = ts
|
||||
return nil
|
||||
}
|
||||
71
command/agent/config/config_test.go
Normal file
71
command/agent/config/config_test.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
log "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/helper/logging"
|
||||
)
|
||||
|
||||
func TestLoadConfigFile(t *testing.T) {
|
||||
logger := logging.NewVaultLogger(log.Debug)
|
||||
|
||||
os.Setenv("TEST_AAD_ENV", "aad")
|
||||
defer os.Unsetenv("TEST_AAD_ENV")
|
||||
|
||||
config, err := LoadConfig("./test-fixtures/config.hcl", logger)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
expected := &Config{
|
||||
AutoAuth: &AutoAuth{
|
||||
Method: &Method{
|
||||
Type: "aws",
|
||||
WrapTTL: 300 * time.Second,
|
||||
MountPath: "auth/aws",
|
||||
Config: map[string]interface{}{
|
||||
"role": "foobar",
|
||||
},
|
||||
},
|
||||
Sinks: []*Sink{
|
||||
&Sink{
|
||||
Type: "file",
|
||||
DHType: "curve25519",
|
||||
DHPath: "/tmp/file-foo-dhpath",
|
||||
AAD: "foobar",
|
||||
Config: map[string]interface{}{
|
||||
"path": "/tmp/file-foo",
|
||||
},
|
||||
},
|
||||
&Sink{
|
||||
Type: "file",
|
||||
WrapTTL: 5 * time.Minute,
|
||||
DHType: "curve25519",
|
||||
DHPath: "/tmp/file-foo-dhpath2",
|
||||
AAD: "aad",
|
||||
Config: map[string]interface{}{
|
||||
"path": "/tmp/file-bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PidFile: "./pidfile",
|
||||
}
|
||||
|
||||
if diff := deep.Equal(config, expected); diff != nil {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
|
||||
config, err = LoadConfig("./test-fixtures/config-embedded-type.hcl", logger)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if diff := deep.Equal(config, expected); diff != nil {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
30
command/agent/config/test-fixtures/config-embedded-type.hcl
Normal file
30
command/agent/config/test-fixtures/config-embedded-type.hcl
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
pid_file = "./pidfile"
|
||||
|
||||
auto_auth {
|
||||
method "aws" {
|
||||
mount_path = "auth/aws"
|
||||
wrap_ttl = 300
|
||||
config = {
|
||||
role = "foobar"
|
||||
}
|
||||
}
|
||||
|
||||
sink "file" {
|
||||
config = {
|
||||
path = "/tmp/file-foo"
|
||||
}
|
||||
aad = "foobar"
|
||||
dh_type = "curve25519"
|
||||
dh_path = "/tmp/file-foo-dhpath"
|
||||
}
|
||||
|
||||
sink "file" {
|
||||
wrap_ttl = "5m"
|
||||
aad_env_var = "TEST_AAD_ENV"
|
||||
dh_type = "curve25519"
|
||||
dh_path = "/tmp/file-foo-dhpath2"
|
||||
config = {
|
||||
path = "/tmp/file-bar"
|
||||
}
|
||||
}
|
||||
}
|
||||
32
command/agent/config/test-fixtures/config.hcl
Normal file
32
command/agent/config/test-fixtures/config.hcl
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
pid_file = "./pidfile"
|
||||
|
||||
auto_auth {
|
||||
method {
|
||||
type = "aws"
|
||||
wrap_ttl = 300
|
||||
config = {
|
||||
role = "foobar"
|
||||
}
|
||||
}
|
||||
|
||||
sink {
|
||||
type = "file"
|
||||
config = {
|
||||
path = "/tmp/file-foo"
|
||||
}
|
||||
aad = "foobar"
|
||||
dh_type = "curve25519"
|
||||
dh_path = "/tmp/file-foo-dhpath"
|
||||
}
|
||||
|
||||
sink {
|
||||
type = "file"
|
||||
wrap_ttl = "5m"
|
||||
aad_env_var = "TEST_AAD_ENV"
|
||||
dh_type = "curve25519"
|
||||
dh_path = "/tmp/file-foo-dhpath2"
|
||||
config = {
|
||||
path = "/tmp/file-bar"
|
||||
}
|
||||
}
|
||||
}
|
||||
409
command/agent/jwt_end_to_end_test.go
Normal file
409
command/agent/jwt_end_to_end_test.go
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
hclog "github.com/hashicorp/go-hclog"
|
||||
vaultjwt "github.com/hashicorp/vault-plugin-auth-jwt"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/command/agent/auth"
|
||||
agentjwt "github.com/hashicorp/vault/command/agent/auth/jwt"
|
||||
"github.com/hashicorp/vault/command/agent/sink"
|
||||
"github.com/hashicorp/vault/command/agent/sink/file"
|
||||
"github.com/hashicorp/vault/helper/dhutil"
|
||||
"github.com/hashicorp/vault/helper/jsonutil"
|
||||
"github.com/hashicorp/vault/helper/logging"
|
||||
vaulthttp "github.com/hashicorp/vault/http"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
jose "gopkg.in/square/go-jose.v2"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
)
|
||||
|
||||
func getTestJWT(t *testing.T) (string, *ecdsa.PrivateKey) {
|
||||
t.Helper()
|
||||
cl := jwt.Claims{
|
||||
Subject: "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients",
|
||||
Issuer: "https://team-vault.auth0.com/",
|
||||
NotBefore: jwt.NewNumericDate(time.Now().Add(-5 * time.Second)),
|
||||
Audience: jwt.Audience{"https://vault.plugin.auth.jwt.test"},
|
||||
}
|
||||
|
||||
privateCl := struct {
|
||||
User string `json:"https://vault/user"`
|
||||
Groups []string `json:"https://vault/groups"`
|
||||
}{
|
||||
"jeff",
|
||||
[]string{"foo", "bar"},
|
||||
}
|
||||
|
||||
var key *ecdsa.PrivateKey
|
||||
block, _ := pem.Decode([]byte(ecdsaPrivKey))
|
||||
if block != nil {
|
||||
var err error
|
||||
key, err = x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: key}, (&jose.SignerOptions{}).WithType("JWT"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
raw, err := jwt.Signed(sig).Claims(cl).Claims(privateCl).CompactSerialize()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return raw, key
|
||||
}
|
||||
|
||||
func TestJWTEndToEnd(t *testing.T) {
|
||||
testJWTEndToEnd(t, false)
|
||||
testJWTEndToEnd(t, true)
|
||||
}
|
||||
|
||||
func testJWTEndToEnd(t *testing.T, ahWrapping bool) {
|
||||
logger := logging.NewVaultLogger(hclog.Trace)
|
||||
coreConfig := &vault.CoreConfig{
|
||||
Logger: logger,
|
||||
CredentialBackends: map[string]logical.Factory{
|
||||
"jwt": vaultjwt.Factory,
|
||||
},
|
||||
}
|
||||
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
|
||||
HandlerFunc: vaulthttp.Handler,
|
||||
})
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
|
||||
vault.TestWaitActive(t, cluster.Cores[0].Core)
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
// Setup Vault
|
||||
err := client.Sys().EnableAuthWithOptions("jwt", &api.EnableAuthOptions{
|
||||
Type: "jwt",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = client.Logical().Write("auth/jwt/config", map[string]interface{}{
|
||||
"bound_issuer": "https://team-vault.auth0.com/",
|
||||
"jwt_validation_pubkeys": ecdsaPubKey,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = client.Logical().Write("auth/jwt/role/test", map[string]interface{}{
|
||||
"bound_subject": "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients",
|
||||
"bound_audiences": "https://vault.plugin.auth.jwt.test",
|
||||
"user_claim": "https://vault/user",
|
||||
"groups_claim": "https://vault/groups",
|
||||
"policies": "test",
|
||||
"period": "3s",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate encryption params
|
||||
pub, pri, err := dhutil.GeneratePublicPrivateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// We close these right away because we're just basically testing
|
||||
// permissions and finding a usable file name
|
||||
inf, err := ioutil.TempFile("", "auth.jwt.test.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
in := inf.Name()
|
||||
inf.Close()
|
||||
os.Remove(in)
|
||||
t.Logf("input: %s", in)
|
||||
|
||||
ouf, err := ioutil.TempFile("", "auth.tokensink.test.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := ouf.Name()
|
||||
ouf.Close()
|
||||
os.Remove(out)
|
||||
t.Logf("output: %s", out)
|
||||
|
||||
dhpathf, err := ioutil.TempFile("", "auth.dhpath.test.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dhpath := dhpathf.Name()
|
||||
dhpathf.Close()
|
||||
os.Remove(dhpath)
|
||||
|
||||
// Write DH public key to file
|
||||
mPubKey, err := jsonutil.EncodeJSON(&dhutil.PublicKeyInfo{
|
||||
Curve25519PublicKey: pub,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(dhpath, mPubKey, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
logger.Trace("wrote dh param file", "path", dhpath)
|
||||
}
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
timer := time.AfterFunc(30*time.Second, func() {
|
||||
cancelFunc()
|
||||
})
|
||||
defer timer.Stop()
|
||||
|
||||
am, err := agentjwt.NewJWTAuthMethod(&auth.AuthConfig{
|
||||
Logger: logger.Named("auth.jwt"),
|
||||
MountPath: "auth/jwt",
|
||||
Config: map[string]interface{}{
|
||||
"path": in,
|
||||
"role": "test",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ahConfig := &auth.AuthHandlerConfig{
|
||||
Logger: logger.Named("auth.handler"),
|
||||
Client: client,
|
||||
}
|
||||
if ahWrapping {
|
||||
ahConfig.WrapTTL = 10 * time.Second
|
||||
}
|
||||
ah := auth.NewAuthHandler(ahConfig)
|
||||
go ah.Run(ctx, am)
|
||||
defer func() {
|
||||
<-ah.DoneCh
|
||||
}()
|
||||
|
||||
config := &sink.SinkConfig{
|
||||
Logger: logger.Named("sink.file"),
|
||||
AAD: "foobar",
|
||||
DHType: "curve25519",
|
||||
DHPath: dhpath,
|
||||
Config: map[string]interface{}{
|
||||
"path": out,
|
||||
},
|
||||
}
|
||||
if !ahWrapping {
|
||||
config.WrapTTL = 10 * time.Second
|
||||
}
|
||||
fs, err := file.NewFileSink(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config.Sink = fs
|
||||
|
||||
ss := sink.NewSinkServer(&sink.SinkServerConfig{
|
||||
Logger: logger.Named("sink.server"),
|
||||
Client: client,
|
||||
})
|
||||
go ss.Run(ctx, ah.OutputCh, []*sink.SinkConfig{config})
|
||||
defer func() {
|
||||
<-ss.DoneCh
|
||||
}()
|
||||
|
||||
// This has to be after the other defers so it happens first
|
||||
defer cancelFunc()
|
||||
|
||||
// Check that no jwt file exists
|
||||
_, err = os.Lstat(in)
|
||||
if err == nil {
|
||||
t.Fatal("expected err")
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
t.Fatal("expected notexist err")
|
||||
}
|
||||
_, err = os.Lstat(out)
|
||||
if err == nil {
|
||||
t.Fatal("expected err")
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
t.Fatal("expected notexist err")
|
||||
}
|
||||
|
||||
cloned, err := client.Clone()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Get a token
|
||||
jwtToken, _ := getTestJWT(t)
|
||||
if err := ioutil.WriteFile(in, []byte(jwtToken), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
logger.Trace("wrote test jwt", "path", in)
|
||||
}
|
||||
|
||||
checkToken := func() string {
|
||||
timeout := time.Now().Add(5 * time.Second)
|
||||
for {
|
||||
if time.Now().After(timeout) {
|
||||
t.Fatal("did not find a written token after timeout")
|
||||
}
|
||||
val, err := ioutil.ReadFile(out)
|
||||
if err == nil {
|
||||
os.Remove(out)
|
||||
if len(val) == 0 {
|
||||
t.Fatal("written token was empty")
|
||||
}
|
||||
|
||||
// First decrypt it
|
||||
resp := new(dhutil.Envelope)
|
||||
if err := jsonutil.DecodeJSON(val, resp); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
aesKey, err := dhutil.GenerateSharedKey(pri, resp.Curve25519PublicKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(aesKey) == 0 {
|
||||
t.Fatal("got empty aes key")
|
||||
}
|
||||
|
||||
val, err = dhutil.DecryptAES(aesKey, resp.EncryptedPayload, resp.Nonce, []byte("foobar"))
|
||||
if err != nil {
|
||||
t.Fatalf("error: %v\nresp: %v", err, string(val))
|
||||
}
|
||||
|
||||
// Now unwrap it
|
||||
wrapInfo := new(api.SecretWrapInfo)
|
||||
if err := jsonutil.DecodeJSON(val, wrapInfo); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
switch {
|
||||
case wrapInfo.TTL != 10:
|
||||
t.Fatalf("bad wrap info: %v", wrapInfo.TTL)
|
||||
case !ahWrapping && wrapInfo.CreationPath != "sys/wrapping/wrap":
|
||||
t.Fatalf("bad wrap path: %v", wrapInfo.CreationPath)
|
||||
case ahWrapping && wrapInfo.CreationPath != "auth/jwt/login":
|
||||
t.Fatalf("bad wrap path: %v", wrapInfo.CreationPath)
|
||||
case wrapInfo.Token == "":
|
||||
t.Fatal("wrap token is empty")
|
||||
}
|
||||
cloned.SetToken(wrapInfo.Token)
|
||||
secret, err := cloned.Logical().Unwrap("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ahWrapping {
|
||||
switch {
|
||||
case secret.Auth == nil:
|
||||
t.Fatal("unwrap secret auth is nil")
|
||||
case secret.Auth.ClientToken == "":
|
||||
t.Fatal("unwrap token is nil")
|
||||
}
|
||||
return secret.Auth.ClientToken
|
||||
} else {
|
||||
switch {
|
||||
case secret.Data == nil:
|
||||
t.Fatal("unwrap secret data is nil")
|
||||
case secret.Data["token"] == nil:
|
||||
t.Fatal("unwrap token is nil")
|
||||
}
|
||||
return secret.Data["token"].(string)
|
||||
}
|
||||
}
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
origToken := checkToken()
|
||||
|
||||
// We only check this if the renewer is actually renewing for us
|
||||
if !ahWrapping {
|
||||
// Period of 3 seconds, so should still be alive after 7
|
||||
timeout := time.Now().Add(7 * time.Second)
|
||||
cloned.SetToken(origToken)
|
||||
for {
|
||||
if time.Now().After(timeout) {
|
||||
break
|
||||
}
|
||||
secret, err := cloned.Auth().Token().LookupSelf()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ttl, err := secret.Data["ttl"].(json.Number).Int64()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ttl > 3 {
|
||||
t.Fatalf("unexpected ttl: %v", secret.Data["ttl"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get another token to test the backend pushing the need to authenticate
|
||||
// to the handler
|
||||
jwtToken, _ = getTestJWT(t)
|
||||
if err := ioutil.WriteFile(in, []byte(jwtToken), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
newToken := checkToken()
|
||||
if newToken == origToken {
|
||||
t.Fatal("found same token written")
|
||||
}
|
||||
|
||||
if !ahWrapping {
|
||||
// Repeat the period test. At the end the old token should have expired and
|
||||
// the new token should still be alive after 7
|
||||
timeout := time.Now().Add(7 * time.Second)
|
||||
cloned.SetToken(newToken)
|
||||
for {
|
||||
if time.Now().After(timeout) {
|
||||
break
|
||||
}
|
||||
secret, err := cloned.Auth().Token().LookupSelf()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ttl, err := secret.Data["ttl"].(json.Number).Int64()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ttl > 3 {
|
||||
t.Fatalf("unexpected ttl: %v", secret.Data["ttl"])
|
||||
}
|
||||
}
|
||||
|
||||
cloned.SetToken(origToken)
|
||||
_, err = cloned.Auth().Token().LookupSelf()
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
ecdsaPrivKey string = `-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIKfldwWLPYsHjRL9EVTsjSbzTtcGRu6icohNfIqcb6A+oAoGCCqGSM49
|
||||
AwEHoUQDQgAE4+SFvPwOy0miy/FiTT05HnwjpEbSq+7+1q9BFxAkzjgKnlkXk5qx
|
||||
hzXQvRmS4w9ZsskoTZtuUI+XX7conJhzCQ==
|
||||
-----END EC PRIVATE KEY-----`
|
||||
|
||||
ecdsaPubKey string = `-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4+SFvPwOy0miy/FiTT05HnwjpEbS
|
||||
q+7+1q9BFxAkzjgKnlkXk5qxhzXQvRmS4w9ZsskoTZtuUI+XX7conJhzCQ==
|
||||
-----END PUBLIC KEY-----`
|
||||
)
|
||||
112
command/agent/sink/file/file_sink.go
Normal file
112
command/agent/sink/file/file_sink.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
package file
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
hclog "github.com/hashicorp/go-hclog"
|
||||
uuid "github.com/hashicorp/go-uuid"
|
||||
"github.com/hashicorp/vault/command/agent/sink"
|
||||
)
|
||||
|
||||
// fileSink is a Sink implementation that writes a token to a file
|
||||
type fileSink struct {
|
||||
path string
|
||||
logger hclog.Logger
|
||||
}
|
||||
|
||||
// NewFileSink creates a new file sink with the given configuration
|
||||
func NewFileSink(conf *sink.SinkConfig) (sink.Sink, error) {
|
||||
if conf.Logger == nil {
|
||||
return nil, errors.New("nil logger provided")
|
||||
}
|
||||
|
||||
conf.Logger.Info("creating file sink")
|
||||
|
||||
f := &fileSink{
|
||||
logger: conf.Logger,
|
||||
}
|
||||
|
||||
pathRaw, ok := conf.Config["path"]
|
||||
if !ok {
|
||||
return nil, errors.New("'path' not specified for file sink")
|
||||
}
|
||||
path, ok := pathRaw.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("could not parse 'path' as string")
|
||||
}
|
||||
|
||||
f.path = path
|
||||
|
||||
if err := f.WriteToken(""); err != nil {
|
||||
return nil, errwrap.Wrapf("error during write check: {{err}}", err)
|
||||
}
|
||||
|
||||
f.logger.Info("file sink configured", "path", f.path)
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// WriteToken implements the Server interface and writes the token to a path on
|
||||
// disk. It writes into the path's directory into a temp file and does an
|
||||
// atomic rename to ensure consistency. If a blank token is passed in, it
|
||||
// performs a write check but does not write a blank value to the final
|
||||
// location.
|
||||
func (f *fileSink) WriteToken(token string) error {
|
||||
f.logger.Trace("enter write_token", "path", f.path)
|
||||
defer f.logger.Trace("exit write_token", "path", f.path)
|
||||
|
||||
u, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("error generating a uuid during write check: {{err}}", err)
|
||||
}
|
||||
|
||||
targetDir := filepath.Dir(f.path)
|
||||
fileName := filepath.Base(f.path)
|
||||
tmpSuffix := strings.Split(u, "-")[0]
|
||||
|
||||
tmpFile, err := os.OpenFile(filepath.Join(targetDir, fmt.Sprintf("%s.tmp.%s", fileName, tmpSuffix)), os.O_WRONLY|os.O_CREATE, 0640)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(fmt.Sprintf("error opening temp file in dir %s for writing: {{err}}", targetDir), err)
|
||||
}
|
||||
|
||||
valToWrite := token
|
||||
if token == "" {
|
||||
valToWrite = u
|
||||
}
|
||||
|
||||
_, err = tmpFile.WriteString(valToWrite)
|
||||
if err != nil {
|
||||
// Attempt closing and deleting but ignore any error
|
||||
tmpFile.Close()
|
||||
os.Remove(tmpFile.Name())
|
||||
return errwrap.Wrapf(fmt.Sprintf("error writing to %s: {{err}}", tmpFile.Name()), err)
|
||||
}
|
||||
|
||||
err = tmpFile.Close()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(fmt.Sprintf("error closing %s: {{err}}", tmpFile.Name()), err)
|
||||
}
|
||||
|
||||
// Now, if we were just doing a write check (blank token), remove the file
|
||||
// and exit; otherwise, atomically rename it
|
||||
if token == "" {
|
||||
err = os.Remove(tmpFile.Name())
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(fmt.Sprintf("error removing temp file %s during write check: {{err}}", tmpFile.Name()), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
err = os.Rename(tmpFile.Name(), f.path)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(fmt.Sprintf("error renaming temp file %s to target file %s: {{err}}", tmpFile.Name(), f.path), err)
|
||||
}
|
||||
|
||||
f.logger.Info("token written", "path", f.path)
|
||||
return nil
|
||||
}
|
||||
82
command/agent/sink/file/file_sink_test.go
Normal file
82
command/agent/sink/file/file_sink_test.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
hclog "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/hashicorp/vault/command/agent/sink"
|
||||
"github.com/hashicorp/vault/helper/logging"
|
||||
)
|
||||
|
||||
const (
|
||||
fileServerTestDir = "vault-agent-file-test"
|
||||
)
|
||||
|
||||
func testFileSink(t *testing.T, log hclog.Logger) (*sink.SinkConfig, string) {
|
||||
tmpDir, err := ioutil.TempDir("", fmt.Sprintf("%s.", fileServerTestDir))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
path := filepath.Join(tmpDir, "token")
|
||||
|
||||
config := &sink.SinkConfig{
|
||||
Logger: log.Named("sink.file"),
|
||||
Config: map[string]interface{}{
|
||||
"path": path,
|
||||
},
|
||||
}
|
||||
|
||||
s, err := NewFileSink(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config.Sink = s
|
||||
|
||||
return config, tmpDir
|
||||
}
|
||||
|
||||
func TestFileSink(t *testing.T) {
|
||||
log := logging.NewVaultLogger(hclog.Trace)
|
||||
|
||||
fs, tmpDir := testFileSink(t, log)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
path := filepath.Join(tmpDir, "token")
|
||||
|
||||
uuidStr, _ := uuid.GenerateUUID()
|
||||
if err := fs.WriteToken(uuidStr); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fi.Mode() != os.FileMode(0640) {
|
||||
t.Fatalf("wrong file mode was detected at %s", path)
|
||||
}
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fileBytes, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(fileBytes) != uuidStr {
|
||||
t.Fatalf("expected %s, got %s", uuidStr, string(fileBytes))
|
||||
}
|
||||
}
|
||||
121
command/agent/sink/file/sink_test.go
Normal file
121
command/agent/sink/file/sink_test.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
hclog "github.com/hashicorp/go-hclog"
|
||||
uuid "github.com/hashicorp/go-uuid"
|
||||
"github.com/hashicorp/vault/command/agent/sink"
|
||||
"github.com/hashicorp/vault/helper/logging"
|
||||
)
|
||||
|
||||
func TestSinkServer(t *testing.T) {
|
||||
log := logging.NewVaultLogger(hclog.Trace)
|
||||
|
||||
fs1, path1 := testFileSink(t, log)
|
||||
defer os.RemoveAll(path1)
|
||||
fs2, path2 := testFileSink(t, log)
|
||||
defer os.RemoveAll(path2)
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
|
||||
ss := sink.NewSinkServer(&sink.SinkServerConfig{
|
||||
Logger: log.Named("sink.server"),
|
||||
})
|
||||
|
||||
uuidStr, _ := uuid.GenerateUUID()
|
||||
in := make(chan string)
|
||||
sinks := []*sink.SinkConfig{fs1, fs2}
|
||||
go ss.Run(ctx, in, sinks)
|
||||
|
||||
// Seed a token
|
||||
in <- uuidStr
|
||||
|
||||
// Give it time to finish writing
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Tell it to shut down and give it time to do so
|
||||
cancelFunc()
|
||||
<-ss.DoneCh
|
||||
|
||||
for _, path := range []string{path1, path2} {
|
||||
fileBytes, err := ioutil.ReadFile(fmt.Sprintf("%s/token", path))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(fileBytes) != uuidStr {
|
||||
t.Fatalf("expected %s, got %s", uuidStr, string(fileBytes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type badSink struct {
|
||||
tryCount uint32
|
||||
logger hclog.Logger
|
||||
}
|
||||
|
||||
func (b *badSink) WriteToken(token string) error {
|
||||
switch token {
|
||||
case "bad":
|
||||
atomic.AddUint32(&b.tryCount, 1)
|
||||
b.logger.Info("got bad")
|
||||
return errors.New("bad")
|
||||
case "good":
|
||||
atomic.StoreUint32(&b.tryCount, 0)
|
||||
b.logger.Info("got good")
|
||||
return nil
|
||||
default:
|
||||
return errors.New("unknown case")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSinkServerRetry(t *testing.T) {
|
||||
log := logging.NewVaultLogger(hclog.Trace)
|
||||
|
||||
b1 := &badSink{logger: log.Named("b1")}
|
||||
b2 := &badSink{logger: log.Named("b2")}
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
|
||||
ss := sink.NewSinkServer(&sink.SinkServerConfig{
|
||||
Logger: log.Named("sink.server"),
|
||||
})
|
||||
|
||||
in := make(chan string)
|
||||
sinks := []*sink.SinkConfig{&sink.SinkConfig{Sink: b1}, &sink.SinkConfig{Sink: b2}}
|
||||
go ss.Run(ctx, in, sinks)
|
||||
|
||||
// Seed a token
|
||||
in <- "bad"
|
||||
|
||||
// During this time we should see it retry multiple times
|
||||
time.Sleep(10 * time.Second)
|
||||
if atomic.LoadUint32(&b1.tryCount) < 2 {
|
||||
t.Fatal("bad try count")
|
||||
}
|
||||
if atomic.LoadUint32(&b2.tryCount) < 2 {
|
||||
t.Fatal("bad try count")
|
||||
}
|
||||
|
||||
in <- "good"
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
if atomic.LoadUint32(&b1.tryCount) != 0 {
|
||||
t.Fatal("bad try count")
|
||||
}
|
||||
if atomic.LoadUint32(&b2.tryCount) != 0 {
|
||||
t.Fatal("bad try count")
|
||||
}
|
||||
|
||||
// Tell it to shut down and give it time to do so
|
||||
cancelFunc()
|
||||
<-ss.DoneCh
|
||||
}
|
||||
228
command/agent/sink/sink.go
Normal file
228
command/agent/sink/sink.go
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
package sink
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/helper/dhutil"
|
||||
"github.com/hashicorp/vault/helper/jsonutil"
|
||||
)
|
||||
|
||||
type Sink interface {
|
||||
WriteToken(string) error
|
||||
}
|
||||
|
||||
type SinkConfig struct {
|
||||
Sink
|
||||
Logger hclog.Logger
|
||||
Config map[string]interface{}
|
||||
Client *api.Client
|
||||
WrapTTL time.Duration
|
||||
DHType string
|
||||
DHPath string
|
||||
AAD string
|
||||
cachedRemotePubKey []byte
|
||||
cachedPubKey []byte
|
||||
cachedPriKey []byte
|
||||
}
|
||||
|
||||
type SinkServerConfig struct {
|
||||
Logger hclog.Logger
|
||||
Client *api.Client
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
// SinkServer is responsible for pushing tokens to sinks
|
||||
type SinkServer struct {
|
||||
DoneCh chan struct{}
|
||||
logger hclog.Logger
|
||||
client *api.Client
|
||||
random *rand.Rand
|
||||
}
|
||||
|
||||
func NewSinkServer(conf *SinkServerConfig) *SinkServer {
|
||||
ss := &SinkServer{
|
||||
DoneCh: make(chan struct{}),
|
||||
logger: conf.Logger,
|
||||
client: conf.Client,
|
||||
random: rand.New(rand.NewSource(int64(time.Now().Nanosecond()))),
|
||||
}
|
||||
|
||||
return ss
|
||||
}
|
||||
|
||||
// Run executes the server's run loop, which is responsible for reading
|
||||
// in new tokens and pushing them out to the various sinks.
|
||||
func (ss *SinkServer) Run(ctx context.Context, incoming chan string, sinks []*SinkConfig) {
|
||||
if incoming == nil {
|
||||
panic("incoming or shutdown channel are nil")
|
||||
}
|
||||
|
||||
ss.logger.Info("starting sink server")
|
||||
defer func() {
|
||||
ss.logger.Info("sink server stopped")
|
||||
close(ss.DoneCh)
|
||||
}()
|
||||
|
||||
latestToken := new(string)
|
||||
sinkCh := make(chan func() error, len(sinks))
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case token := <-incoming:
|
||||
if token != *latestToken {
|
||||
|
||||
// Drain the existing funcs
|
||||
drainLoop:
|
||||
for {
|
||||
select {
|
||||
case <-sinkCh:
|
||||
default:
|
||||
break drainLoop
|
||||
}
|
||||
}
|
||||
|
||||
*latestToken = token
|
||||
|
||||
for _, s := range sinks {
|
||||
sinkFunc := func(currSink *SinkConfig, currToken string) func() error {
|
||||
return func() error {
|
||||
if currToken != *latestToken {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
|
||||
if currSink.WrapTTL != 0 {
|
||||
if currToken, err = s.wrapToken(ss.client, currSink.WrapTTL, currToken); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if s.DHType != "" {
|
||||
if currToken, err = s.encryptToken(currToken); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return currSink.WriteToken(currToken)
|
||||
}
|
||||
}
|
||||
sinkCh <- sinkFunc(s, token)
|
||||
}
|
||||
}
|
||||
|
||||
case sinkFunc := <-sinkCh:
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
if err := sinkFunc(); err != nil {
|
||||
backoff := 2*time.Second + time.Duration(ss.random.Int63()%int64(time.Second*2)-int64(time.Second))
|
||||
ss.logger.Error("error returned by sink function, retrying", "error", err, "backoff", backoff.String())
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(backoff):
|
||||
sinkCh <- sinkFunc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SinkConfig) encryptToken(token string) (string, error) {
|
||||
var aesKey []byte
|
||||
var err error
|
||||
resp := new(dhutil.Envelope)
|
||||
switch s.DHType {
|
||||
case "curve25519":
|
||||
if len(s.cachedRemotePubKey) == 0 {
|
||||
_, err = os.Lstat(s.DHPath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return "", errwrap.Wrapf("error stat-ing dh parameters file: {{err}}", err)
|
||||
}
|
||||
return "", errors.New("no dh parameters file found, and no cached pub key")
|
||||
}
|
||||
fileBytes, err := ioutil.ReadFile(s.DHPath)
|
||||
if err != nil {
|
||||
return "", errwrap.Wrapf("error reading file for dh parameters: {{err}}", err)
|
||||
}
|
||||
theirPubKey := new(dhutil.PublicKeyInfo)
|
||||
if err := jsonutil.DecodeJSON(fileBytes, theirPubKey); err != nil {
|
||||
return "", errwrap.Wrapf("error decoding public key: {{err}}", err)
|
||||
}
|
||||
if len(theirPubKey.Curve25519PublicKey) == 0 {
|
||||
return "", errors.New("public key is nil")
|
||||
}
|
||||
s.cachedRemotePubKey = theirPubKey.Curve25519PublicKey
|
||||
}
|
||||
if len(s.cachedPubKey) == 0 {
|
||||
s.cachedPubKey, s.cachedPriKey, err = dhutil.GeneratePublicPrivateKey()
|
||||
if err != nil {
|
||||
return "", errwrap.Wrapf("error generating pub/pri curve25519 keys: {{err}}", err)
|
||||
}
|
||||
}
|
||||
resp.Curve25519PublicKey = s.cachedPubKey
|
||||
}
|
||||
|
||||
aesKey, err = dhutil.GenerateSharedKey(s.cachedPriKey, s.cachedRemotePubKey)
|
||||
if err != nil {
|
||||
return "", errwrap.Wrapf("error deriving shared key: {{err}}", err)
|
||||
}
|
||||
if len(aesKey) == 0 {
|
||||
return "", errors.New("derived AES key is empty")
|
||||
}
|
||||
|
||||
resp.EncryptedPayload, resp.Nonce, err = dhutil.EncryptAES(aesKey, []byte(token), []byte(s.AAD))
|
||||
if err != nil {
|
||||
return "", errwrap.Wrapf("error encrypting with shared key: {{err}}", err)
|
||||
}
|
||||
m, err := jsonutil.EncodeJSON(resp)
|
||||
if err != nil {
|
||||
return "", errwrap.Wrapf("error encoding encrypted payload: {{err}}", err)
|
||||
}
|
||||
|
||||
return string(m), nil
|
||||
}
|
||||
|
||||
func (s *SinkConfig) wrapToken(client *api.Client, wrapTTL time.Duration, token string) (string, error) {
|
||||
wrapClient, err := client.Clone()
|
||||
if err != nil {
|
||||
return "", errwrap.Wrapf("error deriving client for wrapping, not writing out to sink: {{err}})", err)
|
||||
}
|
||||
wrapClient.SetToken(token)
|
||||
wrapClient.SetWrappingLookupFunc(func(string, string) string {
|
||||
return wrapTTL.String()
|
||||
})
|
||||
secret, err := wrapClient.Logical().Write("sys/wrapping/wrap", map[string]interface{}{
|
||||
"token": token,
|
||||
})
|
||||
if err != nil {
|
||||
return "", errwrap.Wrapf("error wrapping token, not writing out to sink: {{err}})", err)
|
||||
}
|
||||
if secret == nil {
|
||||
return "", errors.New("nil secret returned, not writing out to sink")
|
||||
}
|
||||
if secret.WrapInfo == nil {
|
||||
return "", errors.New("nil wrap info returned, not writing out to sink")
|
||||
}
|
||||
|
||||
m, err := jsonutil.EncodeJSON(secret.WrapInfo)
|
||||
if err != nil {
|
||||
return "", errwrap.Wrapf("error marshaling token, not writing out to sink: {{err}})", err)
|
||||
}
|
||||
|
||||
return string(m), nil
|
||||
}
|
||||
|
|
@ -230,6 +230,14 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
|||
}
|
||||
|
||||
Commands = map[string]cli.CommandFactory{
|
||||
"agent": func() (cli.Command, error) {
|
||||
return &AgentCommand{
|
||||
BaseCommand: &BaseCommand{
|
||||
UI: serverCmdUi,
|
||||
},
|
||||
ShutdownCh: MakeShutdownCh(),
|
||||
}, nil
|
||||
},
|
||||
"audit": func() (cli.Command, error) {
|
||||
return &AuditCommand{
|
||||
BaseCommand: getBaseCommand(),
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@ var commonCommands = []string{
|
|||
"delete",
|
||||
"list",
|
||||
"login",
|
||||
"agent",
|
||||
"server",
|
||||
"status",
|
||||
"unwrap",
|
||||
|
|
|
|||
121
helper/dhutil/dhutil.go
Normal file
121
helper/dhutil/dhutil.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package dhutil
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
type PublicKeyInfo struct {
|
||||
Curve25519PublicKey []byte `json:"curve25519_public_key"`
|
||||
}
|
||||
|
||||
type Envelope struct {
|
||||
Curve25519PublicKey []byte `json:"curve25519_public_key"`
|
||||
Nonce []byte `json:"nonce"`
|
||||
EncryptedPayload []byte `json:"encrypted_payload"`
|
||||
}
|
||||
|
||||
// generatePublicPrivateKey uses curve25519 to generate a public and private key
|
||||
// pair.
|
||||
func GeneratePublicPrivateKey() ([]byte, []byte, error) {
|
||||
var scalar, public [32]byte
|
||||
|
||||
if _, err := io.ReadFull(rand.Reader, scalar[:]); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
curve25519.ScalarBaseMult(&public, &scalar)
|
||||
return public[:], scalar[:], nil
|
||||
}
|
||||
|
||||
// generateSharedKey uses the private key and the other party's public key to
|
||||
// generate the shared secret.
|
||||
func GenerateSharedKey(ourPrivate, theirPublic []byte) ([]byte, error) {
|
||||
if len(ourPrivate) != 32 {
|
||||
return nil, fmt.Errorf("invalid private key length: %d", len(ourPrivate))
|
||||
}
|
||||
if len(theirPublic) != 32 {
|
||||
return nil, fmt.Errorf("invalid public key length: %d", len(theirPublic))
|
||||
}
|
||||
|
||||
var scalar, pub, secret [32]byte
|
||||
copy(scalar[:], ourPrivate)
|
||||
copy(pub[:], theirPublic)
|
||||
|
||||
curve25519.ScalarMult(&secret, &scalar, &pub)
|
||||
|
||||
return secret[:], nil
|
||||
}
|
||||
|
||||
// Use AES256-GCM to encrypt some plaintext with a provided key. The returned values are
|
||||
// the ciphertext, the nonce, and error respectively.
|
||||
func EncryptAES(key, plaintext, aad []byte) ([]byte, []byte, error) {
|
||||
// We enforce AES-256, so check explicitly for 32 bytes on the key
|
||||
if len(key) != 32 {
|
||||
return nil, nil, fmt.Errorf("invalid key length: %d", len(key))
|
||||
}
|
||||
|
||||
if len(plaintext) == 0 {
|
||||
return nil, nil, errors.New("empty plaintext provided")
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Never use more than 2^32 random nonces with a given key because of the risk of a repeat.
|
||||
nonce := make([]byte, 12)
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
aesgcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ciphertext := aesgcm.Seal(nil, nonce, plaintext, aad)
|
||||
|
||||
return ciphertext, nonce, nil
|
||||
}
|
||||
|
||||
// Use AES256-GCM to decrypt some ciphertext with a provided key and nonce. The
|
||||
// returned values are the plaintext and error respectively.
|
||||
func DecryptAES(key, ciphertext, nonce, aad []byte) ([]byte, error) {
|
||||
// We enforce AES-256, so check explicitly for 32 bytes on the key
|
||||
if len(key) != 32 {
|
||||
return nil, fmt.Errorf("invalid key length: %d", len(key))
|
||||
}
|
||||
|
||||
if len(ciphertext) == 0 {
|
||||
return nil, errors.New("empty ciphertext provided")
|
||||
}
|
||||
|
||||
if len(nonce) == 0 {
|
||||
return nil, errors.New("empty nonce provided")
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
aesgcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, aad)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
21
vendor/github.com/kr/pretty/License
generated
vendored
Normal file
21
vendor/github.com/kr/pretty/License
generated
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright 2012 Keith Rarick
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
9
vendor/github.com/kr/pretty/Readme
generated
vendored
Normal file
9
vendor/github.com/kr/pretty/Readme
generated
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package pretty
|
||||
|
||||
import "github.com/kr/pretty"
|
||||
|
||||
Package pretty provides pretty-printing for Go values.
|
||||
|
||||
Documentation
|
||||
|
||||
http://godoc.org/github.com/kr/pretty
|
||||
265
vendor/github.com/kr/pretty/diff.go
generated
vendored
Normal file
265
vendor/github.com/kr/pretty/diff.go
generated
vendored
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
package pretty
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type sbuf []string
|
||||
|
||||
func (p *sbuf) Printf(format string, a ...interface{}) {
|
||||
s := fmt.Sprintf(format, a...)
|
||||
*p = append(*p, s)
|
||||
}
|
||||
|
||||
// Diff returns a slice where each element describes
|
||||
// a difference between a and b.
|
||||
func Diff(a, b interface{}) (desc []string) {
|
||||
Pdiff((*sbuf)(&desc), a, b)
|
||||
return desc
|
||||
}
|
||||
|
||||
// wprintfer calls Fprintf on w for each Printf call
|
||||
// with a trailing newline.
|
||||
type wprintfer struct{ w io.Writer }
|
||||
|
||||
func (p *wprintfer) Printf(format string, a ...interface{}) {
|
||||
fmt.Fprintf(p.w, format+"\n", a...)
|
||||
}
|
||||
|
||||
// Fdiff writes to w a description of the differences between a and b.
|
||||
func Fdiff(w io.Writer, a, b interface{}) {
|
||||
Pdiff(&wprintfer{w}, a, b)
|
||||
}
|
||||
|
||||
type Printfer interface {
|
||||
Printf(format string, a ...interface{})
|
||||
}
|
||||
|
||||
// Pdiff prints to p a description of the differences between a and b.
|
||||
// It calls Printf once for each difference, with no trailing newline.
|
||||
// The standard library log.Logger is a Printfer.
|
||||
func Pdiff(p Printfer, a, b interface{}) {
|
||||
diffPrinter{w: p}.diff(reflect.ValueOf(a), reflect.ValueOf(b))
|
||||
}
|
||||
|
||||
type Logfer interface {
|
||||
Logf(format string, a ...interface{})
|
||||
}
|
||||
|
||||
// logprintfer calls Fprintf on w for each Printf call
|
||||
// with a trailing newline.
|
||||
type logprintfer struct{ l Logfer }
|
||||
|
||||
func (p *logprintfer) Printf(format string, a ...interface{}) {
|
||||
p.l.Logf(format, a...)
|
||||
}
|
||||
|
||||
// Ldiff prints to l a description of the differences between a and b.
|
||||
// It calls Logf once for each difference, with no trailing newline.
|
||||
// The standard library testing.T and testing.B are Logfers.
|
||||
func Ldiff(l Logfer, a, b interface{}) {
|
||||
Pdiff(&logprintfer{l}, a, b)
|
||||
}
|
||||
|
||||
type diffPrinter struct {
|
||||
w Printfer
|
||||
l string // label
|
||||
}
|
||||
|
||||
func (w diffPrinter) printf(f string, a ...interface{}) {
|
||||
var l string
|
||||
if w.l != "" {
|
||||
l = w.l + ": "
|
||||
}
|
||||
w.w.Printf(l+f, a...)
|
||||
}
|
||||
|
||||
func (w diffPrinter) diff(av, bv reflect.Value) {
|
||||
if !av.IsValid() && bv.IsValid() {
|
||||
w.printf("nil != %# v", formatter{v: bv, quote: true})
|
||||
return
|
||||
}
|
||||
if av.IsValid() && !bv.IsValid() {
|
||||
w.printf("%# v != nil", formatter{v: av, quote: true})
|
||||
return
|
||||
}
|
||||
if !av.IsValid() && !bv.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
at := av.Type()
|
||||
bt := bv.Type()
|
||||
if at != bt {
|
||||
w.printf("%v != %v", at, bt)
|
||||
return
|
||||
}
|
||||
|
||||
switch kind := at.Kind(); kind {
|
||||
case reflect.Bool:
|
||||
if a, b := av.Bool(), bv.Bool(); a != b {
|
||||
w.printf("%v != %v", a, b)
|
||||
}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if a, b := av.Int(), bv.Int(); a != b {
|
||||
w.printf("%d != %d", a, b)
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
if a, b := av.Uint(), bv.Uint(); a != b {
|
||||
w.printf("%d != %d", a, b)
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
if a, b := av.Float(), bv.Float(); a != b {
|
||||
w.printf("%v != %v", a, b)
|
||||
}
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
if a, b := av.Complex(), bv.Complex(); a != b {
|
||||
w.printf("%v != %v", a, b)
|
||||
}
|
||||
case reflect.Array:
|
||||
n := av.Len()
|
||||
for i := 0; i < n; i++ {
|
||||
w.relabel(fmt.Sprintf("[%d]", i)).diff(av.Index(i), bv.Index(i))
|
||||
}
|
||||
case reflect.Chan, reflect.Func, reflect.UnsafePointer:
|
||||
if a, b := av.Pointer(), bv.Pointer(); a != b {
|
||||
w.printf("%#x != %#x", a, b)
|
||||
}
|
||||
case reflect.Interface:
|
||||
w.diff(av.Elem(), bv.Elem())
|
||||
case reflect.Map:
|
||||
ak, both, bk := keyDiff(av.MapKeys(), bv.MapKeys())
|
||||
for _, k := range ak {
|
||||
w := w.relabel(fmt.Sprintf("[%#v]", k))
|
||||
w.printf("%q != (missing)", av.MapIndex(k))
|
||||
}
|
||||
for _, k := range both {
|
||||
w := w.relabel(fmt.Sprintf("[%#v]", k))
|
||||
w.diff(av.MapIndex(k), bv.MapIndex(k))
|
||||
}
|
||||
for _, k := range bk {
|
||||
w := w.relabel(fmt.Sprintf("[%#v]", k))
|
||||
w.printf("(missing) != %q", bv.MapIndex(k))
|
||||
}
|
||||
case reflect.Ptr:
|
||||
switch {
|
||||
case av.IsNil() && !bv.IsNil():
|
||||
w.printf("nil != %# v", formatter{v: bv, quote: true})
|
||||
case !av.IsNil() && bv.IsNil():
|
||||
w.printf("%# v != nil", formatter{v: av, quote: true})
|
||||
case !av.IsNil() && !bv.IsNil():
|
||||
w.diff(av.Elem(), bv.Elem())
|
||||
}
|
||||
case reflect.Slice:
|
||||
lenA := av.Len()
|
||||
lenB := bv.Len()
|
||||
if lenA != lenB {
|
||||
w.printf("%s[%d] != %s[%d]", av.Type(), lenA, bv.Type(), lenB)
|
||||
break
|
||||
}
|
||||
for i := 0; i < lenA; i++ {
|
||||
w.relabel(fmt.Sprintf("[%d]", i)).diff(av.Index(i), bv.Index(i))
|
||||
}
|
||||
case reflect.String:
|
||||
if a, b := av.String(), bv.String(); a != b {
|
||||
w.printf("%q != %q", a, b)
|
||||
}
|
||||
case reflect.Struct:
|
||||
for i := 0; i < av.NumField(); i++ {
|
||||
w.relabel(at.Field(i).Name).diff(av.Field(i), bv.Field(i))
|
||||
}
|
||||
default:
|
||||
panic("unknown reflect Kind: " + kind.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (d diffPrinter) relabel(name string) (d1 diffPrinter) {
|
||||
d1 = d
|
||||
if d.l != "" && name[0] != '[' {
|
||||
d1.l += "."
|
||||
}
|
||||
d1.l += name
|
||||
return d1
|
||||
}
|
||||
|
||||
// keyEqual compares a and b for equality.
|
||||
// Both a and b must be valid map keys.
|
||||
func keyEqual(av, bv reflect.Value) bool {
|
||||
if !av.IsValid() && !bv.IsValid() {
|
||||
return true
|
||||
}
|
||||
if !av.IsValid() || !bv.IsValid() || av.Type() != bv.Type() {
|
||||
return false
|
||||
}
|
||||
switch kind := av.Kind(); kind {
|
||||
case reflect.Bool:
|
||||
a, b := av.Bool(), bv.Bool()
|
||||
return a == b
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
a, b := av.Int(), bv.Int()
|
||||
return a == b
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
a, b := av.Uint(), bv.Uint()
|
||||
return a == b
|
||||
case reflect.Float32, reflect.Float64:
|
||||
a, b := av.Float(), bv.Float()
|
||||
return a == b
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
a, b := av.Complex(), bv.Complex()
|
||||
return a == b
|
||||
case reflect.Array:
|
||||
for i := 0; i < av.Len(); i++ {
|
||||
if !keyEqual(av.Index(i), bv.Index(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Chan, reflect.UnsafePointer, reflect.Ptr:
|
||||
a, b := av.Pointer(), bv.Pointer()
|
||||
return a == b
|
||||
case reflect.Interface:
|
||||
return keyEqual(av.Elem(), bv.Elem())
|
||||
case reflect.String:
|
||||
a, b := av.String(), bv.String()
|
||||
return a == b
|
||||
case reflect.Struct:
|
||||
for i := 0; i < av.NumField(); i++ {
|
||||
if !keyEqual(av.Field(i), bv.Field(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
default:
|
||||
panic("invalid map key type " + av.Type().String())
|
||||
}
|
||||
}
|
||||
|
||||
func keyDiff(a, b []reflect.Value) (ak, both, bk []reflect.Value) {
|
||||
for _, av := range a {
|
||||
inBoth := false
|
||||
for _, bv := range b {
|
||||
if keyEqual(av, bv) {
|
||||
inBoth = true
|
||||
both = append(both, av)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !inBoth {
|
||||
ak = append(ak, av)
|
||||
}
|
||||
}
|
||||
for _, bv := range b {
|
||||
inBoth := false
|
||||
for _, av := range a {
|
||||
if keyEqual(av, bv) {
|
||||
inBoth = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !inBoth {
|
||||
bk = append(bk, bv)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
328
vendor/github.com/kr/pretty/formatter.go
generated
vendored
Normal file
328
vendor/github.com/kr/pretty/formatter.go
generated
vendored
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
package pretty
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/kr/text"
|
||||
)
|
||||
|
||||
type formatter struct {
|
||||
v reflect.Value
|
||||
force bool
|
||||
quote bool
|
||||
}
|
||||
|
||||
// Formatter makes a wrapper, f, that will format x as go source with line
|
||||
// breaks and tabs. Object f responds to the "%v" formatting verb when both the
|
||||
// "#" and " " (space) flags are set, for example:
|
||||
//
|
||||
// fmt.Sprintf("%# v", Formatter(x))
|
||||
//
|
||||
// If one of these two flags is not set, or any other verb is used, f will
|
||||
// format x according to the usual rules of package fmt.
|
||||
// In particular, if x satisfies fmt.Formatter, then x.Format will be called.
|
||||
func Formatter(x interface{}) (f fmt.Formatter) {
|
||||
return formatter{v: reflect.ValueOf(x), quote: true}
|
||||
}
|
||||
|
||||
func (fo formatter) String() string {
|
||||
return fmt.Sprint(fo.v.Interface()) // unwrap it
|
||||
}
|
||||
|
||||
func (fo formatter) passThrough(f fmt.State, c rune) {
|
||||
s := "%"
|
||||
for i := 0; i < 128; i++ {
|
||||
if f.Flag(i) {
|
||||
s += string(i)
|
||||
}
|
||||
}
|
||||
if w, ok := f.Width(); ok {
|
||||
s += fmt.Sprintf("%d", w)
|
||||
}
|
||||
if p, ok := f.Precision(); ok {
|
||||
s += fmt.Sprintf(".%d", p)
|
||||
}
|
||||
s += string(c)
|
||||
fmt.Fprintf(f, s, fo.v.Interface())
|
||||
}
|
||||
|
||||
func (fo formatter) Format(f fmt.State, c rune) {
|
||||
if fo.force || c == 'v' && f.Flag('#') && f.Flag(' ') {
|
||||
w := tabwriter.NewWriter(f, 4, 4, 1, ' ', 0)
|
||||
p := &printer{tw: w, Writer: w, visited: make(map[visit]int)}
|
||||
p.printValue(fo.v, true, fo.quote)
|
||||
w.Flush()
|
||||
return
|
||||
}
|
||||
fo.passThrough(f, c)
|
||||
}
|
||||
|
||||
type printer struct {
|
||||
io.Writer
|
||||
tw *tabwriter.Writer
|
||||
visited map[visit]int
|
||||
depth int
|
||||
}
|
||||
|
||||
func (p *printer) indent() *printer {
|
||||
q := *p
|
||||
q.tw = tabwriter.NewWriter(p.Writer, 4, 4, 1, ' ', 0)
|
||||
q.Writer = text.NewIndentWriter(q.tw, []byte{'\t'})
|
||||
return &q
|
||||
}
|
||||
|
||||
func (p *printer) printInline(v reflect.Value, x interface{}, showType bool) {
|
||||
if showType {
|
||||
io.WriteString(p, v.Type().String())
|
||||
fmt.Fprintf(p, "(%#v)", x)
|
||||
} else {
|
||||
fmt.Fprintf(p, "%#v", x)
|
||||
}
|
||||
}
|
||||
|
||||
// printValue must keep track of already-printed pointer values to avoid
|
||||
// infinite recursion.
|
||||
type visit struct {
|
||||
v uintptr
|
||||
typ reflect.Type
|
||||
}
|
||||
|
||||
func (p *printer) printValue(v reflect.Value, showType, quote bool) {
|
||||
if p.depth > 10 {
|
||||
io.WriteString(p, "!%v(DEPTH EXCEEDED)")
|
||||
return
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Bool:
|
||||
p.printInline(v, v.Bool(), showType)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
p.printInline(v, v.Int(), showType)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
p.printInline(v, v.Uint(), showType)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
p.printInline(v, v.Float(), showType)
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
fmt.Fprintf(p, "%#v", v.Complex())
|
||||
case reflect.String:
|
||||
p.fmtString(v.String(), quote)
|
||||
case reflect.Map:
|
||||
t := v.Type()
|
||||
if showType {
|
||||
io.WriteString(p, t.String())
|
||||
}
|
||||
writeByte(p, '{')
|
||||
if nonzero(v) {
|
||||
expand := !canInline(v.Type())
|
||||
pp := p
|
||||
if expand {
|
||||
writeByte(p, '\n')
|
||||
pp = p.indent()
|
||||
}
|
||||
keys := v.MapKeys()
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
showTypeInStruct := true
|
||||
k := keys[i]
|
||||
mv := v.MapIndex(k)
|
||||
pp.printValue(k, false, true)
|
||||
writeByte(pp, ':')
|
||||
if expand {
|
||||
writeByte(pp, '\t')
|
||||
}
|
||||
showTypeInStruct = t.Elem().Kind() == reflect.Interface
|
||||
pp.printValue(mv, showTypeInStruct, true)
|
||||
if expand {
|
||||
io.WriteString(pp, ",\n")
|
||||
} else if i < v.Len()-1 {
|
||||
io.WriteString(pp, ", ")
|
||||
}
|
||||
}
|
||||
if expand {
|
||||
pp.tw.Flush()
|
||||
}
|
||||
}
|
||||
writeByte(p, '}')
|
||||
case reflect.Struct:
|
||||
t := v.Type()
|
||||
if v.CanAddr() {
|
||||
addr := v.UnsafeAddr()
|
||||
vis := visit{addr, t}
|
||||
if vd, ok := p.visited[vis]; ok && vd < p.depth {
|
||||
p.fmtString(t.String()+"{(CYCLIC REFERENCE)}", false)
|
||||
break // don't print v again
|
||||
}
|
||||
p.visited[vis] = p.depth
|
||||
}
|
||||
|
||||
if showType {
|
||||
io.WriteString(p, t.String())
|
||||
}
|
||||
writeByte(p, '{')
|
||||
if nonzero(v) {
|
||||
expand := !canInline(v.Type())
|
||||
pp := p
|
||||
if expand {
|
||||
writeByte(p, '\n')
|
||||
pp = p.indent()
|
||||
}
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
showTypeInStruct := true
|
||||
if f := t.Field(i); f.Name != "" {
|
||||
io.WriteString(pp, f.Name)
|
||||
writeByte(pp, ':')
|
||||
if expand {
|
||||
writeByte(pp, '\t')
|
||||
}
|
||||
showTypeInStruct = labelType(f.Type)
|
||||
}
|
||||
pp.printValue(getField(v, i), showTypeInStruct, true)
|
||||
if expand {
|
||||
io.WriteString(pp, ",\n")
|
||||
} else if i < v.NumField()-1 {
|
||||
io.WriteString(pp, ", ")
|
||||
}
|
||||
}
|
||||
if expand {
|
||||
pp.tw.Flush()
|
||||
}
|
||||
}
|
||||
writeByte(p, '}')
|
||||
case reflect.Interface:
|
||||
switch e := v.Elem(); {
|
||||
case e.Kind() == reflect.Invalid:
|
||||
io.WriteString(p, "nil")
|
||||
case e.IsValid():
|
||||
pp := *p
|
||||
pp.depth++
|
||||
pp.printValue(e, showType, true)
|
||||
default:
|
||||
io.WriteString(p, v.Type().String())
|
||||
io.WriteString(p, "(nil)")
|
||||
}
|
||||
case reflect.Array, reflect.Slice:
|
||||
t := v.Type()
|
||||
if showType {
|
||||
io.WriteString(p, t.String())
|
||||
}
|
||||
if v.Kind() == reflect.Slice && v.IsNil() && showType {
|
||||
io.WriteString(p, "(nil)")
|
||||
break
|
||||
}
|
||||
if v.Kind() == reflect.Slice && v.IsNil() {
|
||||
io.WriteString(p, "nil")
|
||||
break
|
||||
}
|
||||
writeByte(p, '{')
|
||||
expand := !canInline(v.Type())
|
||||
pp := p
|
||||
if expand {
|
||||
writeByte(p, '\n')
|
||||
pp = p.indent()
|
||||
}
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
showTypeInSlice := t.Elem().Kind() == reflect.Interface
|
||||
pp.printValue(v.Index(i), showTypeInSlice, true)
|
||||
if expand {
|
||||
io.WriteString(pp, ",\n")
|
||||
} else if i < v.Len()-1 {
|
||||
io.WriteString(pp, ", ")
|
||||
}
|
||||
}
|
||||
if expand {
|
||||
pp.tw.Flush()
|
||||
}
|
||||
writeByte(p, '}')
|
||||
case reflect.Ptr:
|
||||
e := v.Elem()
|
||||
if !e.IsValid() {
|
||||
writeByte(p, '(')
|
||||
io.WriteString(p, v.Type().String())
|
||||
io.WriteString(p, ")(nil)")
|
||||
} else {
|
||||
pp := *p
|
||||
pp.depth++
|
||||
writeByte(pp, '&')
|
||||
pp.printValue(e, true, true)
|
||||
}
|
||||
case reflect.Chan:
|
||||
x := v.Pointer()
|
||||
if showType {
|
||||
writeByte(p, '(')
|
||||
io.WriteString(p, v.Type().String())
|
||||
fmt.Fprintf(p, ")(%#v)", x)
|
||||
} else {
|
||||
fmt.Fprintf(p, "%#v", x)
|
||||
}
|
||||
case reflect.Func:
|
||||
io.WriteString(p, v.Type().String())
|
||||
io.WriteString(p, " {...}")
|
||||
case reflect.UnsafePointer:
|
||||
p.printInline(v, v.Pointer(), showType)
|
||||
case reflect.Invalid:
|
||||
io.WriteString(p, "nil")
|
||||
}
|
||||
}
|
||||
|
||||
func canInline(t reflect.Type) bool {
|
||||
switch t.Kind() {
|
||||
case reflect.Map:
|
||||
return !canExpand(t.Elem())
|
||||
case reflect.Struct:
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
if canExpand(t.Field(i).Type) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Interface:
|
||||
return false
|
||||
case reflect.Array, reflect.Slice:
|
||||
return !canExpand(t.Elem())
|
||||
case reflect.Ptr:
|
||||
return false
|
||||
case reflect.Chan, reflect.Func, reflect.UnsafePointer:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func canExpand(t reflect.Type) bool {
|
||||
switch t.Kind() {
|
||||
case reflect.Map, reflect.Struct,
|
||||
reflect.Interface, reflect.Array, reflect.Slice,
|
||||
reflect.Ptr:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func labelType(t reflect.Type) bool {
|
||||
switch t.Kind() {
|
||||
case reflect.Interface, reflect.Struct:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *printer) fmtString(s string, quote bool) {
|
||||
if quote {
|
||||
s = strconv.Quote(s)
|
||||
}
|
||||
io.WriteString(p, s)
|
||||
}
|
||||
|
||||
func writeByte(w io.Writer, b byte) {
|
||||
w.Write([]byte{b})
|
||||
}
|
||||
|
||||
func getField(v reflect.Value, i int) reflect.Value {
|
||||
val := v.Field(i)
|
||||
if val.Kind() == reflect.Interface && !val.IsNil() {
|
||||
val = val.Elem()
|
||||
}
|
||||
return val
|
||||
}
|
||||
3
vendor/github.com/kr/pretty/go.mod
generated
vendored
Normal file
3
vendor/github.com/kr/pretty/go.mod
generated
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module "github.com/kr/pretty"
|
||||
|
||||
require "github.com/kr/text" v0.1.0
|
||||
108
vendor/github.com/kr/pretty/pretty.go
generated
vendored
Normal file
108
vendor/github.com/kr/pretty/pretty.go
generated
vendored
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// Package pretty provides pretty-printing for Go values. This is
|
||||
// useful during debugging, to avoid wrapping long output lines in
|
||||
// the terminal.
|
||||
//
|
||||
// It provides a function, Formatter, that can be used with any
|
||||
// function that accepts a format string. It also provides
|
||||
// convenience wrappers for functions in packages fmt and log.
|
||||
package pretty
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Errorf is a convenience wrapper for fmt.Errorf.
|
||||
//
|
||||
// Calling Errorf(f, x, y) is equivalent to
|
||||
// fmt.Errorf(f, Formatter(x), Formatter(y)).
|
||||
func Errorf(format string, a ...interface{}) error {
|
||||
return fmt.Errorf(format, wrap(a, false)...)
|
||||
}
|
||||
|
||||
// Fprintf is a convenience wrapper for fmt.Fprintf.
|
||||
//
|
||||
// Calling Fprintf(w, f, x, y) is equivalent to
|
||||
// fmt.Fprintf(w, f, Formatter(x), Formatter(y)).
|
||||
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, error error) {
|
||||
return fmt.Fprintf(w, format, wrap(a, false)...)
|
||||
}
|
||||
|
||||
// Log is a convenience wrapper for log.Printf.
|
||||
//
|
||||
// Calling Log(x, y) is equivalent to
|
||||
// log.Print(Formatter(x), Formatter(y)), but each operand is
|
||||
// formatted with "%# v".
|
||||
func Log(a ...interface{}) {
|
||||
log.Print(wrap(a, true)...)
|
||||
}
|
||||
|
||||
// Logf is a convenience wrapper for log.Printf.
|
||||
//
|
||||
// Calling Logf(f, x, y) is equivalent to
|
||||
// log.Printf(f, Formatter(x), Formatter(y)).
|
||||
func Logf(format string, a ...interface{}) {
|
||||
log.Printf(format, wrap(a, false)...)
|
||||
}
|
||||
|
||||
// Logln is a convenience wrapper for log.Printf.
|
||||
//
|
||||
// Calling Logln(x, y) is equivalent to
|
||||
// log.Println(Formatter(x), Formatter(y)), but each operand is
|
||||
// formatted with "%# v".
|
||||
func Logln(a ...interface{}) {
|
||||
log.Println(wrap(a, true)...)
|
||||
}
|
||||
|
||||
// Print pretty-prints its operands and writes to standard output.
|
||||
//
|
||||
// Calling Print(x, y) is equivalent to
|
||||
// fmt.Print(Formatter(x), Formatter(y)), but each operand is
|
||||
// formatted with "%# v".
|
||||
func Print(a ...interface{}) (n int, errno error) {
|
||||
return fmt.Print(wrap(a, true)...)
|
||||
}
|
||||
|
||||
// Printf is a convenience wrapper for fmt.Printf.
|
||||
//
|
||||
// Calling Printf(f, x, y) is equivalent to
|
||||
// fmt.Printf(f, Formatter(x), Formatter(y)).
|
||||
func Printf(format string, a ...interface{}) (n int, errno error) {
|
||||
return fmt.Printf(format, wrap(a, false)...)
|
||||
}
|
||||
|
||||
// Println pretty-prints its operands and writes to standard output.
|
||||
//
|
||||
// Calling Print(x, y) is equivalent to
|
||||
// fmt.Println(Formatter(x), Formatter(y)), but each operand is
|
||||
// formatted with "%# v".
|
||||
func Println(a ...interface{}) (n int, errno error) {
|
||||
return fmt.Println(wrap(a, true)...)
|
||||
}
|
||||
|
||||
// Sprint is a convenience wrapper for fmt.Sprintf.
|
||||
//
|
||||
// Calling Sprint(x, y) is equivalent to
|
||||
// fmt.Sprint(Formatter(x), Formatter(y)), but each operand is
|
||||
// formatted with "%# v".
|
||||
func Sprint(a ...interface{}) string {
|
||||
return fmt.Sprint(wrap(a, true)...)
|
||||
}
|
||||
|
||||
// Sprintf is a convenience wrapper for fmt.Sprintf.
|
||||
//
|
||||
// Calling Sprintf(f, x, y) is equivalent to
|
||||
// fmt.Sprintf(f, Formatter(x), Formatter(y)).
|
||||
func Sprintf(format string, a ...interface{}) string {
|
||||
return fmt.Sprintf(format, wrap(a, false)...)
|
||||
}
|
||||
|
||||
func wrap(a []interface{}, force bool) []interface{} {
|
||||
w := make([]interface{}, len(a))
|
||||
for i, x := range a {
|
||||
w[i] = formatter{v: reflect.ValueOf(x), force: force}
|
||||
}
|
||||
return w
|
||||
}
|
||||
41
vendor/github.com/kr/pretty/zero.go
generated
vendored
Normal file
41
vendor/github.com/kr/pretty/zero.go
generated
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package pretty
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func nonzero(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.Bool:
|
||||
return v.Bool()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return v.Int() != 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return v.Uint() != 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return v.Float() != 0
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
return v.Complex() != complex(0, 0)
|
||||
case reflect.String:
|
||||
return v.String() != ""
|
||||
case reflect.Struct:
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
if nonzero(getField(v, i)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
case reflect.Array:
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if nonzero(v.Index(i)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
case reflect.Map, reflect.Interface, reflect.Slice, reflect.Ptr, reflect.Chan, reflect.Func:
|
||||
return !v.IsNil()
|
||||
case reflect.UnsafePointer:
|
||||
return v.Pointer() != 0
|
||||
}
|
||||
return true
|
||||
}
|
||||
6
vendor/vendor.json
vendored
6
vendor/vendor.json
vendored
|
|
@ -1494,6 +1494,12 @@
|
|||
"revision": "670ebd3adf7a737d69ffe83a777a8e34eadc1b32",
|
||||
"revisionTime": "2018-06-27T17:25:17Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "3ohk4dFYrERZ6WTdKkIwnTA0HSI=",
|
||||
"path": "github.com/kr/pretty",
|
||||
"revision": "73f6ac0b30a98e433b289500d779f50c1a6f0712",
|
||||
"revisionTime": "2018-05-06T08:33:45Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "SbguDK5lY8uhaHNrmJmNbiWIGM0=",
|
||||
"path": "github.com/kr/text",
|
||||
|
|
|
|||
161
website/source/docs/agent/autoauth/index.html.md
Normal file
161
website/source/docs/agent/autoauth/index.html.md
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Vault Agent Auto-Auth"
|
||||
sidebar_current: "docs-agent-autoauth"
|
||||
description: |-
|
||||
Vault Agent's Auto-Auth functionality allows easy and automatic
|
||||
authentication to Vault in a variety of environments.
|
||||
---
|
||||
|
||||
# Vault Agent Auto-Auth
|
||||
|
||||
The Auto-Auth functionality of Vault Agent allows for easy authentication in a
|
||||
wide variety of environments.
|
||||
|
||||
## Functionality
|
||||
|
||||
Auto-Auth consists of two parts: a Method, which is the authentication method
|
||||
that should be used in the current environment; and one or more Sinks, which
|
||||
are locations where the agent should write a token any time the current token
|
||||
value has changed.
|
||||
|
||||
When the agent is started with Auto-Auth enabled, it will attempt to acquire a
|
||||
Vault token using the configured Method. On failure, it will back off for a
|
||||
short while (including some randomness to help prevent thundering herd
|
||||
scenarios) and retry. On success, unless the auth method is configured to wrap
|
||||
the tokens, it will keep the resulting token renewed until renewal is no longer
|
||||
allowed or fails, at which point it will attempt to reauthenticate.
|
||||
|
||||
Every time an authentication is successful, the token is written to the
|
||||
configured Sinks, subject to their configuration.
|
||||
|
||||
## Advanced Functionality
|
||||
|
||||
Sinks support some advanced features, including the ability for the written
|
||||
values to be encrypted or
|
||||
[response-wrapped](/docs/concepts/response-wrapping.html).
|
||||
|
||||
Both mechanisms can be used concurrently; in this case, the value will be
|
||||
response-wrapped, then encrypted.
|
||||
|
||||
### Response-Wrapping Tokens
|
||||
|
||||
There are two ways that tokens can be response-wrapped by the agent:
|
||||
|
||||
1. By the auth method. This allows the end client to introspect the
|
||||
`creation_path` of the token, helping prevent Man-In-The-Middle (MITM)
|
||||
attacks. However, because the agent cannot then unwrap the token and rewrap
|
||||
it without modifying the `creation_path`, the agent is not able to renew the
|
||||
token; it is up to the end client to renew the token. The agent stays
|
||||
daemonized in this mode since some auth methods allow for reauthentication
|
||||
on certain events.
|
||||
|
||||
2. By any of the token sinks. Because more than one sink can be configured, the
|
||||
token must be wrapped after it is fetched, rather than wrapped by Vault as
|
||||
it's being returned. As a result, the `creation_path` will always be
|
||||
`sys/wrapping/wrap`, and validation of this field cannot be used as
|
||||
protection against MITM attacks. However, this mode allows the agent to keep
|
||||
the token renewed for the end client and automatically reauthenticate when
|
||||
it expires.
|
||||
|
||||
### Encrypting Tokens
|
||||
|
||||
Tokens can be encrypted, using a Diffie-Hellman exchange to generate an
|
||||
ephemeral key. In this mechanism, the client receiving the token writes a
|
||||
generated private key to a file. The sink responsible for writing the token to
|
||||
that client looks for this public key and uses it to compute a shared secret
|
||||
key, which is then used to encrypt the token via AES-GCM. The nonce, encrypted
|
||||
payload, and the sink's public key are then written to the output file, where
|
||||
the client can compute the shared secret and decrypt the token value.
|
||||
|
||||
~> NOTE: This is not a protection against MITM attacks! The purpose of this
|
||||
feature is for forward-secrecy and coverage against bare token values being
|
||||
persisted. A MITM that can access the sink's output and public-key input files
|
||||
could attack this exchange.
|
||||
|
||||
To help mitigate MITM attacks, additional authenticated data (AAD) can be
|
||||
provided to the agent. This data is written as part of the AES-GCM tag and must
|
||||
match on both the agent and the client. This of course means that protecting
|
||||
this AAD becomes important, but it provides another layer for an attacker to
|
||||
have to overcome. For instance, if the attacker has access to the file system
|
||||
where the token is being written, but not to read agent configuration or read
|
||||
environment variables, this AAD can be generated and passed to the agent and
|
||||
the client in ways that would be difficult for the attacker to find.
|
||||
|
||||
When using AAD, it is always a good idea for this to be as fresh as possible;
|
||||
generate a value and pass it to your client and agent on startup. Additionally,
|
||||
agent uses a Trust On First Use model; after it finds a generated public key,
|
||||
it will reuse that public key instead of looking for new values that have been
|
||||
written.
|
||||
|
||||
If writing a client that uses this feature, it will likely be helpful to look
|
||||
at the
|
||||
[dhutil](https://github.com/hashicorp/vault/blob/master/helper/dhutil/dhutil.go)
|
||||
library. This shows the expected format of the public key input and envelope
|
||||
output formats.
|
||||
|
||||
## Configuration
|
||||
|
||||
The top level `auto_auth` block has two configuration entries:
|
||||
|
||||
- `method` `(object: required)` - Configuration for the method
|
||||
|
||||
- `sinks` `(array of objects: required)` - Configuration for the sinks
|
||||
|
||||
### Configuration (Method)
|
||||
|
||||
These are common configuration values that live within the `method` block:
|
||||
|
||||
- `type` `(string: required)` - The type of the method to use, e.g. `aws`,
|
||||
`gcp`, `azure`, etc. *Note*: when using HCL this can be used as the key for
|
||||
the block, e.g. `method "aws" {...}`.
|
||||
|
||||
- `mount_path` `(string: optional)` - The mount path of the method. If not
|
||||
specified, defaults to a value of `auth/<method type>`.
|
||||
|
||||
- `wrap_ttl` `(string or integer: optional)` - If specified, the written token
|
||||
will be response-wrapped by the agent. This is more secure than wrapping by
|
||||
sinks, but does not allow the agent to keep the token renewed or
|
||||
automatically reauthenticate when it expires. Rather than a simple string,
|
||||
the written value will be a JSON-encoded
|
||||
[SecretWrapInfo](https://godoc.org/github.com/hashicorp/vault/api#SecretWrapInfo)
|
||||
structure. Values can be an integer number of seconds or a stringish value
|
||||
like `5m`.
|
||||
|
||||
- `config` `(object: required)` - Configuration of the method itself. See the
|
||||
sidebar for information about each method.
|
||||
|
||||
### Configuration (Sinks)
|
||||
|
||||
These configuration values are common to all Sinks:
|
||||
|
||||
- `type` `(string: required)` - The type of the method to use, e.g. `file`.
|
||||
*Note*: when using HCL this can be used as the key for the block, e.g. `sink
|
||||
"file" {...}`.
|
||||
|
||||
- `wrap_ttl` `(string or integer: optional)` - If specified, the written token
|
||||
will be response-wrapped by the sink. This is less secure than wrapping by
|
||||
the method, but allows the agent to keep the token renewed and automatically
|
||||
reauthenticate when it expires. Rather than a simple string, the written
|
||||
value will be a JSON-encoded
|
||||
[SecretWrapInfo](https://godoc.org/github.com/hashicorp/vault/api#SecretWrapInfo)
|
||||
structure. Values can be an integer number of seconds or a stringish value
|
||||
like `5m`.
|
||||
|
||||
- `dh_type` `(string: optional)` - If specified, the type of Diffie-Hellman exchange to
|
||||
perform, meaning, which ciphers and/or curves. Currently only `curve25519` is
|
||||
supported.
|
||||
|
||||
- `dh_path` `(string: required if dh_type is set)` - The path from which the
|
||||
agent should read the client's initial parameters (e.g. curve25519 public
|
||||
key).
|
||||
|
||||
- `aad` `(string: optional)` - If specified, additional authenticated data to
|
||||
use with the AES-GCM encryption of the token. Can be any string, including
|
||||
serialized data.
|
||||
|
||||
- `aad_env_var` `(string: optional)` - If specified, AAD will be read from the
|
||||
given environment variable rather than a value in the configuration file.
|
||||
|
||||
- `config` `(object: required)` - Configuration of the sink itself. See the
|
||||
sidebar for information about each sink.
|
||||
41
website/source/docs/agent/autoauth/methods/aws.html.md
Normal file
41
website/source/docs/agent/autoauth/methods/aws.html.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Vault Agent Auto-Auth AWS Method"
|
||||
sidebar_current: "docs-agent-autoauth-methods-aws"
|
||||
description: |-
|
||||
AWS Method for Vault Agent Auto-Auth
|
||||
---
|
||||
|
||||
# Vault Agent Auto-Auth AWS Method
|
||||
|
||||
The `aws` method performs authentication against the [AWS Auth
|
||||
method](https://www.vaultproject.io/docs/auth/aws.html). Both `ec2` and `iam`
|
||||
authentication types are supported. If `ec2` is used, the agent will store the
|
||||
reauthentication value in memory and use it for reauthenticating, but will not
|
||||
persist it to disk.
|
||||
|
||||
Due to the complexity of the TOFU model used in the `ec2` method, we recommend
|
||||
the `iam` method when possible.
|
||||
|
||||
## Credentials
|
||||
|
||||
Vault will use the AWS SDK's normal credential chain behavior, which means it
|
||||
will try to source credentials from the assigned instance profile, a
|
||||
credentials file, the environment, or static credentials. Generally it should
|
||||
not be required to set the `access_key` and `secret_key` parameters.
|
||||
|
||||
## Configuration
|
||||
|
||||
- `type` `(string: required)` - The type of authentication; must be `ec2` or `iam`
|
||||
|
||||
- `role` `(string: required)` - The role to authenticate against on Vault
|
||||
|
||||
- `access_key` `(string: optional)` - When using static credentials, the access key to use
|
||||
|
||||
- `secret_key` `(string: optional)` - When using static credentials, the secret key to use
|
||||
|
||||
- `session_token` `(string: optional)` - The session token to use for authentication, if needed
|
||||
|
||||
- `header_value` `(string: optional)` - If configured in Vault, the value to
|
||||
use for
|
||||
[`iam_server_id_header_value`](https://www.vaultproject.io/api/auth/aws/index.html#iam_server_id_header_value)
|
||||
21
website/source/docs/agent/autoauth/methods/azure.html.md
Normal file
21
website/source/docs/agent/autoauth/methods/azure.html.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Vault Agent Auto-Auth Azure Method"
|
||||
sidebar_current: "docs-agent-autoauth-methods-azure"
|
||||
description: |-
|
||||
Azure Method for Vault Agent Auto-Auth
|
||||
---
|
||||
|
||||
# Vault Agent Auto-Auth Azure Method
|
||||
|
||||
The `azure` method reads in Azure instance credentials and uses them to
|
||||
authenticate with the [Azure Auth
|
||||
method](https://www.vaultproject.io/docs/auth/azure.html). It reads most
|
||||
parameters needed for authentication directly from instance information based
|
||||
on the value of the `resource` parameter.
|
||||
|
||||
## Configuration
|
||||
|
||||
- `role` `(string: required)` - The role to authenticate against on Vault
|
||||
|
||||
- `resource` `(string: required)` - The resource name to use when getting instance information
|
||||
39
website/source/docs/agent/autoauth/methods/gcp.html.md
Normal file
39
website/source/docs/agent/autoauth/methods/gcp.html.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Vault Agent Auto-Auth GCP Method"
|
||||
sidebar_current: "docs-agent-autoauth-methods-gcp"
|
||||
description: |-
|
||||
GCP Method for Vault Agent Auto-Auth
|
||||
---
|
||||
|
||||
# Vault Agent Auto-Auth GCP Method
|
||||
|
||||
The `gcp` method performs authentication against the [GCP Auth
|
||||
method](https://www.vaultproject.io/docs/auth/gcp.html). Both `gce` and `iam`
|
||||
authentication types are supported.
|
||||
|
||||
## Credentials
|
||||
|
||||
Vault will use the GCP SDK's normal credential chain behavior. You can set a
|
||||
static `credentials` value but it is usually not needed. If running on GCE
|
||||
using Application Default Credentials, you may need to specify the service
|
||||
account and project since ADC does not provide metadata used to automatically
|
||||
determine these.
|
||||
|
||||
## Configuration
|
||||
|
||||
- `type` `(string: required)` - The type of authentication; must be `gce` or `iam`
|
||||
|
||||
- `role` `(string: required)` - The role to authenticate against on Vault
|
||||
|
||||
- `credentials` `(string: optional)` - When using static credentials, the
|
||||
contents of the JSON credentials file
|
||||
|
||||
- `service_account` `(string: optional)` - The service account to use, if it
|
||||
cannot be automatically determined
|
||||
|
||||
- `project` `(string: optional)` - The project to use, if it cannot be
|
||||
automatically determined
|
||||
|
||||
- `jwt_exp` `(string or int: optional)` - The number of minutes a generated JWT
|
||||
should be valid for when using the `iam` method; defaults to 15 minutes
|
||||
11
website/source/docs/agent/autoauth/methods/index.html.md
Normal file
11
website/source/docs/agent/autoauth/methods/index.html.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Vault Agent Auto-Auth Methods"
|
||||
sidebar_current: "docs-agent-autoauth-methods"
|
||||
description: |-
|
||||
Methods for Vault Agent Auto-Auth
|
||||
---
|
||||
|
||||
# Vault Agent Auto-Auth Methods
|
||||
|
||||
Please see the sidebar for available methods and their usage/configuration.
|
||||
21
website/source/docs/agent/autoauth/methods/jwt.html.md
Normal file
21
website/source/docs/agent/autoauth/methods/jwt.html.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Vault Agent Auto-Auth JWT Method"
|
||||
sidebar_current: "docs-agent-autoauth-methods-jwt"
|
||||
description: |-
|
||||
JWT Method for Vault Agent Auto-Auth
|
||||
---
|
||||
|
||||
# Vault Agent Auto-Auth JWT Method
|
||||
|
||||
The `jwt` method reads in a JWT from a file and sends it to the [JWT Auth
|
||||
method](https://www.vaultproject.io/docs/auth/jwt.html). Since JWTs often have
|
||||
limited lifetime, it constantly watches for a new JWT to be written, and when
|
||||
found it will immediately ingress this value, delete the file, and use the new
|
||||
JWT to perform a reauthentication.
|
||||
|
||||
## Configuration
|
||||
|
||||
- `path` `(string: required)` - The path to the JWT file
|
||||
|
||||
- `role` `(string: required)` - The role to authenticate against on Vault
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Vault Agent Auto-Auth Kubernetes Method"
|
||||
sidebar_current: "docs-agent-autoauth-methods-kubernetes"
|
||||
description: |-
|
||||
Kubernetes Method for Vault Agent Auto-Auth
|
||||
---
|
||||
|
||||
# Vault Agent Auto-Auth Kubernetes Method
|
||||
|
||||
The `kubernetes` method reads in a Kubernetes service account token from the
|
||||
running pod (via `/var/run/secrets/kubernetes.io/serviceaccount/token`) and
|
||||
sends it to the [Kubernetes Auth
|
||||
method](https://www.vaultproject.io/docs/auth/kubernetes.html).
|
||||
|
||||
## Configuration
|
||||
|
||||
- `role` `(string: required)` - The role to authenticate against on Vault
|
||||
24
website/source/docs/agent/autoauth/sinks/file.html.md
Normal file
24
website/source/docs/agent/autoauth/sinks/file.html.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Vault Agent Auto-Auth File Sink"
|
||||
sidebar_current: "docs-agent-autoauth-sinks-file"
|
||||
description: |-
|
||||
File sink for Vault Agent Auto-Auth
|
||||
---
|
||||
|
||||
# Vault Agent Auto-Auth File Sink
|
||||
|
||||
The `file` sink writes tokens, optionally response-wrapped and/or encrypted, to
|
||||
a file. This may be a local file or a file mapped via some other process (NFS,
|
||||
Gluster, CIFS, etc.).
|
||||
|
||||
Once the sink writes the file, it is up to the client to control lifecycle;
|
||||
generally it is best for the client to remove the file as soon as it is seen.
|
||||
|
||||
It is also best practice to write the file to a ramdisk, ideally an encrypted
|
||||
ramdisk, and use appropriate filesystem permissions. The file is currently
|
||||
always written with `0640` permissions.
|
||||
|
||||
## Configuration
|
||||
|
||||
- `path` `(string: required)` - The path to use to write the token file
|
||||
11
website/source/docs/agent/autoauth/sinks/index.html.md
Normal file
11
website/source/docs/agent/autoauth/sinks/index.html.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Vault Agent Auto-Auth Sinks"
|
||||
sidebar_current: "docs-agent-autoauth-sinks"
|
||||
description: |-
|
||||
Sinks for Vault Agent Auto-Auth
|
||||
---
|
||||
|
||||
# Vault Agent Auto-Auth Sinks
|
||||
|
||||
Please see the sidebar for available sinks and their usage/configuration.
|
||||
65
website/source/docs/agent/index.html.md
Normal file
65
website/source/docs/agent/index.html.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Vault Agent"
|
||||
sidebar_current: "docs-agent"
|
||||
description: |-
|
||||
Vault Agent is a client-side daemon that can be used to perform some Vault
|
||||
functionality automatically.
|
||||
---
|
||||
|
||||
# Vault Agent
|
||||
|
||||
Vault Agent is a client daemon that can perform useful tasks.
|
||||
|
||||
To get help, run:
|
||||
|
||||
```text
|
||||
$ vault agent -h
|
||||
```
|
||||
## Auto-Auth
|
||||
|
||||
Vault Agent allows for easy authentication to Vault in a wide variety of
|
||||
environments. Please see the [Auto-Auth docs](/docs/agent/autoauth/index.html)
|
||||
for information.
|
||||
|
||||
Auto-Auth functionality takes place within an `auto_auth` configuration stanza.
|
||||
|
||||
## Configuration
|
||||
|
||||
There is one currently-available general configuration option:
|
||||
|
||||
- `pid_file` `(string: "")` - Path to the file in which the agent's Process ID
|
||||
(PID) should be stored.
|
||||
|
||||
## Example Configuration
|
||||
|
||||
An example configuration, with very contrived values, follows:
|
||||
|
||||
```python
|
||||
pid_file = "./pidfile"
|
||||
|
||||
auto_auth {
|
||||
method "aws" {
|
||||
mount_path = "auth/aws-subaccount"
|
||||
config = {
|
||||
role = "foobar"
|
||||
}
|
||||
}
|
||||
|
||||
sink "file" {
|
||||
config = {
|
||||
path = "/tmp/file-foo"
|
||||
}
|
||||
}
|
||||
|
||||
sink "file" {
|
||||
wrap_ttl = "5m"
|
||||
aad_env_var = "TEST_AAD_ENV"
|
||||
dh_type = "curve25519"
|
||||
dh_path = "/tmp/file-foo-dhpath2"
|
||||
config = {
|
||||
path = "/tmp/file-bar"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
11
website/source/docs/commands/agent.html.md
Normal file
11
website/source/docs/commands/agent.html.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "agent - Command"
|
||||
sidebar_current: "docs-commands-agent"
|
||||
description: |-
|
||||
The "agent" command is used to start Vault Agent
|
||||
---
|
||||
|
||||
# agent
|
||||
|
||||
Please see the [Vault Agent documentation page](/docs/agent/index.html).
|
||||
|
|
@ -182,6 +182,9 @@
|
|||
<li<%= sidebar_current("docs-commands") %>>
|
||||
<a href="/docs/commands/index.html">Commands (CLI)</a>
|
||||
<ul class="nav">
|
||||
<li<%= sidebar_current("docs-commands-agent") %>>
|
||||
<a href="/docs/commands/agent.html">agent</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-commands-audit") %>>
|
||||
<a href="/docs/commands/audit.html">audit</a>
|
||||
<ul class="nav">
|
||||
|
|
@ -368,7 +371,44 @@
|
|||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-agent") %>>
|
||||
<a href="/docs/agent/index.html">Vault Agent</a>
|
||||
<ul class="nav">
|
||||
<li<%= sidebar_current("docs-agent-autoauth") %>>
|
||||
<a href="/docs/agent/autoauth/index.html">Auto-Auth</a>
|
||||
<ul class="nav">
|
||||
<li<%= sidebar_current("docs-agent-autoauth-methods") %>>
|
||||
<a href="/docs/agent/autoauth/methods/index.html">Methods</a>
|
||||
<ul class="nav">
|
||||
<li<%= sidebar_current("docs-agent-autoauth-methods-aws") %>>
|
||||
<a href="/docs/agent/autoauth/methods/aws.html">AWS</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-agent-autoauth-methods-azure") %>>
|
||||
<a href="/docs/agent/autoauth/methods/azure.html">Azure</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-agent-autoauth-methods-gcp") %>>
|
||||
<a href="/docs/agent/autoauth/methods/gcp.html">GCP</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-agent-autoauth-methods-jwt") %>>
|
||||
<a href="/docs/agent/autoauth/methods/jwt.html">JWT/OIDC</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-agent-autoauth-methods-kubernetes") %>>
|
||||
<a href="/docs/agent/autoauth/methods/kubernetes.html">Kubernetes</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-agent-autoauth-sinks") %>>
|
||||
<a href="/docs/agent/autoauth/sinks/index.html">Sinks</a>
|
||||
<ul class="nav">
|
||||
<li<%= sidebar_current("docs-agent-autoauth-sinks-file") %>>
|
||||
<a href="/docs/agent/autoauth/sinks/file.html">File</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<hr>
|
||||
|
||||
<li<%= sidebar_current("docs-secrets") %>>
|
||||
|
|
|
|||
Loading…
Reference in a new issue