1package config
2
3import (
4	"errors"
5	"fmt"
6	"io/ioutil"
7	"os"
8	"strings"
9	"time"
10
11	ctconfig "github.com/hashicorp/consul-template/config"
12	"github.com/hashicorp/errwrap"
13	"github.com/hashicorp/go-multierror"
14	"github.com/hashicorp/hcl"
15	"github.com/hashicorp/hcl/hcl/ast"
16	"github.com/hashicorp/vault/helper/namespace"
17	"github.com/hashicorp/vault/sdk/helper/parseutil"
18	"github.com/mitchellh/mapstructure"
19)
20
21// Config is the configuration for the vault server.
22type Config struct {
23	AutoAuth      *AutoAuth                  `hcl:"auto_auth"`
24	ExitAfterAuth bool                       `hcl:"exit_after_auth"`
25	PidFile       string                     `hcl:"pid_file"`
26	Listeners     []*Listener                `hcl:"listeners"`
27	Cache         *Cache                     `hcl:"cache"`
28	Vault         *Vault                     `hcl:"vault"`
29	Templates     []*ctconfig.TemplateConfig `hcl:"templates"`
30}
31
32// Vault contains configuration for connnecting to Vault servers
33type Vault struct {
34	Address          string      `hcl:"address"`
35	CACert           string      `hcl:"ca_cert"`
36	CAPath           string      `hcl:"ca_path"`
37	TLSSkipVerify    bool        `hcl:"-"`
38	TLSSkipVerifyRaw interface{} `hcl:"tls_skip_verify"`
39	ClientCert       string      `hcl:"client_cert"`
40	ClientKey        string      `hcl:"client_key"`
41	TLSServerName    string      `hcl:"tls_server_name"`
42}
43
44// Cache contains any configuration needed for Cache mode
45type Cache struct {
46	UseAutoAuthToken bool `hcl:"use_auto_auth_token"`
47}
48
49// Listener contains configuration for any Vault Agent listeners
50type Listener struct {
51	Type   string
52	Config map[string]interface{}
53}
54
55// RequireRequestHeader is a listener configuration option
56const RequireRequestHeader = "require_request_header"
57
58// AutoAuth is the configured authentication method and sinks
59type AutoAuth struct {
60	Method *Method `hcl:"-"`
61	Sinks  []*Sink `hcl:"sinks"`
62
63	// NOTE: This is unsupported outside of testing and may disappear at any
64	// time.
65	EnableReauthOnNewCredentials bool `hcl:"enable_reauth_on_new_credentials"`
66}
67
68// Method represents the configuration for the authentication backend
69type Method struct {
70	Type       string
71	MountPath  string        `hcl:"mount_path"`
72	WrapTTLRaw interface{}   `hcl:"wrap_ttl"`
73	WrapTTL    time.Duration `hcl:"-"`
74	Namespace  string        `hcl:"namespace"`
75	Config     map[string]interface{}
76}
77
78// Sink defines a location to write the authenticated token
79type Sink struct {
80	Type       string
81	WrapTTLRaw interface{}   `hcl:"wrap_ttl"`
82	WrapTTL    time.Duration `hcl:"-"`
83	DHType     string        `hcl:"dh_type"`
84	DHPath     string        `hcl:"dh_path"`
85	AAD        string        `hcl:"aad"`
86	AADEnvVar  string        `hcl:"aad_env_var"`
87	Config     map[string]interface{}
88}
89
90// LoadConfig loads the configuration at the given path, regardless if
91// its a file or directory.
92func LoadConfig(path string) (*Config, error) {
93	fi, err := os.Stat(path)
94	if err != nil {
95		return nil, err
96	}
97
98	if fi.IsDir() {
99		return nil, fmt.Errorf("location is a directory, not a file")
100	}
101
102	// Read the file
103	d, err := ioutil.ReadFile(path)
104	if err != nil {
105		return nil, err
106	}
107
108	// Parse!
109	obj, err := hcl.Parse(string(d))
110	if err != nil {
111		return nil, err
112	}
113
114	// Start building the result
115	var result Config
116	if err := hcl.DecodeObject(&result, obj); err != nil {
117		return nil, err
118	}
119
120	list, ok := obj.Node.(*ast.ObjectList)
121	if !ok {
122		return nil, fmt.Errorf("error parsing: file doesn't contain a root object")
123	}
124
125	if err := parseAutoAuth(&result, list); err != nil {
126		return nil, errwrap.Wrapf("error parsing 'auto_auth': {{err}}", err)
127	}
128
129	if err := parseListeners(&result, list); err != nil {
130		return nil, errwrap.Wrapf("error parsing 'listeners': {{err}}", err)
131	}
132
133	if err := parseCache(&result, list); err != nil {
134		return nil, errwrap.Wrapf("error parsing 'cache':{{err}}", err)
135	}
136
137	if err := parseTemplates(&result, list); err != nil {
138		return nil, errwrap.Wrapf("error parsing 'template': {{err}}", err)
139	}
140
141	if result.Cache != nil {
142		if len(result.Listeners) < 1 {
143			return nil, fmt.Errorf("at least one listener required when cache enabled")
144		}
145
146		if result.Cache.UseAutoAuthToken {
147			if result.AutoAuth == nil {
148				return nil, fmt.Errorf("cache.use_auto_auth_token is true but auto_auth not configured")
149			}
150			if result.AutoAuth.Method.WrapTTL > 0 {
151				return nil, fmt.Errorf("cache.use_auto_auth_token is true and auto_auth uses wrapping")
152			}
153		}
154	}
155
156	if result.AutoAuth != nil {
157		if len(result.AutoAuth.Sinks) == 0 && (result.Cache == nil || !result.Cache.UseAutoAuthToken) {
158			return nil, fmt.Errorf("auto_auth requires at least one sink or cache.use_auto_auth_token=true ")
159		}
160	}
161
162	err = parseVault(&result, list)
163	if err != nil {
164		return nil, errwrap.Wrapf("error parsing 'vault':{{err}}", err)
165	}
166
167	return &result, nil
168}
169
170func parseVault(result *Config, list *ast.ObjectList) error {
171	name := "vault"
172
173	vaultList := list.Filter(name)
174	if len(vaultList.Items) == 0 {
175		return nil
176	}
177
178	if len(vaultList.Items) > 1 {
179		return fmt.Errorf("one and only one %q block is required", name)
180	}
181
182	item := vaultList.Items[0]
183
184	var v Vault
185	err := hcl.DecodeObject(&v, item.Val)
186	if err != nil {
187		return err
188	}
189
190	if v.TLSSkipVerifyRaw != nil {
191		v.TLSSkipVerify, err = parseutil.ParseBool(v.TLSSkipVerifyRaw)
192		if err != nil {
193			return err
194		}
195	}
196
197	result.Vault = &v
198
199	return nil
200}
201
202func parseCache(result *Config, list *ast.ObjectList) error {
203	name := "cache"
204
205	cacheList := list.Filter(name)
206	if len(cacheList.Items) == 0 {
207		return nil
208	}
209
210	if len(cacheList.Items) > 1 {
211		return fmt.Errorf("one and only one %q block is required", name)
212	}
213
214	item := cacheList.Items[0]
215
216	var c Cache
217	err := hcl.DecodeObject(&c, item.Val)
218	if err != nil {
219		return err
220	}
221
222	result.Cache = &c
223	return nil
224}
225
226func parseListeners(result *Config, list *ast.ObjectList) error {
227	name := "listener"
228
229	listenerList := list.Filter(name)
230
231	var listeners []*Listener
232	for _, item := range listenerList.Items {
233		var lnConfig map[string]interface{}
234		err := hcl.DecodeObject(&lnConfig, item.Val)
235		if err != nil {
236			return err
237		}
238
239		var lnType string
240		switch {
241		case lnConfig["type"] != nil:
242			lnType = lnConfig["type"].(string)
243			delete(lnConfig, "type")
244		case len(item.Keys) == 1:
245			lnType = strings.ToLower(item.Keys[0].Token.Value().(string))
246		default:
247			return errors.New("listener type must be specified")
248		}
249
250		switch lnType {
251		case "unix", "tcp":
252		default:
253			return fmt.Errorf("invalid listener type %q", lnType)
254		}
255
256		listeners = append(listeners, &Listener{
257			Type:   lnType,
258			Config: lnConfig,
259		})
260	}
261
262	result.Listeners = listeners
263
264	return nil
265}
266
267func parseAutoAuth(result *Config, list *ast.ObjectList) error {
268	name := "auto_auth"
269
270	autoAuthList := list.Filter(name)
271	if len(autoAuthList.Items) == 0 {
272		return nil
273	}
274	if len(autoAuthList.Items) > 1 {
275		return fmt.Errorf("at most one %q block is allowed", name)
276	}
277
278	// Get our item
279	item := autoAuthList.Items[0]
280
281	var a AutoAuth
282	if err := hcl.DecodeObject(&a, item.Val); err != nil {
283		return err
284	}
285
286	result.AutoAuth = &a
287
288	subs, ok := item.Val.(*ast.ObjectType)
289	if !ok {
290		return fmt.Errorf("could not parse %q as an object", name)
291	}
292	subList := subs.List
293
294	if err := parseMethod(result, subList); err != nil {
295		return errwrap.Wrapf("error parsing 'method': {{err}}", err)
296	}
297	if a.Method == nil {
298		return fmt.Errorf("no 'method' block found")
299	}
300
301	if err := parseSinks(result, subList); err != nil {
302		return errwrap.Wrapf("error parsing 'sink' stanzas: {{err}}", err)
303	}
304
305	if result.AutoAuth.Method.WrapTTL > 0 {
306		if len(result.AutoAuth.Sinks) != 1 {
307			return fmt.Errorf("error parsing auto_auth: wrapping enabled on auth method and 0 or many sinks defined")
308		}
309
310		if result.AutoAuth.Sinks[0].WrapTTL > 0 {
311			return fmt.Errorf("error parsing auto_auth: wrapping enabled both on auth method and sink")
312		}
313	}
314
315	return nil
316}
317
318func parseMethod(result *Config, list *ast.ObjectList) error {
319	name := "method"
320
321	methodList := list.Filter(name)
322	if len(methodList.Items) != 1 {
323		return fmt.Errorf("one and only one %q block is required", name)
324	}
325
326	// Get our item
327	item := methodList.Items[0]
328
329	var m Method
330	if err := hcl.DecodeObject(&m, item.Val); err != nil {
331		return err
332	}
333
334	if m.Type == "" {
335		if len(item.Keys) == 1 {
336			m.Type = strings.ToLower(item.Keys[0].Token.Value().(string))
337		}
338		if m.Type == "" {
339			return errors.New("method type must be specified")
340		}
341	}
342
343	// Default to Vault's default
344	if m.MountPath == "" {
345		m.MountPath = fmt.Sprintf("auth/%s", m.Type)
346	}
347	// Standardize on no trailing slash
348	m.MountPath = strings.TrimSuffix(m.MountPath, "/")
349
350	if m.WrapTTLRaw != nil {
351		var err error
352		if m.WrapTTL, err = parseutil.ParseDurationSecond(m.WrapTTLRaw); err != nil {
353			return err
354		}
355		m.WrapTTLRaw = nil
356	}
357
358	// Canonicalize namespace path if provided
359	m.Namespace = namespace.Canonicalize(m.Namespace)
360
361	result.AutoAuth.Method = &m
362	return nil
363}
364
365func parseSinks(result *Config, list *ast.ObjectList) error {
366	name := "sink"
367
368	sinkList := list.Filter(name)
369	if len(sinkList.Items) < 1 {
370		return nil
371	}
372
373	var ts []*Sink
374
375	for _, item := range sinkList.Items {
376		var s Sink
377		if err := hcl.DecodeObject(&s, item.Val); err != nil {
378			return err
379		}
380
381		if s.Type == "" {
382			if len(item.Keys) == 1 {
383				s.Type = strings.ToLower(item.Keys[0].Token.Value().(string))
384			}
385			if s.Type == "" {
386				return errors.New("sink type must be specified")
387			}
388		}
389
390		if s.WrapTTLRaw != nil {
391			var err error
392			if s.WrapTTL, err = parseutil.ParseDurationSecond(s.WrapTTLRaw); err != nil {
393				return multierror.Prefix(err, fmt.Sprintf("sink.%s", s.Type))
394			}
395			s.WrapTTLRaw = nil
396		}
397
398		switch s.DHType {
399		case "":
400		case "curve25519":
401		default:
402			return multierror.Prefix(errors.New("invalid value for 'dh_type'"), fmt.Sprintf("sink.%s", s.Type))
403		}
404
405		if s.AADEnvVar != "" {
406			s.AAD = os.Getenv(s.AADEnvVar)
407			s.AADEnvVar = ""
408		}
409
410		switch {
411		case s.DHPath == "" && s.DHType == "":
412			if s.AAD != "" {
413				return multierror.Prefix(errors.New("specifying AAD data without 'dh_type' does not make sense"), fmt.Sprintf("sink.%s", s.Type))
414			}
415		case s.DHPath != "" && s.DHType != "":
416		default:
417			return multierror.Prefix(errors.New("'dh_type' and 'dh_path' must be specified together"), fmt.Sprintf("sink.%s", s.Type))
418		}
419
420		ts = append(ts, &s)
421	}
422
423	result.AutoAuth.Sinks = ts
424	return nil
425}
426
427func parseTemplates(result *Config, list *ast.ObjectList) error {
428	name := "template"
429
430	templateList := list.Filter(name)
431	if len(templateList.Items) < 1 {
432		return nil
433	}
434
435	var tcs []*ctconfig.TemplateConfig
436
437	for _, item := range templateList.Items {
438		var shadow interface{}
439		if err := hcl.DecodeObject(&shadow, item.Val); err != nil {
440			return fmt.Errorf("error decoding config: %s", err)
441		}
442
443		// Convert to a map and flatten the keys we want to flatten
444		parsed, ok := shadow.(map[string]interface{})
445		if !ok {
446			return errors.New("error converting config")
447		}
448
449		// flatten the wait field. The initial "wait" value, if given, is a
450		// []map[string]interface{}, but we need it to be map[string]interface{}.
451		// Consul Template has a method flattenKeys that walks all of parsed and
452		// flattens every key. For Vault Agent, we only care about the wait input.
453		// Only one wait stanza is supported, however Consul Template does not error
454		// with multiple instead it flattens them down, with last value winning.
455		// Here we take the last element of the parsed["wait"] slice to keep
456		// consistency with Consul Template behavior.
457		wait, ok := parsed["wait"].([]map[string]interface{})
458		if ok {
459			parsed["wait"] = wait[len(wait)-1]
460		}
461
462		var tc ctconfig.TemplateConfig
463
464		// Use mapstructure to populate the basic config fields
465		var md mapstructure.Metadata
466		decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
467			DecodeHook: mapstructure.ComposeDecodeHookFunc(
468				ctconfig.StringToFileModeFunc(),
469				ctconfig.StringToWaitDurationHookFunc(),
470				mapstructure.StringToSliceHookFunc(","),
471				mapstructure.StringToTimeDurationHookFunc(),
472			),
473			ErrorUnused: true,
474			Metadata:    &md,
475			Result:      &tc,
476		})
477		if err != nil {
478			return errors.New("mapstructure decoder creation failed")
479		}
480		if err := decoder.Decode(parsed); err != nil {
481			return err
482		}
483		tcs = append(tcs, &tc)
484	}
485	result.Templates = tcs
486	return nil
487}
488