github.com/letsencrypt/boulder@v0.20251208.0/cmd/config.go (about) 1 package cmd 2 3 import ( 4 "crypto/tls" 5 "crypto/x509" 6 "encoding/hex" 7 "errors" 8 "fmt" 9 "net" 10 "os" 11 "strings" 12 13 "github.com/prometheus/client_golang/prometheus" 14 "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 15 "google.golang.org/grpc/resolver" 16 17 "github.com/letsencrypt/boulder/config" 18 "github.com/letsencrypt/boulder/core" 19 "github.com/letsencrypt/boulder/identifier" 20 ) 21 22 // PasswordConfig contains a path to a file containing a password. 23 type PasswordConfig struct { 24 PasswordFile string `validate:"required"` 25 } 26 27 // Pass returns a password, extracted from the PasswordConfig's PasswordFile 28 func (pc *PasswordConfig) Pass() (string, error) { 29 // Make PasswordConfigs optional, for backwards compatibility. 30 if pc.PasswordFile == "" { 31 return "", nil 32 } 33 contents, err := os.ReadFile(pc.PasswordFile) 34 if err != nil { 35 return "", err 36 } 37 return strings.TrimRight(string(contents), "\n"), nil 38 } 39 40 // ServiceConfig contains config items that are common to all our services, to 41 // be embedded in other config structs. 42 type ServiceConfig struct { 43 // DebugAddr is the address to run the /debug handlers on. 44 DebugAddr string `validate:"omitempty,hostname_port"` 45 GRPC *GRPCServerConfig 46 TLS TLSConfig 47 48 // HealthCheckInterval is the duration between deep health checks of the 49 // service. Defaults to 5 seconds. 50 HealthCheckInterval config.Duration `validate:"-"` 51 } 52 53 // DBConfig defines how to connect to a database. The connect string is 54 // stored in a file separate from the config, because it can contain a password, 55 // which we want to keep out of configs. 56 type DBConfig struct { 57 // A file containing a connect URL for the DB. 58 DBConnectFile string `validate:"required"` 59 60 // MaxOpenConns sets the maximum number of open connections to the 61 // database. If MaxIdleConns is greater than 0 and MaxOpenConns is 62 // less than MaxIdleConns, then MaxIdleConns will be reduced to 63 // match the new MaxOpenConns limit. If n < 0, then there is no 64 // limit on the number of open connections. 65 MaxOpenConns int `validate:"min=-1"` 66 67 // MaxIdleConns sets the maximum number of connections in the idle 68 // connection pool. If MaxOpenConns is greater than 0 but less than 69 // MaxIdleConns, then MaxIdleConns will be reduced to match the 70 // MaxOpenConns limit. If n < 0, no idle connections are retained. 71 MaxIdleConns int `validate:"min=-1"` 72 73 // ConnMaxLifetime sets the maximum amount of time a connection may 74 // be reused. Expired connections may be closed lazily before reuse. 75 // If d < 0, connections are not closed due to a connection's age. 76 ConnMaxLifetime config.Duration `validate:"-"` 77 78 // ConnMaxIdleTime sets the maximum amount of time a connection may 79 // be idle. Expired connections may be closed lazily before reuse. 80 // If d < 0, connections are not closed due to a connection's idle 81 // time. 82 ConnMaxIdleTime config.Duration `validate:"-"` 83 } 84 85 // URL returns the DBConnect URL represented by this DBConfig object, loading it 86 // from the file on disk. Leading and trailing whitespace is stripped. 87 func (d *DBConfig) URL() (string, error) { 88 url, err := os.ReadFile(d.DBConnectFile) 89 return strings.TrimSpace(string(url)), err 90 } 91 92 // PAConfig specifies how a policy authority should connect to its 93 // database, what policies it should enforce, and what challenges 94 // it should offer. 95 type PAConfig struct { 96 DBConfig `validate:"-"` 97 Challenges map[core.AcmeChallenge]bool `validate:"omitempty,dive,keys,oneof=http-01 dns-01 tls-alpn-01 dns-account-01,endkeys"` 98 Identifiers map[identifier.IdentifierType]bool `validate:"omitempty,dive,keys,oneof=dns ip,endkeys"` 99 } 100 101 // CheckChallenges checks whether the list of challenges in the PA config 102 // actually contains valid challenge names 103 func (pc PAConfig) CheckChallenges() error { 104 if len(pc.Challenges) == 0 { 105 return errors.New("empty challenges map in the Policy Authority config is not allowed") 106 } 107 for c := range pc.Challenges { 108 if !c.IsValid() { 109 return fmt.Errorf("invalid challenge in PA config: %s", c) 110 } 111 } 112 return nil 113 } 114 115 // CheckIdentifiers checks whether the list of identifiers in the PA config 116 // actually contains valid identifier type names 117 func (pc PAConfig) CheckIdentifiers() error { 118 for i := range pc.Identifiers { 119 if !i.IsValid() { 120 return fmt.Errorf("invalid identifier type in PA config: %s", i) 121 } 122 } 123 return nil 124 } 125 126 // HostnamePolicyConfig specifies a file from which to load a policy regarding 127 // what hostnames to issue for. 128 type HostnamePolicyConfig struct { 129 HostnamePolicyFile string `validate:"required"` 130 } 131 132 // TLSConfig represents certificates and a key for authenticated TLS. 133 type TLSConfig struct { 134 CertFile string `validate:"required"` 135 KeyFile string `validate:"required"` 136 // The CACertFile file may contain any number of root certificates and will 137 // be deduplicated internally. 138 CACertFile string `validate:"required"` 139 } 140 141 // Load reads and parses the certificates and key listed in the TLSConfig, and 142 // returns a *tls.Config suitable for either client or server use. The 143 // CACertFile file may contain any number of root certificates and will be 144 // deduplicated internally. Prometheus metrics for various certificate fields 145 // will be exported. 146 func (t *TLSConfig) Load(scope prometheus.Registerer) (*tls.Config, error) { 147 if t == nil { 148 return nil, fmt.Errorf("nil TLS section in config") 149 } 150 if t.CertFile == "" { 151 return nil, fmt.Errorf("nil CertFile in TLSConfig") 152 } 153 if t.KeyFile == "" { 154 return nil, fmt.Errorf("nil KeyFile in TLSConfig") 155 } 156 if t.CACertFile == "" { 157 return nil, fmt.Errorf("nil CACertFile in TLSConfig") 158 } 159 caCertBytes, err := os.ReadFile(t.CACertFile) 160 if err != nil { 161 return nil, fmt.Errorf("reading CA cert from %q: %s", t.CACertFile, err) 162 } 163 rootCAs := x509.NewCertPool() 164 if ok := rootCAs.AppendCertsFromPEM(caCertBytes); !ok { 165 return nil, fmt.Errorf("parsing CA certs from %s failed", t.CACertFile) 166 } 167 cert, err := tls.LoadX509KeyPair(t.CertFile, t.KeyFile) 168 if err != nil { 169 return nil, fmt.Errorf("loading key pair from %q and %q: %s", 170 t.CertFile, t.KeyFile, err) 171 } 172 173 tlsNotBefore := prometheus.NewGaugeVec( 174 prometheus.GaugeOpts{ 175 Name: "tlsconfig_notbefore_seconds", 176 Help: "TLS certificate NotBefore field expressed as Unix epoch time", 177 }, 178 []string{"serial"}) 179 err = scope.Register(tlsNotBefore) 180 if err != nil { 181 are := prometheus.AlreadyRegisteredError{} 182 if errors.As(err, &are) { 183 tlsNotBefore = are.ExistingCollector.(*prometheus.GaugeVec) 184 } else { 185 return nil, err 186 } 187 } 188 189 tlsNotAfter := prometheus.NewGaugeVec( 190 prometheus.GaugeOpts{ 191 Name: "tlsconfig_notafter_seconds", 192 Help: "TLS certificate NotAfter field expressed as Unix epoch time", 193 }, 194 []string{"serial"}) 195 err = scope.Register(tlsNotAfter) 196 if err != nil { 197 are := prometheus.AlreadyRegisteredError{} 198 if errors.As(err, &are) { 199 tlsNotAfter = are.ExistingCollector.(*prometheus.GaugeVec) 200 } else { 201 return nil, err 202 } 203 } 204 205 leaf, err := x509.ParseCertificate(cert.Certificate[0]) 206 if err != nil { 207 return nil, err 208 } 209 210 serial := leaf.SerialNumber.String() 211 tlsNotBefore.WithLabelValues(serial).Set(float64(leaf.NotBefore.Unix())) 212 tlsNotAfter.WithLabelValues(serial).Set(float64(leaf.NotAfter.Unix())) 213 214 return &tls.Config{ 215 RootCAs: rootCAs, 216 ClientCAs: rootCAs, 217 ClientAuth: tls.RequireAndVerifyClientCert, 218 Certificates: []tls.Certificate{cert}, 219 // Set the only acceptable TLS to v1.3. 220 MinVersion: tls.VersionTLS13, 221 }, nil 222 } 223 224 // SyslogConfig defines the config for syslogging. 225 // 3 means "error", 4 means "warning", 6 is "info" and 7 is "debug". 226 // Configuring a given level causes all messages at that level and below to 227 // be logged. 228 type SyslogConfig struct { 229 // When absent or zero, this causes no logs to be emitted on stdout/stderr. 230 // Errors and warnings will be emitted on stderr if the configured level 231 // allows. 232 StdoutLevel int `validate:"min=-1,max=7"` 233 // When absent or zero, this defaults to logging all messages of level 6 234 // or below. To disable syslog logging entirely, set this to -1. 235 SyslogLevel int `validate:"min=-1,max=7"` 236 } 237 238 // ServiceDomain contains the service and domain name the gRPC or bdns provider 239 // will use to construct a SRV DNS query to lookup backends. 240 type ServiceDomain struct { 241 // Service is the service name to be used for SRV lookups. For example: if 242 // record is 'foo.service.consul', then the Service is 'foo'. 243 Service string `validate:"required"` 244 245 // Domain is the domain name to be used for SRV lookups. For example: if the 246 // record is 'foo.service.consul', then the Domain is 'service.consul'. 247 Domain string `validate:"required"` 248 } 249 250 // GRPCClientConfig contains the information necessary to setup a gRPC client 251 // connection. The following field combinations are allowed: 252 // 253 // ServerAddress, DNSAuthority, [Timeout], [HostOverride] 254 // SRVLookup, DNSAuthority, [Timeout], [HostOverride], [SRVResolver] 255 // SRVLookups, DNSAuthority, [Timeout], [HostOverride], [SRVResolver] 256 type GRPCClientConfig struct { 257 // DNSAuthority is a single <hostname|IPv4|[IPv6]>:<port> of the DNS server 258 // to be used for resolution of gRPC backends. If the address contains a 259 // hostname the gRPC client will resolve it via the system DNS. If the 260 // address contains a port, the client will use it directly, otherwise port 261 // 53 is used. 262 DNSAuthority string `validate:"required_with=SRVLookup SRVLookups,omitempty,ip|hostname|hostname_port"` 263 264 // SRVLookup contains the service and domain name the gRPC client will use 265 // to construct a SRV DNS query to lookup backends. For example: if the 266 // resource record is 'foo.service.consul', then the 'Service' is 'foo' and 267 // the 'Domain' is 'service.consul'. The expected dNSName to be 268 // authenticated in the server certificate would be 'foo.service.consul'. 269 // 270 // Note: The 'proto' field of the SRV record MUST contain 'tcp' and the 271 // 'port' field MUST be a valid port. In a Consul configuration file you 272 // would specify 'foo.service.consul' as: 273 // 274 // services { 275 // id = "some-unique-id-1" 276 // name = "foo" 277 // address = "10.77.77.77" 278 // port = 8080 279 // tags = ["tcp"] 280 // } 281 // services { 282 // id = "some-unique-id-2" 283 // name = "foo" 284 // address = "10.77.77.77" 285 // port = 8180 286 // tags = ["tcp"] 287 // } 288 // 289 // If you've added the above to your Consul configuration file (and reloaded 290 // Consul) then you should be able to resolve the following dig query: 291 // 292 // $ dig @10.77.77.10 -t SRV _foo._tcp.service.consul +short 293 // 1 1 8080 0a585858.addr.dc1.consul. 294 // 1 1 8080 0a4d4d4d.addr.dc1.consul. 295 SRVLookup *ServiceDomain `validate:"required_without_all=SRVLookups ServerAddress"` 296 297 // SRVLookups allows you to pass multiple SRV records to the gRPC client. 298 // The gRPC client will resolves each SRV record and use the results to 299 // construct a list of backends to connect to. For more details, see the 300 // documentation for the SRVLookup field. Note: while you can pass multiple 301 // targets to the gRPC client using this field, all of the targets will use 302 // the same HostOverride and TLS configuration. 303 SRVLookups []*ServiceDomain `validate:"required_without_all=SRVLookup ServerAddress"` 304 305 // SRVResolver is an optional override to indicate that a specific 306 // implementation of the SRV resolver should be used. The default is 'srv' 307 // For more details, see the documentation in: 308 // grpc/internal/resolver/dns/dns_resolver.go. 309 SRVResolver string `validate:"excluded_with=ServerAddress,isdefault|oneof=srv nonce-srv"` 310 311 // ServerAddress is a single <hostname|IPv4|[IPv6]>:<port> or `:<port>` that 312 // the gRPC client will, if necessary, resolve via DNS and then connect to. 313 // If the address provided is 'foo.service.consul:8080' then the dNSName to 314 // be authenticated in the server certificate would be 'foo.service.consul'. 315 // 316 // In a Consul configuration file you would specify 'foo.service.consul' as: 317 // 318 // services { 319 // id = "some-unique-id-1" 320 // name = "foo" 321 // address = "10.77.77.77" 322 // } 323 // services { 324 // id = "some-unique-id-2" 325 // name = "foo" 326 // address = "10.88.88.88" 327 // } 328 // 329 // If you've added the above to your Consul configuration file (and reloaded 330 // Consul) then you should be able to resolve the following dig query: 331 // 332 // $ dig A @10.77.77.10 foo.service.consul +short 333 // 10.77.77.77 334 // 10.88.88.88 335 ServerAddress string `validate:"required_without_all=SRVLookup SRVLookups,omitempty,hostname_port"` 336 337 // HostOverride is an optional override for the dNSName the client will 338 // verify in the certificate presented by the server. 339 HostOverride string `validate:"omitempty,hostname"` 340 Timeout config.Duration 341 342 // NoWaitForReady turns off our (current) default of setting grpc.WaitForReady(true). 343 // This means if all of a GRPC client's backends are down, it will error immediately. 344 // The current default, grpc.WaitForReady(true), means that if all of a GRPC client's 345 // backends are down, it will wait until either one becomes available or the RPC 346 // times out. 347 NoWaitForReady bool 348 } 349 350 // MakeTargetAndHostOverride constructs the target URI that the gRPC client will 351 // connect to and the hostname (only for 'ServerAddress' and 'SRVLookup') that 352 // will be validated during the mTLS handshake. An error is returned if the 353 // provided configuration is invalid. 354 func (c *GRPCClientConfig) MakeTargetAndHostOverride() (string, string, error) { 355 var hostOverride string 356 if c.ServerAddress != "" { 357 if c.SRVLookup != nil { 358 return "", "", errors.New( 359 "both 'serverAddress' and 'SRVLookup' in gRPC client config. Only one should be provided", 360 ) 361 } 362 // Lookup backends using DNS A records. 363 targetHost, _, err := net.SplitHostPort(c.ServerAddress) 364 if err != nil { 365 return "", "", err 366 } 367 368 hostOverride = targetHost 369 if c.HostOverride != "" { 370 hostOverride = c.HostOverride 371 } 372 return fmt.Sprintf("dns://%s/%s", c.DNSAuthority, c.ServerAddress), hostOverride, nil 373 374 } else if c.SRVLookup != nil { 375 if c.DNSAuthority == "" { 376 return "", "", errors.New("field 'dnsAuthority' is required in gRPC client config with SRVLookup") 377 } 378 scheme, err := c.makeSRVScheme() 379 if err != nil { 380 return "", "", err 381 } 382 // Lookup backends using DNS SRV records. 383 targetHost := c.SRVLookup.Service + "." + c.SRVLookup.Domain 384 385 hostOverride = targetHost 386 if c.HostOverride != "" { 387 hostOverride = c.HostOverride 388 } 389 return fmt.Sprintf("%s://%s/%s", scheme, c.DNSAuthority, targetHost), hostOverride, nil 390 391 } else if c.SRVLookups != nil { 392 if c.DNSAuthority == "" { 393 return "", "", errors.New("field 'dnsAuthority' is required in gRPC client config with SRVLookups") 394 } 395 scheme, err := c.makeSRVScheme() 396 if err != nil { 397 return "", "", err 398 } 399 // Lookup backends using multiple DNS SRV records. 400 var targetHosts []string 401 for _, s := range c.SRVLookups { 402 targetHosts = append(targetHosts, s.Service+"."+s.Domain) 403 } 404 if c.HostOverride != "" { 405 hostOverride = c.HostOverride 406 } 407 return fmt.Sprintf("%s://%s/%s", scheme, c.DNSAuthority, strings.Join(targetHosts, ",")), hostOverride, nil 408 409 } else { 410 return "", "", errors.New( 411 "at least one of 'serverAddress', 'SRVLookup', or 'SRVLookups' required in gRPC client config", 412 ) 413 } 414 } 415 416 // makeSRVScheme returns the scheme to use for SRV lookups. If the SRVResolver 417 // field is empty, it returns "srv". Otherwise it checks that the specified 418 // SRVResolver is registered with the gRPC runtime and returns it. 419 func (c *GRPCClientConfig) makeSRVScheme() (string, error) { 420 if c.SRVResolver == "" { 421 return "srv", nil 422 } 423 rb := resolver.Get(c.SRVResolver) 424 if rb == nil { 425 return "", fmt.Errorf("resolver %q is not registered", c.SRVResolver) 426 } 427 return c.SRVResolver, nil 428 } 429 430 // GRPCServerConfig contains the information needed to start a gRPC server. 431 type GRPCServerConfig struct { 432 Address string `json:"address" validate:"omitempty,hostname_port"` 433 // Services is a map of service names to configuration specific to that service. 434 // These service names must match the service names advertised by gRPC itself, 435 // which are identical to the names set in our gRPC .proto files prefixed by 436 // the package names set in those files (e.g. "ca.CertificateAuthority"). 437 Services map[string]*GRPCServiceConfig `json:"services" validate:"required,dive,required"` 438 // MaxConnectionAge specifies how long a connection may live before the server sends a GoAway to the 439 // client. Because gRPC connections re-resolve DNS after a connection close, 440 // this controls how long it takes before a client learns about changes to its 441 // backends. 442 // https://pkg.go.dev/google.golang.org/grpc/keepalive#ServerParameters 443 MaxConnectionAge config.Duration `validate:"required"` 444 } 445 446 // GRPCServiceConfig contains the information needed to configure a gRPC service. 447 type GRPCServiceConfig struct { 448 // ClientNames is the list of accepted gRPC client certificate SANs. 449 // Connections from clients not in this list will be rejected by the 450 // upstream listener, and RPCs from unlisted clients will be denied by the 451 // server interceptor. 452 ClientNames []string `json:"clientNames" validate:"min=1,dive,hostname,required"` 453 } 454 455 // OpenTelemetryConfig configures tracing via OpenTelemetry. 456 // To enable tracing, set a nonzero SampleRatio and configure an Endpoint 457 type OpenTelemetryConfig struct { 458 // Endpoint to connect to with the OTLP protocol over gRPC. 459 // It should be of the form "localhost:4317" 460 // 461 // It always connects over plaintext, and so is only intended to connect 462 // to a local OpenTelemetry collector. This should not be used over an 463 // insecure network. 464 Endpoint string 465 466 // SampleRatio is the ratio of new traces to head sample. 467 // This only affects new traces without a parent with its own sampling 468 // decision, and otherwise use the parent's sampling decision. 469 // 470 // Set to something between 0 and 1, where 1 is sampling all traces. 471 // This is primarily meant as a pressure relief if the Endpoint we connect to 472 // is being overloaded, and we otherwise handle sampling in the collectors. 473 // See otel trace.ParentBased and trace.TraceIDRatioBased for details. 474 SampleRatio float64 475 } 476 477 // OpenTelemetryHTTPConfig configures the otelhttp server tracing. 478 type OpenTelemetryHTTPConfig struct { 479 // TrustIncomingSpans should only be set true if there's a trusted service 480 // connecting to Boulder, such as a load balancer that's tracing-aware. 481 // If false, the default, incoming traces won't be set as the parent. 482 // See otelhttp.WithPublicEndpoint 483 TrustIncomingSpans bool 484 } 485 486 // Options returns the otelhttp options for this configuration. They can be 487 // passed to otelhttp.NewHandler or Boulder's wrapper, measured_http.New. 488 func (c *OpenTelemetryHTTPConfig) Options() []otelhttp.Option { 489 var options []otelhttp.Option 490 if !c.TrustIncomingSpans { 491 options = append(options, otelhttp.WithPublicEndpoint()) 492 } 493 return options 494 } 495 496 // DNSProvider contains the configuration for a DNS provider in the bdns package 497 // which supports dynamic reloading of its backends. 498 type DNSProvider struct { 499 // DNSAuthority is the single <hostname|IPv4|[IPv6]>:<port> of the DNS 500 // server to be used for resolution of DNS backends. If the address contains 501 // a hostname it will be resolved via the system DNS. If the port is left 502 // unspecified it will default to '53'. If this field is left unspecified 503 // the system DNS will be used for resolution of DNS backends. 504 DNSAuthority string `validate:"required,ip|hostname|hostname_port"` 505 506 // SRVLookup contains the service and domain name used to construct a SRV 507 // DNS query to lookup DNS backends. 'Domain' is required. 'Service' is 508 // optional and will be defaulted to 'dns' if left unspecified. 509 // 510 // Usage: If the resource record is 'unbound.service.consul', then the 511 // 'Service' is 'unbound' and the 'Domain' is 'service.consul'. The expected 512 // dNSName to be authenticated in the server certificate would be 513 // 'unbound.service.consul'. The 'proto' field of the SRV record MUST 514 // contain 'udp' and the 'port' field MUST be a valid port. In a Consul 515 // configuration file you would specify 'unbound.service.consul' as: 516 // 517 // services { 518 // id = "unbound-1" // Must be unique 519 // name = "unbound" 520 // address = "10.77.77.77" 521 // port = 8053 522 // tags = ["udp"] 523 // } 524 // 525 // services { 526 // id = "unbound-2" // Must be unique 527 // name = "unbound" 528 // address = "10.77.77.77" 529 // port = 8153 530 // tags = ["udp"] 531 // } 532 // 533 // If you've added the above to your Consul configuration file (and reloaded 534 // Consul) then you should be able to resolve the following dig query: 535 // 536 // $ dig @10.77.77.10 -t SRV _unbound._udp.service.consul +short 537 // 1 1 8053 0a4d4d4d.addr.dc1.consul. 538 // 1 1 8153 0a4d4d4d.addr.dc1.consul. 539 SRVLookup ServiceDomain `validate:"required"` 540 } 541 542 // HMACKeyConfig specifies a path to a file containing a hexadecimal-encoded 543 // HMAC key. The key must represent exactly 256 bits (32 bytes) of random data 544 // to be suitable for use as a 256-bit hashing key (e.g., the output of `openssl 545 // rand -hex 32`). 546 type HMACKeyConfig struct { 547 KeyFile string `validate:"required"` 548 } 549 550 // Load reads the HMAC key from the file, decodes it from hexadecimal, ensures 551 // it represents exactly 256 bits (32 bytes), and returns it as a byte slice. 552 func (hc *HMACKeyConfig) Load() ([]byte, error) { 553 contents, err := os.ReadFile(hc.KeyFile) 554 if err != nil { 555 return nil, err 556 } 557 558 decoded, err := hex.DecodeString(strings.TrimSpace(string(contents))) 559 if err != nil { 560 return nil, fmt.Errorf("invalid hexadecimal encoding: %w", err) 561 } 562 563 if len(decoded) != 32 { 564 return nil, fmt.Errorf( 565 "validating HMAC key, must be exactly 256 bits (32 bytes) after decoding, got %d", 566 len(decoded), 567 ) 568 } 569 return decoded, nil 570 }