From 903ab7c4858085e4ef8c4cb05d071bd5f3bdbe10 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Tue, 24 Jul 2018 22:02:27 -0400 Subject: [PATCH] VSI (#4985) --- .gitignore | 2 + command/agent.go | 384 ++++++++++++++++ command/agent/auth/auth.go | 215 +++++++++ command/agent/auth/auth_test.go | 100 +++++ command/agent/auth/aws/aws.go | 207 +++++++++ command/agent/auth/azure/azure.go | 180 ++++++++ command/agent/auth/gcp/gcp.go | 241 +++++++++++ command/agent/auth/jwt/jwt.go | 184 ++++++++ command/agent/auth/kubernetes/kubernetes.go | 76 ++++ command/agent/config/config.go | 238 ++++++++++ command/agent/config/config_test.go | 71 +++ .../test-fixtures/config-embedded-type.hcl | 30 ++ command/agent/config/test-fixtures/config.hcl | 32 ++ command/agent/jwt_end_to_end_test.go | 409 ++++++++++++++++++ command/agent/sink/file/file_sink.go | 112 +++++ command/agent/sink/file/file_sink_test.go | 82 ++++ command/agent/sink/file/sink_test.go | 121 ++++++ command/agent/sink/sink.go | 228 ++++++++++ command/commands.go | 8 + command/main.go | 1 + helper/dhutil/dhutil.go | 121 ++++++ vendor/github.com/kr/pretty/License | 21 + vendor/github.com/kr/pretty/Readme | 9 + vendor/github.com/kr/pretty/diff.go | 265 ++++++++++++ vendor/github.com/kr/pretty/formatter.go | 328 ++++++++++++++ vendor/github.com/kr/pretty/go.mod | 3 + vendor/github.com/kr/pretty/pretty.go | 108 +++++ vendor/github.com/kr/pretty/zero.go | 41 ++ vendor/vendor.json | 6 + .../source/docs/agent/autoauth/index.html.md | 161 +++++++ .../docs/agent/autoauth/methods/aws.html.md | 41 ++ .../docs/agent/autoauth/methods/azure.html.md | 21 + .../docs/agent/autoauth/methods/gcp.html.md | 39 ++ .../docs/agent/autoauth/methods/index.html.md | 11 + .../docs/agent/autoauth/methods/jwt.html.md | 21 + .../agent/autoauth/methods/kubernetes.html.md | 18 + .../docs/agent/autoauth/sinks/file.html.md | 24 + .../docs/agent/autoauth/sinks/index.html.md | 11 + website/source/docs/agent/index.html.md | 65 +++ website/source/docs/commands/agent.html.md | 11 + website/source/layouts/docs.erb | 42 +- 41 files changed, 4287 insertions(+), 1 deletion(-) create mode 100644 command/agent.go create mode 100644 command/agent/auth/auth.go create mode 100644 command/agent/auth/auth_test.go create mode 100644 command/agent/auth/aws/aws.go create mode 100644 command/agent/auth/azure/azure.go create mode 100644 command/agent/auth/gcp/gcp.go create mode 100644 command/agent/auth/jwt/jwt.go create mode 100644 command/agent/auth/kubernetes/kubernetes.go create mode 100644 command/agent/config/config.go create mode 100644 command/agent/config/config_test.go create mode 100644 command/agent/config/test-fixtures/config-embedded-type.hcl create mode 100644 command/agent/config/test-fixtures/config.hcl create mode 100644 command/agent/jwt_end_to_end_test.go create mode 100644 command/agent/sink/file/file_sink.go create mode 100644 command/agent/sink/file/file_sink_test.go create mode 100644 command/agent/sink/file/sink_test.go create mode 100644 command/agent/sink/sink.go create mode 100644 helper/dhutil/dhutil.go create mode 100644 vendor/github.com/kr/pretty/License create mode 100644 vendor/github.com/kr/pretty/Readme create mode 100644 vendor/github.com/kr/pretty/diff.go create mode 100644 vendor/github.com/kr/pretty/formatter.go create mode 100644 vendor/github.com/kr/pretty/go.mod create mode 100644 vendor/github.com/kr/pretty/pretty.go create mode 100644 vendor/github.com/kr/pretty/zero.go create mode 100644 website/source/docs/agent/autoauth/index.html.md create mode 100644 website/source/docs/agent/autoauth/methods/aws.html.md create mode 100644 website/source/docs/agent/autoauth/methods/azure.html.md create mode 100644 website/source/docs/agent/autoauth/methods/gcp.html.md create mode 100644 website/source/docs/agent/autoauth/methods/index.html.md create mode 100644 website/source/docs/agent/autoauth/methods/jwt.html.md create mode 100644 website/source/docs/agent/autoauth/methods/kubernetes.html.md create mode 100644 website/source/docs/agent/autoauth/sinks/file.html.md create mode 100644 website/source/docs/agent/autoauth/sinks/index.html.md create mode 100644 website/source/docs/agent/index.html.md create mode 100644 website/source/docs/commands/agent.html.md diff --git a/.gitignore b/.gitignore index 7dc579c775..d6260c7f89 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/command/agent.go b/command/agent.go new file mode 100644 index 0000000000..688e4a93f4 --- /dev/null +++ b/command/agent.go @@ -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) +} diff --git a/command/agent/auth/auth.go b/command/agent/auth/auth.go new file mode 100644 index 0000000000..79f7c6c583 --- /dev/null +++ b/command/agent/auth/auth.go @@ -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 + } + } + } +} diff --git a/command/agent/auth/auth_test.go b/command/agent/auth/auth_test.go new file mode 100644 index 0000000000..113ca5eb39 --- /dev/null +++ b/command/agent/auth/auth_test.go @@ -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 + } + } +} diff --git a/command/agent/auth/aws/aws.go b/command/agent/auth/aws/aws.go new file mode 100644 index 0000000000..7ff9d2bdfd --- /dev/null +++ b/command/agent/auth/aws/aws.go @@ -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() { +} diff --git a/command/agent/auth/azure/azure.go b/command/agent/auth/azure/azure.go new file mode 100644 index 0000000000..7d468d56df --- /dev/null +++ b/command/agent/auth/azure/azure.go @@ -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 +} diff --git a/command/agent/auth/gcp/gcp.go b/command/agent/auth/gcp/gcp.go new file mode 100644 index 0000000000..75956633c5 --- /dev/null +++ b/command/agent/auth/gcp/gcp.go @@ -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() { +} diff --git a/command/agent/auth/jwt/jwt.go b/command/agent/auth/jwt/jwt.go new file mode 100644 index 0000000000..06e640ebd2 --- /dev/null +++ b/command/agent/auth/jwt/jwt.go @@ -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) + } +} diff --git a/command/agent/auth/kubernetes/kubernetes.go b/command/agent/auth/kubernetes/kubernetes.go new file mode 100644 index 0000000000..1f27ab478b --- /dev/null +++ b/command/agent/auth/kubernetes/kubernetes.go @@ -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() { +} diff --git a/command/agent/config/config.go b/command/agent/config/config.go new file mode 100644 index 0000000000..c809b0be19 --- /dev/null +++ b/command/agent/config/config.go @@ -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 +} diff --git a/command/agent/config/config_test.go b/command/agent/config/config_test.go new file mode 100644 index 0000000000..2f78b4fb04 --- /dev/null +++ b/command/agent/config/config_test.go @@ -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) + } +} diff --git a/command/agent/config/test-fixtures/config-embedded-type.hcl b/command/agent/config/test-fixtures/config-embedded-type.hcl new file mode 100644 index 0000000000..dcf375fc04 --- /dev/null +++ b/command/agent/config/test-fixtures/config-embedded-type.hcl @@ -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" + } + } +} diff --git a/command/agent/config/test-fixtures/config.hcl b/command/agent/config/test-fixtures/config.hcl new file mode 100644 index 0000000000..af2aa4e772 --- /dev/null +++ b/command/agent/config/test-fixtures/config.hcl @@ -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" + } + } +} diff --git a/command/agent/jwt_end_to_end_test.go b/command/agent/jwt_end_to_end_test.go new file mode 100644 index 0000000000..2fa6c438b9 --- /dev/null +++ b/command/agent/jwt_end_to_end_test.go @@ -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-----` +) diff --git a/command/agent/sink/file/file_sink.go b/command/agent/sink/file/file_sink.go new file mode 100644 index 0000000000..43ca805e8b --- /dev/null +++ b/command/agent/sink/file/file_sink.go @@ -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 +} diff --git a/command/agent/sink/file/file_sink_test.go b/command/agent/sink/file/file_sink_test.go new file mode 100644 index 0000000000..6cc4c54f69 --- /dev/null +++ b/command/agent/sink/file/file_sink_test.go @@ -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)) + } +} diff --git a/command/agent/sink/file/sink_test.go b/command/agent/sink/file/sink_test.go new file mode 100644 index 0000000000..8be1e45872 --- /dev/null +++ b/command/agent/sink/file/sink_test.go @@ -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 +} diff --git a/command/agent/sink/sink.go b/command/agent/sink/sink.go new file mode 100644 index 0000000000..98d2238ede --- /dev/null +++ b/command/agent/sink/sink.go @@ -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 +} diff --git a/command/commands.go b/command/commands.go index a72d00564c..ba83d6cfc5 100644 --- a/command/commands.go +++ b/command/commands.go @@ -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(), diff --git a/command/main.go b/command/main.go index 9b52e622ae..cf9b0b777c 100644 --- a/command/main.go +++ b/command/main.go @@ -182,6 +182,7 @@ var commonCommands = []string{ "delete", "list", "login", + "agent", "server", "status", "unwrap", diff --git a/helper/dhutil/dhutil.go b/helper/dhutil/dhutil.go new file mode 100644 index 0000000000..86e23298e0 --- /dev/null +++ b/helper/dhutil/dhutil.go @@ -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 +} diff --git a/vendor/github.com/kr/pretty/License b/vendor/github.com/kr/pretty/License new file mode 100644 index 0000000000..05c783ccf6 --- /dev/null +++ b/vendor/github.com/kr/pretty/License @@ -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. diff --git a/vendor/github.com/kr/pretty/Readme b/vendor/github.com/kr/pretty/Readme new file mode 100644 index 0000000000..c589fc622b --- /dev/null +++ b/vendor/github.com/kr/pretty/Readme @@ -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 diff --git a/vendor/github.com/kr/pretty/diff.go b/vendor/github.com/kr/pretty/diff.go new file mode 100644 index 0000000000..6aa7f743a2 --- /dev/null +++ b/vendor/github.com/kr/pretty/diff.go @@ -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 +} diff --git a/vendor/github.com/kr/pretty/formatter.go b/vendor/github.com/kr/pretty/formatter.go new file mode 100644 index 0000000000..a317d7b8ee --- /dev/null +++ b/vendor/github.com/kr/pretty/formatter.go @@ -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 +} diff --git a/vendor/github.com/kr/pretty/go.mod b/vendor/github.com/kr/pretty/go.mod new file mode 100644 index 0000000000..1e29533143 --- /dev/null +++ b/vendor/github.com/kr/pretty/go.mod @@ -0,0 +1,3 @@ +module "github.com/kr/pretty" + +require "github.com/kr/text" v0.1.0 diff --git a/vendor/github.com/kr/pretty/pretty.go b/vendor/github.com/kr/pretty/pretty.go new file mode 100644 index 0000000000..49423ec7f5 --- /dev/null +++ b/vendor/github.com/kr/pretty/pretty.go @@ -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 +} diff --git a/vendor/github.com/kr/pretty/zero.go b/vendor/github.com/kr/pretty/zero.go new file mode 100644 index 0000000000..abb5b6fc14 --- /dev/null +++ b/vendor/github.com/kr/pretty/zero.go @@ -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 +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 6758c30b40..67e490be3b 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -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", diff --git a/website/source/docs/agent/autoauth/index.html.md b/website/source/docs/agent/autoauth/index.html.md new file mode 100644 index 0000000000..6318b7ec6b --- /dev/null +++ b/website/source/docs/agent/autoauth/index.html.md @@ -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/`. + +- `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. diff --git a/website/source/docs/agent/autoauth/methods/aws.html.md b/website/source/docs/agent/autoauth/methods/aws.html.md new file mode 100644 index 0000000000..5582b1a761 --- /dev/null +++ b/website/source/docs/agent/autoauth/methods/aws.html.md @@ -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) diff --git a/website/source/docs/agent/autoauth/methods/azure.html.md b/website/source/docs/agent/autoauth/methods/azure.html.md new file mode 100644 index 0000000000..5aca4bb359 --- /dev/null +++ b/website/source/docs/agent/autoauth/methods/azure.html.md @@ -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 diff --git a/website/source/docs/agent/autoauth/methods/gcp.html.md b/website/source/docs/agent/autoauth/methods/gcp.html.md new file mode 100644 index 0000000000..5b8bc98721 --- /dev/null +++ b/website/source/docs/agent/autoauth/methods/gcp.html.md @@ -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 diff --git a/website/source/docs/agent/autoauth/methods/index.html.md b/website/source/docs/agent/autoauth/methods/index.html.md new file mode 100644 index 0000000000..88b6d0ab3f --- /dev/null +++ b/website/source/docs/agent/autoauth/methods/index.html.md @@ -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. diff --git a/website/source/docs/agent/autoauth/methods/jwt.html.md b/website/source/docs/agent/autoauth/methods/jwt.html.md new file mode 100644 index 0000000000..2d7a18a94e --- /dev/null +++ b/website/source/docs/agent/autoauth/methods/jwt.html.md @@ -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 diff --git a/website/source/docs/agent/autoauth/methods/kubernetes.html.md b/website/source/docs/agent/autoauth/methods/kubernetes.html.md new file mode 100644 index 0000000000..1a21ee8839 --- /dev/null +++ b/website/source/docs/agent/autoauth/methods/kubernetes.html.md @@ -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 diff --git a/website/source/docs/agent/autoauth/sinks/file.html.md b/website/source/docs/agent/autoauth/sinks/file.html.md new file mode 100644 index 0000000000..605b97089e --- /dev/null +++ b/website/source/docs/agent/autoauth/sinks/file.html.md @@ -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 diff --git a/website/source/docs/agent/autoauth/sinks/index.html.md b/website/source/docs/agent/autoauth/sinks/index.html.md new file mode 100644 index 0000000000..ce52365fd2 --- /dev/null +++ b/website/source/docs/agent/autoauth/sinks/index.html.md @@ -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. diff --git a/website/source/docs/agent/index.html.md b/website/source/docs/agent/index.html.md new file mode 100644 index 0000000000..3a11c0ce9c --- /dev/null +++ b/website/source/docs/agent/index.html.md @@ -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" + } + } +} +``` diff --git a/website/source/docs/commands/agent.html.md b/website/source/docs/commands/agent.html.md new file mode 100644 index 0000000000..e61aab1374 --- /dev/null +++ b/website/source/docs/commands/agent.html.md @@ -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). diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index c0842d5563..f94348e371 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -182,6 +182,9 @@ > Commands (CLI)