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