internal/pkg/config/config.go (235 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
// Package config handles fleet-server configuration.
package config
import (
"context"
"errors"
"strings"
"sync"
"github.com/gofrs/uuid"
"github.com/rs/zerolog"
"github.com/elastic/fleet-server/v7/version"
"github.com/elastic/go-ucfg"
"github.com/elastic/go-ucfg/flag"
"github.com/elastic/go-ucfg/yaml"
)
// DefaultOptions defaults options used to read the configuration
var DefaultOptions = []ucfg.Option{
ucfg.PathSep("."),
ucfg.ResolveEnv,
ucfg.VarExp,
ucfg.FieldReplaceValues("inputs"),
}
var MergeOptions = []ucfg.Option{
ucfg.PathSep("."),
ucfg.ResolveEnv,
ucfg.VarExp,
ucfg.FieldMergeValues("inputs"),
}
const kRedacted = "[redacted]"
// Config is the global configuration.
//
// fleet-server does not provide any builtin env var mappings.
// The DefaultOptions are set to use env var substitution if it's defined explicitly in go-ucfg's input.
// For example:
//
// output.elasticsearch.service_token: ${MY_TOKEN_VAR}
//
// The env vars that `elastic-agent container` command uses are unrelated.
// The agent will do all substitutions before sending fleet-server the complete config.
type Config struct {
Fleet Fleet `config:"fleet"`
Output Output `config:"output"`
Inputs []Input `config:"inputs"`
Logging Logging `config:"logging"`
HTTP HTTP `config:"http"`
m sync.Mutex
}
var deprecatedConfigOptions = map[string]string{
"inputs[0].limits.max_connections": "max_connections has been deprecated and will be removed in a future release. Please configure server limits using max_agents instead.",
}
// InitDefaults initializes the defaults for the configuration.
func (c *Config) InitDefaults() {
c.Inputs = make([]Input, 1)
c.Inputs[0].InitDefaults()
c.Logging.InitDefaults()
c.HTTP.InitDefaults()
}
func (c *Config) GetFleetInput() (Input, error) {
c.m.Lock()
defer c.m.Unlock()
if err := c.Validate(); err != nil {
return Input{}, err
}
return c.Inputs[0], nil
}
// Validate ensures that the configuration is valid.
func (c *Config) Validate() error {
if len(c.Inputs) == 0 {
return errors.New("a fleet-server input must be defined")
}
if len(c.Inputs) > 1 {
return errors.New("only 1 fleet-server input can be defined")
}
return nil
}
// LoadServerLimits should be called after initialization, so we may access the user defined
// agent limit setting.
func (c *Config) LoadServerLimits(log *zerolog.Logger) error {
c.m.Lock()
defer c.m.Unlock()
err := c.Validate()
if err != nil {
log.Error().Err(err).Msgf("failed to validate while calculating limits")
return err
}
fleetInput := &c.Inputs[0]
agentLimits := loadLimits(log, fleetInput.Server.Limits.MaxAgents)
fleetInput.Cache.LoadLimits(agentLimits)
fleetInput.Server.Limits.LoadLimits(agentLimits)
return nil
}
// LoadStandaloneAgent should be called after initialization
// this create a fake agent id and version
func (c *Config) LoadStandaloneAgentMetadata() error {
c.m.Lock()
defer c.m.Unlock()
agentID, err := uuid.NewV4()
if err != nil {
return err
}
c.Fleet.Agent = Agent{
ID: agentID.String(),
Version: version.DefaultVersion,
}
return nil
}
// Merge merges two configurations together.
func (c *Config) Merge(other *Config) (*Config, error) {
c.m.Lock()
defer c.m.Unlock()
other.m.Lock()
defer other.m.Unlock()
repr, err := ucfg.NewFrom(c, DefaultOptions...)
if err != nil {
return nil, err
}
err = repr.Merge(other, DefaultOptions...)
if err != nil {
return nil, err
}
cfg := &Config{}
err = repr.Unpack(cfg, DefaultOptions...)
if err != nil {
return nil, err
}
return cfg, nil
}
func RedactOutput(cfg *Config) Output {
redacted := cfg.Output
if redacted.Elasticsearch.ServiceToken != "" {
redacted.Elasticsearch.ServiceToken = kRedacted
}
if redacted.Elasticsearch.TLS != nil {
newTLS := *redacted.Elasticsearch.TLS
if newTLS.Certificate.Key != "" {
newTLS.Certificate.Key = kRedacted
}
if newTLS.Certificate.Passphrase != "" {
newTLS.Certificate.Passphrase = kRedacted
}
redacted.Elasticsearch.TLS = &newTLS
}
if redacted.Elasticsearch.Headers != nil {
redacted.Elasticsearch.Headers = redactHeaders(redacted.Elasticsearch.Headers)
}
if redacted.Elasticsearch.ProxyHeaders != nil {
redacted.Elasticsearch.ProxyHeaders = redactHeaders(redacted.Elasticsearch.ProxyHeaders)
}
return redacted
}
// redactHeaders returns a copy of the passed headers map.
// It will do a best-effort attempt to redact sensitive headers based on header names.
func redactHeaders(headers map[string]string) map[string]string {
redactedHeaders := make(map[string]string)
for k, v := range headers {
redactedHeaders[k] = v
lk := strings.ToLower(k)
if strings.Contains(lk, "auth") || strings.Contains(lk, "token") || strings.Contains(lk, "key") || strings.Contains(lk, "bearer") {
redactedHeaders[k] = kRedacted
}
}
return redactedHeaders
}
func redactServer(cfg *Config) Server {
redacted := cfg.Inputs[0].Server
if redacted.TLS != nil {
newTLS := *redacted.TLS
if newTLS.Certificate.Key != "" {
newTLS.Certificate.Key = kRedacted
}
if newTLS.Certificate.Passphrase != "" {
newTLS.Certificate.Passphrase = kRedacted
}
redacted.TLS = &newTLS
}
if redacted.Instrumentation.APIKey != "" {
redacted.Instrumentation.APIKey = kRedacted
}
if redacted.Instrumentation.SecretToken != "" {
redacted.Instrumentation.SecretToken = kRedacted
}
if redacted.StaticPolicyTokens.PolicyTokens != nil {
policyTokens := make([]PolicyToken, len(redacted.StaticPolicyTokens.PolicyTokens))
for i := range redacted.StaticPolicyTokens.PolicyTokens {
policyTokens[i] = PolicyToken{
TokenKey: kRedacted,
PolicyID: redacted.StaticPolicyTokens.PolicyTokens[i].PolicyID,
}
}
redacted.StaticPolicyTokens.PolicyTokens = policyTokens
}
return redacted
}
// Redact returns a copy of the config with all sensitive attributes redacted.
func (c *Config) Redact() *Config {
redacted := &Config{
Fleet: c.Fleet,
Output: c.Output,
Inputs: make([]Input, 1),
Logging: c.Logging,
HTTP: c.HTTP,
}
redacted.Inputs[0].Server = redactServer(c)
redacted.Output = RedactOutput(c)
return redacted
}
func checkDeprecatedOptions(deprecatedOpts map[string]string, c *ucfg.Config) {
for opt, message := range deprecatedOpts {
if c.HasField(opt) {
zerolog.Ctx(context.TODO()).Warn().Msg(message) // TODO is used as this may be called before logger config is read.
}
}
}
// FromConfig returns Config from the ucfg.Config.
func FromConfig(c *ucfg.Config) (*Config, error) {
checkDeprecatedOptions(deprecatedConfigOptions, c)
cfg := &Config{}
err := c.Unpack(cfg, DefaultOptions...)
if err != nil {
return nil, err
}
return cfg, nil
}
// LoadFile take a path and load the file and return a new configuration.
// Only used in tests
func LoadFile(path string) (*Config, error) {
c, err := yaml.NewConfigWithFile(path, DefaultOptions...)
if err != nil {
return nil, err
}
return FromConfig(c)
}
// Flag captures key/values pairs into an ucfg.Config object.
type Flag flag.FlagValue
// NewFlag creates an instance that allows the `-E` flag to overwrite
// the configuration from the command-line.
func NewFlag() *Flag {
opts := append(
[]ucfg.Option{
ucfg.MetaData(ucfg.Meta{Source: "command line flag"}),
},
DefaultOptions...,
)
tmp := flag.NewFlagKeyValue(ucfg.New(), true, opts...)
return (*Flag)(tmp)
}
func (f *Flag) access() *flag.FlagValue {
return (*flag.FlagValue)(f)
}
// Config returns the config object the Flag stores applied settings to.
func (f *Flag) Config() *ucfg.Config {
return f.access().Config()
}
// Set sets a settings value in the Config object. The input string must be a
// key-value pair like `key=value`. If the value is missing, the value is set
// to the boolean value `true`.
func (f *Flag) Set(s string) error {
return f.access().Set(s)
}
// Get returns the Config object used to store values.
func (f *Flag) Get() interface{} {
return f.Config()
}
// String always returns an empty string. It is required to fulfil
// the flag.Value interface.
func (f *Flag) String() string {
return ""
}
// Type reports the type of contents (setting=value) expected to be parsed by Set.
// It is used to build the CLI usage string.
func (f *Flag) Type() string {
return "setting=value"
}