This commit is contained in:
Jeff Mitchell 2018-07-24 22:02:27 -04:00 committed by GitHub
parent 8152811b38
commit 903ab7c485
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 4287 additions and 1 deletions

2
.gitignore vendored
View file

@ -43,6 +43,8 @@ Vagrantfile
# Configs
*.hcl
!command/agent/config/test-fixtures/config.hcl
!command/agent/config/test-fixtures/config-embedded-type.hcl
.DS_Store
.idea

384
command/agent.go Normal file
View file

@ -0,0 +1,384 @@
package command
import (
"context"
"fmt"
"io"
"os"
"sort"
"strings"
"sync"
"github.com/kr/pretty"
"github.com/mitchellh/cli"
"github.com/posener/complete"
"github.com/hashicorp/errwrap"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/command/agent/auth"
"github.com/hashicorp/vault/command/agent/auth/aws"
"github.com/hashicorp/vault/command/agent/auth/azure"
"github.com/hashicorp/vault/command/agent/auth/gcp"
"github.com/hashicorp/vault/command/agent/auth/jwt"
"github.com/hashicorp/vault/command/agent/auth/kubernetes"
"github.com/hashicorp/vault/command/agent/config"
"github.com/hashicorp/vault/command/agent/sink"
"github.com/hashicorp/vault/command/agent/sink/file"
"github.com/hashicorp/vault/helper/gated-writer"
"github.com/hashicorp/vault/helper/logging"
"github.com/hashicorp/vault/version"
)
var _ cli.Command = (*AgentCommand)(nil)
var _ cli.CommandAutocomplete = (*AgentCommand)(nil)
type AgentCommand struct {
*BaseCommand
ShutdownCh chan struct{}
SighupCh chan struct{}
logWriter io.Writer
logGate *gatedwriter.Writer
logger log.Logger
cleanupGuard sync.Once
startedCh chan (struct{}) // for tests
flagConfigs []string
flagLogLevel string
flagTestVerifyOnly bool
flagCombineLogs bool
}
func (c *AgentCommand) Synopsis() string {
return "Start a Vault agent"
}
func (c *AgentCommand) Help() string {
helpText := `
Usage: vault agent [options]
This command starts a Vault agent that can perform automatic authentication
in certain environments.
Start an agent with a configuration file:
$ vault agent -config=/etc/vault/config.hcl
For a full list of examples, please see the documentation.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *AgentCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP)
f := set.NewFlagSet("Command Options")
f.StringSliceVar(&StringSliceVar{
Name: "config",
Target: &c.flagConfigs,
Completion: complete.PredictOr(
complete.PredictFiles("*.hcl"),
complete.PredictFiles("*.json"),
),
Usage: "Path to a configuration file. This configuration file should " +
"contain only agent directives.",
})
f.StringVar(&StringVar{
Name: "log-level",
Target: &c.flagLogLevel,
Default: "info",
EnvVar: "VAULT_LOG_LEVEL",
Completion: complete.PredictSet("trace", "debug", "info", "warn", "err"),
Usage: "Log verbosity level. Supported values (in order of detail) are " +
"\"trace\", \"debug\", \"info\", \"warn\", and \"err\".",
})
// Internal-only flags to follow.
//
// Why hello there little source code reader! Welcome to the Vault source
// code. The remaining options are intentionally undocumented and come with
// no warranty or backwards-compatability promise. Do not use these flags
// in production. Do not build automation using these flags. Unless you are
// developing against Vault, you should not need any of these flags.
// TODO: should the below flags be public?
f.BoolVar(&BoolVar{
Name: "combine-logs",
Target: &c.flagCombineLogs,
Default: false,
Hidden: true,
})
f.BoolVar(&BoolVar{
Name: "test-verify-only",
Target: &c.flagTestVerifyOnly,
Default: false,
Hidden: true,
})
// End internal-only flags.
return set
}
func (c *AgentCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictNothing
}
func (c *AgentCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *AgentCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
// Create a logger. We wrap it in a gated writer so that it doesn't
// start logging too early.
c.logGate = &gatedwriter.Writer{Writer: os.Stderr}
c.logWriter = c.logGate
if c.flagCombineLogs {
c.logWriter = os.Stdout
}
var level log.Level
c.flagLogLevel = strings.ToLower(strings.TrimSpace(c.flagLogLevel))
switch c.flagLogLevel {
case "trace":
level = log.Trace
case "debug":
level = log.Debug
case "notice", "info", "":
level = log.Info
case "warn", "warning":
level = log.Warn
case "err", "error":
level = log.Error
default:
c.UI.Error(fmt.Sprintf("Unknown log level: %s", c.flagLogLevel))
return 1
}
c.logger = logging.NewVaultLoggerWithWriter(c.logWriter, level)
// Validation
if len(c.flagConfigs) != 1 {
c.UI.Error("Must specify exactly one config path using -config")
return 1
}
// Load the configuration
config, err := config.LoadConfig(c.flagConfigs[0], c.logger)
if err != nil {
c.UI.Error(fmt.Sprintf("Error loading configuration from %s: %s", c.flagConfigs[0], err))
return 1
}
// Ensure at least one config was found.
if config == nil {
c.UI.Output(wrapAtLength(
"No configuration read. Please provide the configuration with the " +
"-config flag."))
return 1
}
if config.AutoAuth == nil {
c.UI.Error("No auto_auth block found in config file")
return 1
}
infoKeys := make([]string, 0, 10)
info := make(map[string]string)
info["log level"] = c.flagLogLevel
infoKeys = append(infoKeys, "log level")
infoKeys = append(infoKeys, "version")
verInfo := version.GetVersion()
info["version"] = verInfo.FullVersionNumber(false)
if verInfo.Revision != "" {
info["version sha"] = strings.Trim(verInfo.Revision, "'")
infoKeys = append(infoKeys, "version sha")
}
infoKeys = append(infoKeys, "cgo")
info["cgo"] = "disabled"
if version.CgoEnabled {
info["cgo"] = "enabled"
}
// Server configuration output
padding := 24
sort.Strings(infoKeys)
c.UI.Output("==> Vault agent configuration:\n")
for _, k := range infoKeys {
c.UI.Output(fmt.Sprintf(
"%s%s: %s",
strings.Repeat(" ", padding-len(k)),
strings.Title(k),
info[k]))
}
c.UI.Output("")
// Tests might not want to start a vault server and just want to verify
// the configuration.
if c.flagTestVerifyOnly {
if os.Getenv("VAULT_TEST_VERIFY_ONLY_DUMP_CONFIG") != "" {
c.UI.Output(fmt.Sprintf(
"\nConfiguration:\n%s\n",
pretty.Sprint(*config)))
}
return 0
}
client, err := c.Client()
if err != nil {
c.UI.Error(fmt.Sprintf(
"Error fetching client: %v",
err))
return 1
}
ctx, cancelFunc := context.WithCancel(context.Background())
var sinks []*sink.SinkConfig
for _, sc := range config.AutoAuth.Sinks {
switch sc.Type {
case "file":
config := &sink.SinkConfig{
Logger: c.logger.Named("sink.file"),
Config: sc.Config,
Client: client,
WrapTTL: sc.WrapTTL,
DHType: sc.DHType,
DHPath: sc.DHPath,
AAD: sc.AAD,
}
s, err := file.NewFileSink(config)
if err != nil {
c.UI.Error(errwrap.Wrapf("Error creating file sink: {{err}}", err).Error())
return 1
}
config.Sink = s
sinks = append(sinks, config)
default:
c.UI.Error(fmt.Sprintf("Unknown sink type %q", sc.Type))
return 1
}
}
var method auth.AuthMethod
authConfig := &auth.AuthConfig{
Logger: c.logger.Named(fmt.Sprintf("auth.%s", config.AutoAuth.Method.Type)),
MountPath: config.AutoAuth.Method.MountPath,
WrapTTL: config.AutoAuth.Method.WrapTTL,
Config: config.AutoAuth.Method.Config,
}
switch config.AutoAuth.Method.Type {
case "aws":
method, err = aws.NewAWSAuthMethod(authConfig)
case "azure":
method, err = azure.NewAzureAuthMethod(authConfig)
case "gcp":
method, err = gcp.NewGCPAuthMethod(authConfig)
case "jwt":
method, err = jwt.NewJWTAuthMethod(authConfig)
case "kubernetes":
method, err = kubernetes.NewKubernetesAuthMethod(authConfig)
default:
c.UI.Error(fmt.Sprintf("Unknown auth method %q", config.AutoAuth.Method.Type))
return 1
}
if err != nil {
c.UI.Error(errwrap.Wrapf(fmt.Sprintf("Error creating %s auth method: {{err}}", config.AutoAuth.Method.Type), err).Error())
return 1
}
// Output the header that the server has started
if !c.flagCombineLogs {
c.UI.Output("==> Vault server started! Log data will stream in below:\n")
}
// Inform any tests that the server is ready
select {
case c.startedCh <- struct{}{}:
default:
}
ss := sink.NewSinkServer(&sink.SinkServerConfig{
Logger: c.logger.Named("sink.server"),
Client: client,
})
ah := auth.NewAuthHandler(&auth.AuthHandlerConfig{
Logger: c.logger.Named("auth.handler"),
Client: c.client,
})
// Start things running
go ah.Run(ctx, method)
go ss.Run(ctx, ah.OutputCh, sinks)
// Release the log gate.
c.logGate.Flush()
// Write out the PID to the file now that server has successfully started
if err := c.storePidFile(config.PidFile); err != nil {
c.UI.Error(fmt.Sprintf("Error storing PID: %s", err))
return 1
}
defer func() {
if err := c.removePidFile(config.PidFile); err != nil {
c.UI.Error(fmt.Sprintf("Error deleting the PID file: %s", err))
}
}()
select {
case <-c.ShutdownCh:
c.UI.Output("==> Vault agent shutdown triggered")
cancelFunc()
<-ah.DoneCh
<-ss.DoneCh
}
return 0
}
// storePidFile is used to write out our PID to a file if necessary
func (c *AgentCommand) storePidFile(pidPath string) error {
// Quit fast if no pidfile
if pidPath == "" {
return nil
}
// Open the PID file
pidFile, err := os.OpenFile(pidPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return errwrap.Wrapf("could not open pid file: {{err}}", err)
}
defer pidFile.Close()
// Write out the PID
pid := os.Getpid()
_, err = pidFile.WriteString(fmt.Sprintf("%d", pid))
if err != nil {
return errwrap.Wrapf("could not write to pid file: {{err}}", err)
}
return nil
}
// removePidFile is used to cleanup the PID file if necessary
func (c *AgentCommand) removePidFile(pidPath string) error {
if pidPath == "" {
return nil
}
return os.Remove(pidPath)
}

215
command/agent/auth/auth.go Normal file
View file

@ -0,0 +1,215 @@
package auth
import (
"context"
"math/rand"
"time"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/jsonutil"
)
type AuthMethod interface {
Authenticate(context.Context, *api.Client) (string, map[string]interface{}, error)
NewCreds() chan struct{}
CredSuccess()
Shutdown()
}
type AuthConfig struct {
Logger hclog.Logger
MountPath string
WrapTTL time.Duration
Config map[string]interface{}
}
// AuthHandler is responsible for keeping a token alive and renewed and passing
// new tokens to the sink server
type AuthHandler struct {
DoneCh chan struct{}
OutputCh chan string
logger hclog.Logger
client *api.Client
random *rand.Rand
wrapTTL time.Duration
}
type AuthHandlerConfig struct {
Logger hclog.Logger
Client *api.Client
WrapTTL time.Duration
}
func NewAuthHandler(conf *AuthHandlerConfig) *AuthHandler {
ah := &AuthHandler{
DoneCh: make(chan struct{}),
OutputCh: make(chan string),
logger: conf.Logger,
client: conf.Client,
random: rand.New(rand.NewSource(int64(time.Now().Nanosecond()))),
wrapTTL: conf.WrapTTL,
}
return ah
}
func backoffOrQuit(ctx context.Context, backoff time.Duration) {
select {
case <-time.After(backoff):
case <-ctx.Done():
}
}
func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) {
if am == nil {
panic("nil auth method")
}
ah.logger.Info("starting auth handler")
defer func() {
ah.logger.Info("auth handler stopped")
close(ah.DoneCh)
}()
credCh := am.NewCreds()
if credCh == nil {
credCh = make(chan struct{})
}
var renewer *api.Renewer
for {
select {
case <-ctx.Done():
am.Shutdown()
return
default:
}
// Create a fresh backoff value
backoff := 2*time.Second + time.Duration(ah.random.Int63()%int64(time.Second*2)-int64(time.Second))
ah.logger.Info("authenticating")
path, data, err := am.Authenticate(ctx, ah.client)
if err != nil {
ah.logger.Error("error getting path or data from method", "error", err, "backoff", backoff.Seconds())
backoffOrQuit(ctx, backoff)
continue
}
clientToUse := ah.client
if ah.wrapTTL > 0 {
wrapClient, err := ah.client.Clone()
if err != nil {
ah.logger.Error("error creating client for wrapped call", "error", err, "backoff", backoff.Seconds())
backoffOrQuit(ctx, backoff)
continue
}
wrapClient.SetWrappingLookupFunc(func(string, string) string {
return ah.wrapTTL.String()
})
clientToUse = wrapClient
}
secret, err := clientToUse.Logical().Write(path, data)
// Check errors/sanity
if err != nil {
ah.logger.Error("error authenticating", "error", err, "backoff", backoff.Seconds())
backoffOrQuit(ctx, backoff)
continue
}
switch {
case ah.wrapTTL > 0:
if secret.WrapInfo == nil {
ah.logger.Error("authentication returned nil wrap info", "backoff", backoff.Seconds())
backoffOrQuit(ctx, backoff)
continue
}
if secret.WrapInfo.Token == "" {
ah.logger.Error("authentication returned empty wrapped client token", "backoff", backoff.Seconds())
backoffOrQuit(ctx, backoff)
continue
}
wrappedResp, err := jsonutil.EncodeJSON(secret.WrapInfo)
if err != nil {
ah.logger.Error("failed to encode wrapinfo", "error", err, "backoff", backoff.Seconds())
backoffOrQuit(ctx, backoff)
continue
}
ah.logger.Info("authentication successful, sending wrapped token to sinks and pausing")
ah.OutputCh <- string(wrappedResp)
am.CredSuccess()
select {
case <-ctx.Done():
ah.logger.Info("shutdown triggered")
return
case <-credCh:
ah.logger.Info("auth method found new credentials, re-authenticating")
continue
}
default:
if secret.Auth == nil {
ah.logger.Error("authentication returned nil auth info", "backoff", backoff.Seconds())
backoffOrQuit(ctx, backoff)
continue
}
if secret.Auth.ClientToken == "" {
ah.logger.Error("authentication returned empty client token", "backoff", backoff.Seconds())
backoffOrQuit(ctx, backoff)
continue
}
ah.logger.Info("authentication successful, sending token to sinks")
ah.OutputCh <- secret.Auth.ClientToken
am.CredSuccess()
}
if renewer != nil {
renewer.Stop()
}
renewer, err = ah.client.NewRenewer(&api.RenewerInput{
Secret: secret,
})
if err != nil {
ah.logger.Error("error creating renewer, backing off and retrying", "error", err, "backoff", backoff.Seconds())
backoffOrQuit(ctx, backoff)
continue
}
// Start the renewal process
ah.logger.Info("starting renewal process")
go renewer.Renew()
RenewerLoop:
for {
select {
case <-ctx.Done():
ah.logger.Info("shutdown triggered, stopping renewer")
renewer.Stop()
break RenewerLoop
case err := <-renewer.DoneCh():
ah.logger.Info("renewer done channel triggered")
if err != nil {
ah.logger.Error("error renewing token", "error", err)
}
break RenewerLoop
case <-renewer.RenewCh():
ah.logger.Info("renewed auth token")
case <-credCh:
ah.logger.Info("auth method found new credentials, re-authenticating")
break RenewerLoop
}
}
}
}

View file

@ -0,0 +1,100 @@
package auth
import (
"context"
"testing"
"time"
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/builtin/credential/userpass"
"github.com/hashicorp/vault/helper/logging"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/vault"
)
type userpassTestMethod struct{}
func newUserpassTestMethod(t *testing.T, client *api.Client) AuthMethod {
err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{
Type: "userpass",
Config: api.AuthConfigInput{
DefaultLeaseTTL: "1s",
MaxLeaseTTL: "3s",
},
})
if err != nil {
t.Fatal(err)
}
return &userpassTestMethod{}
}
func (u *userpassTestMethod) Authenticate(_ context.Context, client *api.Client) (string, map[string]interface{}, error) {
_, err := client.Logical().Write("auth/userpass/users/foo", map[string]interface{}{
"password": "bar",
})
if err != nil {
return "", nil, err
}
return "auth/userpass/login/foo", map[string]interface{}{
"password": "bar",
}, nil
}
func (u *userpassTestMethod) NewCreds() chan struct{} {
return nil
}
func (u *userpassTestMethod) CredSuccess() {
}
func (u *userpassTestMethod) Shutdown() {
}
func TestAuthHandler(t *testing.T) {
logger := logging.NewVaultLogger(hclog.Trace)
coreConfig := &vault.CoreConfig{
Logger: logger,
CredentialBackends: map[string]logical.Factory{
"userpass": userpass.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
vault.TestWaitActive(t, cluster.Cores[0].Core)
client := cluster.Cores[0].Client
ctx, cancelFunc := context.WithCancel(context.Background())
ah := NewAuthHandler(&AuthHandlerConfig{
Logger: logger.Named("auth.handler"),
Client: client,
})
am := newUserpassTestMethod(t, client)
go ah.Run(ctx, am)
// Consume tokens so we don't block
stopTime := time.Now().Add(5 * time.Second)
closed := false
consumption:
for {
select {
case <-ah.OutputCh:
// Nothing
case <-time.After(stopTime.Sub(time.Now())):
if !closed {
cancelFunc()
closed = true
}
case <-ah.DoneCh:
break consumption
}
}
}

View file

@ -0,0 +1,207 @@
package aws
import (
"context"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"net/http"
"github.com/hashicorp/errwrap"
cleanhttp "github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-hclog"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/api"
awsauth "github.com/hashicorp/vault/builtin/credential/aws"
"github.com/hashicorp/vault/command/agent/auth"
)
const (
typeEC2 = "ec2"
typeIAM = "iam"
identityEndpoint = "http://169.254.169.254/latest/dynamic/instance-identity"
)
type awsMethod struct {
logger hclog.Logger
authType string
nonce string
mountPath string
role string
headerValue string
accessKey string
secretKey string
sessionToken string
}
func NewAWSAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
if conf == nil {
return nil, errors.New("empty config")
}
if conf.Config == nil {
return nil, errors.New("empty config data")
}
a := &awsMethod{
logger: conf.Logger,
mountPath: conf.MountPath,
}
typeRaw, ok := conf.Config["type"]
if !ok {
return nil, errors.New("missing 'type' value")
}
a.authType, ok = typeRaw.(string)
if !ok {
return nil, errors.New("could not convert 'type' config value to string")
}
roleRaw, ok := conf.Config["role"]
if !ok {
return nil, errors.New("missing 'role' value")
}
a.role, ok = roleRaw.(string)
if !ok {
return nil, errors.New("could not convert 'role' config value to string")
}
switch {
case a.role == "":
return nil, errors.New("'role' value is empty")
case a.authType == "":
return nil, errors.New("'type' value is empty")
case a.authType != typeEC2 && a.authType != typeIAM:
return nil, errors.New("'type' value is invalid")
}
accessKeyRaw, ok := conf.Config["access_key"]
if ok {
a.accessKey, ok = accessKeyRaw.(string)
if !ok {
return nil, errors.New("could not convert 'access_key' value into string")
}
}
secretKeyRaw, ok := conf.Config["secret_key"]
if ok {
a.secretKey, ok = secretKeyRaw.(string)
if !ok {
return nil, errors.New("could not convert 'secret_key' value into string")
}
}
sessionTokenRaw, ok := conf.Config["session_token"]
if ok {
a.sessionToken, ok = sessionTokenRaw.(string)
if !ok {
return nil, errors.New("could not convert 'session_token' value into string")
}
}
headerValueRaw, ok := conf.Config["header_value"]
if ok {
a.headerValue, ok = headerValueRaw.(string)
if !ok {
return nil, errors.New("could not convert 'header_value' value into string")
}
}
return a, nil
}
func (a *awsMethod) Authenticate(ctx context.Context, client *api.Client) (retToken string, retData map[string]interface{}, retErr error) {
a.logger.Trace("beginning authentication")
data := make(map[string]interface{})
switch a.authType {
case typeEC2:
client := cleanhttp.DefaultClient()
// Fetch document
{
req, err := http.NewRequest("GET", fmt.Sprintf("%s/document", identityEndpoint), nil)
if err != nil {
retErr = errwrap.Wrapf("error creating request: {{err}}", err)
return
}
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
retErr = errwrap.Wrapf("error fetching instance document: {{err}}", err)
return
}
if resp == nil {
retErr = errors.New("empty response fetching instance document")
return
}
defer resp.Body.Close()
doc, err := ioutil.ReadAll(resp.Body)
if err != nil {
retErr = errwrap.Wrapf("error reading instance document response body: {{err}}", err)
return
}
data["identity"] = base64.StdEncoding.EncodeToString(doc)
}
// Fetch signature
{
req, err := http.NewRequest("GET", fmt.Sprintf("%s/signature", identityEndpoint), nil)
if err != nil {
retErr = errwrap.Wrapf("error creating request: {{err}}", err)
return
}
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
retErr = errwrap.Wrapf("error fetching instance document signature: {{err}}", err)
return
}
if resp == nil {
retErr = errors.New("empty response fetching instance document signature")
return
}
defer resp.Body.Close()
sig, err := ioutil.ReadAll(resp.Body)
if err != nil {
retErr = errwrap.Wrapf("error reading instance document signature response body: {{err}}", err)
return
}
data["signature"] = string(sig)
}
// Add the reauthentication value, if we have one
if a.nonce == "" {
uuid, err := uuid.GenerateUUID()
if err != nil {
retErr = errwrap.Wrapf("error generating uuid for reauthentication value: {{err}}", err)
return
}
a.nonce = uuid
}
data["nonce"] = a.nonce
default:
var err error
data, err = awsauth.GenerateLoginData(a.accessKey, a.secretKey, a.sessionToken, a.headerValue)
if err != nil {
retErr = errwrap.Wrapf("error creating login value: {{err}}", err)
return
}
}
data["role"] = a.role
return fmt.Sprintf("%s/login", a.mountPath), data, nil
}
func (a *awsMethod) NewCreds() chan struct{} {
return nil
}
func (a *awsMethod) CredSuccess() {
}
func (a *awsMethod) Shutdown() {
}

View file

@ -0,0 +1,180 @@
package azure
import (
"context"
"errors"
"fmt"
"io/ioutil"
"net/http"
"github.com/hashicorp/errwrap"
cleanhttp "github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/command/agent/auth"
"github.com/hashicorp/vault/helper/jsonutil"
"github.com/hashicorp/vault/helper/useragent"
)
const (
instanceEndpoint = "http://169.254.169.254/metadata/instance"
identityEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
// minimum version 2018-02-01 needed for identity metadata
// regional availability: https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service
apiVersion = "2018-02-01"
)
type azureMethod struct {
logger hclog.Logger
mountPath string
role string
resource string
}
func NewAzureAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
if conf == nil {
return nil, errors.New("empty config")
}
if conf.Config == nil {
return nil, errors.New("empty config data")
}
a := &azureMethod{
logger: conf.Logger,
mountPath: conf.MountPath,
}
roleRaw, ok := conf.Config["role"]
if !ok {
return nil, errors.New("missing 'role' value")
}
a.role, ok = roleRaw.(string)
if !ok {
return nil, errors.New("could not convert 'role' config value to string")
}
resourceRaw, ok := conf.Config["resource"]
if !ok {
return nil, errors.New("missing 'resource' value")
}
a.resource, ok = resourceRaw.(string)
if !ok {
return nil, errors.New("could not convert 'resource' config value to string")
}
switch {
case a.role == "":
return nil, errors.New("'role' value is empty")
case a.resource == "":
return nil, errors.New("'resource' value is empty")
}
return a, nil
}
func (a *azureMethod) Authenticate(ctx context.Context, client *api.Client) (retPath string, retData map[string]interface{}, retErr error) {
a.logger.Trace("beginning authentication")
// Fetch instance data
var instance struct {
Compute struct {
Name string
ResourceGroupName string
SubscriptionID string
VMScaleSetName string
}
}
body, err := getMetadataInfo(ctx, instanceEndpoint, "")
if err != nil {
retErr = err
return
}
err = jsonutil.DecodeJSON(body, &instance)
if err != nil {
retErr = errwrap.Wrapf("error parsing instance metadata response: {{err}}", err)
return
}
// Fetch JWT
var identity struct {
AccessToken string `json:"access_token"`
}
body, err = getMetadataInfo(ctx, identityEndpoint, a.resource)
if err != nil {
retErr = err
return
}
err = jsonutil.DecodeJSON(body, &identity)
if err != nil {
retErr = errwrap.Wrapf("error parsing identity metadata response: {{err}}", err)
return
}
// Attempt login
data := map[string]interface{}{
"role": a.role,
"vm_name": instance.Compute.Name,
"vmss_name": instance.Compute.VMScaleSetName,
"resource_group_name": instance.Compute.ResourceGroupName,
"subscription_id": instance.Compute.SubscriptionID,
"jwt": identity.AccessToken,
}
return fmt.Sprintf("%s/login", a.mountPath), data, nil
}
func (a *azureMethod) NewCreds() chan struct{} {
return nil
}
func (a *azureMethod) CredSuccess() {
}
func (a *azureMethod) Shutdown() {
}
func getMetadataInfo(ctx context.Context, endpoint, resource string) ([]byte, error) {
req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
return nil, err
}
q := req.URL.Query()
q.Add("api-version", apiVersion)
if resource != "" {
q.Add("resource", resource)
}
req.URL.RawQuery = q.Encode()
req.Header.Set("Metadata", "true")
req.Header.Set("User-Agent", useragent.String())
req = req.WithContext(ctx)
client := cleanhttp.DefaultClient()
resp, err := client.Do(req)
if err != nil {
return nil, errwrap.Wrapf(fmt.Sprintf("error fetching metadata from %s: {{err}}", endpoint), err)
}
if resp == nil {
return nil, fmt.Errorf("empty response fetching metadata from %s", endpoint)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errwrap.Wrapf(fmt.Sprintf("error reading metadata from %s: {{err}}", endpoint), err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("error response in metadata from %s: %s", endpoint, body)
}
return body, nil
}

View file

@ -0,0 +1,241 @@
package gcp
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/hashicorp/errwrap"
cleanhttp "github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-gcp-common/gcputil"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/command/agent/auth"
"github.com/hashicorp/vault/helper/parseutil"
"golang.org/x/oauth2"
iam "google.golang.org/api/iam/v1"
)
const (
typeGCE = "gce"
typeIAM = "iam"
identityEndpoint = "http://metadata/computeMetadata/v1/instance/service-accounts/%s/identity"
defaultIamMaxJwtExpMinutes = 15
)
type gcpMethod struct {
logger hclog.Logger
authType string
mountPath string
role string
credentials string
serviceAccount string
project string
jwtExp int64
}
func NewGCPAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
if conf == nil {
return nil, errors.New("empty config")
}
if conf.Config == nil {
return nil, errors.New("empty config data")
}
var err error
g := &gcpMethod{
logger: conf.Logger,
mountPath: conf.MountPath,
serviceAccount: "default",
}
typeRaw, ok := conf.Config["type"]
if !ok {
return nil, errors.New("missing 'type' value")
}
g.authType, ok = typeRaw.(string)
if !ok {
return nil, errors.New("could not convert 'type' config value to string")
}
roleRaw, ok := conf.Config["role"]
if !ok {
return nil, errors.New("missing 'role' value")
}
g.role, ok = roleRaw.(string)
if !ok {
return nil, errors.New("could not convert 'role' config value to string")
}
switch {
case g.role == "":
return nil, errors.New("'role' value is empty")
case g.authType == "":
return nil, errors.New("'type' value is empty")
case g.authType != typeGCE && g.authType != typeIAM:
return nil, errors.New("'type' value is invalid")
}
credentialsRaw, ok := conf.Config["credentials"]
if ok {
g.credentials, ok = credentialsRaw.(string)
if !ok {
return nil, errors.New("could not convert 'credentials' value into string")
}
}
serviceAccountRaw, ok := conf.Config["service_account"]
if ok {
g.serviceAccount, ok = serviceAccountRaw.(string)
if !ok {
return nil, errors.New("could not convert 'service_account' value into string")
}
}
projectRaw, ok := conf.Config["project"]
if ok {
g.project, ok = projectRaw.(string)
if !ok {
return nil, errors.New("could not convert 'project' value into string")
}
}
jwtExpRaw, ok := conf.Config["jwt_exp"]
if ok {
g.jwtExp, err = parseutil.ParseInt(jwtExpRaw)
if err != nil {
return nil, errwrap.Wrapf("error parsing 'jwt_raw' into integer: {{err}}", err)
}
}
return g, nil
}
func (g *gcpMethod) Authenticate(ctx context.Context, client *api.Client) (retPath string, retData map[string]interface{}, retErr error) {
g.logger.Trace("beginning authentication")
data := make(map[string]interface{})
var jwt string
switch g.authType {
case typeGCE:
httpClient := cleanhttp.DefaultClient()
// Fetch token
{
req, err := http.NewRequest("GET", fmt.Sprintf(identityEndpoint, g.serviceAccount), nil)
if err != nil {
retErr = errwrap.Wrapf("error creating request: {{err}}", err)
return
}
req = req.WithContext(ctx)
req.Header.Add("Metadata-Flavor", "Google")
q := req.URL.Query()
q.Add("audience", fmt.Sprintf("%s/vault/%s", client.Address(), g.role))
q.Add("format", "full")
req.URL.RawQuery = q.Encode()
resp, err := httpClient.Do(req)
if err != nil {
retErr = errwrap.Wrapf("error fetching instance token: {{err}}", err)
return
}
if resp == nil {
retErr = errors.New("empty response fetching instance toke")
return
}
defer resp.Body.Close()
jwtBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
retErr = errwrap.Wrapf("error reading instance token response body: {{err}}", err)
return
}
jwt = string(jwtBytes)
}
default:
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, cleanhttp.DefaultClient())
credentials, tokenSource, err := gcputil.FindCredentials(g.credentials, ctx, iam.CloudPlatformScope)
if err != nil {
retErr = errwrap.Wrapf("could not obtain credentials: {{err}}", err)
return
}
httpClient := oauth2.NewClient(ctx, tokenSource)
var serviceAccount string
if g.serviceAccount == "" && credentials != nil {
serviceAccount = credentials.ClientEmail
} else {
serviceAccount = g.serviceAccount
}
if serviceAccount == "" {
retErr = errors.New("could not obtain service account from credentials (possibly Application Default Credentials are being used); a service account to authenticate as must be provided")
return
}
project := "-"
if g.project != "" {
project = g.project
} else if credentials != nil {
project = credentials.ProjectId
}
ttlMin := int64(defaultIamMaxJwtExpMinutes)
if g.jwtExp != 0 {
ttlMin = g.jwtExp
}
ttl := time.Minute * time.Duration(ttlMin)
jwtPayload := map[string]interface{}{
"aud": fmt.Sprintf("http://vault/%s", g.role),
"sub": serviceAccount,
"exp": time.Now().Add(ttl).Unix(),
}
payloadBytes, err := json.Marshal(jwtPayload)
if err != nil {
retErr = errwrap.Wrapf("could not convert JWT payload to JSON string: {{err}}", err)
return
}
jwtReq := &iam.SignJwtRequest{
Payload: string(payloadBytes),
}
iamClient, err := iam.New(httpClient)
if err != nil {
retErr = errwrap.Wrapf("could not create IAM client: {{err}}", err)
return
}
resourceName := fmt.Sprintf("projects/%s/serviceAccounts/%s", project, serviceAccount)
resp, err := iamClient.Projects.ServiceAccounts.SignJwt(resourceName, jwtReq).Do()
if err != nil {
retErr = errwrap.Wrapf(fmt.Sprintf("unable to sign JWT for %s using given Vault credentials: {{err}}", resourceName), err)
return
}
jwt = resp.SignedJwt
}
data["role"] = g.role
data["jwt"] = jwt
return fmt.Sprintf("%s/login", g.mountPath), data, nil
}
func (g *gcpMethod) NewCreds() chan struct{} {
return nil
}
func (g *gcpMethod) CredSuccess() {
}
func (g *gcpMethod) Shutdown() {
}

View file

@ -0,0 +1,184 @@
package jwt
import (
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"sync"
"sync/atomic"
"time"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/command/agent/auth"
)
type jwtMethod struct {
logger hclog.Logger
path string
mountPath string
role string
credsFound chan struct{}
watchCh chan string
stopCh chan struct{}
doneCh chan struct{}
credSuccessGate chan struct{}
ticker *time.Ticker
once *sync.Once
latestToken *atomic.Value
}
func NewJWTAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
if conf == nil {
return nil, errors.New("empty config")
}
if conf.Config == nil {
return nil, errors.New("empty config data")
}
j := &jwtMethod{
logger: conf.Logger,
mountPath: conf.MountPath,
credsFound: make(chan struct{}),
watchCh: make(chan string),
stopCh: make(chan struct{}),
doneCh: make(chan struct{}),
credSuccessGate: make(chan struct{}),
once: new(sync.Once),
latestToken: new(atomic.Value),
}
j.latestToken.Store("")
pathRaw, ok := conf.Config["path"]
if !ok {
return nil, errors.New("missing 'path' value")
}
j.path, ok = pathRaw.(string)
if !ok {
return nil, errors.New("could not convert 'path' config value to string")
}
roleRaw, ok := conf.Config["role"]
if !ok {
return nil, errors.New("missing 'role' value")
}
j.role, ok = roleRaw.(string)
if !ok {
return nil, errors.New("could not convert 'role' config value to string")
}
switch {
case j.path == "":
return nil, errors.New("'path' value is empty")
case j.role == "":
return nil, errors.New("'role' value is empty")
}
j.ticker = time.NewTicker(500 * time.Millisecond)
go j.runWatcher()
j.logger.Info("jwt auth method created", "path", j.path)
return j, nil
}
func (j *jwtMethod) Authenticate(_ context.Context, client *api.Client) (string, map[string]interface{}, error) {
j.logger.Trace("beginning authentication")
j.ingressToken()
latestToken := j.latestToken.Load().(string)
if latestToken == "" {
return "", nil, errors.New("latest known jwt is empty, cannot authenticate")
}
return fmt.Sprintf("%s/login", j.mountPath), map[string]interface{}{
"role": j.role,
"jwt": latestToken,
}, nil
}
func (j *jwtMethod) NewCreds() chan struct{} {
return j.credsFound
}
func (j *jwtMethod) CredSuccess() {
j.once.Do(func() {
close(j.credSuccessGate)
})
}
func (j *jwtMethod) Shutdown() {
j.ticker.Stop()
close(j.stopCh)
<-j.doneCh
}
func (j *jwtMethod) runWatcher() {
defer close(j.doneCh)
select {
case <-j.stopCh:
return
case <-j.credSuccessGate:
// We only start the next loop once we're initially successful,
// since at startup Authenticate will be called and we don't want
// to end up immediately reauthenticating by having found a new
// value
}
for {
select {
case <-j.stopCh:
return
case <-j.ticker.C:
latestToken := j.latestToken.Load().(string)
j.ingressToken()
newToken := j.latestToken.Load().(string)
if newToken != latestToken {
j.credsFound <- struct{}{}
}
}
}
}
func (j *jwtMethod) ingressToken() {
fi, err := os.Lstat(j.path)
if err != nil {
if os.IsNotExist(err) {
return
}
j.logger.Error("error encountered stat'ing jwt file", "error", err)
return
}
j.logger.Debug("new jwt file found")
if !fi.Mode().IsRegular() {
j.logger.Error("jwt file is not a regular file")
return
}
token, err := ioutil.ReadFile(j.path)
if err != nil {
j.logger.Error("failed to read jwt file", "error", err)
return
}
switch len(token) {
case 0:
j.logger.Warn("empty jwt file read")
default:
j.latestToken.Store(string(token))
}
if err := os.Remove(j.path); err != nil {
j.logger.Error("error removing jwt file", "error", err)
}
}

View file

@ -0,0 +1,76 @@
package kubernetes
import (
"context"
"errors"
"fmt"
"io/ioutil"
"log"
"strings"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/command/agent/auth"
)
const (
serviceAccountFile = "var/run/secrets/kubernetes.io/serviceaccount/token"
)
type kubernetesMethod struct {
logger hclog.Logger
mountPath string
role string
}
func NewKubernetesAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
if conf == nil {
return nil, errors.New("empty config")
}
if conf.Config == nil {
return nil, errors.New("empty config data")
}
k := &kubernetesMethod{
logger: conf.Logger,
mountPath: conf.MountPath,
}
roleRaw, ok := conf.Config["role"]
if !ok {
return nil, errors.New("missing 'role' value")
}
k.role, ok = roleRaw.(string)
if !ok {
return nil, errors.New("could not convert 'role' config value to string")
}
if k.role == "" {
return nil, errors.New("'role' value is empty")
}
return k, nil
}
func (k *kubernetesMethod) Authenticate(ctx context.Context, client *api.Client) (string, map[string]interface{}, error) {
k.logger.Trace("beginning authentication")
content, err := ioutil.ReadFile(serviceAccountFile)
if err != nil {
log.Fatal(err)
}
return fmt.Sprintf("%s/login", k.mountPath), map[string]interface{}{
"role": k.role,
"jwt": strings.TrimSpace(string(content)),
}, nil
}
func (k *kubernetesMethod) NewCreds() chan struct{} {
return nil
}
func (k *kubernetesMethod) CredSuccess() {
}
func (k *kubernetesMethod) Shutdown() {
}

View file

@ -0,0 +1,238 @@
package config
import (
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"time"
"github.com/hashicorp/errwrap"
log "github.com/hashicorp/go-hclog"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault/helper/parseutil"
"github.com/hashicorp/hcl"
"github.com/hashicorp/hcl/hcl/ast"
)
// Config is the configuration for the vault server.
type Config struct {
AutoAuth *AutoAuth `hcl:"auto_auth"`
PidFile string `hcl:"pid_file"`
}
type AutoAuth struct {
Method *Method `hcl:"-"`
Sinks []*Sink `hcl:"sinks"`
}
type Method struct {
Type string
MountPath string `hcl:"mount_path"`
WrapTTLRaw interface{} `hcl:"wrap_ttl"`
WrapTTL time.Duration `hcl:"-"`
Config map[string]interface{}
}
type Sink struct {
Type string
WrapTTLRaw interface{} `hcl:"wrap_ttl"`
WrapTTL time.Duration `hcl:"-"`
DHType string `hcl:"dh_type"`
DHPath string `hcl:"dh_path"`
AAD string `hcl:"aad"`
AADEnvVar string `hcl:"aad_env_var"`
Config map[string]interface{}
}
// LoadConfig loads the configuration at the given path, regardless if
// its a file or directory.
func LoadConfig(path string, logger log.Logger) (*Config, error) {
fi, err := os.Stat(path)
if err != nil {
return nil, err
}
if fi.IsDir() {
return nil, fmt.Errorf("location is a directory, not a file")
}
// Read the file
d, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
// Parse!
obj, err := hcl.Parse(string(d))
if err != nil {
return nil, err
}
// Start building the result
var result Config
if err := hcl.DecodeObject(&result, obj); err != nil {
return nil, err
}
list, ok := obj.Node.(*ast.ObjectList)
if !ok {
return nil, fmt.Errorf("error parsing: file doesn't contain a root object")
}
if err := parseAutoAuth(&result, list); err != nil {
return nil, errwrap.Wrapf("error parsing 'auto_auth': {{err}}", err)
}
return &result, nil
}
func parseAutoAuth(result *Config, list *ast.ObjectList) error {
name := "auto_auth"
autoAuthList := list.Filter(name)
if len(autoAuthList.Items) != 1 {
return fmt.Errorf("one and only one %q block is required", name)
}
// Get our item
item := autoAuthList.Items[0]
var a AutoAuth
if err := hcl.DecodeObject(&a, item.Val); err != nil {
return err
}
result.AutoAuth = &a
subs, ok := item.Val.(*ast.ObjectType)
if !ok {
return fmt.Errorf("could not parse %q as an object", name)
}
subList := subs.List
if err := parseMethod(result, subList); err != nil {
return errwrap.Wrapf("error parsing 'method': {{err}}", err)
}
if err := parseSinks(result, subList); err != nil {
return errwrap.Wrapf("error parsing 'sink' stanzas: {{err}}", err)
}
switch {
case a.Method == nil:
return fmt.Errorf("no 'method' block found")
case len(a.Sinks) == 0:
return fmt.Errorf("at least one 'sink' block must be provided")
}
return nil
}
func parseMethod(result *Config, list *ast.ObjectList) error {
name := "method"
methodList := list.Filter(name)
if len(methodList.Items) != 1 {
return fmt.Errorf("one and only one %q block is required", name)
}
// Get our item
item := methodList.Items[0]
var m Method
if err := hcl.DecodeObject(&m, item.Val); err != nil {
return err
}
if m.Type == "" {
if len(item.Keys) == 1 {
m.Type = strings.ToLower(item.Keys[0].Token.Value().(string))
}
if m.Type == "" {
return errors.New("method type must be specified")
}
}
// Default to Vault's default
if m.MountPath == "" {
m.MountPath = fmt.Sprintf("auth/%s", m.Type)
}
// Standardize on no trailing slash
m.MountPath = strings.TrimSuffix(m.MountPath, "/")
if m.WrapTTLRaw != nil {
var err error
if m.WrapTTL, err = parseutil.ParseDurationSecond(m.WrapTTLRaw); err != nil {
return err
}
m.WrapTTLRaw = nil
}
result.AutoAuth.Method = &m
return nil
}
func parseSinks(result *Config, list *ast.ObjectList) error {
name := "sink"
sinkList := list.Filter(name)
if len(sinkList.Items) < 1 {
return fmt.Errorf("at least one %q block is required", name)
}
var ts []*Sink
for _, item := range sinkList.Items {
var s Sink
if err := hcl.DecodeObject(&s, item.Val); err != nil {
return err
}
if s.Type == "" {
if len(item.Keys) == 1 {
s.Type = strings.ToLower(item.Keys[0].Token.Value().(string))
}
if s.Type == "" {
return errors.New("sink type must be specified")
}
}
if s.WrapTTLRaw != nil {
var err error
if s.WrapTTL, err = parseutil.ParseDurationSecond(s.WrapTTLRaw); err != nil {
return multierror.Prefix(err, fmt.Sprintf("sink.%s", s.Type))
}
s.WrapTTLRaw = nil
}
switch s.DHType {
case "":
case "curve25519":
default:
return multierror.Prefix(errors.New("invalid value for 'dh_type'"), fmt.Sprintf("sink.%s", s.Type))
}
if s.AADEnvVar != "" {
s.AAD = os.Getenv(s.AADEnvVar)
s.AADEnvVar = ""
}
switch {
case s.DHPath == "" && s.DHType == "":
if s.AAD != "" {
return multierror.Prefix(errors.New("specifying AAD data without 'dh_type' does not make sense"), fmt.Sprintf("sink.%s", s.Type))
}
case s.DHPath != "" && s.DHType != "":
default:
return multierror.Prefix(errors.New("'dh_type' and 'dh_path' must be specified together"), fmt.Sprintf("sink.%s", s.Type))
}
ts = append(ts, &s)
}
result.AutoAuth.Sinks = ts
return nil
}

View file

@ -0,0 +1,71 @@
package config
import (
"os"
"testing"
"time"
"github.com/go-test/deep"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/helper/logging"
)
func TestLoadConfigFile(t *testing.T) {
logger := logging.NewVaultLogger(log.Debug)
os.Setenv("TEST_AAD_ENV", "aad")
defer os.Unsetenv("TEST_AAD_ENV")
config, err := LoadConfig("./test-fixtures/config.hcl", logger)
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &Config{
AutoAuth: &AutoAuth{
Method: &Method{
Type: "aws",
WrapTTL: 300 * time.Second,
MountPath: "auth/aws",
Config: map[string]interface{}{
"role": "foobar",
},
},
Sinks: []*Sink{
&Sink{
Type: "file",
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath",
AAD: "foobar",
Config: map[string]interface{}{
"path": "/tmp/file-foo",
},
},
&Sink{
Type: "file",
WrapTTL: 5 * time.Minute,
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath2",
AAD: "aad",
Config: map[string]interface{}{
"path": "/tmp/file-bar",
},
},
},
},
PidFile: "./pidfile",
}
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
config, err = LoadConfig("./test-fixtures/config-embedded-type.hcl", logger)
if err != nil {
t.Fatalf("err: %s", err)
}
if diff := deep.Equal(config, expected); diff != nil {
t.Fatal(diff)
}
}

View file

@ -0,0 +1,30 @@
pid_file = "./pidfile"
auto_auth {
method "aws" {
mount_path = "auth/aws"
wrap_ttl = 300
config = {
role = "foobar"
}
}
sink "file" {
config = {
path = "/tmp/file-foo"
}
aad = "foobar"
dh_type = "curve25519"
dh_path = "/tmp/file-foo-dhpath"
}
sink "file" {
wrap_ttl = "5m"
aad_env_var = "TEST_AAD_ENV"
dh_type = "curve25519"
dh_path = "/tmp/file-foo-dhpath2"
config = {
path = "/tmp/file-bar"
}
}
}

View file

@ -0,0 +1,32 @@
pid_file = "./pidfile"
auto_auth {
method {
type = "aws"
wrap_ttl = 300
config = {
role = "foobar"
}
}
sink {
type = "file"
config = {
path = "/tmp/file-foo"
}
aad = "foobar"
dh_type = "curve25519"
dh_path = "/tmp/file-foo-dhpath"
}
sink {
type = "file"
wrap_ttl = "5m"
aad_env_var = "TEST_AAD_ENV"
dh_type = "curve25519"
dh_path = "/tmp/file-foo-dhpath2"
config = {
path = "/tmp/file-bar"
}
}
}

View file

@ -0,0 +1,409 @@
package agent
import (
"context"
"crypto/ecdsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"io/ioutil"
"os"
"testing"
"time"
hclog "github.com/hashicorp/go-hclog"
vaultjwt "github.com/hashicorp/vault-plugin-auth-jwt"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/command/agent/auth"
agentjwt "github.com/hashicorp/vault/command/agent/auth/jwt"
"github.com/hashicorp/vault/command/agent/sink"
"github.com/hashicorp/vault/command/agent/sink/file"
"github.com/hashicorp/vault/helper/dhutil"
"github.com/hashicorp/vault/helper/jsonutil"
"github.com/hashicorp/vault/helper/logging"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/vault"
jose "gopkg.in/square/go-jose.v2"
"gopkg.in/square/go-jose.v2/jwt"
)
func getTestJWT(t *testing.T) (string, *ecdsa.PrivateKey) {
t.Helper()
cl := jwt.Claims{
Subject: "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients",
Issuer: "https://team-vault.auth0.com/",
NotBefore: jwt.NewNumericDate(time.Now().Add(-5 * time.Second)),
Audience: jwt.Audience{"https://vault.plugin.auth.jwt.test"},
}
privateCl := struct {
User string `json:"https://vault/user"`
Groups []string `json:"https://vault/groups"`
}{
"jeff",
[]string{"foo", "bar"},
}
var key *ecdsa.PrivateKey
block, _ := pem.Decode([]byte(ecdsaPrivKey))
if block != nil {
var err error
key, err = x509.ParseECPrivateKey(block.Bytes)
if err != nil {
t.Fatal(err)
}
}
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: key}, (&jose.SignerOptions{}).WithType("JWT"))
if err != nil {
t.Fatal(err)
}
raw, err := jwt.Signed(sig).Claims(cl).Claims(privateCl).CompactSerialize()
if err != nil {
t.Fatal(err)
}
return raw, key
}
func TestJWTEndToEnd(t *testing.T) {
testJWTEndToEnd(t, false)
testJWTEndToEnd(t, true)
}
func testJWTEndToEnd(t *testing.T, ahWrapping bool) {
logger := logging.NewVaultLogger(hclog.Trace)
coreConfig := &vault.CoreConfig{
Logger: logger,
CredentialBackends: map[string]logical.Factory{
"jwt": vaultjwt.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
vault.TestWaitActive(t, cluster.Cores[0].Core)
client := cluster.Cores[0].Client
// Setup Vault
err := client.Sys().EnableAuthWithOptions("jwt", &api.EnableAuthOptions{
Type: "jwt",
})
if err != nil {
t.Fatal(err)
}
_, err = client.Logical().Write("auth/jwt/config", map[string]interface{}{
"bound_issuer": "https://team-vault.auth0.com/",
"jwt_validation_pubkeys": ecdsaPubKey,
})
if err != nil {
t.Fatal(err)
}
_, err = client.Logical().Write("auth/jwt/role/test", map[string]interface{}{
"bound_subject": "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients",
"bound_audiences": "https://vault.plugin.auth.jwt.test",
"user_claim": "https://vault/user",
"groups_claim": "https://vault/groups",
"policies": "test",
"period": "3s",
})
if err != nil {
t.Fatal(err)
}
// Generate encryption params
pub, pri, err := dhutil.GeneratePublicPrivateKey()
if err != nil {
t.Fatal(err)
}
// We close these right away because we're just basically testing
// permissions and finding a usable file name
inf, err := ioutil.TempFile("", "auth.jwt.test.")
if err != nil {
t.Fatal(err)
}
in := inf.Name()
inf.Close()
os.Remove(in)
t.Logf("input: %s", in)
ouf, err := ioutil.TempFile("", "auth.tokensink.test.")
if err != nil {
t.Fatal(err)
}
out := ouf.Name()
ouf.Close()
os.Remove(out)
t.Logf("output: %s", out)
dhpathf, err := ioutil.TempFile("", "auth.dhpath.test.")
if err != nil {
t.Fatal(err)
}
dhpath := dhpathf.Name()
dhpathf.Close()
os.Remove(dhpath)
// Write DH public key to file
mPubKey, err := jsonutil.EncodeJSON(&dhutil.PublicKeyInfo{
Curve25519PublicKey: pub,
})
if err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(dhpath, mPubKey, 0600); err != nil {
t.Fatal(err)
} else {
logger.Trace("wrote dh param file", "path", dhpath)
}
ctx, cancelFunc := context.WithCancel(context.Background())
timer := time.AfterFunc(30*time.Second, func() {
cancelFunc()
})
defer timer.Stop()
am, err := agentjwt.NewJWTAuthMethod(&auth.AuthConfig{
Logger: logger.Named("auth.jwt"),
MountPath: "auth/jwt",
Config: map[string]interface{}{
"path": in,
"role": "test",
},
})
if err != nil {
t.Fatal(err)
}
ahConfig := &auth.AuthHandlerConfig{
Logger: logger.Named("auth.handler"),
Client: client,
}
if ahWrapping {
ahConfig.WrapTTL = 10 * time.Second
}
ah := auth.NewAuthHandler(ahConfig)
go ah.Run(ctx, am)
defer func() {
<-ah.DoneCh
}()
config := &sink.SinkConfig{
Logger: logger.Named("sink.file"),
AAD: "foobar",
DHType: "curve25519",
DHPath: dhpath,
Config: map[string]interface{}{
"path": out,
},
}
if !ahWrapping {
config.WrapTTL = 10 * time.Second
}
fs, err := file.NewFileSink(config)
if err != nil {
t.Fatal(err)
}
config.Sink = fs
ss := sink.NewSinkServer(&sink.SinkServerConfig{
Logger: logger.Named("sink.server"),
Client: client,
})
go ss.Run(ctx, ah.OutputCh, []*sink.SinkConfig{config})
defer func() {
<-ss.DoneCh
}()
// This has to be after the other defers so it happens first
defer cancelFunc()
// Check that no jwt file exists
_, err = os.Lstat(in)
if err == nil {
t.Fatal("expected err")
}
if !os.IsNotExist(err) {
t.Fatal("expected notexist err")
}
_, err = os.Lstat(out)
if err == nil {
t.Fatal("expected err")
}
if !os.IsNotExist(err) {
t.Fatal("expected notexist err")
}
cloned, err := client.Clone()
if err != nil {
t.Fatal(err)
}
// Get a token
jwtToken, _ := getTestJWT(t)
if err := ioutil.WriteFile(in, []byte(jwtToken), 0600); err != nil {
t.Fatal(err)
} else {
logger.Trace("wrote test jwt", "path", in)
}
checkToken := func() string {
timeout := time.Now().Add(5 * time.Second)
for {
if time.Now().After(timeout) {
t.Fatal("did not find a written token after timeout")
}
val, err := ioutil.ReadFile(out)
if err == nil {
os.Remove(out)
if len(val) == 0 {
t.Fatal("written token was empty")
}
// First decrypt it
resp := new(dhutil.Envelope)
if err := jsonutil.DecodeJSON(val, resp); err != nil {
continue
}
aesKey, err := dhutil.GenerateSharedKey(pri, resp.Curve25519PublicKey)
if err != nil {
t.Fatal(err)
}
if len(aesKey) == 0 {
t.Fatal("got empty aes key")
}
val, err = dhutil.DecryptAES(aesKey, resp.EncryptedPayload, resp.Nonce, []byte("foobar"))
if err != nil {
t.Fatalf("error: %v\nresp: %v", err, string(val))
}
// Now unwrap it
wrapInfo := new(api.SecretWrapInfo)
if err := jsonutil.DecodeJSON(val, wrapInfo); err != nil {
t.Fatal(err)
}
switch {
case wrapInfo.TTL != 10:
t.Fatalf("bad wrap info: %v", wrapInfo.TTL)
case !ahWrapping && wrapInfo.CreationPath != "sys/wrapping/wrap":
t.Fatalf("bad wrap path: %v", wrapInfo.CreationPath)
case ahWrapping && wrapInfo.CreationPath != "auth/jwt/login":
t.Fatalf("bad wrap path: %v", wrapInfo.CreationPath)
case wrapInfo.Token == "":
t.Fatal("wrap token is empty")
}
cloned.SetToken(wrapInfo.Token)
secret, err := cloned.Logical().Unwrap("")
if err != nil {
t.Fatal(err)
}
if ahWrapping {
switch {
case secret.Auth == nil:
t.Fatal("unwrap secret auth is nil")
case secret.Auth.ClientToken == "":
t.Fatal("unwrap token is nil")
}
return secret.Auth.ClientToken
} else {
switch {
case secret.Data == nil:
t.Fatal("unwrap secret data is nil")
case secret.Data["token"] == nil:
t.Fatal("unwrap token is nil")
}
return secret.Data["token"].(string)
}
}
time.Sleep(250 * time.Millisecond)
}
}
origToken := checkToken()
// We only check this if the renewer is actually renewing for us
if !ahWrapping {
// Period of 3 seconds, so should still be alive after 7
timeout := time.Now().Add(7 * time.Second)
cloned.SetToken(origToken)
for {
if time.Now().After(timeout) {
break
}
secret, err := cloned.Auth().Token().LookupSelf()
if err != nil {
t.Fatal(err)
}
ttl, err := secret.Data["ttl"].(json.Number).Int64()
if err != nil {
t.Fatal(err)
}
if ttl > 3 {
t.Fatalf("unexpected ttl: %v", secret.Data["ttl"])
}
}
}
// Get another token to test the backend pushing the need to authenticate
// to the handler
jwtToken, _ = getTestJWT(t)
if err := ioutil.WriteFile(in, []byte(jwtToken), 0600); err != nil {
t.Fatal(err)
}
newToken := checkToken()
if newToken == origToken {
t.Fatal("found same token written")
}
if !ahWrapping {
// Repeat the period test. At the end the old token should have expired and
// the new token should still be alive after 7
timeout := time.Now().Add(7 * time.Second)
cloned.SetToken(newToken)
for {
if time.Now().After(timeout) {
break
}
secret, err := cloned.Auth().Token().LookupSelf()
if err != nil {
t.Fatal(err)
}
ttl, err := secret.Data["ttl"].(json.Number).Int64()
if err != nil {
t.Fatal(err)
}
if ttl > 3 {
t.Fatalf("unexpected ttl: %v", secret.Data["ttl"])
}
}
cloned.SetToken(origToken)
_, err = cloned.Auth().Token().LookupSelf()
if err == nil {
t.Fatal("expected error")
}
}
}
const (
ecdsaPrivKey string = `-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIKfldwWLPYsHjRL9EVTsjSbzTtcGRu6icohNfIqcb6A+oAoGCCqGSM49
AwEHoUQDQgAE4+SFvPwOy0miy/FiTT05HnwjpEbSq+7+1q9BFxAkzjgKnlkXk5qx
hzXQvRmS4w9ZsskoTZtuUI+XX7conJhzCQ==
-----END EC PRIVATE KEY-----`
ecdsaPubKey string = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4+SFvPwOy0miy/FiTT05HnwjpEbS
q+7+1q9BFxAkzjgKnlkXk5qxhzXQvRmS4w9ZsskoTZtuUI+XX7conJhzCQ==
-----END PUBLIC KEY-----`
)

View file

@ -0,0 +1,112 @@
package file
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/hashicorp/errwrap"
hclog "github.com/hashicorp/go-hclog"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/command/agent/sink"
)
// fileSink is a Sink implementation that writes a token to a file
type fileSink struct {
path string
logger hclog.Logger
}
// NewFileSink creates a new file sink with the given configuration
func NewFileSink(conf *sink.SinkConfig) (sink.Sink, error) {
if conf.Logger == nil {
return nil, errors.New("nil logger provided")
}
conf.Logger.Info("creating file sink")
f := &fileSink{
logger: conf.Logger,
}
pathRaw, ok := conf.Config["path"]
if !ok {
return nil, errors.New("'path' not specified for file sink")
}
path, ok := pathRaw.(string)
if !ok {
return nil, errors.New("could not parse 'path' as string")
}
f.path = path
if err := f.WriteToken(""); err != nil {
return nil, errwrap.Wrapf("error during write check: {{err}}", err)
}
f.logger.Info("file sink configured", "path", f.path)
return f, nil
}
// WriteToken implements the Server interface and writes the token to a path on
// disk. It writes into the path's directory into a temp file and does an
// atomic rename to ensure consistency. If a blank token is passed in, it
// performs a write check but does not write a blank value to the final
// location.
func (f *fileSink) WriteToken(token string) error {
f.logger.Trace("enter write_token", "path", f.path)
defer f.logger.Trace("exit write_token", "path", f.path)
u, err := uuid.GenerateUUID()
if err != nil {
return errwrap.Wrapf("error generating a uuid during write check: {{err}}", err)
}
targetDir := filepath.Dir(f.path)
fileName := filepath.Base(f.path)
tmpSuffix := strings.Split(u, "-")[0]
tmpFile, err := os.OpenFile(filepath.Join(targetDir, fmt.Sprintf("%s.tmp.%s", fileName, tmpSuffix)), os.O_WRONLY|os.O_CREATE, 0640)
if err != nil {
return errwrap.Wrapf(fmt.Sprintf("error opening temp file in dir %s for writing: {{err}}", targetDir), err)
}
valToWrite := token
if token == "" {
valToWrite = u
}
_, err = tmpFile.WriteString(valToWrite)
if err != nil {
// Attempt closing and deleting but ignore any error
tmpFile.Close()
os.Remove(tmpFile.Name())
return errwrap.Wrapf(fmt.Sprintf("error writing to %s: {{err}}", tmpFile.Name()), err)
}
err = tmpFile.Close()
if err != nil {
return errwrap.Wrapf(fmt.Sprintf("error closing %s: {{err}}", tmpFile.Name()), err)
}
// Now, if we were just doing a write check (blank token), remove the file
// and exit; otherwise, atomically rename it
if token == "" {
err = os.Remove(tmpFile.Name())
if err != nil {
return errwrap.Wrapf(fmt.Sprintf("error removing temp file %s during write check: {{err}}", tmpFile.Name()), err)
}
return nil
}
err = os.Rename(tmpFile.Name(), f.path)
if err != nil {
return errwrap.Wrapf(fmt.Sprintf("error renaming temp file %s to target file %s: {{err}}", tmpFile.Name(), f.path), err)
}
f.logger.Info("token written", "path", f.path)
return nil
}

View file

@ -0,0 +1,82 @@
package file
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/command/agent/sink"
"github.com/hashicorp/vault/helper/logging"
)
const (
fileServerTestDir = "vault-agent-file-test"
)
func testFileSink(t *testing.T, log hclog.Logger) (*sink.SinkConfig, string) {
tmpDir, err := ioutil.TempDir("", fmt.Sprintf("%s.", fileServerTestDir))
if err != nil {
t.Fatal(err)
}
path := filepath.Join(tmpDir, "token")
config := &sink.SinkConfig{
Logger: log.Named("sink.file"),
Config: map[string]interface{}{
"path": path,
},
}
s, err := NewFileSink(config)
if err != nil {
t.Fatal(err)
}
config.Sink = s
return config, tmpDir
}
func TestFileSink(t *testing.T) {
log := logging.NewVaultLogger(hclog.Trace)
fs, tmpDir := testFileSink(t, log)
defer os.RemoveAll(tmpDir)
path := filepath.Join(tmpDir, "token")
uuidStr, _ := uuid.GenerateUUID()
if err := fs.WriteToken(uuidStr); err != nil {
t.Fatal(err)
}
file, err := os.Open(path)
if err != nil {
t.Fatal(err)
}
fi, err := file.Stat()
if err != nil {
t.Fatal(err)
}
if fi.Mode() != os.FileMode(0640) {
t.Fatalf("wrong file mode was detected at %s", path)
}
err = file.Close()
if err != nil {
t.Fatal(err)
}
fileBytes, err := ioutil.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(fileBytes) != uuidStr {
t.Fatalf("expected %s, got %s", uuidStr, string(fileBytes))
}
}

View file

@ -0,0 +1,121 @@
package file
import (
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"sync/atomic"
"testing"
"time"
hclog "github.com/hashicorp/go-hclog"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/command/agent/sink"
"github.com/hashicorp/vault/helper/logging"
)
func TestSinkServer(t *testing.T) {
log := logging.NewVaultLogger(hclog.Trace)
fs1, path1 := testFileSink(t, log)
defer os.RemoveAll(path1)
fs2, path2 := testFileSink(t, log)
defer os.RemoveAll(path2)
ctx, cancelFunc := context.WithCancel(context.Background())
ss := sink.NewSinkServer(&sink.SinkServerConfig{
Logger: log.Named("sink.server"),
})
uuidStr, _ := uuid.GenerateUUID()
in := make(chan string)
sinks := []*sink.SinkConfig{fs1, fs2}
go ss.Run(ctx, in, sinks)
// Seed a token
in <- uuidStr
// Give it time to finish writing
time.Sleep(1 * time.Second)
// Tell it to shut down and give it time to do so
cancelFunc()
<-ss.DoneCh
for _, path := range []string{path1, path2} {
fileBytes, err := ioutil.ReadFile(fmt.Sprintf("%s/token", path))
if err != nil {
t.Fatal(err)
}
if string(fileBytes) != uuidStr {
t.Fatalf("expected %s, got %s", uuidStr, string(fileBytes))
}
}
}
type badSink struct {
tryCount uint32
logger hclog.Logger
}
func (b *badSink) WriteToken(token string) error {
switch token {
case "bad":
atomic.AddUint32(&b.tryCount, 1)
b.logger.Info("got bad")
return errors.New("bad")
case "good":
atomic.StoreUint32(&b.tryCount, 0)
b.logger.Info("got good")
return nil
default:
return errors.New("unknown case")
}
}
func TestSinkServerRetry(t *testing.T) {
log := logging.NewVaultLogger(hclog.Trace)
b1 := &badSink{logger: log.Named("b1")}
b2 := &badSink{logger: log.Named("b2")}
ctx, cancelFunc := context.WithCancel(context.Background())
ss := sink.NewSinkServer(&sink.SinkServerConfig{
Logger: log.Named("sink.server"),
})
in := make(chan string)
sinks := []*sink.SinkConfig{&sink.SinkConfig{Sink: b1}, &sink.SinkConfig{Sink: b2}}
go ss.Run(ctx, in, sinks)
// Seed a token
in <- "bad"
// During this time we should see it retry multiple times
time.Sleep(10 * time.Second)
if atomic.LoadUint32(&b1.tryCount) < 2 {
t.Fatal("bad try count")
}
if atomic.LoadUint32(&b2.tryCount) < 2 {
t.Fatal("bad try count")
}
in <- "good"
time.Sleep(2 * time.Second)
if atomic.LoadUint32(&b1.tryCount) != 0 {
t.Fatal("bad try count")
}
if atomic.LoadUint32(&b2.tryCount) != 0 {
t.Fatal("bad try count")
}
// Tell it to shut down and give it time to do so
cancelFunc()
<-ss.DoneCh
}

228
command/agent/sink/sink.go Normal file
View file

@ -0,0 +1,228 @@
package sink
import (
"context"
"errors"
"io/ioutil"
"math/rand"
"os"
"time"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/dhutil"
"github.com/hashicorp/vault/helper/jsonutil"
)
type Sink interface {
WriteToken(string) error
}
type SinkConfig struct {
Sink
Logger hclog.Logger
Config map[string]interface{}
Client *api.Client
WrapTTL time.Duration
DHType string
DHPath string
AAD string
cachedRemotePubKey []byte
cachedPubKey []byte
cachedPriKey []byte
}
type SinkServerConfig struct {
Logger hclog.Logger
Client *api.Client
Context context.Context
}
// SinkServer is responsible for pushing tokens to sinks
type SinkServer struct {
DoneCh chan struct{}
logger hclog.Logger
client *api.Client
random *rand.Rand
}
func NewSinkServer(conf *SinkServerConfig) *SinkServer {
ss := &SinkServer{
DoneCh: make(chan struct{}),
logger: conf.Logger,
client: conf.Client,
random: rand.New(rand.NewSource(int64(time.Now().Nanosecond()))),
}
return ss
}
// Run executes the server's run loop, which is responsible for reading
// in new tokens and pushing them out to the various sinks.
func (ss *SinkServer) Run(ctx context.Context, incoming chan string, sinks []*SinkConfig) {
if incoming == nil {
panic("incoming or shutdown channel are nil")
}
ss.logger.Info("starting sink server")
defer func() {
ss.logger.Info("sink server stopped")
close(ss.DoneCh)
}()
latestToken := new(string)
sinkCh := make(chan func() error, len(sinks))
for {
select {
case <-ctx.Done():
return
case token := <-incoming:
if token != *latestToken {
// Drain the existing funcs
drainLoop:
for {
select {
case <-sinkCh:
default:
break drainLoop
}
}
*latestToken = token
for _, s := range sinks {
sinkFunc := func(currSink *SinkConfig, currToken string) func() error {
return func() error {
if currToken != *latestToken {
return nil
}
var err error
if currSink.WrapTTL != 0 {
if currToken, err = s.wrapToken(ss.client, currSink.WrapTTL, currToken); err != nil {
return err
}
}
if s.DHType != "" {
if currToken, err = s.encryptToken(currToken); err != nil {
return err
}
}
return currSink.WriteToken(currToken)
}
}
sinkCh <- sinkFunc(s, token)
}
}
case sinkFunc := <-sinkCh:
select {
case <-ctx.Done():
return
default:
}
if err := sinkFunc(); err != nil {
backoff := 2*time.Second + time.Duration(ss.random.Int63()%int64(time.Second*2)-int64(time.Second))
ss.logger.Error("error returned by sink function, retrying", "error", err, "backoff", backoff.String())
select {
case <-ctx.Done():
return
case <-time.After(backoff):
sinkCh <- sinkFunc
}
}
}
}
}
func (s *SinkConfig) encryptToken(token string) (string, error) {
var aesKey []byte
var err error
resp := new(dhutil.Envelope)
switch s.DHType {
case "curve25519":
if len(s.cachedRemotePubKey) == 0 {
_, err = os.Lstat(s.DHPath)
if err != nil {
if !os.IsNotExist(err) {
return "", errwrap.Wrapf("error stat-ing dh parameters file: {{err}}", err)
}
return "", errors.New("no dh parameters file found, and no cached pub key")
}
fileBytes, err := ioutil.ReadFile(s.DHPath)
if err != nil {
return "", errwrap.Wrapf("error reading file for dh parameters: {{err}}", err)
}
theirPubKey := new(dhutil.PublicKeyInfo)
if err := jsonutil.DecodeJSON(fileBytes, theirPubKey); err != nil {
return "", errwrap.Wrapf("error decoding public key: {{err}}", err)
}
if len(theirPubKey.Curve25519PublicKey) == 0 {
return "", errors.New("public key is nil")
}
s.cachedRemotePubKey = theirPubKey.Curve25519PublicKey
}
if len(s.cachedPubKey) == 0 {
s.cachedPubKey, s.cachedPriKey, err = dhutil.GeneratePublicPrivateKey()
if err != nil {
return "", errwrap.Wrapf("error generating pub/pri curve25519 keys: {{err}}", err)
}
}
resp.Curve25519PublicKey = s.cachedPubKey
}
aesKey, err = dhutil.GenerateSharedKey(s.cachedPriKey, s.cachedRemotePubKey)
if err != nil {
return "", errwrap.Wrapf("error deriving shared key: {{err}}", err)
}
if len(aesKey) == 0 {
return "", errors.New("derived AES key is empty")
}
resp.EncryptedPayload, resp.Nonce, err = dhutil.EncryptAES(aesKey, []byte(token), []byte(s.AAD))
if err != nil {
return "", errwrap.Wrapf("error encrypting with shared key: {{err}}", err)
}
m, err := jsonutil.EncodeJSON(resp)
if err != nil {
return "", errwrap.Wrapf("error encoding encrypted payload: {{err}}", err)
}
return string(m), nil
}
func (s *SinkConfig) wrapToken(client *api.Client, wrapTTL time.Duration, token string) (string, error) {
wrapClient, err := client.Clone()
if err != nil {
return "", errwrap.Wrapf("error deriving client for wrapping, not writing out to sink: {{err}})", err)
}
wrapClient.SetToken(token)
wrapClient.SetWrappingLookupFunc(func(string, string) string {
return wrapTTL.String()
})
secret, err := wrapClient.Logical().Write("sys/wrapping/wrap", map[string]interface{}{
"token": token,
})
if err != nil {
return "", errwrap.Wrapf("error wrapping token, not writing out to sink: {{err}})", err)
}
if secret == nil {
return "", errors.New("nil secret returned, not writing out to sink")
}
if secret.WrapInfo == nil {
return "", errors.New("nil wrap info returned, not writing out to sink")
}
m, err := jsonutil.EncodeJSON(secret.WrapInfo)
if err != nil {
return "", errwrap.Wrapf("error marshaling token, not writing out to sink: {{err}})", err)
}
return string(m), nil
}

View file

@ -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(),

View file

@ -182,6 +182,7 @@ var commonCommands = []string{
"delete",
"list",
"login",
"agent",
"server",
"status",
"unwrap",

121
helper/dhutil/dhutil.go Normal file
View file

@ -0,0 +1,121 @@
package dhutil
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"errors"
"fmt"
"io"
"golang.org/x/crypto/curve25519"
)
type PublicKeyInfo struct {
Curve25519PublicKey []byte `json:"curve25519_public_key"`
}
type Envelope struct {
Curve25519PublicKey []byte `json:"curve25519_public_key"`
Nonce []byte `json:"nonce"`
EncryptedPayload []byte `json:"encrypted_payload"`
}
// generatePublicPrivateKey uses curve25519 to generate a public and private key
// pair.
func GeneratePublicPrivateKey() ([]byte, []byte, error) {
var scalar, public [32]byte
if _, err := io.ReadFull(rand.Reader, scalar[:]); err != nil {
return nil, nil, err
}
curve25519.ScalarBaseMult(&public, &scalar)
return public[:], scalar[:], nil
}
// generateSharedKey uses the private key and the other party's public key to
// generate the shared secret.
func GenerateSharedKey(ourPrivate, theirPublic []byte) ([]byte, error) {
if len(ourPrivate) != 32 {
return nil, fmt.Errorf("invalid private key length: %d", len(ourPrivate))
}
if len(theirPublic) != 32 {
return nil, fmt.Errorf("invalid public key length: %d", len(theirPublic))
}
var scalar, pub, secret [32]byte
copy(scalar[:], ourPrivate)
copy(pub[:], theirPublic)
curve25519.ScalarMult(&secret, &scalar, &pub)
return secret[:], nil
}
// Use AES256-GCM to encrypt some plaintext with a provided key. The returned values are
// the ciphertext, the nonce, and error respectively.
func EncryptAES(key, plaintext, aad []byte) ([]byte, []byte, error) {
// We enforce AES-256, so check explicitly for 32 bytes on the key
if len(key) != 32 {
return nil, nil, fmt.Errorf("invalid key length: %d", len(key))
}
if len(plaintext) == 0 {
return nil, nil, errors.New("empty plaintext provided")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, nil, err
}
// Never use more than 2^32 random nonces with a given key because of the risk of a repeat.
nonce := make([]byte, 12)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, nil, err
}
ciphertext := aesgcm.Seal(nil, nonce, plaintext, aad)
return ciphertext, nonce, nil
}
// Use AES256-GCM to decrypt some ciphertext with a provided key and nonce. The
// returned values are the plaintext and error respectively.
func DecryptAES(key, ciphertext, nonce, aad []byte) ([]byte, error) {
// We enforce AES-256, so check explicitly for 32 bytes on the key
if len(key) != 32 {
return nil, fmt.Errorf("invalid key length: %d", len(key))
}
if len(ciphertext) == 0 {
return nil, errors.New("empty ciphertext provided")
}
if len(nonce) == 0 {
return nil, errors.New("empty nonce provided")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, aad)
if err != nil {
return nil, err
}
return plaintext, nil
}

21
vendor/github.com/kr/pretty/License generated vendored Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright 2012 Keith Rarick
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

9
vendor/github.com/kr/pretty/Readme generated vendored Normal file
View file

@ -0,0 +1,9 @@
package pretty
import "github.com/kr/pretty"
Package pretty provides pretty-printing for Go values.
Documentation
http://godoc.org/github.com/kr/pretty

265
vendor/github.com/kr/pretty/diff.go generated vendored Normal file
View file

@ -0,0 +1,265 @@
package pretty
import (
"fmt"
"io"
"reflect"
)
type sbuf []string
func (p *sbuf) Printf(format string, a ...interface{}) {
s := fmt.Sprintf(format, a...)
*p = append(*p, s)
}
// Diff returns a slice where each element describes
// a difference between a and b.
func Diff(a, b interface{}) (desc []string) {
Pdiff((*sbuf)(&desc), a, b)
return desc
}
// wprintfer calls Fprintf on w for each Printf call
// with a trailing newline.
type wprintfer struct{ w io.Writer }
func (p *wprintfer) Printf(format string, a ...interface{}) {
fmt.Fprintf(p.w, format+"\n", a...)
}
// Fdiff writes to w a description of the differences between a and b.
func Fdiff(w io.Writer, a, b interface{}) {
Pdiff(&wprintfer{w}, a, b)
}
type Printfer interface {
Printf(format string, a ...interface{})
}
// Pdiff prints to p a description of the differences between a and b.
// It calls Printf once for each difference, with no trailing newline.
// The standard library log.Logger is a Printfer.
func Pdiff(p Printfer, a, b interface{}) {
diffPrinter{w: p}.diff(reflect.ValueOf(a), reflect.ValueOf(b))
}
type Logfer interface {
Logf(format string, a ...interface{})
}
// logprintfer calls Fprintf on w for each Printf call
// with a trailing newline.
type logprintfer struct{ l Logfer }
func (p *logprintfer) Printf(format string, a ...interface{}) {
p.l.Logf(format, a...)
}
// Ldiff prints to l a description of the differences between a and b.
// It calls Logf once for each difference, with no trailing newline.
// The standard library testing.T and testing.B are Logfers.
func Ldiff(l Logfer, a, b interface{}) {
Pdiff(&logprintfer{l}, a, b)
}
type diffPrinter struct {
w Printfer
l string // label
}
func (w diffPrinter) printf(f string, a ...interface{}) {
var l string
if w.l != "" {
l = w.l + ": "
}
w.w.Printf(l+f, a...)
}
func (w diffPrinter) diff(av, bv reflect.Value) {
if !av.IsValid() && bv.IsValid() {
w.printf("nil != %# v", formatter{v: bv, quote: true})
return
}
if av.IsValid() && !bv.IsValid() {
w.printf("%# v != nil", formatter{v: av, quote: true})
return
}
if !av.IsValid() && !bv.IsValid() {
return
}
at := av.Type()
bt := bv.Type()
if at != bt {
w.printf("%v != %v", at, bt)
return
}
switch kind := at.Kind(); kind {
case reflect.Bool:
if a, b := av.Bool(), bv.Bool(); a != b {
w.printf("%v != %v", a, b)
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if a, b := av.Int(), bv.Int(); a != b {
w.printf("%d != %d", a, b)
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
if a, b := av.Uint(), bv.Uint(); a != b {
w.printf("%d != %d", a, b)
}
case reflect.Float32, reflect.Float64:
if a, b := av.Float(), bv.Float(); a != b {
w.printf("%v != %v", a, b)
}
case reflect.Complex64, reflect.Complex128:
if a, b := av.Complex(), bv.Complex(); a != b {
w.printf("%v != %v", a, b)
}
case reflect.Array:
n := av.Len()
for i := 0; i < n; i++ {
w.relabel(fmt.Sprintf("[%d]", i)).diff(av.Index(i), bv.Index(i))
}
case reflect.Chan, reflect.Func, reflect.UnsafePointer:
if a, b := av.Pointer(), bv.Pointer(); a != b {
w.printf("%#x != %#x", a, b)
}
case reflect.Interface:
w.diff(av.Elem(), bv.Elem())
case reflect.Map:
ak, both, bk := keyDiff(av.MapKeys(), bv.MapKeys())
for _, k := range ak {
w := w.relabel(fmt.Sprintf("[%#v]", k))
w.printf("%q != (missing)", av.MapIndex(k))
}
for _, k := range both {
w := w.relabel(fmt.Sprintf("[%#v]", k))
w.diff(av.MapIndex(k), bv.MapIndex(k))
}
for _, k := range bk {
w := w.relabel(fmt.Sprintf("[%#v]", k))
w.printf("(missing) != %q", bv.MapIndex(k))
}
case reflect.Ptr:
switch {
case av.IsNil() && !bv.IsNil():
w.printf("nil != %# v", formatter{v: bv, quote: true})
case !av.IsNil() && bv.IsNil():
w.printf("%# v != nil", formatter{v: av, quote: true})
case !av.IsNil() && !bv.IsNil():
w.diff(av.Elem(), bv.Elem())
}
case reflect.Slice:
lenA := av.Len()
lenB := bv.Len()
if lenA != lenB {
w.printf("%s[%d] != %s[%d]", av.Type(), lenA, bv.Type(), lenB)
break
}
for i := 0; i < lenA; i++ {
w.relabel(fmt.Sprintf("[%d]", i)).diff(av.Index(i), bv.Index(i))
}
case reflect.String:
if a, b := av.String(), bv.String(); a != b {
w.printf("%q != %q", a, b)
}
case reflect.Struct:
for i := 0; i < av.NumField(); i++ {
w.relabel(at.Field(i).Name).diff(av.Field(i), bv.Field(i))
}
default:
panic("unknown reflect Kind: " + kind.String())
}
}
func (d diffPrinter) relabel(name string) (d1 diffPrinter) {
d1 = d
if d.l != "" && name[0] != '[' {
d1.l += "."
}
d1.l += name
return d1
}
// keyEqual compares a and b for equality.
// Both a and b must be valid map keys.
func keyEqual(av, bv reflect.Value) bool {
if !av.IsValid() && !bv.IsValid() {
return true
}
if !av.IsValid() || !bv.IsValid() || av.Type() != bv.Type() {
return false
}
switch kind := av.Kind(); kind {
case reflect.Bool:
a, b := av.Bool(), bv.Bool()
return a == b
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
a, b := av.Int(), bv.Int()
return a == b
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
a, b := av.Uint(), bv.Uint()
return a == b
case reflect.Float32, reflect.Float64:
a, b := av.Float(), bv.Float()
return a == b
case reflect.Complex64, reflect.Complex128:
a, b := av.Complex(), bv.Complex()
return a == b
case reflect.Array:
for i := 0; i < av.Len(); i++ {
if !keyEqual(av.Index(i), bv.Index(i)) {
return false
}
}
return true
case reflect.Chan, reflect.UnsafePointer, reflect.Ptr:
a, b := av.Pointer(), bv.Pointer()
return a == b
case reflect.Interface:
return keyEqual(av.Elem(), bv.Elem())
case reflect.String:
a, b := av.String(), bv.String()
return a == b
case reflect.Struct:
for i := 0; i < av.NumField(); i++ {
if !keyEqual(av.Field(i), bv.Field(i)) {
return false
}
}
return true
default:
panic("invalid map key type " + av.Type().String())
}
}
func keyDiff(a, b []reflect.Value) (ak, both, bk []reflect.Value) {
for _, av := range a {
inBoth := false
for _, bv := range b {
if keyEqual(av, bv) {
inBoth = true
both = append(both, av)
break
}
}
if !inBoth {
ak = append(ak, av)
}
}
for _, bv := range b {
inBoth := false
for _, av := range a {
if keyEqual(av, bv) {
inBoth = true
break
}
}
if !inBoth {
bk = append(bk, bv)
}
}
return
}

328
vendor/github.com/kr/pretty/formatter.go generated vendored Normal file
View file

@ -0,0 +1,328 @@
package pretty
import (
"fmt"
"io"
"reflect"
"strconv"
"text/tabwriter"
"github.com/kr/text"
)
type formatter struct {
v reflect.Value
force bool
quote bool
}
// Formatter makes a wrapper, f, that will format x as go source with line
// breaks and tabs. Object f responds to the "%v" formatting verb when both the
// "#" and " " (space) flags are set, for example:
//
// fmt.Sprintf("%# v", Formatter(x))
//
// If one of these two flags is not set, or any other verb is used, f will
// format x according to the usual rules of package fmt.
// In particular, if x satisfies fmt.Formatter, then x.Format will be called.
func Formatter(x interface{}) (f fmt.Formatter) {
return formatter{v: reflect.ValueOf(x), quote: true}
}
func (fo formatter) String() string {
return fmt.Sprint(fo.v.Interface()) // unwrap it
}
func (fo formatter) passThrough(f fmt.State, c rune) {
s := "%"
for i := 0; i < 128; i++ {
if f.Flag(i) {
s += string(i)
}
}
if w, ok := f.Width(); ok {
s += fmt.Sprintf("%d", w)
}
if p, ok := f.Precision(); ok {
s += fmt.Sprintf(".%d", p)
}
s += string(c)
fmt.Fprintf(f, s, fo.v.Interface())
}
func (fo formatter) Format(f fmt.State, c rune) {
if fo.force || c == 'v' && f.Flag('#') && f.Flag(' ') {
w := tabwriter.NewWriter(f, 4, 4, 1, ' ', 0)
p := &printer{tw: w, Writer: w, visited: make(map[visit]int)}
p.printValue(fo.v, true, fo.quote)
w.Flush()
return
}
fo.passThrough(f, c)
}
type printer struct {
io.Writer
tw *tabwriter.Writer
visited map[visit]int
depth int
}
func (p *printer) indent() *printer {
q := *p
q.tw = tabwriter.NewWriter(p.Writer, 4, 4, 1, ' ', 0)
q.Writer = text.NewIndentWriter(q.tw, []byte{'\t'})
return &q
}
func (p *printer) printInline(v reflect.Value, x interface{}, showType bool) {
if showType {
io.WriteString(p, v.Type().String())
fmt.Fprintf(p, "(%#v)", x)
} else {
fmt.Fprintf(p, "%#v", x)
}
}
// printValue must keep track of already-printed pointer values to avoid
// infinite recursion.
type visit struct {
v uintptr
typ reflect.Type
}
func (p *printer) printValue(v reflect.Value, showType, quote bool) {
if p.depth > 10 {
io.WriteString(p, "!%v(DEPTH EXCEEDED)")
return
}
switch v.Kind() {
case reflect.Bool:
p.printInline(v, v.Bool(), showType)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
p.printInline(v, v.Int(), showType)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
p.printInline(v, v.Uint(), showType)
case reflect.Float32, reflect.Float64:
p.printInline(v, v.Float(), showType)
case reflect.Complex64, reflect.Complex128:
fmt.Fprintf(p, "%#v", v.Complex())
case reflect.String:
p.fmtString(v.String(), quote)
case reflect.Map:
t := v.Type()
if showType {
io.WriteString(p, t.String())
}
writeByte(p, '{')
if nonzero(v) {
expand := !canInline(v.Type())
pp := p
if expand {
writeByte(p, '\n')
pp = p.indent()
}
keys := v.MapKeys()
for i := 0; i < v.Len(); i++ {
showTypeInStruct := true
k := keys[i]
mv := v.MapIndex(k)
pp.printValue(k, false, true)
writeByte(pp, ':')
if expand {
writeByte(pp, '\t')
}
showTypeInStruct = t.Elem().Kind() == reflect.Interface
pp.printValue(mv, showTypeInStruct, true)
if expand {
io.WriteString(pp, ",\n")
} else if i < v.Len()-1 {
io.WriteString(pp, ", ")
}
}
if expand {
pp.tw.Flush()
}
}
writeByte(p, '}')
case reflect.Struct:
t := v.Type()
if v.CanAddr() {
addr := v.UnsafeAddr()
vis := visit{addr, t}
if vd, ok := p.visited[vis]; ok && vd < p.depth {
p.fmtString(t.String()+"{(CYCLIC REFERENCE)}", false)
break // don't print v again
}
p.visited[vis] = p.depth
}
if showType {
io.WriteString(p, t.String())
}
writeByte(p, '{')
if nonzero(v) {
expand := !canInline(v.Type())
pp := p
if expand {
writeByte(p, '\n')
pp = p.indent()
}
for i := 0; i < v.NumField(); i++ {
showTypeInStruct := true
if f := t.Field(i); f.Name != "" {
io.WriteString(pp, f.Name)
writeByte(pp, ':')
if expand {
writeByte(pp, '\t')
}
showTypeInStruct = labelType(f.Type)
}
pp.printValue(getField(v, i), showTypeInStruct, true)
if expand {
io.WriteString(pp, ",\n")
} else if i < v.NumField()-1 {
io.WriteString(pp, ", ")
}
}
if expand {
pp.tw.Flush()
}
}
writeByte(p, '}')
case reflect.Interface:
switch e := v.Elem(); {
case e.Kind() == reflect.Invalid:
io.WriteString(p, "nil")
case e.IsValid():
pp := *p
pp.depth++
pp.printValue(e, showType, true)
default:
io.WriteString(p, v.Type().String())
io.WriteString(p, "(nil)")
}
case reflect.Array, reflect.Slice:
t := v.Type()
if showType {
io.WriteString(p, t.String())
}
if v.Kind() == reflect.Slice && v.IsNil() && showType {
io.WriteString(p, "(nil)")
break
}
if v.Kind() == reflect.Slice && v.IsNil() {
io.WriteString(p, "nil")
break
}
writeByte(p, '{')
expand := !canInline(v.Type())
pp := p
if expand {
writeByte(p, '\n')
pp = p.indent()
}
for i := 0; i < v.Len(); i++ {
showTypeInSlice := t.Elem().Kind() == reflect.Interface
pp.printValue(v.Index(i), showTypeInSlice, true)
if expand {
io.WriteString(pp, ",\n")
} else if i < v.Len()-1 {
io.WriteString(pp, ", ")
}
}
if expand {
pp.tw.Flush()
}
writeByte(p, '}')
case reflect.Ptr:
e := v.Elem()
if !e.IsValid() {
writeByte(p, '(')
io.WriteString(p, v.Type().String())
io.WriteString(p, ")(nil)")
} else {
pp := *p
pp.depth++
writeByte(pp, '&')
pp.printValue(e, true, true)
}
case reflect.Chan:
x := v.Pointer()
if showType {
writeByte(p, '(')
io.WriteString(p, v.Type().String())
fmt.Fprintf(p, ")(%#v)", x)
} else {
fmt.Fprintf(p, "%#v", x)
}
case reflect.Func:
io.WriteString(p, v.Type().String())
io.WriteString(p, " {...}")
case reflect.UnsafePointer:
p.printInline(v, v.Pointer(), showType)
case reflect.Invalid:
io.WriteString(p, "nil")
}
}
func canInline(t reflect.Type) bool {
switch t.Kind() {
case reflect.Map:
return !canExpand(t.Elem())
case reflect.Struct:
for i := 0; i < t.NumField(); i++ {
if canExpand(t.Field(i).Type) {
return false
}
}
return true
case reflect.Interface:
return false
case reflect.Array, reflect.Slice:
return !canExpand(t.Elem())
case reflect.Ptr:
return false
case reflect.Chan, reflect.Func, reflect.UnsafePointer:
return false
}
return true
}
func canExpand(t reflect.Type) bool {
switch t.Kind() {
case reflect.Map, reflect.Struct,
reflect.Interface, reflect.Array, reflect.Slice,
reflect.Ptr:
return true
}
return false
}
func labelType(t reflect.Type) bool {
switch t.Kind() {
case reflect.Interface, reflect.Struct:
return true
}
return false
}
func (p *printer) fmtString(s string, quote bool) {
if quote {
s = strconv.Quote(s)
}
io.WriteString(p, s)
}
func writeByte(w io.Writer, b byte) {
w.Write([]byte{b})
}
func getField(v reflect.Value, i int) reflect.Value {
val := v.Field(i)
if val.Kind() == reflect.Interface && !val.IsNil() {
val = val.Elem()
}
return val
}

3
vendor/github.com/kr/pretty/go.mod generated vendored Normal file
View file

@ -0,0 +1,3 @@
module "github.com/kr/pretty"
require "github.com/kr/text" v0.1.0

108
vendor/github.com/kr/pretty/pretty.go generated vendored Normal file
View file

@ -0,0 +1,108 @@
// Package pretty provides pretty-printing for Go values. This is
// useful during debugging, to avoid wrapping long output lines in
// the terminal.
//
// It provides a function, Formatter, that can be used with any
// function that accepts a format string. It also provides
// convenience wrappers for functions in packages fmt and log.
package pretty
import (
"fmt"
"io"
"log"
"reflect"
)
// Errorf is a convenience wrapper for fmt.Errorf.
//
// Calling Errorf(f, x, y) is equivalent to
// fmt.Errorf(f, Formatter(x), Formatter(y)).
func Errorf(format string, a ...interface{}) error {
return fmt.Errorf(format, wrap(a, false)...)
}
// Fprintf is a convenience wrapper for fmt.Fprintf.
//
// Calling Fprintf(w, f, x, y) is equivalent to
// fmt.Fprintf(w, f, Formatter(x), Formatter(y)).
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, error error) {
return fmt.Fprintf(w, format, wrap(a, false)...)
}
// Log is a convenience wrapper for log.Printf.
//
// Calling Log(x, y) is equivalent to
// log.Print(Formatter(x), Formatter(y)), but each operand is
// formatted with "%# v".
func Log(a ...interface{}) {
log.Print(wrap(a, true)...)
}
// Logf is a convenience wrapper for log.Printf.
//
// Calling Logf(f, x, y) is equivalent to
// log.Printf(f, Formatter(x), Formatter(y)).
func Logf(format string, a ...interface{}) {
log.Printf(format, wrap(a, false)...)
}
// Logln is a convenience wrapper for log.Printf.
//
// Calling Logln(x, y) is equivalent to
// log.Println(Formatter(x), Formatter(y)), but each operand is
// formatted with "%# v".
func Logln(a ...interface{}) {
log.Println(wrap(a, true)...)
}
// Print pretty-prints its operands and writes to standard output.
//
// Calling Print(x, y) is equivalent to
// fmt.Print(Formatter(x), Formatter(y)), but each operand is
// formatted with "%# v".
func Print(a ...interface{}) (n int, errno error) {
return fmt.Print(wrap(a, true)...)
}
// Printf is a convenience wrapper for fmt.Printf.
//
// Calling Printf(f, x, y) is equivalent to
// fmt.Printf(f, Formatter(x), Formatter(y)).
func Printf(format string, a ...interface{}) (n int, errno error) {
return fmt.Printf(format, wrap(a, false)...)
}
// Println pretty-prints its operands and writes to standard output.
//
// Calling Print(x, y) is equivalent to
// fmt.Println(Formatter(x), Formatter(y)), but each operand is
// formatted with "%# v".
func Println(a ...interface{}) (n int, errno error) {
return fmt.Println(wrap(a, true)...)
}
// Sprint is a convenience wrapper for fmt.Sprintf.
//
// Calling Sprint(x, y) is equivalent to
// fmt.Sprint(Formatter(x), Formatter(y)), but each operand is
// formatted with "%# v".
func Sprint(a ...interface{}) string {
return fmt.Sprint(wrap(a, true)...)
}
// Sprintf is a convenience wrapper for fmt.Sprintf.
//
// Calling Sprintf(f, x, y) is equivalent to
// fmt.Sprintf(f, Formatter(x), Formatter(y)).
func Sprintf(format string, a ...interface{}) string {
return fmt.Sprintf(format, wrap(a, false)...)
}
func wrap(a []interface{}, force bool) []interface{} {
w := make([]interface{}, len(a))
for i, x := range a {
w[i] = formatter{v: reflect.ValueOf(x), force: force}
}
return w
}

41
vendor/github.com/kr/pretty/zero.go generated vendored Normal file
View file

@ -0,0 +1,41 @@
package pretty
import (
"reflect"
)
func nonzero(v reflect.Value) bool {
switch v.Kind() {
case reflect.Bool:
return v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() != 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() != 0
case reflect.Float32, reflect.Float64:
return v.Float() != 0
case reflect.Complex64, reflect.Complex128:
return v.Complex() != complex(0, 0)
case reflect.String:
return v.String() != ""
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
if nonzero(getField(v, i)) {
return true
}
}
return false
case reflect.Array:
for i := 0; i < v.Len(); i++ {
if nonzero(v.Index(i)) {
return true
}
}
return false
case reflect.Map, reflect.Interface, reflect.Slice, reflect.Ptr, reflect.Chan, reflect.Func:
return !v.IsNil()
case reflect.UnsafePointer:
return v.Pointer() != 0
}
return true
}

6
vendor/vendor.json vendored
View file

@ -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",

View file

@ -0,0 +1,161 @@
---
layout: "docs"
page_title: "Vault Agent Auto-Auth"
sidebar_current: "docs-agent-autoauth"
description: |-
Vault Agent's Auto-Auth functionality allows easy and automatic
authentication to Vault in a variety of environments.
---
# Vault Agent Auto-Auth
The Auto-Auth functionality of Vault Agent allows for easy authentication in a
wide variety of environments.
## Functionality
Auto-Auth consists of two parts: a Method, which is the authentication method
that should be used in the current environment; and one or more Sinks, which
are locations where the agent should write a token any time the current token
value has changed.
When the agent is started with Auto-Auth enabled, it will attempt to acquire a
Vault token using the configured Method. On failure, it will back off for a
short while (including some randomness to help prevent thundering herd
scenarios) and retry. On success, unless the auth method is configured to wrap
the tokens, it will keep the resulting token renewed until renewal is no longer
allowed or fails, at which point it will attempt to reauthenticate.
Every time an authentication is successful, the token is written to the
configured Sinks, subject to their configuration.
## Advanced Functionality
Sinks support some advanced features, including the ability for the written
values to be encrypted or
[response-wrapped](/docs/concepts/response-wrapping.html).
Both mechanisms can be used concurrently; in this case, the value will be
response-wrapped, then encrypted.
### Response-Wrapping Tokens
There are two ways that tokens can be response-wrapped by the agent:
1. By the auth method. This allows the end client to introspect the
`creation_path` of the token, helping prevent Man-In-The-Middle (MITM)
attacks. However, because the agent cannot then unwrap the token and rewrap
it without modifying the `creation_path`, the agent is not able to renew the
token; it is up to the end client to renew the token. The agent stays
daemonized in this mode since some auth methods allow for reauthentication
on certain events.
2. By any of the token sinks. Because more than one sink can be configured, the
token must be wrapped after it is fetched, rather than wrapped by Vault as
it's being returned. As a result, the `creation_path` will always be
`sys/wrapping/wrap`, and validation of this field cannot be used as
protection against MITM attacks. However, this mode allows the agent to keep
the token renewed for the end client and automatically reauthenticate when
it expires.
### Encrypting Tokens
Tokens can be encrypted, using a Diffie-Hellman exchange to generate an
ephemeral key. In this mechanism, the client receiving the token writes a
generated private key to a file. The sink responsible for writing the token to
that client looks for this public key and uses it to compute a shared secret
key, which is then used to encrypt the token via AES-GCM. The nonce, encrypted
payload, and the sink's public key are then written to the output file, where
the client can compute the shared secret and decrypt the token value.
~> NOTE: This is not a protection against MITM attacks! The purpose of this
feature is for forward-secrecy and coverage against bare token values being
persisted. A MITM that can access the sink's output and public-key input files
could attack this exchange.
To help mitigate MITM attacks, additional authenticated data (AAD) can be
provided to the agent. This data is written as part of the AES-GCM tag and must
match on both the agent and the client. This of course means that protecting
this AAD becomes important, but it provides another layer for an attacker to
have to overcome. For instance, if the attacker has access to the file system
where the token is being written, but not to read agent configuration or read
environment variables, this AAD can be generated and passed to the agent and
the client in ways that would be difficult for the attacker to find.
When using AAD, it is always a good idea for this to be as fresh as possible;
generate a value and pass it to your client and agent on startup. Additionally,
agent uses a Trust On First Use model; after it finds a generated public key,
it will reuse that public key instead of looking for new values that have been
written.
If writing a client that uses this feature, it will likely be helpful to look
at the
[dhutil](https://github.com/hashicorp/vault/blob/master/helper/dhutil/dhutil.go)
library. This shows the expected format of the public key input and envelope
output formats.
## Configuration
The top level `auto_auth` block has two configuration entries:
- `method` `(object: required)` - Configuration for the method
- `sinks` `(array of objects: required)` - Configuration for the sinks
### Configuration (Method)
These are common configuration values that live within the `method` block:
- `type` `(string: required)` - The type of the method to use, e.g. `aws`,
`gcp`, `azure`, etc. *Note*: when using HCL this can be used as the key for
the block, e.g. `method "aws" {...}`.
- `mount_path` `(string: optional)` - The mount path of the method. If not
specified, defaults to a value of `auth/<method type>`.
- `wrap_ttl` `(string or integer: optional)` - If specified, the written token
will be response-wrapped by the agent. This is more secure than wrapping by
sinks, but does not allow the agent to keep the token renewed or
automatically reauthenticate when it expires. Rather than a simple string,
the written value will be a JSON-encoded
[SecretWrapInfo](https://godoc.org/github.com/hashicorp/vault/api#SecretWrapInfo)
structure. Values can be an integer number of seconds or a stringish value
like `5m`.
- `config` `(object: required)` - Configuration of the method itself. See the
sidebar for information about each method.
### Configuration (Sinks)
These configuration values are common to all Sinks:
- `type` `(string: required)` - The type of the method to use, e.g. `file`.
*Note*: when using HCL this can be used as the key for the block, e.g. `sink
"file" {...}`.
- `wrap_ttl` `(string or integer: optional)` - If specified, the written token
will be response-wrapped by the sink. This is less secure than wrapping by
the method, but allows the agent to keep the token renewed and automatically
reauthenticate when it expires. Rather than a simple string, the written
value will be a JSON-encoded
[SecretWrapInfo](https://godoc.org/github.com/hashicorp/vault/api#SecretWrapInfo)
structure. Values can be an integer number of seconds or a stringish value
like `5m`.
- `dh_type` `(string: optional)` - If specified, the type of Diffie-Hellman exchange to
perform, meaning, which ciphers and/or curves. Currently only `curve25519` is
supported.
- `dh_path` `(string: required if dh_type is set)` - The path from which the
agent should read the client's initial parameters (e.g. curve25519 public
key).
- `aad` `(string: optional)` - If specified, additional authenticated data to
use with the AES-GCM encryption of the token. Can be any string, including
serialized data.
- `aad_env_var` `(string: optional)` - If specified, AAD will be read from the
given environment variable rather than a value in the configuration file.
- `config` `(object: required)` - Configuration of the sink itself. See the
sidebar for information about each sink.

View file

@ -0,0 +1,41 @@
---
layout: "docs"
page_title: "Vault Agent Auto-Auth AWS Method"
sidebar_current: "docs-agent-autoauth-methods-aws"
description: |-
AWS Method for Vault Agent Auto-Auth
---
# Vault Agent Auto-Auth AWS Method
The `aws` method performs authentication against the [AWS Auth
method](https://www.vaultproject.io/docs/auth/aws.html). Both `ec2` and `iam`
authentication types are supported. If `ec2` is used, the agent will store the
reauthentication value in memory and use it for reauthenticating, but will not
persist it to disk.
Due to the complexity of the TOFU model used in the `ec2` method, we recommend
the `iam` method when possible.
## Credentials
Vault will use the AWS SDK's normal credential chain behavior, which means it
will try to source credentials from the assigned instance profile, a
credentials file, the environment, or static credentials. Generally it should
not be required to set the `access_key` and `secret_key` parameters.
## Configuration
- `type` `(string: required)` - The type of authentication; must be `ec2` or `iam`
- `role` `(string: required)` - The role to authenticate against on Vault
- `access_key` `(string: optional)` - When using static credentials, the access key to use
- `secret_key` `(string: optional)` - When using static credentials, the secret key to use
- `session_token` `(string: optional)` - The session token to use for authentication, if needed
- `header_value` `(string: optional)` - If configured in Vault, the value to
use for
[`iam_server_id_header_value`](https://www.vaultproject.io/api/auth/aws/index.html#iam_server_id_header_value)

View file

@ -0,0 +1,21 @@
---
layout: "docs"
page_title: "Vault Agent Auto-Auth Azure Method"
sidebar_current: "docs-agent-autoauth-methods-azure"
description: |-
Azure Method for Vault Agent Auto-Auth
---
# Vault Agent Auto-Auth Azure Method
The `azure` method reads in Azure instance credentials and uses them to
authenticate with the [Azure Auth
method](https://www.vaultproject.io/docs/auth/azure.html). It reads most
parameters needed for authentication directly from instance information based
on the value of the `resource` parameter.
## Configuration
- `role` `(string: required)` - The role to authenticate against on Vault
- `resource` `(string: required)` - The resource name to use when getting instance information

View file

@ -0,0 +1,39 @@
---
layout: "docs"
page_title: "Vault Agent Auto-Auth GCP Method"
sidebar_current: "docs-agent-autoauth-methods-gcp"
description: |-
GCP Method for Vault Agent Auto-Auth
---
# Vault Agent Auto-Auth GCP Method
The `gcp` method performs authentication against the [GCP Auth
method](https://www.vaultproject.io/docs/auth/gcp.html). Both `gce` and `iam`
authentication types are supported.
## Credentials
Vault will use the GCP SDK's normal credential chain behavior. You can set a
static `credentials` value but it is usually not needed. If running on GCE
using Application Default Credentials, you may need to specify the service
account and project since ADC does not provide metadata used to automatically
determine these.
## Configuration
- `type` `(string: required)` - The type of authentication; must be `gce` or `iam`
- `role` `(string: required)` - The role to authenticate against on Vault
- `credentials` `(string: optional)` - When using static credentials, the
contents of the JSON credentials file
- `service_account` `(string: optional)` - The service account to use, if it
cannot be automatically determined
- `project` `(string: optional)` - The project to use, if it cannot be
automatically determined
- `jwt_exp` `(string or int: optional)` - The number of minutes a generated JWT
should be valid for when using the `iam` method; defaults to 15 minutes

View file

@ -0,0 +1,11 @@
---
layout: "docs"
page_title: "Vault Agent Auto-Auth Methods"
sidebar_current: "docs-agent-autoauth-methods"
description: |-
Methods for Vault Agent Auto-Auth
---
# Vault Agent Auto-Auth Methods
Please see the sidebar for available methods and their usage/configuration.

View file

@ -0,0 +1,21 @@
---
layout: "docs"
page_title: "Vault Agent Auto-Auth JWT Method"
sidebar_current: "docs-agent-autoauth-methods-jwt"
description: |-
JWT Method for Vault Agent Auto-Auth
---
# Vault Agent Auto-Auth JWT Method
The `jwt` method reads in a JWT from a file and sends it to the [JWT Auth
method](https://www.vaultproject.io/docs/auth/jwt.html). Since JWTs often have
limited lifetime, it constantly watches for a new JWT to be written, and when
found it will immediately ingress this value, delete the file, and use the new
JWT to perform a reauthentication.
## Configuration
- `path` `(string: required)` - The path to the JWT file
- `role` `(string: required)` - The role to authenticate against on Vault

View file

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

View file

@ -0,0 +1,24 @@
---
layout: "docs"
page_title: "Vault Agent Auto-Auth File Sink"
sidebar_current: "docs-agent-autoauth-sinks-file"
description: |-
File sink for Vault Agent Auto-Auth
---
# Vault Agent Auto-Auth File Sink
The `file` sink writes tokens, optionally response-wrapped and/or encrypted, to
a file. This may be a local file or a file mapped via some other process (NFS,
Gluster, CIFS, etc.).
Once the sink writes the file, it is up to the client to control lifecycle;
generally it is best for the client to remove the file as soon as it is seen.
It is also best practice to write the file to a ramdisk, ideally an encrypted
ramdisk, and use appropriate filesystem permissions. The file is currently
always written with `0640` permissions.
## Configuration
- `path` `(string: required)` - The path to use to write the token file

View file

@ -0,0 +1,11 @@
---
layout: "docs"
page_title: "Vault Agent Auto-Auth Sinks"
sidebar_current: "docs-agent-autoauth-sinks"
description: |-
Sinks for Vault Agent Auto-Auth
---
# Vault Agent Auto-Auth Sinks
Please see the sidebar for available sinks and their usage/configuration.

View file

@ -0,0 +1,65 @@
---
layout: "docs"
page_title: "Vault Agent"
sidebar_current: "docs-agent"
description: |-
Vault Agent is a client-side daemon that can be used to perform some Vault
functionality automatically.
---
# Vault Agent
Vault Agent is a client daemon that can perform useful tasks.
To get help, run:
```text
$ vault agent -h
```
## Auto-Auth
Vault Agent allows for easy authentication to Vault in a wide variety of
environments. Please see the [Auto-Auth docs](/docs/agent/autoauth/index.html)
for information.
Auto-Auth functionality takes place within an `auto_auth` configuration stanza.
## Configuration
There is one currently-available general configuration option:
- `pid_file` `(string: "")` - Path to the file in which the agent's Process ID
(PID) should be stored.
## Example Configuration
An example configuration, with very contrived values, follows:
```python
pid_file = "./pidfile"
auto_auth {
method "aws" {
mount_path = "auth/aws-subaccount"
config = {
role = "foobar"
}
}
sink "file" {
config = {
path = "/tmp/file-foo"
}
}
sink "file" {
wrap_ttl = "5m"
aad_env_var = "TEST_AAD_ENV"
dh_type = "curve25519"
dh_path = "/tmp/file-foo-dhpath2"
config = {
path = "/tmp/file-bar"
}
}
}
```

View file

@ -0,0 +1,11 @@
---
layout: "docs"
page_title: "agent - Command"
sidebar_current: "docs-commands-agent"
description: |-
The "agent" command is used to start Vault Agent
---
# agent
Please see the [Vault Agent documentation page](/docs/agent/index.html).

View file

@ -182,6 +182,9 @@
<li<%= sidebar_current("docs-commands") %>>
<a href="/docs/commands/index.html">Commands (CLI)</a>
<ul class="nav">
<li<%= sidebar_current("docs-commands-agent") %>>
<a href="/docs/commands/agent.html">agent</a>
</li>
<li<%= sidebar_current("docs-commands-audit") %>>
<a href="/docs/commands/audit.html">audit</a>
<ul class="nav">
@ -368,7 +371,44 @@
</li>
</ul>
</li>
<li<%= sidebar_current("docs-agent") %>>
<a href="/docs/agent/index.html">Vault Agent</a>
<ul class="nav">
<li<%= sidebar_current("docs-agent-autoauth") %>>
<a href="/docs/agent/autoauth/index.html">Auto-Auth</a>
<ul class="nav">
<li<%= sidebar_current("docs-agent-autoauth-methods") %>>
<a href="/docs/agent/autoauth/methods/index.html">Methods</a>
<ul class="nav">
<li<%= sidebar_current("docs-agent-autoauth-methods-aws") %>>
<a href="/docs/agent/autoauth/methods/aws.html">AWS</a>
</li>
<li<%= sidebar_current("docs-agent-autoauth-methods-azure") %>>
<a href="/docs/agent/autoauth/methods/azure.html">Azure</a>
</li>
<li<%= sidebar_current("docs-agent-autoauth-methods-gcp") %>>
<a href="/docs/agent/autoauth/methods/gcp.html">GCP</a>
</li>
<li<%= sidebar_current("docs-agent-autoauth-methods-jwt") %>>
<a href="/docs/agent/autoauth/methods/jwt.html">JWT/OIDC</a>
</li>
<li<%= sidebar_current("docs-agent-autoauth-methods-kubernetes") %>>
<a href="/docs/agent/autoauth/methods/kubernetes.html">Kubernetes</a>
</li>
</ul>
</li>
<li<%= sidebar_current("docs-agent-autoauth-sinks") %>>
<a href="/docs/agent/autoauth/sinks/index.html">Sinks</a>
<ul class="nav">
<li<%= sidebar_current("docs-agent-autoauth-sinks-file") %>>
<a href="/docs/agent/autoauth/sinks/file.html">File</a>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<hr>
<li<%= sidebar_current("docs-secrets") %>>