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