You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/cmd/config/config.go

519 lines
15 KiB

package config
import (
"bytes"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
"io/ioutil"
"strings"
"time"
"github.com/hashicorp/boundary/internal/observability/event"
wrapping "github.com/hashicorp/go-kms-wrapping"
"github.com/hashicorp/go-secure-stdlib/base62"
"github.com/hashicorp/go-secure-stdlib/configutil"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/go-secure-stdlib/strutil"
"github.com/hashicorp/hcl"
"github.com/hashicorp/hcl/hcl/ast"
"github.com/mitchellh/mapstructure"
)
const (
desktopCorsOrigin = "serve://boundary"
devConfig = `
disable_mlock = true
telemetry {
prometheus_retention_time = "24h"
disable_hostname = true
}
`
devControllerExtraConfig = `
controller {
name = "dev-controller"
description = "A default controller created in dev mode"
}
kms "aead" {
purpose = "root"
aead_type = "aes-gcm"
key = "%s"
key_id = "global_root"
}
kms "aead" {
purpose = "worker-auth"
aead_type = "aes-gcm"
key = "%s"
key_id = "global_worker-auth"
}
kms "aead" {
purpose = "recovery"
aead_type = "aes-gcm"
key = "%s"
key_id = "global_recovery"
}
listener "tcp" {
purpose = "api"
tls_disable = true
cors_enabled = true
cors_allowed_origins = ["*"]
}
listener "tcp" {
purpose = "cluster"
}
`
devWorkerExtraConfig = `
listener "tcp" {
purpose = "proxy"
}
worker {
name = "dev-worker"
description = "A default worker created in dev mode"
controllers = ["127.0.0.1"]
tags {
type = ["dev", "local"]
}
}
`
)
// Config is the configuration for the boundary controller
type Config struct {
*configutil.SharedConfig `hcl:"-"`
Worker *Worker `hcl:"worker"`
Controller *Controller `hcl:"controller"`
// Dev-related options
DevController bool `hcl:"-"`
PassthroughDirectory string `hcl:"-"`
DevControllerKey string `hcl:"-"`
DevWorkerAuthKey string `hcl:"-"`
DevRecoveryKey string `hcl:"-"`
// Eventing configuration for the controller
Eventing *event.EventerConfig `hcl:"events"`
}
type Controller struct {
Name string `hcl:"name"`
Description string `hcl:"description"`
Database *Database `hcl:"database"`
PublicClusterAddr string `hcl:"public_cluster_addr"`
// AuthTokenTimeToLive is the total valid lifetime of a token denoted by time.Duration
AuthTokenTimeToLive interface{} `hcl:"auth_token_time_to_live"`
AuthTokenTimeToLiveDuration time.Duration
// AuthTokenTimeToStale is the total time a token can go unused before becoming invalid
// denoted by time.Duration
AuthTokenTimeToStale interface{} `hcl:"auth_token_time_to_stale"`
AuthTokenTimeToStaleDuration time.Duration
// StatusGracePeriod represents the period of time (as a duration) that the
// controller will wait before marking connections from a disconnected worker
// as invalid.
//
// TODO: This field is currently internal.
StatusGracePeriodDuration time.Duration `hcl:"-"`
}
func (c *Controller) InitNameIfEmpty() (string, error) {
if c == nil {
return "", fmt.Errorf("controller config is empty")
}
if err := initNameIfEmpty(&c.Name); err != nil {
return "", fmt.Errorf("error auto-generating controller name: %w", err)
}
return c.Name, nil
}
type Worker struct {
Name string `hcl:"name"`
Description string `hcl:"description"`
Controllers []string `hcl:"controllers"`
PublicAddr string `hcl:"public_addr"`
// We use a raw interface for parsing so that people can use JSON-like
// syntax that maps directly to the filter input or possibly more familiar
// key=value syntax. This is trued up in the Parse function below.
TagsRaw interface{} `hcl:"tags"`
Tags map[string][]string `hcl:"-"`
// StatusGracePeriod represents the period of time (as a duration) that the
// worker will wait before disconnecting connections if it cannot make a
// status report to a controller.
//
// TODO: This field is currently internal.
StatusGracePeriodDuration time.Duration `hcl:"-"`
}
func (w *Worker) InitNameIfEmpty() (string, error) {
if w == nil {
return "", fmt.Errorf("worker config is empty")
}
if err := initNameIfEmpty(&w.Name); err != nil {
return "", fmt.Errorf("error auto-generating worker name: %w", err)
}
return w.Name, nil
}
func initNameIfEmpty(name *string) error {
if *name == "" {
var err error
if *name, err = base62.Random(10); err != nil {
return err
}
}
return nil
}
type Database struct {
Url string `hcl:"url"`
MigrationUrl string `hcl:"migration_url"`
MaxOpenConnections int `hcl:"max_open_connections"`
}
// DevWorker is a Config that is used for dev mode of Boundary
// workers
func DevWorker() (*Config, error) {
parsed, err := Parse(devConfig + devWorkerExtraConfig)
if err != nil {
return nil, fmt.Errorf("error parsing dev config: %w", err)
}
return parsed, nil
}
func DevKeyGeneration() (string, string, string) {
var numBytes int64 = 96
randBuf := new(bytes.Buffer)
n, err := randBuf.ReadFrom(&io.LimitedReader{
R: rand.Reader,
N: numBytes,
})
if err != nil {
panic(err)
}
if n != numBytes {
panic(fmt.Errorf("expected to read 64 bytes, read %d", n))
}
controllerKey := base64.StdEncoding.EncodeToString(randBuf.Bytes()[0:32])
workerAuthKey := base64.StdEncoding.EncodeToString(randBuf.Bytes()[32:64])
recoveryKey := base64.StdEncoding.EncodeToString(randBuf.Bytes()[64:numBytes])
return controllerKey, workerAuthKey, recoveryKey
}
// DevController is a Config that is used for dev mode of Boundary
// controllers
func DevController() (*Config, error) {
controllerKey, workerAuthKey, recoveryKey := DevKeyGeneration()
hclStr := fmt.Sprintf(devConfig+devControllerExtraConfig, controllerKey, workerAuthKey, recoveryKey)
parsed, err := Parse(hclStr)
if err != nil {
return nil, fmt.Errorf("error parsing dev config: %w", err)
}
parsed.DevController = true
parsed.DevControllerKey = controllerKey
parsed.DevWorkerAuthKey = workerAuthKey
parsed.DevRecoveryKey = recoveryKey
return parsed, nil
}
func DevCombined() (*Config, error) {
controllerKey, workerAuthKey, recoveryKey := DevKeyGeneration()
hclStr := fmt.Sprintf(devConfig+devControllerExtraConfig+devWorkerExtraConfig, controllerKey, workerAuthKey, recoveryKey)
parsed, err := Parse(hclStr)
if err != nil {
return nil, fmt.Errorf("error parsing dev config: %w", err)
}
parsed.DevController = true
parsed.DevControllerKey = controllerKey
parsed.DevWorkerAuthKey = workerAuthKey
parsed.DevRecoveryKey = recoveryKey
return parsed, nil
}
func New() *Config {
return &Config{
SharedConfig: new(configutil.SharedConfig),
}
}
// LoadFile loads the configuration from the given file.
func LoadFile(path string, wrapper wrapping.Wrapper) (*Config, error) {
d, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
raw := string(d)
if wrapper != nil {
raw, err = configutil.EncryptDecrypt(raw, true, true, wrapper)
if err != nil {
return nil, err
}
}
return Parse(raw)
}
func Parse(d string) (*Config, error) {
obj, err := hcl.Parse(d)
if err != nil {
return nil, err
}
result := New()
if err := hcl.DecodeObject(result, obj); err != nil {
return nil, err
}
// Perform controller configuration overrides for auth token settings
if result.Controller != nil {
result.Controller.Name, err = parseutil.ParsePath(result.Controller.Name)
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return nil, fmt.Errorf("Error parsing controller name: %w", err)
}
if result.Controller.Name != strings.ToLower(result.Controller.Name) {
return nil, errors.New("Controller name must be all lower-case")
}
if !strutil.Printable(result.Controller.Name) {
return nil, errors.New("Controller name contains non-printable characters")
}
if result.Controller.AuthTokenTimeToLive != "" {
t, err := parseutil.ParseDurationSecond(result.Controller.AuthTokenTimeToLive)
if err != nil {
return result, err
}
result.Controller.AuthTokenTimeToLiveDuration = t
}
if result.Controller.AuthTokenTimeToStale != "" {
t, err := parseutil.ParseDurationSecond(result.Controller.AuthTokenTimeToStale)
if err != nil {
return result, err
}
result.Controller.AuthTokenTimeToStaleDuration = t
}
}
// Parse worker tags
if result.Worker != nil {
result.Worker.Name, err = parseutil.ParsePath(result.Worker.Name)
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return nil, fmt.Errorf("Error parsing worker name: %w", err)
}
if result.Worker.Name != strings.ToLower(result.Worker.Name) {
return nil, errors.New("Worker name must be all lower-case")
}
if !strutil.Printable(result.Worker.Name) {
return nil, errors.New("Worker name contains non-printable characters")
}
if result.Worker.TagsRaw != nil {
switch t := result.Worker.TagsRaw.(type) {
// HCL allows multiple labeled blocks with the same name, turning it
// into a slice of maps, hence the slice here. This format is the
// one that ends up matching the JSON that we use in the expression.
case []map[string]interface{}:
if err := mapstructure.WeakDecode(t, &result.Worker.Tags); err != nil {
return nil, fmt.Errorf("Error decoding the worker's %q section: %w", "tags", err)
}
// However for those that are used to other systems, we also accept
// key=value pairs
case []interface{}:
var strs []string
if err := mapstructure.WeakDecode(t, &strs); err != nil {
return nil, fmt.Errorf("Error decoding the worker's %q section: %w", "tags", err)
}
result.Worker.Tags = make(map[string][]string, len(strs))
// Aggregate the values by key. We care about the first equal
// sign only, to allow equals to be in values if needed. This
// also means we don't support equal signs in keys.
for _, str := range strs {
splitStr := strings.SplitN(str, "=", 2)
switch len(splitStr) {
case 1:
return nil, fmt.Errorf("Error decoding tag %q from string: must be in key = value format", str)
case 2:
key := splitStr[0]
v := result.Worker.Tags[key]
if len(v) == 0 {
v = make([]string, 0, 1)
}
result.Worker.Tags[key] = append(v, splitStr[1])
}
}
}
}
for k, v := range result.Worker.Tags {
if k != strings.ToLower(k) {
return nil, fmt.Errorf("Tag key %q is not all lower-case letters", k)
}
if !strutil.Printable(k) {
return nil, fmt.Errorf("Tag key %q contains non-printable characters", k)
}
for _, val := range v {
if val != strings.ToLower(val) {
return nil, fmt.Errorf("Tag value %q for tag key %q is not all lower-case letters", val, k)
}
if !strutil.Printable(k) {
return nil, fmt.Errorf("Tag value %q for tag key %q contains non-printable characters", v, k)
}
}
}
}
sharedConfig, err := configutil.ParseConfig(d)
if err != nil {
return nil, err
}
result.SharedConfig = sharedConfig
for _, listener := range result.SharedConfig.Listeners {
if strutil.StrListContains(listener.Purpose, "api") &&
(listener.CorsDisableDefaultAllowedOriginValues == nil || !*listener.CorsDisableDefaultAllowedOriginValues) {
switch listener.CorsEnabled {
case nil:
// If CORS wasn't specified, enable default value of *, which allows
// both the admin UI (without the user having to explicitly set an
// origin) and the desktop origin
listener.CorsEnabled = new(bool)
*listener.CorsEnabled = true
listener.CorsAllowedOrigins = []string{"*"}
default:
// If not the wildcard and they haven't disabled us auto-adding
// origin values, add the desktop client origin
if *listener.CorsEnabled &&
!strutil.StrListContains(listener.CorsAllowedOrigins, "*") {
listener.CorsAllowedOrigins = strutil.AppendIfMissing(listener.CorsAllowedOrigins, desktopCorsOrigin)
}
}
}
}
list, ok := obj.Node.(*ast.ObjectList)
if !ok {
return nil, fmt.Errorf("error parsing: file doesn't contain a root object")
}
eventList := list.Filter("events")
switch len(eventList.Items) {
case 0:
result.Eventing = event.DefaultEventerConfig()
case 1:
if result.Eventing, err = parseEventing(eventList.Items[0]); err != nil {
return nil, fmt.Errorf(`error parsing "events": %w`, err)
}
default:
return nil, fmt.Errorf(`too many "events" nodes (max 1, got %d)`, len(eventList.Items))
}
return result, nil
}
func parseEventing(eventObj *ast.ObjectItem) (*event.EventerConfig, error) {
// Decode the outside struct
var result event.EventerConfig
if err := hcl.DecodeObject(&result, eventObj.Val); err != nil {
return nil, fmt.Errorf(`error decoding "events" node: %w`, err)
}
// Now, find the sinks
eventObjType, ok := eventObj.Val.(*ast.ObjectType)
if !ok {
return nil, fmt.Errorf(`error interpreting "events" node as an object type`)
}
list := eventObjType.List
sinkList := list.Filter("sink")
// Go through each sink and decode
for i, item := range sinkList.Items {
var s event.SinkConfig
if err := hcl.DecodeObject(&s, item.Val); err != nil {
return nil, fmt.Errorf("error decoding eventer sink entry %d", i)
}
// Fix up type and validate
switch {
case s.Type != "":
case len(item.Keys) == 1:
s.Type = event.SinkType(item.Keys[0].Token.Value().(string))
default:
switch {
case s.StderrConfig != nil:
// If we haven't found the type any other way, they _must_
// specify this block even though there are no config parameters
s.Type = event.StderrSink
case s.FileConfig != nil:
s.Type = event.FileSink
default:
return nil, fmt.Errorf("sink type could not be determined")
}
}
s.Type = event.SinkType(strings.ToLower(string(s.Type)))
if s.Type == event.StderrSink && s.StderrConfig == nil {
// StderrConfig is optional as it has no values, but ensure it's
// always populated if it's the type
s.StderrConfig = new(event.StderrSinkTypeConfig)
}
// parse the duration string specified in a file config into a time.Duration
if s.FileConfig != nil && s.FileConfig.RotateDurationHCL != "" {
var err error
s.FileConfig.RotateDuration, err = parseutil.ParseDurationSecond(s.FileConfig.RotateDurationHCL)
if err != nil {
return nil, fmt.Errorf("can't parse rotation duration %s", s.FileConfig.RotateDurationHCL)
}
}
if err := s.Validate(); err != nil {
return nil, err
}
// Append to result
result.Sinks = append(result.Sinks, s)
}
if len(result.Sinks) == 0 {
result.Sinks = []event.SinkConfig{event.DefaultSink()}
}
return &result, nil
}
// Sanitized returns a copy of the config with all values that are considered
// sensitive stripped. It also strips all `*Raw` values that are mainly
// used for parsing.
//
// Specifically, the fields that this method strips are:
// - KMS.Config
// - Telemetry.CirconusAPIToken
func (c *Config) Sanitized() map[string]interface{} {
// Create shared config if it doesn't exist (e.g. in tests) so that map
// keys are actually populated
if c.SharedConfig == nil {
c.SharedConfig = new(configutil.SharedConfig)
}
sharedResult := c.SharedConfig.Sanitized()
result := map[string]interface{}{}
for k, v := range sharedResult {
result[k] = v
}
return result
}