// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package configutil import ( "errors" "fmt" "net/textproto" "regexp" "strings" "time" "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/go-secure-stdlib/strutil" "github.com/hashicorp/go-secure-stdlib/tlsutil" "github.com/hashicorp/go-sockaddr" "github.com/hashicorp/go-sockaddr/template" "github.com/hashicorp/hcl" "github.com/hashicorp/hcl/hcl/ast" ) type ListenerTelemetry struct { UnusedKeys UnusedKeyMap `hcl:",unusedKeyPositions"` UnauthenticatedMetricsAccess bool `hcl:"-"` UnauthenticatedMetricsAccessRaw interface{} `hcl:"unauthenticated_metrics_access,alias:UnauthenticatedMetricsAccess"` } type ListenerProfiling struct { UnusedKeys UnusedKeyMap `hcl:",unusedKeyPositions"` UnauthenticatedPProfAccess bool `hcl:"-"` UnauthenticatedPProfAccessRaw interface{} `hcl:"unauthenticated_pprof_access,alias:UnauthenticatedPProfAccessRaw"` } type ListenerInFlightRequestLogging struct { UnusedKeys UnusedKeyMap `hcl:",unusedKeyPositions"` UnauthenticatedInFlightAccess bool `hcl:"-"` UnauthenticatedInFlightAccessRaw interface{} `hcl:"unauthenticated_in_flight_requests_access,alias:unauthenticatedInFlightAccessRaw"` } // Listener is the listener configuration for the server. type Listener struct { UnusedKeys UnusedKeyMap `hcl:",unusedKeyPositions"` RawConfig map[string]interface{} Type string Purpose []string `hcl:"-"` PurposeRaw interface{} `hcl:"purpose"` Role string `hcl:"role"` Address string `hcl:"address"` ClusterAddress string `hcl:"cluster_address"` MaxRequestSize int64 `hcl:"-"` MaxRequestSizeRaw interface{} `hcl:"max_request_size"` MaxRequestDuration time.Duration `hcl:"-"` MaxRequestDurationRaw interface{} `hcl:"max_request_duration"` RequireRequestHeader bool `hcl:"-"` RequireRequestHeaderRaw interface{} `hcl:"require_request_header"` TLSDisable bool `hcl:"-"` TLSDisableRaw interface{} `hcl:"tls_disable"` TLSCertFile string `hcl:"tls_cert_file"` TLSKeyFile string `hcl:"tls_key_file"` TLSMinVersion string `hcl:"tls_min_version"` TLSMaxVersion string `hcl:"tls_max_version"` TLSCipherSuites []uint16 `hcl:"-"` TLSCipherSuitesRaw string `hcl:"tls_cipher_suites"` TLSRequireAndVerifyClientCert bool `hcl:"-"` TLSRequireAndVerifyClientCertRaw interface{} `hcl:"tls_require_and_verify_client_cert"` TLSClientCAFile string `hcl:"tls_client_ca_file"` TLSDisableClientCerts bool `hcl:"-"` TLSDisableClientCertsRaw interface{} `hcl:"tls_disable_client_certs"` HTTPReadTimeout time.Duration `hcl:"-"` HTTPReadTimeoutRaw interface{} `hcl:"http_read_timeout"` HTTPReadHeaderTimeout time.Duration `hcl:"-"` HTTPReadHeaderTimeoutRaw interface{} `hcl:"http_read_header_timeout"` HTTPWriteTimeout time.Duration `hcl:"-"` HTTPWriteTimeoutRaw interface{} `hcl:"http_write_timeout"` HTTPIdleTimeout time.Duration `hcl:"-"` HTTPIdleTimeoutRaw interface{} `hcl:"http_idle_timeout"` ProxyProtocolBehavior string `hcl:"proxy_protocol_behavior"` ProxyProtocolAuthorizedAddrs []*sockaddr.SockAddrMarshaler `hcl:"-"` ProxyProtocolAuthorizedAddrsRaw interface{} `hcl:"proxy_protocol_authorized_addrs,alias:ProxyProtocolAuthorizedAddrs"` XForwardedForAuthorizedAddrs []*sockaddr.SockAddrMarshaler `hcl:"-"` XForwardedForAuthorizedAddrsRaw interface{} `hcl:"x_forwarded_for_authorized_addrs,alias:XForwardedForAuthorizedAddrs"` XForwardedForHopSkips int64 `hcl:"-"` XForwardedForHopSkipsRaw interface{} `hcl:"x_forwarded_for_hop_skips,alias:XForwardedForHopSkips"` XForwardedForRejectNotPresent bool `hcl:"-"` XForwardedForRejectNotPresentRaw interface{} `hcl:"x_forwarded_for_reject_not_present,alias:XForwardedForRejectNotPresent"` XForwardedForRejectNotAuthorized bool `hcl:"-"` XForwardedForRejectNotAuthorizedRaw interface{} `hcl:"x_forwarded_for_reject_not_authorized,alias:XForwardedForRejectNotAuthorized"` SocketMode string `hcl:"socket_mode"` SocketUser string `hcl:"socket_user"` SocketGroup string `hcl:"socket_group"` AgentAPI *AgentAPI `hcl:"agent_api"` ProxyAPI *ProxyAPI `hcl:"proxy_api"` Telemetry ListenerTelemetry `hcl:"telemetry"` Profiling ListenerProfiling `hcl:"profiling"` InFlightRequestLogging ListenerInFlightRequestLogging `hcl:"inflight_requests_logging"` // RandomPort is used only for some testing purposes RandomPort bool `hcl:"-"` CorsEnabledRaw interface{} `hcl:"cors_enabled"` CorsEnabled bool `hcl:"-"` CorsAllowedOrigins []string `hcl:"cors_allowed_origins"` CorsAllowedHeaders []string `hcl:"-"` CorsAllowedHeadersRaw []string `hcl:"cors_allowed_headers,alias:cors_allowed_headers"` // Custom Http response headers CustomResponseHeaders map[string]map[string]string `hcl:"-"` CustomResponseHeadersRaw interface{} `hcl:"custom_response_headers"` } // AgentAPI allows users to select which parts of the Agent API they want enabled. type AgentAPI struct { EnableQuit bool `hcl:"enable_quit"` } // ProxyAPI allows users to select which parts of the Vault Proxy API they want enabled. type ProxyAPI struct { EnableQuit bool `hcl:"enable_quit"` } func (l *Listener) GoString() string { return fmt.Sprintf("*%#v", *l) } func (l *Listener) Validate(path string) []ConfigError { results := append(ValidateUnusedFields(l.UnusedKeys, path), ValidateUnusedFields(l.Telemetry.UnusedKeys, path)...) return append(results, ValidateUnusedFields(l.Profiling.UnusedKeys, path)...) } func ParseListeners(result *SharedConfig, list *ast.ObjectList) error { var err error result.Listeners = make([]*Listener, 0, len(list.Items)) for i, item := range list.Items { var l Listener if err := hcl.DecodeObject(&l, item.Val); err != nil { return multierror.Prefix(err, fmt.Sprintf("listeners.%d:", i)) } if rendered, err := ParseSingleIPTemplate(l.Address); err != nil { return multierror.Prefix(err, fmt.Sprintf("listeners.%d:", i)) } else { l.Address = rendered } if rendered, err := ParseSingleIPTemplate(l.ClusterAddress); err != nil { return multierror.Prefix(err, fmt.Sprintf("listeners.%d:", i)) } else { l.ClusterAddress = rendered } // Hacky way, for now, to get the values we want for sanitizing var m map[string]interface{} if err := hcl.DecodeObject(&m, item.Val); err != nil { return multierror.Prefix(err, fmt.Sprintf("listeners.%d:", i)) } l.RawConfig = m // Base values { switch { case l.Type != "": case len(item.Keys) == 1: l.Type = strings.ToLower(item.Keys[0].Token.Value().(string)) default: return multierror.Prefix(errors.New("listener type must be specified"), fmt.Sprintf("listeners.%d:", i)) } l.Type = strings.ToLower(l.Type) switch l.Type { case "tcp", "unix": result.found(l.Type, l.Type) default: return multierror.Prefix(fmt.Errorf("unsupported listener type %q", l.Type), fmt.Sprintf("listeners.%d:", i)) } if l.PurposeRaw != nil { if l.Purpose, err = parseutil.ParseCommaStringSlice(l.PurposeRaw); err != nil { return multierror.Prefix(fmt.Errorf("unable to parse 'purpose' in listener type %q: %w", l.Type, err), fmt.Sprintf("listeners.%d:", i)) } for i, v := range l.Purpose { l.Purpose[i] = strings.ToLower(v) } l.PurposeRaw = nil } switch l.Role { case "default", "metrics_only", "": result.found(l.Type, l.Type) default: return multierror.Prefix(fmt.Errorf("unsupported listener role %q", l.Role), fmt.Sprintf("listeners.%d:", i)) } } // Request Parameters { if l.MaxRequestSizeRaw != nil { if l.MaxRequestSize, err = parseutil.ParseInt(l.MaxRequestSizeRaw); err != nil { return multierror.Prefix(fmt.Errorf("error parsing max_request_size: %w", err), fmt.Sprintf("listeners.%d", i)) } l.MaxRequestSizeRaw = nil } if l.MaxRequestDurationRaw != nil { if l.MaxRequestDuration, err = parseutil.ParseDurationSecond(l.MaxRequestDurationRaw); err != nil { return multierror.Prefix(fmt.Errorf("error parsing max_request_duration: %w", err), fmt.Sprintf("listeners.%d", i)) } if l.MaxRequestDuration < 0 { return multierror.Prefix(errors.New("max_request_duration cannot be negative"), fmt.Sprintf("listeners.%d", i)) } l.MaxRequestDurationRaw = nil } if l.RequireRequestHeaderRaw != nil { if l.RequireRequestHeader, err = parseutil.ParseBool(l.RequireRequestHeaderRaw); err != nil { return multierror.Prefix(fmt.Errorf("invalid value for require_request_header: %w", err), fmt.Sprintf("listeners.%d", i)) } l.RequireRequestHeaderRaw = nil } } // TLS Parameters { if l.TLSDisableRaw != nil { if l.TLSDisable, err = parseutil.ParseBool(l.TLSDisableRaw); err != nil { return multierror.Prefix(fmt.Errorf("invalid value for tls_disable: %w", err), fmt.Sprintf("listeners.%d", i)) } l.TLSDisableRaw = nil } if l.TLSCipherSuitesRaw != "" { if l.TLSCipherSuites, err = tlsutil.ParseCiphers(l.TLSCipherSuitesRaw); err != nil { return multierror.Prefix(fmt.Errorf("invalid value for tls_cipher_suites: %w", err), fmt.Sprintf("listeners.%d", i)) } } if l.TLSRequireAndVerifyClientCertRaw != nil { if l.TLSRequireAndVerifyClientCert, err = parseutil.ParseBool(l.TLSRequireAndVerifyClientCertRaw); err != nil { return multierror.Prefix(fmt.Errorf("invalid value for tls_require_and_verify_client_cert: %w", err), fmt.Sprintf("listeners.%d", i)) } l.TLSRequireAndVerifyClientCertRaw = nil } if l.TLSDisableClientCertsRaw != nil { if l.TLSDisableClientCerts, err = parseutil.ParseBool(l.TLSDisableClientCertsRaw); err != nil { return multierror.Prefix(fmt.Errorf("invalid value for tls_disable_client_certs: %w", err), fmt.Sprintf("listeners.%d", i)) } l.TLSDisableClientCertsRaw = nil } } // HTTP timeouts { if l.HTTPReadTimeoutRaw != nil { if l.HTTPReadTimeout, err = parseutil.ParseDurationSecond(l.HTTPReadTimeoutRaw); err != nil { return multierror.Prefix(fmt.Errorf("error parsing http_read_timeout: %w", err), fmt.Sprintf("listeners.%d", i)) } l.HTTPReadTimeoutRaw = nil } if l.HTTPReadHeaderTimeoutRaw != nil { if l.HTTPReadHeaderTimeout, err = parseutil.ParseDurationSecond(l.HTTPReadHeaderTimeoutRaw); err != nil { return multierror.Prefix(fmt.Errorf("error parsing http_read_header_timeout: %w", err), fmt.Sprintf("listeners.%d", i)) } l.HTTPReadHeaderTimeoutRaw = nil } if l.HTTPWriteTimeoutRaw != nil { if l.HTTPWriteTimeout, err = parseutil.ParseDurationSecond(l.HTTPWriteTimeoutRaw); err != nil { return multierror.Prefix(fmt.Errorf("error parsing http_write_timeout: %w", err), fmt.Sprintf("listeners.%d", i)) } l.HTTPWriteTimeoutRaw = nil } if l.HTTPIdleTimeoutRaw != nil { if l.HTTPIdleTimeout, err = parseutil.ParseDurationSecond(l.HTTPIdleTimeoutRaw); err != nil { return multierror.Prefix(fmt.Errorf("error parsing http_idle_timeout: %w", err), fmt.Sprintf("listeners.%d", i)) } l.HTTPIdleTimeoutRaw = nil } } // Proxy Protocol config { if l.ProxyProtocolAuthorizedAddrsRaw != nil { if l.ProxyProtocolAuthorizedAddrs, err = parseutil.ParseAddrs(l.ProxyProtocolAuthorizedAddrsRaw); err != nil { return multierror.Prefix(fmt.Errorf("error parsing proxy_protocol_authorized_addrs: %w", err), fmt.Sprintf("listeners.%d", i)) } switch l.ProxyProtocolBehavior { case "allow_authorized", "deny_authorized": if len(l.ProxyProtocolAuthorizedAddrs) == 0 { return multierror.Prefix(errors.New("proxy_protocol_behavior set to allow or deny only authorized addresses but no proxy_protocol_authorized_addrs value"), fmt.Sprintf("listeners.%d", i)) } } l.ProxyProtocolAuthorizedAddrsRaw = nil } } // X-Forwarded-For config { if l.XForwardedForAuthorizedAddrsRaw != nil { if l.XForwardedForAuthorizedAddrs, err = parseutil.ParseAddrs(l.XForwardedForAuthorizedAddrsRaw); err != nil { return multierror.Prefix(fmt.Errorf("error parsing x_forwarded_for_authorized_addrs: %w", err), fmt.Sprintf("listeners.%d", i)) } l.XForwardedForAuthorizedAddrsRaw = nil } if l.XForwardedForHopSkipsRaw != nil { if l.XForwardedForHopSkips, err = parseutil.ParseInt(l.XForwardedForHopSkipsRaw); err != nil { return multierror.Prefix(fmt.Errorf("error parsing x_forwarded_for_hop_skips: %w", err), fmt.Sprintf("listeners.%d", i)) } if l.XForwardedForHopSkips < 0 { return multierror.Prefix(fmt.Errorf("x_forwarded_for_hop_skips cannot be negative but set to %d", l.XForwardedForHopSkips), fmt.Sprintf("listeners.%d", i)) } l.XForwardedForHopSkipsRaw = nil } if l.XForwardedForRejectNotAuthorizedRaw != nil { if l.XForwardedForRejectNotAuthorized, err = parseutil.ParseBool(l.XForwardedForRejectNotAuthorizedRaw); err != nil { return multierror.Prefix(fmt.Errorf("invalid value for x_forwarded_for_reject_not_authorized: %w", err), fmt.Sprintf("listeners.%d", i)) } l.XForwardedForRejectNotAuthorizedRaw = nil } if l.XForwardedForRejectNotPresentRaw != nil { if l.XForwardedForRejectNotPresent, err = parseutil.ParseBool(l.XForwardedForRejectNotPresentRaw); err != nil { return multierror.Prefix(fmt.Errorf("invalid value for x_forwarded_for_reject_not_present: %w", err), fmt.Sprintf("listeners.%d", i)) } l.XForwardedForRejectNotPresentRaw = nil } } // Telemetry { if l.Telemetry.UnauthenticatedMetricsAccessRaw != nil { if l.Telemetry.UnauthenticatedMetricsAccess, err = parseutil.ParseBool(l.Telemetry.UnauthenticatedMetricsAccessRaw); err != nil { return multierror.Prefix(fmt.Errorf("invalid value for telemetry.unauthenticated_metrics_access: %w", err), fmt.Sprintf("listeners.%d", i)) } l.Telemetry.UnauthenticatedMetricsAccessRaw = nil } } // Profiling { if l.Profiling.UnauthenticatedPProfAccessRaw != nil { if l.Profiling.UnauthenticatedPProfAccess, err = parseutil.ParseBool(l.Profiling.UnauthenticatedPProfAccessRaw); err != nil { return multierror.Prefix(fmt.Errorf("invalid value for profiling.unauthenticated_pprof_access: %w", err), fmt.Sprintf("listeners.%d", i)) } l.Profiling.UnauthenticatedPProfAccessRaw = nil } } // InFlight Request logging { if l.InFlightRequestLogging.UnauthenticatedInFlightAccessRaw != nil { if l.InFlightRequestLogging.UnauthenticatedInFlightAccess, err = parseutil.ParseBool(l.InFlightRequestLogging.UnauthenticatedInFlightAccessRaw); err != nil { return multierror.Prefix(fmt.Errorf("invalid value for inflight_requests_logging.unauthenticated_in_flight_requests_access: %w", err), fmt.Sprintf("listeners.%d", i)) } l.InFlightRequestLogging.UnauthenticatedInFlightAccessRaw = "" } } // CORS { if l.CorsEnabledRaw != nil { if l.CorsEnabled, err = parseutil.ParseBool(l.CorsEnabledRaw); err != nil { return multierror.Prefix(fmt.Errorf("invalid value for cors_enabled: %w", err), fmt.Sprintf("listeners.%d", i)) } l.CorsEnabledRaw = nil } if strutil.StrListContains(l.CorsAllowedOrigins, "*") && len(l.CorsAllowedOrigins) > 1 { return multierror.Prefix(errors.New("cors_allowed_origins must only contain a wildcard or only non-wildcard values"), fmt.Sprintf("listeners.%d", i)) } if len(l.CorsAllowedHeadersRaw) > 0 { for _, header := range l.CorsAllowedHeadersRaw { l.CorsAllowedHeaders = append(l.CorsAllowedHeaders, textproto.CanonicalMIMEHeaderKey(header)) } } } // HTTP Headers { // if CustomResponseHeadersRaw is nil, we still need to set the default headers customHeadersMap, err := ParseCustomResponseHeaders(l.CustomResponseHeadersRaw) if err != nil { return multierror.Prefix(fmt.Errorf("failed to parse custom_response_headers: %w", err), fmt.Sprintf("listeners.%d", i)) } l.CustomResponseHeaders = customHeadersMap l.CustomResponseHeadersRaw = nil } result.Listeners = append(result.Listeners, &l) } return nil } // ParseSingleIPTemplate is used as a helper function to parse out a single IP // address from a config parameter. // If the input doesn't appear to contain the 'template' format, // it will return the specified input unchanged. func ParseSingleIPTemplate(ipTmpl string) (string, error) { r := regexp.MustCompile("{{.*?}}") if !r.MatchString(ipTmpl) { return ipTmpl, nil } out, err := template.Parse(ipTmpl) if err != nil { return "", fmt.Errorf("unable to parse address template %q: %v", ipTmpl, err) } ips := strings.Split(out, " ") switch len(ips) { case 0: return "", errors.New("no addresses found, please configure one") case 1: return strings.TrimSpace(ips[0]), nil default: return "", fmt.Errorf("multiple addresses found (%q), please configure one", out) } }