488 lines
12 KiB
Go
488 lines
12 KiB
Go
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
|
|
}
|