Repository: avenga/couper
Pull request #633 (2022-11-22 17:39:16)

Fix unstable config tests
cleanup files, ordered types and remove obsolete ones

related run: https://github.com/avenga/couper/actions/runs/3523439756/jobs/5907605729#step:7:651
---
<details>
    <summary>Reviewer checklist</summary>
    <ul>
        <li>Read PR description: a summary about the changes is required</li>
        <li>Changelog updated</li>
        <li>Documentation: docs/{Reference, Cli, ...}, Docker and cli help/usage</li>
        <li>Pulled branch, manually tested</li>
        <li>Verified requirements are met</li>
        <li>Reviewed the code</li>
        <li>Reviewed the related tests</li>
    </ul>
</details>



Patch:
diff --git a/command/run.go b/command/run.go
index 8397676a..c14b8df6 100644
--- a/command/run.go
+++ b/command/run.go
@@ -9,8 +9,6 @@ import (
 	"net"
 	"net/http"
 	"os"
-	"strings"
-	"sync"
 	"time"
 
 	"github.com/sirupsen/logrus"
@@ -28,67 +26,19 @@ import (
 
 var _ Cmd = &Run{}
 var RunCmdTestCallback func()
+var RunCmdConfigTestCallback func(*config.Settings)
 
 // Run starts the frontend gateway server and listen
 // for requests on the configured hosts and ports.
 type Run struct {
-	context  context.Context
-	flagSet  *flag.FlagSet
-	settings *config.Settings
-
-	// required for testing purposes
-	// TODO: provide a testable interface
-	settingsMu sync.Mutex
+	context context.Context
+	flagSet *flag.FlagSet
 }
 
 func NewRun(ctx context.Context) *Run {
-	settings := config.DefaultSettings
-	set := flag.NewFlagSet("run", flag.ContinueOnError)
-	set.StringVar(&settings.CAFile, "ca-file", settings.CAFile, "-ca-file certificate.pem")
-	set.StringVar(&settings.HealthPath, "health-path", settings.HealthPath, "-health-path /healthz")
-	set.IntVar(&settings.DefaultPort, "p", settings.DefaultPort, "-p 8080")
-	set.BoolVar(&settings.XForwardedHost, "xfh", settings.XForwardedHost, "-xfh")
-	set.Var(&AcceptForwardedValue{settings: &settings}, "accept-forwarded-url", "-accept-forwarded-url [proto][,host][,port]")
-	set.Var(&settings.TLSDevProxy, "https-dev-proxy", "-https-dev-proxy 8443:8080,9443:9000")
-	set.BoolVar(&settings.NoProxyFromEnv, "no-proxy-from-env", settings.NoProxyFromEnv, "-no-proxy-from-env")
-	set.StringVar(&settings.RequestIDAcceptFromHeader, "request-id-accept-from-header", settings.RequestIDAcceptFromHeader, "-request-id-accept-from-header X-UID")
-	set.StringVar(&settings.RequestIDBackendHeader, "request-id-backend-header", settings.RequestIDBackendHeader, "-request-id-backend-header Couper-Request-ID")
-	set.StringVar(&settings.RequestIDClientHeader, "request-id-client-header", settings.RequestIDClientHeader, "-request-id-client-header Couper-Request-ID")
-	set.StringVar(&settings.RequestIDFormat, "request-id-format", settings.RequestIDFormat, "-request-id-format uuid4")
-	set.StringVar(&settings.SecureCookies, "secure-cookies", settings.SecureCookies, "-secure-cookies strip")
-	set.BoolVar(&settings.TelemetryMetrics, "beta-metrics", settings.TelemetryMetrics, "-beta-metrics")
-	set.IntVar(&settings.TelemetryMetricsPort, "beta-metrics-port", settings.TelemetryMetricsPort, "-beta-metrics-port 9090")
-	set.StringVar(&settings.TelemetryMetricsEndpoint, "beta-metrics-endpoint", settings.TelemetryMetricsEndpoint, "-beta-metrics-endpoint [host:port]")
-	set.StringVar(&settings.TelemetryMetricsExporter, "beta-metrics-exporter", settings.TelemetryMetricsExporter, "-beta-metrics-exporter [name]")
-	set.StringVar(&settings.TelemetryServiceName, "beta-service-name", settings.TelemetryServiceName, "-beta-service-name [name]")
-	set.BoolVar(&settings.TelemetryTraces, "beta-traces", settings.TelemetryTraces, "-beta-traces")
-	set.StringVar(&settings.TelemetryTracesEndpoint, "beta-traces-endpoint", settings.TelemetryTracesEndpoint, "-beta-traces-endpoint [host:port]")
 	return &Run{
-		context:  ctx,
-		flagSet:  set,
-		settings: &settings,
-	}
-}
-
-type AcceptForwardedValue struct {
-	settings *config.Settings
-}
-
-func (a AcceptForwardedValue) String() string {
-	if a.settings == nil || a.settings.AcceptForwarded == nil {
-		return ""
-	}
-	return a.settings.AcceptForwarded.String()
-}
-
-func (a AcceptForwardedValue) Set(s string) error {
-	forwarded := strings.Split(s, ",")
-	err := a.settings.AcceptForwarded.Set(forwarded)
-	if err != nil {
-		return err
+		context: ctx,
 	}
-	a.settings.AcceptForwardedURL = forwarded
-	return nil
 }
 
 // limitFn depends on current OS, set via build flags
@@ -97,19 +47,11 @@ var limitFn func(entry *logrus.Entry)
 func (r *Run) Execute(args Args, config *config.Couper, logEntry *logrus.Entry) error {
 	logEntry.WithField("files", config.Files.AsList()).Debug("loaded files")
 
-	r.settingsMu.Lock()
-	*r.settings = *config.Settings
-	r.settingsMu.Unlock()
-
 	// apply command context
 	config.Context = config.Context.(*eval.Context).WithContext(r.context)
 
-	if f := r.flagSet.Lookup("accept-forwarded-url"); f != nil {
-		if afv, ok := f.Value.(*AcceptForwardedValue); ok {
-			afv.settings = r.settings
-		}
-	}
-
+	// apply cli flags to file settings obj
+	r.flagSet = newFlagSet(config.Settings, "run")
 	if err := r.flagSet.Parse(args.Filter(r.flagSet)); err != nil {
 		return err
 	}
@@ -120,20 +62,15 @@ func (r *Run) Execute(args Args, config *config.Couper, logEntry *logrus.Entry)
 		return fmt.Errorf("invalid value for the -secure-cookies flag given: '%s' only 'strip' is supported", config.Settings.SecureCookies)
 	}
 
-	// Some remapping due to flag set pre-definition
-	env.Decode(r.settings)
-	err := r.settings.SetAcceptForwarded()
-	if err != nil {
+	// finally apply environment variables to settings obj
+	env.Decode(config.Settings)
+
+	if err := config.Settings.ApplyAcceptForwarded(); err != nil {
 		return err
 	}
-	r.settingsMu.Lock()
-	config.Settings = r.settings
-	r.settingsMu.Unlock()
-
-	timings := runtime.DefaultTimings
-	env.Decode(&timings)
 
 	if config.Settings.CAFile != "" {
+		var err error
 		config.Settings.Certificate, err = readCertificateFile(config.Settings.CAFile)
 		if err != nil {
 			return err
@@ -141,6 +78,13 @@ func (r *Run) Execute(args Args, config *config.Couper, logEntry *logrus.Entry)
 		logEntry.Infof("configured with ca-certificate: %s", config.Settings.CAFile)
 	}
 
+	if RunCmdConfigTestCallback != nil {
+		RunCmdConfigTestCallback(config.Settings)
+	}
+
+	timings := runtime.DefaultTimings
+	env.Decode(&timings)
+
 	memStore := cache.New(logEntry, r.context.Done())
 	// logEntry has still the 'daemon' type which can be used for config related load errors.
 	srvConf, err := runtime.NewServerConfiguration(config, logEntry, memStore)
@@ -151,13 +95,13 @@ func (r *Run) Execute(args Args, config *config.Couper, logEntry *logrus.Entry)
 
 	telemetry.InitExporter(r.context, &telemetry.Options{
 		MetricsCollectPeriod: time.Second * 2,
-		Metrics:              r.settings.TelemetryMetrics,
-		MetricsEndpoint:      r.settings.TelemetryMetricsEndpoint,
-		MetricsExporter:      r.settings.TelemetryMetricsExporter,
-		MetricsPort:          r.settings.TelemetryMetricsPort,
-		ServiceName:          r.settings.TelemetryServiceName,
-		Traces:               r.settings.TelemetryTraces,
-		TracesEndpoint:       r.settings.TelemetryTracesEndpoint,
+		Metrics:              config.Settings.TelemetryMetrics,
+		MetricsEndpoint:      config.Settings.TelemetryMetricsEndpoint,
+		MetricsExporter:      config.Settings.TelemetryMetricsExporter,
+		MetricsPort:          config.Settings.TelemetryMetricsPort,
+		ServiceName:          config.Settings.TelemetryServiceName,
+		Traces:               config.Settings.TelemetryTraces,
+		TracesEndpoint:       config.Settings.TelemetryTracesEndpoint,
 	}, memStore, logEntry)
 
 	if limitFn != nil {
@@ -251,3 +195,28 @@ func readCertificateFile(file string) ([]byte, error) {
 func (r *Run) Usage() {
 	r.flagSet.Usage()
 }
+
+func newFlagSet(settings *config.Settings, cmdName string) *flag.FlagSet {
+	set := flag.NewFlagSet(cmdName, flag.ContinueOnError)
+	set.StringVar(&settings.CAFile, "ca-file", settings.CAFile, "-ca-file certificate.pem")
+	set.StringVar(&settings.HealthPath, "health-path", settings.HealthPath, "-health-path /healthz")
+	set.IntVar(&settings.DefaultPort, "p", settings.DefaultPort, "-p 8080")
+	set.BoolVar(&settings.XForwardedHost, "xfh", settings.XForwardedHost, "-xfh")
+	set.Var(&settings.AcceptForwardedURL, "accept-forwarded-url", "-accept-forwarded-url [proto][,host][,port]")
+	set.Var(&settings.TLSDevProxy, "https-dev-proxy", "-https-dev-proxy 8443:8080,9443:9000")
+	set.BoolVar(&settings.NoProxyFromEnv, "no-proxy-from-env", settings.NoProxyFromEnv, "-no-proxy-from-env")
+	set.StringVar(&settings.RequestIDAcceptFromHeader, "request-id-accept-from-header", settings.RequestIDAcceptFromHeader, "-request-id-accept-from-header X-UID")
+	set.StringVar(&settings.RequestIDBackendHeader, "request-id-backend-header", settings.RequestIDBackendHeader, "-request-id-backend-header Couper-Request-ID")
+	set.StringVar(&settings.RequestIDClientHeader, "request-id-client-header", settings.RequestIDClientHeader, "-request-id-client-header Couper-Request-ID")
+	set.StringVar(&settings.RequestIDFormat, "request-id-format", settings.RequestIDFormat, "-request-id-format uuid4")
+	set.StringVar(&settings.SecureCookies, "secure-cookies", settings.SecureCookies, "-secure-cookies strip")
+	set.BoolVar(&settings.TelemetryMetrics, "beta-metrics", settings.TelemetryMetrics, "-beta-metrics")
+	set.IntVar(&settings.TelemetryMetricsPort, "beta-metrics-port", settings.TelemetryMetricsPort, "-beta-metrics-port 9090")
+	set.StringVar(&settings.TelemetryMetricsEndpoint, "beta-metrics-endpoint", settings.TelemetryMetricsEndpoint, "-beta-metrics-endpoint [host:port]")
+	set.StringVar(&settings.TelemetryMetricsExporter, "beta-metrics-exporter", settings.TelemetryMetricsExporter, "-beta-metrics-exporter [name]")
+	set.StringVar(&settings.TelemetryServiceName, "beta-service-name", settings.TelemetryServiceName, "-beta-service-name [name]")
+	set.BoolVar(&settings.TelemetryTraces, "beta-traces", settings.TelemetryTraces, "-beta-traces")
+	set.StringVar(&settings.TelemetryTracesEndpoint, "beta-traces-endpoint", settings.TelemetryTracesEndpoint, "-beta-traces-endpoint [host:port]")
+
+	return set
+}
diff --git a/config/configload/helper.go b/config/configload/helper.go
index fd634726..2dfe7182 100644
--- a/config/configload/helper.go
+++ b/config/configload/helper.go
@@ -29,13 +29,11 @@ func newHelper(body hcl.Body, src [][]byte, environment string) (*helper, error)
 		return nil, diags
 	}
 
-	defSettings := config.DefaultSettings
-
 	couperConfig := &config.Couper{
 		Context:     eval.NewContext(src, defaultsBlock.Defaults, environment),
 		Definitions: &config.Definitions{},
 		Defaults:    defaultsBlock.Defaults,
-		Settings:    &defSettings,
+		Settings:    config.NewDefaultSettings(),
 	}
 
 	schema, _ := gohcl.ImpliedBodySchema(couperConfig)
diff --git a/config/configload/load.go b/config/configload/load.go
index 0afe9d82..c4b78156 100644
--- a/config/configload/load.go
+++ b/config/configload/load.go
@@ -262,7 +262,7 @@ func LoadBytes(src []byte, filename string) (*config.Couper, error) {
 		return nil, err
 	}
 
-	if err := validateBody(hclBody, false); err != nil {
+	if err = validateBody(hclBody, false); err != nil {
 		return nil, err
 	}
 
@@ -318,10 +318,6 @@ func LoadConfig(body *hclsyntax.Body, src [][]byte, environment string) (*config
 			if diags := gohcl.DecodeBody(outerBlock.Body, helper.context, helper.config.Settings); diags.HasErrors() {
 				return nil, diags
 			}
-
-			if err = helper.config.Settings.SetAcceptForwarded(); err != nil {
-				return nil, newDiagErr(&outerBlock.DefRange, fmt.Sprintf("invalid accept_forwarded_url: %q", err))
-			}
 		}
 	}
 
diff --git a/config/settings.go b/config/settings.go
index b8cd0ffa..fd1d1303 100644
--- a/config/settings.go
+++ b/config/settings.go
@@ -6,52 +6,9 @@ import (
 	"strings"
 )
 
-type AcceptForwarded struct {
-	port, protocol, host bool
-}
-
-func (a *AcceptForwarded) Set(forwarded []string) error {
-	a.protocol = false
-	a.host = false
-	a.port = false
-	for _, part := range forwarded {
-		switch strings.TrimSpace(part) {
-		case "":
-			continue
-		case "port":
-			a.port = true
-		case "proto":
-			a.protocol = true
-		case "host":
-			a.host = true
-		default:
-			return fmt.Errorf("invalid X-Forwarded-* name (%s)", part)
-		}
-	}
-
-	return nil
-}
-
-func (a AcceptForwarded) String() string {
-	var parts []string
-
-	if a.protocol {
-		parts = append(parts, "proto")
-	}
-	if a.host {
-		parts = append(parts, "host")
-	}
-	if a.port {
-		parts = append(parts, "port")
-	}
-
-	return strings.Join(parts, ",")
-}
-
 const otelCollectorEndpoint = "localhost:4317"
 
-// DefaultSettings defines the <DefaultSettings> object.
-var DefaultSettings = Settings{
+var defaultSettings = Settings{
 	DefaultPort:              8080,
 	Environment:              "",
 	HealthPath:               "/healthz",
@@ -70,10 +27,6 @@ var DefaultSettings = Settings{
 	TelemetryServiceName:     "couper",
 	TelemetryTracesEndpoint:  otelCollectorEndpoint,
 	XForwardedHost:           false,
-
-	// TODO: refactor
-	AcceptForwardedURL: []string{},
-	AcceptForwarded:    &AcceptForwarded{},
 }
 
 // Settings represents the <Settings> object.
@@ -81,31 +34,37 @@ type Settings struct {
 	AcceptForwarded *AcceptForwarded
 	Certificate     []byte
 
-	CAFile                    string   `hcl:"ca_file,optional" docs:"adds the given PEM encoded CA certificate to the existing system certificate pool for all outgoing connections"`
-	AcceptForwardedURL        []string `hcl:"accept_forwarded_url,optional" docs:"Which {X-Forwarded-*} request headers should be accepted to change the [request variables](../variables#request) {url}, {origin}, {protocol}, {host}, {port}. Valid values: {\"proto\"}, {\"host\"} and {\"port\"}. The port in a {X-Forwarded-Port} header takes precedence over a port in {X-Forwarded-Host}. Affects relative URL values for [{sp_acs_url}](saml) attribute and {redirect_uri} attribute within [{beta_oauth2}](oauth2) and [{oidc}](oidc)."`
-	DefaultPort               int      `hcl:"default_port,optional" docs:"Port which will be used if not explicitly specified per host within the [{hosts}](server) attribute." default:"8080"`
-	Environment               string   `hcl:"environment,optional" docs:"[environment](../command-line#global-options) Couper is to run in"`
-	HealthPath                string   `hcl:"health_path,optional" docs:"Health path for all configured servers and ports" default:"/healthz"`
-	LogFormat                 string   `hcl:"log_format,optional" docs:"tab/field based colored logs or JSON logs: {\"common\"} or {\"json\"}" default:"common"`
-	LogLevel                  string   `hcl:"log_level,optional" docs:"sets the log level: {\"panic\"}, {\"fatal\"}, {\"error\"}, {\"warn\"}, {\"info\"}, {\"debug\"}, {\"trace\"}" default:"info"`
-	LogPretty                 bool     `hcl:"log_pretty,optional" docs:"global option for {json} log format which pretty prints with basic key coloring"`
-	NoProxyFromEnv            bool     `hcl:"no_proxy_from_env,optional" docs:"disables the connect hop to configured [proxy via environment](https://godoc.org/golang.org/x/net/http/httpproxy)"`
-	PProf                     bool     `hcl:"pprof,optional" docs:"enables [profiling](https://github.com/google/pprof/blob/main/doc/README.md#pprof)"`
-	PProfPort                 int      `hcl:"pprof_port,optional" docs:"Port for profiling interface" default:"6060"`
-	RequestIDAcceptFromHeader string   `hcl:"request_id_accept_from_header,optional" docs:"client request HTTP header field that transports the {request.id} which Couper takes for logging and transport to the backend (if configured)"`
-	RequestIDBackendHeader    string   `hcl:"request_id_backend_header,optional" docs:"HTTP header field which Couper uses to transport the {request.id} to the backend" default:"Couper-Request-ID"`
-	RequestIDClientHeader     string   `hcl:"request_id_client_header,optional" docs:"HTTP header field which Couper uses to transport the {request.id} to the client" default:"Couper-Request-ID"`
-	RequestIDFormat           string   `hcl:"request_id_format,optional" docs:"{\"common\"} or {\"uuid4\"}. If set to {\"uuid4\"} an RFC 4122 UUID is used for {request.id} and related log fields. " default:"common"`
-	SecureCookies             string   `hcl:"secure_cookies,optional" docs:"{\"\"} or {\"strip\"}. If set to {\"strip\"}, the {Secure} flag is removed from all {Set-Cookie} HTTP header fields." default:"\u200C"`
-	TLSDevProxy               List     `hcl:"https_dev_proxy,optional" docs:"TLS port mappings to define the TLS listen port and the target one. Self-signed certificates will be generated on the fly based on the given hostname. Certificates will be held in memory."`
-	TelemetryMetrics          bool     `hcl:"beta_metrics,optional" docs:"enables the Prometheus [metrics](/observation/metrics) exporter"`
-	TelemetryMetricsEndpoint  string   `hcl:"beta_metrics_endpoint,optional" docs:"" default:""`
-	TelemetryMetricsExporter  string   `hcl:"beta_metrics_exporter,optional" docs:"" default:""`
-	TelemetryMetricsPort      int      `hcl:"beta_metrics_port,optional" docs:"Prometheus exporter listen port" default:"9090"`
-	TelemetryServiceName      string   `hcl:"beta_service_name,optional" docs:"service name which applies to the {service_name} metric labels" default:"couper"`
-	TelemetryTraces           bool     `hcl:"beta_traces,optional" docs:"" default:""`
-	TelemetryTracesEndpoint   string   `hcl:"beta_traces_endpoint,optional" docs:"" default:""`
-	XForwardedHost            bool     `hcl:"xfh,optional" docs:"whether to use the {X-Forwarded-Host} header as the request host"`
+	CAFile                    string `hcl:"ca_file,optional" docs:"adds the given PEM encoded CA certificate to the existing system certificate pool for all outgoing connections"`
+	AcceptForwardedURL        List   `hcl:"accept_forwarded_url,optional" docs:"Which {X-Forwarded-*} request headers should be accepted to change the [request variables](../variables#request) {url}, {origin}, {protocol}, {host}, {port}. Valid values: {\"proto\"}, {\"host\"} and {\"port\"}. The port in a {X-Forwarded-Port} header takes precedence over a port in {X-Forwarded-Host}. Affects relative URL values for [{sp_acs_url}](saml) attribute and {redirect_uri} attribute within [{beta_oauth2}](oauth2) and [{oidc}](oidc)."`
+	DefaultPort               int    `hcl:"default_port,optional" docs:"Port which will be used if not explicitly specified per host within the [{hosts}](server) attribute." default:"8080"`
+	Environment               string `hcl:"environment,optional" docs:"[environment](../command-line#global-options) Couper is to run in"`
+	HealthPath                string `hcl:"health_path,optional" docs:"Health path for all configured servers and ports" default:"/healthz"`
+	LogFormat                 string `hcl:"log_format,optional" docs:"tab/field based colored logs or JSON logs: {\"common\"} or {\"json\"}" default:"common"`
+	LogLevel                  string `hcl:"log_level,optional" docs:"sets the log level: {\"panic\"}, {\"fatal\"}, {\"error\"}, {\"warn\"}, {\"info\"}, {\"debug\"}, {\"trace\"}" default:"info"`
+	LogPretty                 bool   `hcl:"log_pretty,optional" docs:"global option for {json} log format which pretty prints with basic key coloring"`
+	NoProxyFromEnv            bool   `hcl:"no_proxy_from_env,optional" docs:"disables the connect hop to configured [proxy via environment](https://godoc.org/golang.org/x/net/http/httpproxy)"`
+	PProf                     bool   `hcl:"pprof,optional" docs:"enables [profiling](https://github.com/google/pprof/blob/main/doc/README.md#pprof)"`
+	PProfPort                 int    `hcl:"pprof_port,optional" docs:"Port for profiling interface" default:"6060"`
+	RequestIDAcceptFromHeader string `hcl:"request_id_accept_from_header,optional" docs:"client request HTTP header field that transports the {request.id} which Couper takes for logging and transport to the backend (if configured)"`
+	RequestIDBackendHeader    string `hcl:"request_id_backend_header,optional" docs:"HTTP header field which Couper uses to transport the {request.id} to the backend" default:"Couper-Request-ID"`
+	RequestIDClientHeader     string `hcl:"request_id_client_header,optional" docs:"HTTP header field which Couper uses to transport the {request.id} to the client" default:"Couper-Request-ID"`
+	RequestIDFormat           string `hcl:"request_id_format,optional" docs:"{\"common\"} or {\"uuid4\"}. If set to {\"uuid4\"} an RFC 4122 UUID is used for {request.id} and related log fields. " default:"common"`
+	SecureCookies             string `hcl:"secure_cookies,optional" docs:"{\"\"} or {\"strip\"}. If set to {\"strip\"}, the {Secure} flag is removed from all {Set-Cookie} HTTP header fields." default:"\u200C"`
+	TLSDevProxy               List   `hcl:"https_dev_proxy,optional" docs:"TLS port mappings to define the TLS listen port and the target one. Self-signed certificates will be generated on the fly based on the given hostname. Certificates will be held in memory."`
+	TelemetryMetrics          bool   `hcl:"beta_metrics,optional" docs:"enables the Prometheus [metrics](/observation/metrics) exporter"`
+	TelemetryMetricsEndpoint  string `hcl:"beta_metrics_endpoint,optional" docs:"" default:""`
+	TelemetryMetricsExporter  string `hcl:"beta_metrics_exporter,optional" docs:"" default:""`
+	TelemetryMetricsPort      int    `hcl:"beta_metrics_port,optional" docs:"Prometheus exporter listen port" default:"9090"`
+	TelemetryServiceName      string `hcl:"beta_service_name,optional" docs:"service name which applies to the {service_name} metric labels" default:"couper"`
+	TelemetryTraces           bool   `hcl:"beta_traces,optional" docs:"" default:""`
+	TelemetryTracesEndpoint   string `hcl:"beta_traces_endpoint,optional" docs:"" default:""`
+	XForwardedHost            bool   `hcl:"xfh,optional" docs:"whether to use the {X-Forwarded-Host} header as the request host"`
+}
+
+func NewDefaultSettings() *Settings {
+	settings := defaultSettings
+	settings.AcceptForwarded = &AcceptForwarded{}
+	return &settings
 }
 
 var _ flag.Value = &List{}
@@ -124,7 +83,7 @@ func (s *List) Set(val string) error {
 	return nil
 }
 
-func (s *Settings) SetAcceptForwarded() error {
+func (s *Settings) ApplyAcceptForwarded() error {
 	return s.AcceptForwarded.Set(s.AcceptForwardedURL)
 }
 
@@ -139,3 +98,29 @@ func (s *Settings) AcceptsForwardedProtocol() bool {
 func (s *Settings) AcceptsForwardedHost() bool {
 	return s.AcceptForwarded.host
 }
+
+type AcceptForwarded struct {
+	port, protocol, host bool
+}
+
+func (a *AcceptForwarded) Set(forwarded []string) error {
+	if len(forwarded) > 0 {
+		a.port, a.protocol, a.host = false, false, false
+	}
+
+	for _, part := range forwarded {
+		switch strings.TrimSpace(part) {
+		case "":
+			continue
+		case "port":
+			a.port = true
+		case "proto":
+			a.protocol = true
+		case "host":
+			a.host = true
+		default:
+			return fmt.Errorf("invalid X-Forwarded-* name (%s)", part)
+		}
+	}
+	return nil
+}
diff --git a/main.go b/main.go
index ae0d2096..aab6ee39 100644
--- a/main.go
+++ b/main.go
@@ -69,18 +69,20 @@ func realmain(ctx context.Context, arguments []string) int {
 	}
 	var flags globalFlags
 
+	defaultSettings := config.NewDefaultSettings()
+
 	set := flag.NewFlagSet("global options", flag.ContinueOnError)
 	set.BoolVar(&flags.DebugEndpoint, "pprof", false, "-pprof")
-	set.IntVar(&flags.DebugPort, "pprof-port", config.DefaultSettings.PProfPort, "-pprof-port 1234")
+	set.IntVar(&flags.DebugPort, "pprof-port", defaultSettings.PProfPort, "-pprof-port 1234")
 	set.Var(&filesList, "f", "-f /path/to/couper.hcl ...")
 	set.Var(&filesList, "d", "-d /path/to/couper.d/ ...")
 	set.StringVar(&flags.Environment, "e", "", "-e stage")
 	set.BoolVar(&flags.FileWatch, "watch", false, "-watch")
 	set.DurationVar(&flags.FileWatchRetryDelay, "watch-retry-delay", time.Millisecond*500, "-watch-retry-delay 1s")
 	set.IntVar(&flags.FileWatchRetries, "watch-retries", 5, "-watch-retries 10")
-	set.StringVar(&flags.LogFormat, "log-format", config.DefaultSettings.LogFormat, "-log-format=json")
-	set.StringVar(&flags.LogLevel, "log-level", config.DefaultSettings.LogLevel, "-log-level info")
-	set.BoolVar(&flags.LogPretty, "log-pretty", config.DefaultSettings.LogPretty, "-log-pretty")
+	set.StringVar(&flags.LogFormat, "log-format", defaultSettings.LogFormat, "-log-format=json")
+	set.StringVar(&flags.LogLevel, "log-level", defaultSettings.LogLevel, "-log-level info")
+	set.BoolVar(&flags.LogPretty, "log-pretty", defaultSettings.LogPretty, "-log-pretty")
 
 	if len(args) == 0 || command.NewCommand(ctx, args[0]) == nil {
 		command.Synopsis()
@@ -145,13 +147,13 @@ func realmain(ctx context.Context, arguments []string) int {
 
 	// The file gets initialized with the default settings, flag args are preferred over file settings.
 	// Only override file settings if the flag value differ from the default.
-	if flags.LogFormat != config.DefaultSettings.LogFormat {
+	if flags.LogFormat != defaultSettings.LogFormat {
 		confFile.Settings.LogFormat = flags.LogFormat
 	}
-	if flags.LogLevel != config.DefaultSettings.LogLevel {
+	if flags.LogLevel != defaultSettings.LogLevel {
 		confFile.Settings.LogLevel = flags.LogLevel
 	}
-	if flags.LogPretty != config.DefaultSettings.LogPretty {
+	if flags.LogPretty != defaultSettings.LogPretty {
 		confFile.Settings.LogPretty = flags.LogPretty
 	}
 	logger := newLogger(confFile.Settings.LogFormat, confFile.Settings.LogLevel, confFile.Settings.LogPretty)
@@ -266,7 +268,7 @@ func realmain(ctx context.Context, arguments []string) int {
 }
 
 // newLogger creates a log instance with the configured formatter.
-// Since the format option may required to be correct in early states
+// Since the format option may require to be correct in early states
 // we parse the env configuration on every call.
 func newLogger(format, level string, pretty bool) *logrus.Entry {
 	logger := logrus.New()
diff --git a/telemetry/handler/wrapped.go b/telemetry/handler/wrapped.go
index cb767709..ca202309 100644
--- a/telemetry/handler/wrapped.go
+++ b/telemetry/handler/wrapped.go
@@ -16,7 +16,7 @@ import (
 func NewWrappedHandler(log *logrus.Entry, handler http.Handler) http.Handler {
 	accessLog := logging.NewAccessLog(nil, log)
 
-	uidHandler := middleware.NewUIDHandler(&config.DefaultSettings, "")(handler)
+	uidHandler := middleware.NewUIDHandler(config.NewDefaultSettings(), "")(handler)
 	logHandler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
 		ctx := context.WithValue(req.Context(), request.LogDebugLevel, true)
 		ctx = context.WithValue(ctx, request.StartTime, time.Now())


Test patch:
diff --git a/command/run_test.go b/command/run_test.go
index aeefe0c5..e53f893a 100644
--- a/command/run_test.go
+++ b/command/run_test.go
@@ -32,7 +32,7 @@ func TestNewRun(t *testing.T) {
 	log, hook := logrustest.NewNullLogger()
 	//log.Out = os.Stdout
 
-	defaultSettings := config.DefaultSettings
+	defaultSettings := config.NewDefaultSettings()
 
 	tests := []struct {
 		name     string
@@ -41,10 +41,9 @@ func TestNewRun(t *testing.T) {
 		envs     []string
 		settings *config.Settings
 	}{
-		{"defaults from file", "01_defaults.hcl", nil, nil, &defaultSettings},
+		{"defaults from file", "01_defaults.hcl", nil, nil, defaultSettings},
 		{"overrides from file", "02_changed_defaults.hcl", nil, nil, &config.Settings{
 			AcceptForwarded:          &config.AcceptForwarded{},
-			AcceptForwardedURL:       []string{},
 			DefaultPort:              9090,
 			HealthPath:               "/status/health",
 			LogFormat:                defaultSettings.LogFormat,
@@ -63,7 +62,6 @@ func TestNewRun(t *testing.T) {
 		}},
 		{"defaults with flag port", "01_defaults.hcl", Args{"-p", "9876"}, nil, &config.Settings{
 			AcceptForwarded:          &config.AcceptForwarded{},
-			AcceptForwardedURL:       []string{},
 			DefaultPort:              9876,
 			HealthPath:               defaultSettings.HealthPath,
 			LogFormat:                defaultSettings.LogFormat,
@@ -80,7 +78,6 @@ func TestNewRun(t *testing.T) {
 		}},
 		{"defaults with flag and env port", "01_defaults.hcl", Args{"-p", "9876"}, []string{"COUPER_DEFAULT_PORT=4561"}, &config.Settings{
 			AcceptForwarded:          &config.AcceptForwarded{},
-			AcceptForwardedURL:       []string{},
 			DefaultPort:              4561,
 			HealthPath:               defaultSettings.HealthPath,
 			LogFormat:                defaultSettings.LogFormat,
@@ -100,6 +97,13 @@ func TestNewRun(t *testing.T) {
 	defer shutdown()
 	for _, tt := range tests {
 		t.Run(tt.name, func(subT *testing.T) {
+			resultSettings := make(chan *config.Settings, 1)
+			RunCmdConfigTestCallback = func(s *config.Settings) {
+				resultSettings <- s
+				close(resultSettings)
+			}
+			defer func() { RunCmdConfigTestCallback = nil }()
+
 			runCmd := NewRun(ctx)
 			if runCmd == nil {
 				subT.Error("create run cmd failed")
@@ -131,15 +135,14 @@ func TestNewRun(t *testing.T) {
 			}()
 			test.WaitForOpenPort(port)
 
-			runCmd.settingsMu.Lock()
-			if !reflect.DeepEqual(couperFile.Settings, tt.settings) {
-				subT.Errorf("Settings differ: %s:\nwant:\t%#v\ngot:\t%#v\n", tt.name, tt.settings, couperFile.Settings)
+			result := <-resultSettings
+			if !reflect.DeepEqual(result, tt.settings) {
+				subT.Errorf("Settings differ: %s:\nwant:\t%#v\ngot:\t%#v\n", tt.name, tt.settings, result)
 			}
-			runCmd.settingsMu.Unlock()
 
 			hook.Reset()
 
-			res, err := test.NewHTTPClient().Get("http://localhost:" + strconv.Itoa(couperFile.Settings.DefaultPort) + couperFile.Settings.HealthPath)
+			res, err := test.NewHTTPClient().Get("http://localhost:" + strconv.Itoa(result.DefaultPort) + result.HealthPath)
 			if err != nil {
 				subT.Error(err)
 			}
@@ -150,7 +153,7 @@ func TestNewRun(t *testing.T) {
 
 			uid := hook.LastEntry().Data["uid"].(string)
 			xidLen := len(xid.New().String())
-			if couperFile.Settings.RequestIDFormat == "uuid4" {
+			if result.RequestIDFormat == "uuid4" {
 				if len(uid) <= xidLen {
 					subT.Errorf("expected uuid4 format, got: %s", uid)
 				}
@@ -193,6 +196,13 @@ func TestAcceptForwarded(t *testing.T) {
 			caseCtx, caseCancel := context.WithCancel(ctx)
 			defer caseCancel()
 
+			resultSettings := make(chan *config.Settings, 1)
+			RunCmdConfigTestCallback = func(s *config.Settings) {
+				resultSettings <- s
+				close(resultSettings)
+			}
+			defer func() { RunCmdConfigTestCallback = nil }()
+
 			runCmd := NewRun(caseCtx)
 			if runCmd == nil {
 				t.Error("create run cmd failed")
@@ -211,22 +221,24 @@ func TestAcceptForwarded(t *testing.T) {
 				defer env.SetTestOsEnviron(os.Environ)
 			}
 
-			go runCmd.Execute(tc.args, couperFile, log.WithContext(caseCtx))
+			go func(asyncT *testing.T) {
+				err = runCmd.Execute(tc.args, couperFile, log.WithContext(caseCtx))
+				if err != nil {
+					asyncT.Error(err)
+				}
+			}(subT)
 			test.WaitForOpenPort(8080)
 
-			runCmd.settingsMu.Lock()
-
-			if couperFile.Settings.AcceptsForwardedProtocol() != tc.expProto {
-				subT.Errorf("%s: AcceptsForwardedProtocol() differ:\nwant:\t%#v\ngot:\t%#v\n", tc.name, tc.expProto, couperFile.Settings.AcceptsForwardedProtocol())
+			settings := <-resultSettings
+			if settings.AcceptsForwardedProtocol() != tc.expProto {
+				subT.Errorf("%s: AcceptsForwardedProtocol() differ:\nwant:\t%#v\ngot:\t%#v\n", tc.name, tc.expProto, settings.AcceptsForwardedProtocol())
 			}
-			if couperFile.Settings.AcceptsForwardedHost() != tc.expHost {
-				subT.Errorf("%s: AcceptsForwardedHost() differ:\nwant:\t%#v\ngot:\t%#v\n", tc.name, tc.expHost, couperFile.Settings.AcceptsForwardedHost())
+			if settings.AcceptsForwardedHost() != tc.expHost {
+				subT.Errorf("%s: AcceptsForwardedHost() differ:\nwant:\t%#v\ngot:\t%#v\n", tc.name, tc.expHost, settings.AcceptsForwardedHost())
 			}
-			if couperFile.Settings.AcceptsForwardedPort() != tc.expPort {
-				subT.Errorf("%s: AcceptsForwardedPort() differ:\nwant:\t%#v\ngot:\t%#v\n", tc.name, tc.expPort, couperFile.Settings.AcceptsForwardedPort())
+			if settings.AcceptsForwardedPort() != tc.expPort {
+				subT.Errorf("%s: AcceptsForwardedPort() differ:\nwant:\t%#v\ngot:\t%#v\n", tc.name, tc.expPort, settings.AcceptsForwardedPort())
 			}
-
-			runCmd.settingsMu.Unlock()
 		})
 	}
 }
diff --git a/config/runtime/server_internal_test.go b/config/runtime/server_internal_test.go
index 548f07d5..bec7570e 100644
--- a/config/runtime/server_internal_test.go
+++ b/config/runtime/server_internal_test.go
@@ -179,7 +179,7 @@ func TestServer_validatePortHosts(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(subT *testing.T) {
 			tt.args.conf.Context = eval.NewDefaultContext()
-			tt.args.conf.Settings = &config.DefaultSettings
+			tt.args.conf.Settings = config.NewDefaultSettings()
 
 			logger, _ := logrustest.NewNullLogger()
 			log := logger.WithContext(context.Background())
diff --git a/handler/transport/backend_test.go b/handler/transport/backend_test.go
index 0d833340..14e44a09 100644
--- a/handler/transport/backend_test.go
+++ b/handler/transport/backend_test.go
@@ -500,7 +500,7 @@ func TestBackend_HealthCheck(t *testing.T) {
 
 			health, err := config.
 				NewHealthCheck("http://origin:8080/foo", tc.health, &config.Couper{
-					Settings: &config.DefaultSettings,
+					Settings: config.NewDefaultSettings(),
 				})
 			h.Must(err)
 
diff --git a/server/http_telemetry_test.go b/server/http_telemetry_test.go
index c1795fcc..84fe6e55 100644
--- a/server/http_telemetry_test.go
+++ b/server/http_telemetry_test.go
@@ -20,7 +20,7 @@ func TestServeMetrics(t *testing.T) {
 
 	client := test.NewHTTPClient()
 	mreq, err := http.NewRequest(http.MethodGet,
-		fmt.Sprintf("http://localhost:%d/metrics", config.DefaultSettings.TelemetryMetricsPort), nil)
+		fmt.Sprintf("http://localhost:%d/metrics", config.NewDefaultSettings().TelemetryMetricsPort), nil)
 	helper.Must(err)
 
 	paths := []string{"/", "/down", "/notfound"}