package config import ( "errors" "fmt" "io/ioutil" "os" "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"` 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"` CAPath string `hcl:"ca_path"` TLSSkipVerify bool `hcl:"-"` TLSSkipVerifyRaw interface{} `hcl:"tls_skip_verify"` ClientCert string `hcl:"client_cert"` ClientKey string `hcl:"client_key"` TLSServerName string `hcl:"tls_server_name"` } // 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{} } // 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"` // NOTE: This is unsupported outside of testing and may disappear at any // time. 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"` WrapTTLRaw interface{} `hcl:"wrap_ttl"` WrapTTL time.Duration `hcl:"-"` Namespace string `hcl:"namespace"` Config map[string]interface{} } // Sink defines a location to write the authenticated token 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) (*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) } if err := parseListeners(&result, list); err != nil { return nil, errwrap.Wrapf("error parsing 'listeners': {{err}}", err) } 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") } if result.Cache.UseAutoAuthToken { if result.AutoAuth == nil { return nil, fmt.Errorf("cache.use_auto_auth_token is true but auto_auth not configured") } if result.AutoAuth.Method.WrapTTL > 0 { return nil, fmt.Errorf("cache.use_auto_auth_token is true and auto_auth uses wrapping") } } } if result.AutoAuth != nil { if len(result.AutoAuth.Sinks) == 0 && (result.Cache == nil || !result.Cache.UseAutoAuthToken) { return nil, fmt.Errorf("auto_auth requires at least one sink or cache.use_auto_auth_token=true ") } } err = parseVault(&result, list) if err != nil { return nil, errwrap.Wrapf("error parsing 'vault':{{err}}", err) } return &result, nil } func parseVault(result *Config, list *ast.ObjectList) error { name := "vault" vaultList := list.Filter(name) if len(vaultList.Items) == 0 { return nil } if len(vaultList.Items) > 1 { return fmt.Errorf("one and only one %q block is required", name) } item := vaultList.Items[0] var v Vault err := hcl.DecodeObject(&v, item.Val) if err != nil { return err } if v.TLSSkipVerifyRaw != nil { v.TLSSkipVerify, err = parseutil.ParseBool(v.TLSSkipVerifyRaw) if err != nil { return err } } result.Vault = &v return nil } func parseCache(result *Config, list *ast.ObjectList) error { name := "cache" cacheList := list.Filter(name) if len(cacheList.Items) == 0 { return nil } if len(cacheList.Items) > 1 { return fmt.Errorf("one and only one %q block is required", name) } item := cacheList.Items[0] var c Cache err := hcl.DecodeObject(&c, item.Val) if err != nil { return err } result.Cache = &c return nil } func parseListeners(result *Config, list *ast.ObjectList) error { name := "listener" listenerList := list.Filter(name) var listeners []*Listener for _, item := range listenerList.Items { var lnConfig map[string]interface{} err := hcl.DecodeObject(&lnConfig, item.Val) if err != nil { return err } var lnType string switch { case lnConfig["type"] != nil: lnType = lnConfig["type"].(string) delete(lnConfig, "type") case len(item.Keys) == 1: lnType = strings.ToLower(item.Keys[0].Token.Value().(string)) default: return errors.New("listener type must be specified") } switch lnType { case "unix", "tcp": default: return fmt.Errorf("invalid listener type %q", lnType) } listeners = append(listeners, &Listener{ Type: lnType, Config: lnConfig, }) } result.Listeners = listeners return nil } func parseAutoAuth(result *Config, list *ast.ObjectList) error { name := "auto_auth" autoAuthList := list.Filter(name) if len(autoAuthList.Items) == 0 { return nil } if len(autoAuthList.Items) > 1 { return fmt.Errorf("at most one %q block is allowed", 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 a.Method == nil { return fmt.Errorf("no 'method' block found") } if err := parseSinks(result, subList); err != nil { return errwrap.Wrapf("error parsing 'sink' stanzas: {{err}}", err) } if result.AutoAuth.Method.WrapTTL > 0 { if len(result.AutoAuth.Sinks) != 1 { return fmt.Errorf("error parsing auto_auth: wrapping enabled on auth method and 0 or many sinks defined") } if result.AutoAuth.Sinks[0].WrapTTL > 0 { return fmt.Errorf("error parsing auto_auth: wrapping enabled both on auth method and sink") } } 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 } // Canonicalize namespace path if provided m.Namespace = namespace.Canonicalize(m.Namespace) 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 nil } 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 } 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 }