open-vault/internalshared/configutil/listener.go

456 lines
17 KiB
Go

// 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)
}
}