diff --git a/command/agent.go b/command/agent.go index abb5e8cebd..b8423a34c5 100644 --- a/command/agent.go +++ b/command/agent.go @@ -29,10 +29,12 @@ import ( "github.com/hashicorp/vault/command/agent/auth/jwt" "github.com/hashicorp/vault/command/agent/auth/kubernetes" "github.com/hashicorp/vault/command/agent/cache" + "github.com/hashicorp/vault/command/agent/config" agentConfig "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/command/agent/sink/inmem" + "github.com/hashicorp/vault/command/agent/template" gatedwriter "github.com/hashicorp/vault/helper/gated-writer" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/logging" @@ -215,44 +217,48 @@ func (c *AgentCommand) Run(args []string) int { c.UI.Info("No auto_auth block found in config file, not starting automatic authentication feature") } - if config.Vault != nil { - c.setStringFlag(f, config.Vault.Address, &StringVar{ - Name: flagNameAddress, - Target: &c.flagAddress, - Default: "https://127.0.0.1:8200", - EnvVar: api.EnvVaultAddress, - }) - c.setStringFlag(f, config.Vault.CACert, &StringVar{ - Name: flagNameCACert, - Target: &c.flagCACert, - Default: "", - EnvVar: api.EnvVaultCACert, - }) - c.setStringFlag(f, config.Vault.CAPath, &StringVar{ - Name: flagNameCAPath, - Target: &c.flagCAPath, - Default: "", - EnvVar: api.EnvVaultCAPath, - }) - c.setStringFlag(f, config.Vault.ClientCert, &StringVar{ - Name: flagNameClientCert, - Target: &c.flagClientCert, - Default: "", - EnvVar: api.EnvVaultClientCert, - }) - c.setStringFlag(f, config.Vault.ClientKey, &StringVar{ - Name: flagNameClientKey, - Target: &c.flagClientKey, - Default: "", - EnvVar: api.EnvVaultClientKey, - }) - c.setBoolFlag(f, config.Vault.TLSSkipVerify, &BoolVar{ - Name: flagNameTLSSkipVerify, - Target: &c.flagTLSSkipVerify, - Default: false, - EnvVar: api.EnvVaultSkipVerify, - }) + // create an empty Vault configuration if none was loaded from file. The + // follow-up setStringFlag calls will populate with defaults if otherwise + // omitted + if config.Vault == nil { + config.Vault = new(agentConfig.Vault) } + c.setStringFlag(f, config.Vault.Address, &StringVar{ + Name: flagNameAddress, + Target: &c.flagAddress, + Default: "https://127.0.0.1:8200", + EnvVar: api.EnvVaultAddress, + }) + c.setStringFlag(f, config.Vault.CACert, &StringVar{ + Name: flagNameCACert, + Target: &c.flagCACert, + Default: "", + EnvVar: api.EnvVaultCACert, + }) + c.setStringFlag(f, config.Vault.CAPath, &StringVar{ + Name: flagNameCAPath, + Target: &c.flagCAPath, + Default: "", + EnvVar: api.EnvVaultCAPath, + }) + c.setStringFlag(f, config.Vault.ClientCert, &StringVar{ + Name: flagNameClientCert, + Target: &c.flagClientCert, + Default: "", + EnvVar: api.EnvVaultClientCert, + }) + c.setStringFlag(f, config.Vault.ClientKey, &StringVar{ + Name: flagNameClientKey, + Target: &c.flagClientKey, + Default: "", + EnvVar: api.EnvVaultClientKey, + }) + c.setBoolFlag(f, config.Vault.TLSSkipVerify, &BoolVar{ + Name: flagNameTLSSkipVerify, + Target: &c.flagTLSSkipVerify, + Default: false, + EnvVar: api.EnvVaultSkipVerify, + }) infoKeys := make([]string, 0, 10) info := make(map[string]string) @@ -294,10 +300,14 @@ func (c *AgentCommand) Run(args []string) int { return 1 } + // ctx and cancelFunc are passed to the AuthHandler, SinkServer, and + // TemplateServer that periodically listen for ctx.Done() to fire and shut + // down accordingly. ctx, cancelFunc := context.WithCancel(context.Background()) var method auth.AuthMethod var sinks []*sink.SinkConfig + var namespace string if config.AutoAuth != nil { for _, sc := range config.AutoAuth.Sinks { switch sc.Type { @@ -327,7 +337,8 @@ func (c *AgentCommand) Run(args []string) int { // Check if a default namespace has been set mountPath := config.AutoAuth.Method.MountPath if config.AutoAuth.Method.Namespace != "" { - mountPath = path.Join(config.AutoAuth.Method.Namespace, mountPath) + namespace = config.AutoAuth.Method.Namespace + mountPath = path.Join(namespace, mountPath) } authConfig := &auth.AuthConfig{ @@ -486,14 +497,21 @@ func (c *AgentCommand) Run(args []string) int { defer c.cleanupGuard.Do(listenerCloseFunc) } - var ssDoneCh, ahDoneCh chan struct{} + // Listen for signals + // TODO: implement support for SIGHUP reloading of configuration + // signal.Notify(c.signalCh) + + var ssDoneCh, ahDoneCh, tsDoneCh, unblockCh chan struct{} + var ts *template.Server // Start auto-auth and sink servers if method != nil { + enableTokenCh := len(config.Templates) > 0 ah := auth.NewAuthHandler(&auth.AuthHandlerConfig{ Logger: c.logger.Named("auth.handler"), Client: c.client, WrapTTL: config.AutoAuth.Method.WrapTTL, EnableReauthOnNewCredentials: config.AutoAuth.EnableReauthOnNewCredentials, + EnableTemplateTokenCh: enableTokenCh, }) ahDoneCh = ah.DoneCh @@ -504,8 +522,20 @@ func (c *AgentCommand) Run(args []string) int { }) ssDoneCh = ss.DoneCh + // create an independent vault configuration for Consul Template to use + vaultConfig := c.setupTemplateConfig() + ts = template.NewServer(&template.ServerConfig{ + Logger: c.logger.Named("template.server"), + VaultConf: vaultConfig, + Namespace: namespace, + ExitAfterAuth: config.ExitAfterAuth, + }) + tsDoneCh = ts.DoneCh + unblockCh = ts.UnblockCh + go ah.Run(ctx, method) go ss.Run(ctx, ah.OutputCh, sinks) + go ts.Run(ctx, ah.TemplateTokenCh, config.Templates) } // Server configuration output @@ -536,6 +566,15 @@ func (c *AgentCommand) Run(args []string) int { } }() + // If the template server is running and we've assinged the Unblock channel, + // wait for the template to render + if unblockCh != nil { + select { + case <-ctx.Done(): + case <-ts.UnblockCh: + } + } + select { case <-ssDoneCh: // This will happen if we exit-on-auth @@ -549,6 +588,10 @@ func (c *AgentCommand) Run(args []string) int { if ssDoneCh != nil { <-ssDoneCh } + + if tsDoneCh != nil { + <-tsDoneCh + } } return 0 @@ -648,3 +691,21 @@ func (c *AgentCommand) removePidFile(pidPath string) error { } return os.Remove(pidPath) } + +// setupTemplateConfig creates a config.Vault struct for use by Consul Template. +// Consul Template does not currently allow us to pass in a configured API +// client, unlike the AuthHandler and SinkServer that reuse the client created +// in this Run() method. Here we build a config.Vault struct for use by the +// Template Server that matches the configuration used to create the client +// (c.client), but in a struct of type config.Vault so that Consul Template can +// create it's own api client internally. +func (c *AgentCommand) setupTemplateConfig() *config.Vault { + return &config.Vault{ + Address: c.flagAddress, + CACert: c.flagCACert, + CAPath: c.flagCAPath, + ClientCert: c.flagClientCert, + ClientKey: c.flagClientKey, + TLSSkipVerify: c.flagTLSSkipVerify, + } +} diff --git a/command/agent/README.md b/command/agent/README.md new file mode 100644 index 0000000000..02ef02159f --- /dev/null +++ b/command/agent/README.md @@ -0,0 +1,15 @@ +# Vault Agent + +Vault Agent is a client daemon that provides Auth-Auth, Caching, and Template +features. + +Vault Agent provides a number of different helper features, specifically +addressing the following challenges: + +- Automatic authentication +- Secure delivery/storage of tokens +- Lifecycle management of these tokens (renewal & re-authentication) + +See the usage documentation on the Vault website here: + +- https://www.vaultproject.io/docs/agent/ diff --git a/command/agent/auth/auth.go b/command/agent/auth/auth.go index 7111bab93e..c293f91f9e 100644 --- a/command/agent/auth/auth.go +++ b/command/agent/auth/auth.go @@ -30,11 +30,13 @@ type AuthConfig struct { type AuthHandler struct { DoneCh chan struct{} OutputCh chan string + TemplateTokenCh chan string logger hclog.Logger client *api.Client random *rand.Rand wrapTTL time.Duration enableReauthOnNewCredentials bool + enableTemplateTokenCh bool } type AuthHandlerConfig struct { @@ -42,6 +44,7 @@ type AuthHandlerConfig struct { Client *api.Client WrapTTL time.Duration EnableReauthOnNewCredentials bool + EnableTemplateTokenCh bool } func NewAuthHandler(conf *AuthHandlerConfig) *AuthHandler { @@ -50,11 +53,13 @@ func NewAuthHandler(conf *AuthHandlerConfig) *AuthHandler { // This is buffered so that if we try to output after the sink server // has been shut down, during agent shutdown, we won't block OutputCh: make(chan string, 1), + TemplateTokenCh: make(chan string, 1), logger: conf.Logger, client: conf.Client, random: rand.New(rand.NewSource(int64(time.Now().Nanosecond()))), wrapTTL: conf.WrapTTL, enableReauthOnNewCredentials: conf.EnableReauthOnNewCredentials, + enableTemplateTokenCh: conf.EnableTemplateTokenCh, } return ah @@ -77,6 +82,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) { am.Shutdown() close(ah.OutputCh) close(ah.DoneCh) + close(ah.TemplateTokenCh) ah.logger.Info("auth handler stopped") }() @@ -163,6 +169,9 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) { } ah.logger.Info("authentication successful, sending wrapped token to sinks and pausing") ah.OutputCh <- string(wrappedResp) + if ah.enableTemplateTokenCh { + ah.TemplateTokenCh <- string(wrappedResp) + } am.CredSuccess() @@ -189,6 +198,9 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) { } ah.logger.Info("authentication successful, sending token to sinks") ah.OutputCh <- secret.Auth.ClientToken + if ah.enableTemplateTokenCh { + ah.TemplateTokenCh <- secret.Auth.ClientToken + } am.CredSuccess() } diff --git a/command/agent/auth/auth_test.go b/command/agent/auth/auth_test.go index b8e40ae0b9..297b518257 100644 --- a/command/agent/auth/auth_test.go +++ b/command/agent/auth/auth_test.go @@ -87,7 +87,8 @@ consumption: for { select { case <-ah.OutputCh: - // Nothing + case <-ah.TemplateTokenCh: + // Nothing case <-time.After(stopTime.Sub(time.Now())): if !closed { cancelFunc() diff --git a/command/agent/config/config.go b/command/agent/config/config.go index 3d77640c12..97da8170b2 100644 --- a/command/agent/config/config.go +++ b/command/agent/config/config.go @@ -8,24 +8,28 @@ import ( "strings" "time" + ctconfig "github.com/hashicorp/consul-template/config" "github.com/hashicorp/errwrap" "github.com/hashicorp/go-multierror" "github.com/hashicorp/hcl" "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/sdk/helper/parseutil" + "github.com/mitchellh/mapstructure" ) // Config is the configuration for the vault server. type Config struct { - AutoAuth *AutoAuth `hcl:"auto_auth"` - ExitAfterAuth bool `hcl:"exit_after_auth"` - PidFile string `hcl:"pid_file"` - Listeners []*Listener `hcl:"listeners"` - Cache *Cache `hcl:"cache"` - Vault *Vault `hcl:"vault"` + AutoAuth *AutoAuth `hcl:"auto_auth"` + ExitAfterAuth bool `hcl:"exit_after_auth"` + PidFile string `hcl:"pid_file"` + Listeners []*Listener `hcl:"listeners"` + Cache *Cache `hcl:"cache"` + Vault *Vault `hcl:"vault"` + Templates []*ctconfig.TemplateConfig `hcl:"templates"` } +// Vault contains configuration for connnecting to Vault servers type Vault struct { Address string `hcl:"address"` CACert string `hcl:"ca_cert"` @@ -36,10 +40,12 @@ type Vault struct { ClientKey string `hcl:"client_key"` } +// Cache contains any configuration needed for Cache mode type Cache struct { UseAutoAuthToken bool `hcl:"use_auto_auth_token"` } +// Listener contains configuration for any Vault Agent listeners type Listener struct { Type string Config map[string]interface{} @@ -48,6 +54,7 @@ type Listener struct { // RequireRequestHeader is a listener configuration option const RequireRequestHeader = "require_request_header" +// AutoAuth is the configured authentication method and sinks type AutoAuth struct { Method *Method `hcl:"-"` Sinks []*Sink `hcl:"sinks"` @@ -57,6 +64,7 @@ type AutoAuth struct { EnableReauthOnNewCredentials bool `hcl:"enable_reauth_on_new_credentials"` } +// Method represents the configuration for the authentication backend type Method struct { Type string MountPath string `hcl:"mount_path"` @@ -66,6 +74,7 @@ type Method struct { Config map[string]interface{} } +// Sink defines a location to write the authenticated token type Sink struct { Type string WrapTTLRaw interface{} `hcl:"wrap_ttl"` @@ -116,16 +125,18 @@ func LoadConfig(path string) (*Config, error) { return nil, errwrap.Wrapf("error parsing 'auto_auth': {{err}}", err) } - err = parseListeners(&result, list) - if err != nil { + if err := parseListeners(&result, list); err != nil { return nil, errwrap.Wrapf("error parsing 'listeners': {{err}}", err) } - err = parseCache(&result, list) - if err != nil { + if err := parseCache(&result, list); err != nil { return nil, errwrap.Wrapf("error parsing 'cache':{{err}}", err) } + if err := parseTemplates(&result, list); err != nil { + return nil, errwrap.Wrapf("error parsing 'template': {{err}}", err) + } + if result.Cache != nil { if len(result.Listeners) < 1 { return nil, fmt.Errorf("at least one listener required when cache enabled") @@ -411,3 +422,65 @@ func parseSinks(result *Config, list *ast.ObjectList) error { result.AutoAuth.Sinks = ts return nil } + +func parseTemplates(result *Config, list *ast.ObjectList) error { + name := "template" + + templateList := list.Filter(name) + if len(templateList.Items) < 1 { + return nil + } + + var tcs []*ctconfig.TemplateConfig + + for _, item := range templateList.Items { + var shadow interface{} + if err := hcl.DecodeObject(&shadow, item.Val); err != nil { + return fmt.Errorf("error decoding config: %s", err) + } + + // Convert to a map and flatten the keys we want to flatten + parsed, ok := shadow.(map[string]interface{}) + if !ok { + return errors.New("error converting config") + } + + // flatten the wait field. The initial "wait" value, if given, is a + // []map[string]interface{}, but we need it to be map[string]interface{}. + // Consul Template has a method flattenKeys that walks all of parsed and + // flattens every key. For Vault Agent, we only care about the wait input. + // Only one wait stanza is supported, however Consul Template does not error + // with multiple instead it flattens them down, with last value winning. + // Here we take the last element of the parsed["wait"] slice to keep + // consistency with Consul Template behavior. + wait, ok := parsed["wait"].([]map[string]interface{}) + if ok { + parsed["wait"] = wait[len(wait)-1] + } + + var tc ctconfig.TemplateConfig + + // Use mapstructure to populate the basic config fields + var md mapstructure.Metadata + decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + DecodeHook: mapstructure.ComposeDecodeHookFunc( + ctconfig.StringToFileModeFunc(), + ctconfig.StringToWaitDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + mapstructure.StringToTimeDurationHookFunc(), + ), + ErrorUnused: true, + Metadata: &md, + Result: &tc, + }) + if err != nil { + return errors.New("mapstructure decoder creation failed") + } + if err := decoder.Decode(parsed); err != nil { + return err + } + tcs = append(tcs, &tc) + } + result.Templates = tcs + return nil +} diff --git a/command/agent/config/config_test.go b/command/agent/config/config_test.go index ff133ae576..ea46bf0f7d 100644 --- a/command/agent/config/config_test.go +++ b/command/agent/config/config_test.go @@ -6,6 +6,8 @@ import ( "time" "github.com/go-test/deep" + ctconfig "github.com/hashicorp/consul-template/config" + "github.com/hashicorp/vault/sdk/helper/pointerutil" ) func TestLoadConfigFile_AgentCache(t *testing.T) { @@ -93,8 +95,14 @@ func TestLoadConfigFile_AgentCache(t *testing.T) { } func TestLoadConfigFile(t *testing.T) { - os.Setenv("TEST_AAD_ENV", "aad") - defer os.Unsetenv("TEST_AAD_ENV") + if err := os.Setenv("TEST_AAD_ENV", "aad"); err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Unsetenv("TEST_AAD_ENV"); err != nil { + t.Fatal(err) + } + }() config, err := LoadConfig("./test-fixtures/config.hcl") if err != nil { @@ -278,3 +286,106 @@ func TestLoadConfigFile_AgentCache_AutoAuth_NoSink(t *testing.T) { t.Fatal(diff) } } + +// TestLoadConfigFile_Template tests template definitions in Vault Agent +func TestLoadConfigFile_Template(t *testing.T) { + testCases := map[string]struct { + fixturePath string + expectedTemplates []*ctconfig.TemplateConfig + }{ + "min": { + fixturePath: "./test-fixtures/config-template-min.hcl", + expectedTemplates: []*ctconfig.TemplateConfig{ + &ctconfig.TemplateConfig{ + Source: pointerutil.StringPtr("/path/on/disk/to/template.ctmpl"), + Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"), + }, + }, + }, + "full": { + fixturePath: "./test-fixtures/config-template-full.hcl", + expectedTemplates: []*ctconfig.TemplateConfig{ + &ctconfig.TemplateConfig{ + Backup: pointerutil.BoolPtr(true), + Command: pointerutil.StringPtr("restart service foo"), + CommandTimeout: pointerutil.TimeDurationPtr("60s"), + Contents: pointerutil.StringPtr("{{ keyOrDefault \"service/redis/maxconns@east-aws\" \"5\" }}"), + CreateDestDirs: pointerutil.BoolPtr(true), + Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"), + ErrMissingKey: pointerutil.BoolPtr(true), + LeftDelim: pointerutil.StringPtr("<<"), + Perms: pointerutil.FileModePtr(0655), + RightDelim: pointerutil.StringPtr(">>"), + SandboxPath: pointerutil.StringPtr("/path/on/disk/where"), + + Wait: &ctconfig.WaitConfig{ + Min: pointerutil.TimeDurationPtr("10s"), + Max: pointerutil.TimeDurationPtr("40s"), + }, + }, + }, + }, + "many": { + fixturePath: "./test-fixtures/config-template-many.hcl", + expectedTemplates: []*ctconfig.TemplateConfig{ + &ctconfig.TemplateConfig{ + Source: pointerutil.StringPtr("/path/on/disk/to/template.ctmpl"), + Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render.txt"), + ErrMissingKey: pointerutil.BoolPtr(false), + CreateDestDirs: pointerutil.BoolPtr(true), + Command: pointerutil.StringPtr("restart service foo"), + Perms: pointerutil.FileModePtr(0600), + }, + &ctconfig.TemplateConfig{ + Source: pointerutil.StringPtr("/path/on/disk/to/template2.ctmpl"), + Destination: pointerutil.StringPtr("/path/on/disk/where/template/will/render2.txt"), + Backup: pointerutil.BoolPtr(true), + Perms: pointerutil.FileModePtr(0755), + Wait: &ctconfig.WaitConfig{ + Min: pointerutil.TimeDurationPtr("2s"), + Max: pointerutil.TimeDurationPtr("10s"), + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + config, err := LoadConfig(tc.fixturePath) + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := &Config{ + AutoAuth: &AutoAuth{ + Method: &Method{ + Type: "aws", + MountPath: "auth/aws", + Namespace: "my-namespace/", + 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", + }, + }, + }, + }, + Templates: tc.expectedTemplates, + PidFile: "./pidfile", + } + + if diff := deep.Equal(config, expected); diff != nil { + t.Fatal(diff) + } + }) + } +} diff --git a/command/agent/config/test-fixtures/config-template-full.hcl b/command/agent/config/test-fixtures/config-template-full.hcl new file mode 100644 index 0000000000..ee05c23098 --- /dev/null +++ b/command/agent/config/test-fixtures/config-template-full.hcl @@ -0,0 +1,49 @@ +pid_file = "./pidfile" + +auto_auth { + method { + type = "aws" + namespace = "/my-namespace" + + config = { + role = "foobar" + } + } + + sink { + type = "file" + + config = { + path = "/tmp/file-foo" + } + + aad = "foobar" + dh_type = "curve25519" + dh_path = "/tmp/file-foo-dhpath" + } +} + +template { + destination = "/path/on/disk/where/template/will/render.txt" + create_dest_dirs = true + contents = "{{ keyOrDefault \"service/redis/maxconns@east-aws\" \"5\" }}" + + command = "restart service foo" + command_timeout = "60s" + + error_on_missing_key = true + perms = 0655 + backup = true + left_delimiter = "<<" + right_delimiter = ">>" + + sandbox_path = "/path/on/disk/where" + wait { + min = "5s" + max = "30s" + } + wait { + min = "10s" + max = "40s" + } +} \ No newline at end of file diff --git a/command/agent/config/test-fixtures/config-template-many.hcl b/command/agent/config/test-fixtures/config-template-many.hcl new file mode 100644 index 0000000000..2f6fe7b70b --- /dev/null +++ b/command/agent/config/test-fixtures/config-template-many.hcl @@ -0,0 +1,50 @@ +pid_file = "./pidfile" + +auto_auth { + method { + type = "aws" + namespace = "/my-namespace" + + config = { + role = "foobar" + } + } + + sink { + type = "file" + + config = { + path = "/tmp/file-foo" + } + + aad = "foobar" + dh_type = "curve25519" + dh_path = "/tmp/file-foo-dhpath" + } +} + +template { + source = "/path/on/disk/to/template.ctmpl" + destination = "/path/on/disk/where/template/will/render.txt" + + create_dest_dirs = true + + command = "restart service foo" + + error_on_missing_key = false + perms = 0600 +} + +template { + source = "/path/on/disk/to/template2.ctmpl" + destination = "/path/on/disk/where/template/will/render2.txt" + + perms = 0755 + + backup = true + + wait { + min = "2s" + max = "10s" + } +} diff --git a/command/agent/config/test-fixtures/config-template-min.hcl b/command/agent/config/test-fixtures/config-template-min.hcl new file mode 100644 index 0000000000..5d37dbefba --- /dev/null +++ b/command/agent/config/test-fixtures/config-template-min.hcl @@ -0,0 +1,29 @@ +pid_file = "./pidfile" + +auto_auth { + method { + type = "aws" + namespace = "/my-namespace" + + config = { + role = "foobar" + } + } + + sink { + type = "file" + + config = { + path = "/tmp/file-foo" + } + + aad = "foobar" + dh_type = "curve25519" + dh_path = "/tmp/file-foo-dhpath" + } +} + +template { + source = "/path/on/disk/to/template.ctmpl" + destination = "/path/on/disk/where/template/will/render.txt" +} diff --git a/command/agent/doc.go b/command/agent/doc.go new file mode 100644 index 0000000000..0786f5c1d3 --- /dev/null +++ b/command/agent/doc.go @@ -0,0 +1,8 @@ +/* +Package agent implements a daemon mode of Vault designed to provide helper +features like auto-auth, caching, and templating. + +Agent has it's own configuration stanza and operates as a proxy to a Vault +service. +*/ +package agent diff --git a/command/agent/template/template.go b/command/agent/template/template.go new file mode 100644 index 0000000000..2616877ffa --- /dev/null +++ b/command/agent/template/template.go @@ -0,0 +1,188 @@ +// Package template is responsible for rendering user supplied templates to +// disk. The Server type accepts configuration to communicate to a Vault server +// and a Vault token for authentication. Internally, the Server creates a Consul +// Template Runner which manages reading secrets from Vault and rendering +// templates to disk at configured locations +package template + +import ( + "context" + "strings" + + ctconfig "github.com/hashicorp/consul-template/config" + "github.com/hashicorp/consul-template/manager" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/command/agent/config" + "github.com/hashicorp/vault/sdk/helper/pointerutil" +) + +// ServerConfig is a config struct for setting up the basic parts of the +// Server +type ServerConfig struct { + Logger hclog.Logger + // Client *api.Client + VaultConf *config.Vault + ExitAfterAuth bool + + Namespace string +} + +// Server manages the Consul Template Runner which renders templates +type Server struct { + // UnblockCh is used to block until a template is rendered + UnblockCh chan struct{} + + // config holds the ServerConfig used to create it. It's passed along in other + // methods + config *ServerConfig + + // runner is the consul-template runner + runner *manager.Runner + + // Templates holds the parsed Consul Templates + Templates []*ctconfig.TemplateConfig + + // TODO: remove donech? + DoneCh chan struct{} + logger hclog.Logger + exitAfterAuth bool +} + +// NewServer returns a new configured server +func NewServer(conf *ServerConfig) *Server { + ts := Server{ + DoneCh: make(chan struct{}), + logger: conf.Logger, + UnblockCh: make(chan struct{}), + config: conf, + exitAfterAuth: conf.ExitAfterAuth, + } + return &ts +} + +// Run kicks off the internal Consul Template runner, and listens for changes to +// the token from the AuthHandler. If Done() is called on the context, shut down +// the Runner and return +func (ts *Server) Run(ctx context.Context, incoming chan string, templates []*ctconfig.TemplateConfig) { + latestToken := new(string) + ts.logger.Info("starting template server") + defer func() { + ts.logger.Info("template server stopped") + close(ts.DoneCh) + }() + + if incoming == nil { + panic("incoming channel is nil") + } + + // If there are no templates, close the UnblockCh + if len(templates) == 0 { + // nothing to do + ts.logger.Info("no templates found") + close(ts.UnblockCh) + return + } + + // construct a consul template vault config based the agents vault + // configuration + var runnerConfig *ctconfig.Config + if runnerConfig = newRunnerConfig(ts.config, templates); runnerConfig == nil { + ts.logger.Error("template server failed to generate runner config") + close(ts.UnblockCh) + return + } + + var err error + ts.runner, err = manager.NewRunner(runnerConfig, false) + if err != nil { + ts.logger.Error("template server failed to create", "error", err) + close(ts.UnblockCh) + return + } + + for { + select { + case <-ctx.Done(): + ts.runner.StopImmediately() + ts.runner = nil + return + + case token := <-incoming: + if token != *latestToken { + ts.logger.Info("template server received new token") + ts.runner.Stop() + *latestToken = token + ctv := ctconfig.Config{ + Vault: &ctconfig.VaultConfig{ + Token: latestToken, + }, + } + runnerConfig.Merge(&ctv) + runnerConfig.Finalize() + var runnerErr error + ts.runner, runnerErr = manager.NewRunner(runnerConfig, false) + if runnerErr != nil { + ts.logger.Error("template server failed with new Vault token", "error", runnerErr) + continue + } else { + go ts.runner.Start() + } + } + case err := <-ts.runner.ErrCh: + ts.logger.Error("template server error", "error", err.Error()) + close(ts.UnblockCh) + return + case <-ts.runner.TemplateRenderedCh(): + // A template has been rendered, unblock + if ts.exitAfterAuth { + // if we want to exit after auth, go ahead and shut down the runner + ts.runner.Stop() + } + close(ts.UnblockCh) + } + } +} + +// newRunnerConfig returns a consul-template runner configuration, setting the +// Vault and Consul configurations based on the clients configs. +func newRunnerConfig(sc *ServerConfig, templates ctconfig.TemplateConfigs) *ctconfig.Config { + // TODO only use default Vault config + conf := ctconfig.DefaultConfig() + conf.Templates = templates.Copy() + + // Setup the Vault config + // Always set these to ensure nothing is picked up from the environment + conf.Vault.RenewToken = pointerutil.BoolPtr(false) + conf.Vault.Token = pointerutil.StringPtr("") + conf.Vault.Address = &sc.VaultConf.Address + + if sc.Namespace != "" { + conf.Vault.Namespace = &sc.Namespace + } + + conf.Vault.SSL = &ctconfig.SSLConfig{ + Enabled: pointerutil.BoolPtr(false), + Verify: pointerutil.BoolPtr(false), + Cert: pointerutil.StringPtr(""), + Key: pointerutil.StringPtr(""), + CaCert: pointerutil.StringPtr(""), + CaPath: pointerutil.StringPtr(""), + ServerName: pointerutil.StringPtr(""), + } + + if strings.HasPrefix(sc.VaultConf.Address, "https") || sc.VaultConf.CACert != "" { + skipVerify := sc.VaultConf.TLSSkipVerify + verify := !skipVerify + conf.Vault.SSL = &ctconfig.SSLConfig{ + Enabled: pointerutil.BoolPtr(true), + Verify: &verify, + Cert: &sc.VaultConf.ClientCert, + Key: &sc.VaultConf.ClientKey, + CaCert: &sc.VaultConf.CACert, + CaPath: &sc.VaultConf.CAPath, + } + } + + conf.Finalize() + return conf +} diff --git a/command/agent/template/template_test.go b/command/agent/template/template_test.go new file mode 100644 index 0000000000..6c676e5601 --- /dev/null +++ b/command/agent/template/template_test.go @@ -0,0 +1,159 @@ +package template + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "testing" + + ctconfig "github.com/hashicorp/consul-template/config" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/command/agent/config" + "github.com/hashicorp/vault/sdk/helper/logging" + "github.com/hashicorp/vault/sdk/helper/pointerutil" +) + +// TestNewServer is a simple test to make sure NewServer returns a Server and +// channel +func TestNewServer(t *testing.T) { + server := NewServer(&ServerConfig{}) + if server == nil { + t.Fatal("nil server returned") + } + if server.UnblockCh == nil { + t.Fatal("nil blocking channel returned") + } +} + +func TestServerRun(t *testing.T) { + // create http test server + ts := httptest.NewServer(http.HandlerFunc(handleRequest)) + defer ts.Close() + tmpDir, err := ioutil.TempDir("", "agent-tests") + defer os.RemoveAll(tmpDir) + if err != nil { + t.Fatal(err) + } + + testCases := map[string]struct { + templates []*ctconfig.TemplateConfig + }{ + "basic": { + templates: []*ctconfig.TemplateConfig{ + &ctconfig.TemplateConfig{ + Contents: pointerutil.StringPtr(templateContents), + }, + }, + }, + } + + // secretRender is a simple struct that represents the secret we render to + // disk. It's used to unmarshal the file contents and test against + type secretRender struct { + Username string `json:"username"` + Password string `json:"password"` + Version string `json:"version"` + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + templateTokenCh := make(chan string, 1) + for i, template := range tc.templates { + dstFile := fmt.Sprintf("%s/render_%d.txt", tmpDir, i) + template.Destination = pointerutil.StringPtr(dstFile) + } + + ctx, cancelFunc := context.WithCancel(context.Background()) + sc := ServerConfig{ + Logger: logging.NewVaultLogger(hclog.Trace), + VaultConf: &config.Vault{ + Address: ts.URL, + }, + } + + var server *Server + server = NewServer(&sc) + if ts == nil { + t.Fatal("nil server returned") + } + if server.UnblockCh == nil { + t.Fatal("nil blocking channel returned") + } + + go server.Run(ctx, templateTokenCh, tc.templates) + + // send a dummy value to trigger the internal Runner to query for secret + // info + templateTokenCh <- "test" + + select { + case <-ctx.Done(): + case <-server.UnblockCh: + } + + // cancel to clean things up + cancelFunc() + + // verify test file exists and has the content we're looking for + for _, template := range tc.templates { + if template.Destination == nil { + t.Fatal("nil template destination") + } + content, err := ioutil.ReadFile(*template.Destination) + if err != nil { + t.Fatal(err) + } + + secret := secretRender{} + if err := json.Unmarshal(content, &secret); err != nil { + t.Fatal(err) + } + if secret.Username != "appuser" || secret.Password != "password" || secret.Version != "3" { + t.Fatalf("secret didn't match: %#v", secret) + } + } + }) + } +} + +func handleRequest(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, jsonResponse) +} + +var jsonResponse = ` +{ + "request_id": "8af096e9-518c-7351-eff5-5ba20554b21f", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "data": { + "password": "password", + "username": "appuser" + }, + "metadata": { + "created_time": "2019-10-07T22:18:44.233247Z", + "deletion_time": "", + "destroyed": false, + "version": 3 + } + }, + "wrap_info": null, + "warnings": null, + "auth": null +} +` + +var templateContents = ` +{{ with secret "kv/myapp/config"}} +{ +{{ if .Data.data.username}}"username":"{{ .Data.data.username}}",{{ end }} +{{ if .Data.data.password }}"password":"{{ .Data.data.password }}",{{ end }} +{{ if .Data.metadata.version}}"version":"{{ .Data.metadata.version }}"{{ end }} +} +{{ end }} +` diff --git a/go.mod b/go.mod index b22a219726..2bacaa87e8 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,8 @@ require ( github.com/google/go-github v17.0.0+incompatible github.com/google/go-metrics-stackdriver v0.0.0-20190816035513-b52628e82e2a github.com/google/go-querystring v1.0.0 // indirect - github.com/hashicorp/consul/api v1.0.1 + github.com/hashicorp/consul-template v0.22.0 + github.com/hashicorp/consul/api v1.1.0 github.com/hashicorp/errwrap v1.0.0 github.com/hashicorp/go-cleanhttp v0.5.1 github.com/hashicorp/go-gcp-common v0.5.0 diff --git a/go.sum b/go.sum index c83c6a4dd6..8bdced0f10 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,7 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7O github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest v11.7.1+incompatible h1:M2YZIajBBVekV86x0rr1443Lc1F/Ylxb9w+5EtSyX3Q= github.com/Azure/go-autorest v11.7.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/datadog-go v2.2.0+incompatible h1:V5BKkxACZLjzHjSgBbr2gvLA2Ae49yhc6CSY7MLy5k4= github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= @@ -151,6 +152,7 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/frankban/quicktest v1.4.0/go.mod h1:36zfPVQyHxymz4cH7wlDmVwDrJuljRB60qkgn7rorfQ= github.com/frankban/quicktest v1.4.1 h1:Wv2VwvNn73pAdFIVUQRXYDFp31lXKbqblIXo/Q5GPSg= github.com/frankban/quicktest v1.4.1/go.mod h1:36zfPVQyHxymz4cH7wlDmVwDrJuljRB60qkgn7rorfQ= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= @@ -213,6 +215,8 @@ github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a h1:ZJu5NB1Bk5ms4vw0Xu github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= @@ -260,10 +264,10 @@ github.com/grpc-ecosystem/grpc-gateway v1.8.5 h1:2+KSC78XiO6Qy0hIjfc1OD9H+hsaJdJ github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= -github.com/hashicorp/consul/api v1.0.1 h1:LkHu3cLXjya4lgrAyZVe/CUBXgJ7AcDWKSeCjAYN9w0= -github.com/hashicorp/consul/api v1.0.1/go.mod h1:LQlewHPiuaRhn1mP2XE4RrjnlRgOeWa/ZM0xWLCen2M= -github.com/hashicorp/consul/sdk v0.1.0 h1:tTfutTNVUTDXpNM4YCImLfiiY3yCDpfgS6tNlUioIUE= -github.com/hashicorp/consul/sdk v0.1.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul-template v0.22.0 h1:ti5cqAekOeMfFYLJCjlPtKGwBcqwVxoZO/Y2vctwuUE= +github.com/hashicorp/consul-template v0.22.0/go.mod h1:lHrykBIcPobCuEcIMLJryKxDyk2lUMnQWmffOEONH0k= +github.com/hashicorp/consul/api v1.1.0 h1:BNQPM9ytxj6jbjjdRPioQ94T6YXriSopn0i8COv6SRA= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1 h1:LnuDWGNsoajlhGyHJvuWW6FVqRl8JOTPqS6CPTsYjhY= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= @@ -271,6 +275,8 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-gatedio v0.5.0 h1:Jm1X5yP4yCqqWj5L1TgW7iZwCVPGtVc+mro5r/XX7Tg= +github.com/hashicorp/go-gatedio v0.5.0/go.mod h1:Lr3t8L6IyxD3DAeaUxGcgl2JnRUpWMCsmBl4Omu/2t4= github.com/hashicorp/go-gcp-common v0.5.0 h1:kkIQTjNTopn4eXQ1+lCiHYZXUtgIZvbc6YtAQkMnTos= github.com/hashicorp/go-gcp-common v0.5.0/go.mod h1:IDGUI2N/OS3PiU4qZcXJeWKPI6O/9Y8hOrbSiMcqyYw= github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= @@ -329,8 +335,9 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/memberlist v0.1.4 h1:gkyML/r71w3FL8gUi74Vk76avkj/9lYAY9lvg0OcoGs= +github.com/hashicorp/memberlist v0.1.4/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/nomad/api v0.0.0-20190412184103-1c38ced33adf h1:U/40PQvWkaXCDdK9QHKf1pVDVcA+NIDVbzzonFGkgIA= github.com/hashicorp/nomad/api v0.0.0-20190412184103-1c38ced33adf/go.mod h1:BDngVi1f4UA6aJq9WYTgxhfWSE1+42xshvstLU2fRGk= github.com/hashicorp/raft v1.0.1/go.mod h1:DVSAWItjLjTOkVbSpWQ0j0kUADIvDaCtBxIcbNAQLkI= @@ -341,6 +348,8 @@ github.com/hashicorp/raft-snapshot v1.0.2-0.20190827162939-8117efcc5aab h1:WzGMw github.com/hashicorp/raft-snapshot v1.0.2-0.20190827162939-8117efcc5aab/go.mod h1:5sL9eUn72lH5DzsFIJ9jaysITbHksSSszImWSOTC8Ic= github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/serf v0.8.3 h1:MWYcmct5EtKz0efYooPcL0yNkem+7kWxqXDi/UIh+8k= +github.com/hashicorp/serf v0.8.3/go.mod h1:UpNcs7fFbpKIyZaUuSW6EPiH+eZC7OuyFD+wc1oal+k= github.com/hashicorp/vault-plugin-auth-alicloud v0.5.2-0.20190814210027-93970f08f2ec h1:HXVE8h6RXFsPJgwWpE+5CscsgekqtX4nhDlZGV9jEe4= github.com/hashicorp/vault-plugin-auth-alicloud v0.5.2-0.20190814210027-93970f08f2ec/go.mod h1:TYFfVFgKF9x92T7uXouI9rLPkNnyXo/KkNcj5t+mjdM= github.com/hashicorp/vault-plugin-auth-azure v0.5.2-0.20190814210035-08e00d801115 h1:E57y918o+c+NoI5k7ohbpZu7vRm1XZKZfC5VQVpJvDI= @@ -438,6 +447,8 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-shellwords v1.0.5 h1:JhhFTIOslh5ZsPrpa3Wdg8bF0WI3b44EMblmU9wIsXc= +github.com/mattn/go-shellwords v1.0.5/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/matttproud/golang_protobuf_extensions v1.0.0 h1:YNOwxxSJzSUARoD9KRZLzM9Y858MNGCOACTvCW9TSAc= github.com/matttproud/golang_protobuf_extensions v1.0.0/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -446,8 +457,9 @@ github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1w github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU= github.com/michaelklishin/rabbit-hole v1.5.0 h1:Bex27BiFDsijCM9D0ezSHqyy0kehpYHuNKaPqq/a4RM= github.com/michaelklishin/rabbit-hole v1.5.0/go.mod h1:vvI1uOitYZi0O5HEGXhaWC1XT80Gy+HvFheJ+5Krlhk= -github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.15 h1:CSSIDtllwGLMoA6zjdKnaE6Tx6eVUxQ29LUgGetiDCI= +github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= @@ -460,6 +472,8 @@ github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdI github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/hashstructure v1.0.0 h1:ZkRJX1CyOoTkar7p/mLS5TZU4nJ1Rn/F8u9dGS02Q3Y= +github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -515,6 +529,7 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4 v2.2.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.2.6+incompatible h1:6aCX4/YZ9v8q69hTyiR7dNLnTA3fgtKHVVW5BCd5Znw= github.com/pierrec/lz4 v2.2.6+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= @@ -678,6 +693,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2eP golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -719,6 +735,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190523142557-0e01d883c5c5 h1:sM3evRHxE/1RuMe1FYAL3j7C7fUfIjkbE+NiDAYUF8U= golang.org/x/sys v0.0.0-20190523142557-0e01d883c5c5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190730183949-1393eb018365/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= diff --git a/sdk/helper/pointerutil/pointer.go b/sdk/helper/pointerutil/pointer.go new file mode 100644 index 0000000000..73952313fe --- /dev/null +++ b/sdk/helper/pointerutil/pointer.go @@ -0,0 +1,28 @@ +package pointerutil + +import ( + "os" + "time" +) + +// StringPtr returns a pointer to a string value +func StringPtr(s string) *string { + return &s +} + +// BoolPtr returns a pointer to a boolean value +func BoolPtr(b bool) *bool { + return &b +} + +// TimeDurationPtr returns a pointer to a time duration value +func TimeDurationPtr(duration string) *time.Duration { + d, _ := time.ParseDuration(duration) + + return &d +} + +// FileModePtr returns a pointer to the given os.FileMode +func FileModePtr(o os.FileMode) *os.FileMode { + return &o +} diff --git a/website/source/docs/agent/index.html.md b/website/source/docs/agent/index.html.md index c4f40137f1..1da2544d7b 100644 --- a/website/source/docs/agent/index.html.md +++ b/website/source/docs/agent/index.html.md @@ -54,6 +54,8 @@ These are the currently-available general configuration option: with code `0` after a single successful auth, where success means that a token was retrieved and all sinks successfully wrote it +- `template` ([template][template`]: \) - Specifies options used for templating Vault secrets to files. + ### vault Stanza There can at most be one top level `vault` block and it has the following @@ -150,10 +152,21 @@ listener "tcp" { address = "127.0.0.1:8100" tls_disable = true } + +template { + source = "/etc/vault/server.key.ctmpl" + destination = "/etc/vault/server.key" +} + +template { + source = "/etc/vault/server.crt.ctmpl" + destination = "/etc/vault/server.crt" +} ``` [vault]: /docs/agent/index.html#vault-stanza [autoauth]: /docs/agent/autoauth/index.html [caching]: /docs/agent/caching/index.html +[template]: /docs/agent/template/index.html [listener]: /docs/agent/index.html#listener-stanza [listener_main]: /docs/configuration/listener/tcp.html diff --git a/website/source/docs/agent/template/index.html.md b/website/source/docs/agent/template/index.html.md new file mode 100644 index 0000000000..59a2b5ecdf --- /dev/null +++ b/website/source/docs/agent/template/index.html.md @@ -0,0 +1,121 @@ +--- +layout: "docs" +page_title: "Vault Agent Templates" +sidebar_title: "TemplatesAuto-Auth" +sidebar_current: "docs-agent-templates" +description: |- + Vault Agent's Template functionality allows Vault secrets to be rendered to files + using Consul Template markup. +--- + +# Vault Agent Templates + +Vault Agent's Template functionality allows Vault secrets to be rendered to files +using Consul Template markup. + +## Functionality + +The `template` stanza configures the templating engine in the Vault agent for rendering +secrets to files using Consul Template markup language. Multiple `template` stanzas +can be defined to render multiple files. + +When the agent is started with templating 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, secrets defined in the templates will be retrieved from Vault and +rendered locally. + +## Configuration + +The top level `template` block has multiple configurations entries: + +- `source` `(object: optional)` - Path on disk to use as the input template. This +option is required if not using the `contents` option. +- `destination` `(object: required)` - Path on disk where the rendered secrets should +be created. If the parent directories If the parent directories do not exist, Vault +Agent will attempt to create them, unless `create_dest_dirs` is false. +- `create_dest_dirs` `(object: required)` - This option tells Vault Agent to create +the parent directories of the destination path if they do not exist. The default +value is true. +- `contents` `(object: optional)` - This option allows embedding the contents of +a template in the configuration file rather then supplying the `source` path to +the template file. This is useful for short templates. This option is mutually +exclusive with the `source` option. +- `command` `(object: optional)` - This is the optional command to run when the +template is rendered. The command will only run if the resulting template changes. +The command must return within 30s (configurable), and it must have a successful +exit code. Vault Agent is not a replacement for a process monitor or init system. +- `command_timeout` `(object: optional)` - This is the maximum amount of time to +wait for the optional command to return. Default is 30s. +- `error_on_missing_key` `(object: optional)` - Exit with an error when accessing +a struct or map field/key that does notexist. The default behavior will print "" +when accessing a field that does not exist. It is highly recommended you set this +to "true". +- `perms` `(object: optional)` - This is the permission to render the file. If +this option is left unspecified, Vault Agent will attempt to match the permissions +of the file that already exists at the destination path. If no file exists at that +path, the permissions are 0644. +- `backup` `(object: optional)` - This option backs up the previously rendered template +at the destination path before writing a new one. It keeps exactly one backup. +This option is useful for preventing accidental changes to the data without having +a rollback strategy. +- `left_delimiter` `(object: optional)` - Delimiter to use in the template. The +default is "{{" but for some templates, it may be easier to use a different +delimiter that does not conflict with the output file itself. +- `right_delimiter` `(object: optional)` - Delimiter to use in the template. The +default is "}}" but for some templates, it may be easier to use a different +delimiter that does not conflict with the output file itself. +- `sandbox_path` `(object: optional)` - If a sandbox path is provided, any path +provided to the `file` function is checked that it falls within the sandbox path. +Relative paths that try to traverse outside the sandbox path will exit with an error. +- `wait` `(object: required)` - This is the `minimum(:maximum)` to wait before rendering +a new template to disk and triggering a command, separated by a colon (`:`). + +## Example Template + +Template with Vault Agent requires the use of the `secret` function from Consul Template. +The following is an example of a template that retrieves a generic secret from Vault's +KV store: + +``` +{{ with secret "secret/my-secret" }} +{{ .Data.data.foo }} +{{ end }} +``` + +## Example Configuration + +The following demonstrates configuring Vault Agent to template secrets using the +AppRole Auth method: + +```python +pid_file = "./pidfile" + +vault { + address = "https://127.0.0.1:8200" +} + +auto_auth { + method { + type = "approle" + + config = { + role_id_file_path = "/etc/vault/roleid" + secret_id_file_path = "/etc/vault/secretid" + } + } + + sink { + type = "file" + + config = { + path = "/tmp/file-foo" + } + } +} + +template { + source = "/tmp/agent/template.ctmpl" + destination = "/tmp/agent/render.txt" +} +```