github.com/letsencrypt/boulder@v0.20251208.0/cmd/boulder-wfe2/main.go (about)

     1  package notmain
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/pem"
     7  	"flag"
     8  	"fmt"
     9  	"net/http"
    10  	"os"
    11  	"time"
    12  
    13  	"github.com/jmhodges/clock"
    14  
    15  	"github.com/letsencrypt/boulder/cmd"
    16  	"github.com/letsencrypt/boulder/config"
    17  	emailpb "github.com/letsencrypt/boulder/email/proto"
    18  	"github.com/letsencrypt/boulder/features"
    19  	"github.com/letsencrypt/boulder/goodkey"
    20  	"github.com/letsencrypt/boulder/goodkey/sagoodkey"
    21  	bgrpc "github.com/letsencrypt/boulder/grpc"
    22  	"github.com/letsencrypt/boulder/grpc/noncebalancer"
    23  	"github.com/letsencrypt/boulder/issuance"
    24  	"github.com/letsencrypt/boulder/nonce"
    25  	rapb "github.com/letsencrypt/boulder/ra/proto"
    26  	"github.com/letsencrypt/boulder/ratelimits"
    27  	bredis "github.com/letsencrypt/boulder/redis"
    28  	sapb "github.com/letsencrypt/boulder/sa/proto"
    29  	"github.com/letsencrypt/boulder/unpause"
    30  	"github.com/letsencrypt/boulder/web"
    31  	"github.com/letsencrypt/boulder/wfe2"
    32  )
    33  
    34  type Config struct {
    35  	WFE struct {
    36  		DebugAddr string `validate:"omitempty,hostname_port"`
    37  
    38  		// ListenAddress is the address:port on which to listen for incoming
    39  		// HTTP requests. Defaults to ":80".
    40  		ListenAddress string `validate:"omitempty,hostname_port"`
    41  
    42  		// TLSListenAddress is the address:port on which to listen for incoming
    43  		// HTTPS requests. If none is provided the WFE will not listen for HTTPS
    44  		// requests.
    45  		TLSListenAddress string `validate:"omitempty,hostname_port"`
    46  
    47  		// Timeout is the per-request overall timeout. This should be slightly
    48  		// lower than the upstream's timeout when making requests to this service.
    49  		Timeout config.Duration `validate:"-"`
    50  
    51  		// ShutdownStopTimeout determines the maximum amount of time to wait
    52  		// for extant request handlers to complete before exiting. It should be
    53  		// greater than Timeout.
    54  		ShutdownStopTimeout config.Duration
    55  
    56  		ServerCertificatePath string `validate:"required_with=TLSListenAddress"`
    57  		ServerKeyPath         string `validate:"required_with=TLSListenAddress"`
    58  
    59  		AllowOrigins []string
    60  
    61  		SubscriberAgreementURL string
    62  
    63  		TLS cmd.TLSConfig
    64  
    65  		RAService     *cmd.GRPCClientConfig
    66  		SAService     *cmd.GRPCClientConfig
    67  		EmailExporter *cmd.GRPCClientConfig
    68  
    69  		// GetNonceService is a gRPC config which contains a single SRV name
    70  		// used to lookup nonce-service instances used exclusively for nonce
    71  		// creation. In a multi-DC deployment this should refer to local
    72  		// nonce-service instances only.
    73  		GetNonceService *cmd.GRPCClientConfig `validate:"required"`
    74  
    75  		// RedeemNonceService is a gRPC config which contains a list of SRV
    76  		// names used to lookup nonce-service instances used exclusively for
    77  		// nonce redemption. In a multi-DC deployment this should contain both
    78  		// local and remote nonce-service instances.
    79  		RedeemNonceService *cmd.GRPCClientConfig `validate:"required"`
    80  
    81  		// NonceHMACKey is a path to a file containing an HMAC key which is a
    82  		// secret used for deriving the prefix of each nonce instance. It should
    83  		// contain 256 bits (32 bytes) of random data to be suitable as an
    84  		// HMAC-SHA256 key (e.g. the output of `openssl rand -hex 32`). In a
    85  		// multi-DC deployment this value should be the same across all
    86  		// boulder-wfe and nonce-service instances.
    87  		NonceHMACKey cmd.HMACKeyConfig `validate:"-"`
    88  
    89  		// Chains is a list of lists of certificate filenames. Each inner list is
    90  		// a chain (starting with the issuing intermediate, followed by one or
    91  		// more additional certificates, up to and including a root) which we are
    92  		// willing to serve. Chains that start with a given intermediate will only
    93  		// be offered for certificates which were issued by the key pair represented
    94  		// by that intermediate. The first chain representing any given issuing
    95  		// key pair will be the default for that issuer, served if the client does
    96  		// not request a specific chain.
    97  		Chains [][]string `validate:"required,min=1,dive,min=2,dive,required"`
    98  
    99  		Features features.Config
   100  
   101  		// DirectoryCAAIdentity is used for the /directory response's "meta"
   102  		// element's "caaIdentities" field. It should match the VA's "issuerDomain"
   103  		// configuration value (this value is the one used to enforce CAA)
   104  		DirectoryCAAIdentity string `validate:"required,fqdn"`
   105  		// DirectoryWebsite is used for the /directory response's "meta" element's
   106  		// "website" field.
   107  		DirectoryWebsite string `validate:"required,url"`
   108  
   109  		// ACMEv2 requests (outside some registration/revocation messages) use a JWS with
   110  		// a KeyID header containing the full account URL. For new accounts this
   111  		// will be a KeyID based on the HTTP request's Host header and the ACMEv2
   112  		// account path. For legacy ACMEv1 accounts we need to whitelist the account
   113  		// ID prefix that legacy accounts would have been using based on the Host
   114  		// header of the WFE1 instance and the legacy 'reg' path component. This
   115  		// will differ in configuration for production and staging.
   116  		LegacyKeyIDPrefix string `validate:"required,url"`
   117  
   118  		// GoodKey is an embedded config stanza for the goodkey library.
   119  		GoodKey goodkey.Config
   120  
   121  		// StaleTimeout determines how old should data be to be accessed via Boulder-specific GET-able APIs
   122  		StaleTimeout config.Duration `validate:"-"`
   123  
   124  		// AuthorizationLifetimeDays duplicates the RA's config of the same name.
   125  		// Deprecated: This field no longer has any effect.
   126  		AuthorizationLifetimeDays int `validate:"-"`
   127  
   128  		// PendingAuthorizationLifetimeDays duplicates the RA's config of the same name.
   129  		// Deprecated: This field no longer has any effect.
   130  		PendingAuthorizationLifetimeDays int `validate:"-"`
   131  
   132  		// MaxContactsPerRegistration limits the number of contact addresses which
   133  		// can be provided in a single NewAccount request. Requests containing more
   134  		// contacts than this are rejected. Default: 10.
   135  		MaxContactsPerRegistration int `validate:"omitempty,min=1"`
   136  
   137  		AccountCache *CacheConfig
   138  
   139  		Limiter struct {
   140  			// Redis contains the configuration necessary to connect to Redis
   141  			// for rate limiting. This field is required to enable rate
   142  			// limiting.
   143  			Redis *bredis.Config `validate:"required_with=Defaults"`
   144  
   145  			// Defaults is a path to a YAML file containing default rate limits.
   146  			// See: ratelimits/README.md for details. This field is required to
   147  			// enable rate limiting. If any individual rate limit is not set,
   148  			// that limit will be disabled. Failed Authorizations limits passed
   149  			// in this file must be identical to those in the RA.
   150  			Defaults string `validate:"required_with=Redis"`
   151  
   152  			// Overrides is a path to a YAML file containing overrides for the
   153  			// default rate limits. See: ratelimits/README.md for details. If
   154  			// neither this field nor OverridesFromDB is set, all requesters
   155  			// will be subject to the default rate limits. Overrides for the
   156  			// Failed Authorizations overrides passed in this file must be
   157  			// identical to those in the RA.
   158  			Overrides string
   159  
   160  			// OverridesFromDB causes the WFE and RA to retrieve rate limit
   161  			// overrides from the database, instead of from a file.
   162  			OverridesFromDB bool
   163  		}
   164  
   165  		// CertProfiles is a map of acceptable certificate profile names to
   166  		// descriptions (perhaps including URLs) of those profiles. NewOrder
   167  		// Requests with a profile name not present in this map will be rejected.
   168  		// This field is optional; if unset, no profile names are accepted.
   169  		CertProfiles map[string]string `validate:"omitempty,dive,keys,alphanum,min=1,max=32,endkeys"`
   170  
   171  		Unpause struct {
   172  			// HMACKey signs outgoing JWTs for redemption at the unpause
   173  			// endpoint. This key must match the one configured for all SFEs.
   174  			// This field is required to enable the pausing feature.
   175  			HMACKey cmd.HMACKeyConfig `validate:"required_with=JWTLifetime URL,structonly"`
   176  
   177  			// JWTLifetime is the lifetime of the unpause JWTs generated by the
   178  			// WFE for redemption at the SFE. The minimum value for this field
   179  			// is 336h (14 days). This field is required to enable the pausing
   180  			// feature.
   181  			JWTLifetime config.Duration `validate:"omitempty,required_with=HMACKey URL,min=336h"`
   182  
   183  			// URL is the URL of the Self-Service Frontend (SFE). This is used
   184  			// to build URLs sent to end-users in error messages. This field
   185  			// must be a URL with a scheme of 'https://' This field is required
   186  			// to enable the pausing feature.
   187  			URL string `validate:"omitempty,required_with=HMACKey JWTLifetime,url,startswith=https://,endsnotwith=/"`
   188  		}
   189  	}
   190  
   191  	Syslog        cmd.SyslogConfig
   192  	OpenTelemetry cmd.OpenTelemetryConfig
   193  
   194  	// OpenTelemetryHTTPConfig configures tracing on incoming HTTP requests
   195  	OpenTelemetryHTTPConfig cmd.OpenTelemetryHTTPConfig
   196  }
   197  
   198  type CacheConfig struct {
   199  	Size int
   200  	TTL  config.Duration
   201  }
   202  
   203  // loadChain takes a list of filenames containing pem-formatted certificates,
   204  // and returns a chain representing all of those certificates in order. It
   205  // ensures that the resulting chain is valid. The final file is expected to be
   206  // a root certificate, which the chain will be verified against, but which will
   207  // not be included in the resulting chain.
   208  func loadChain(certFiles []string) (*issuance.Certificate, []byte, error) {
   209  	certs, err := issuance.LoadChain(certFiles)
   210  	if err != nil {
   211  		return nil, nil, err
   212  	}
   213  
   214  	// Iterate over all certs appending their pem to the buf.
   215  	var buf bytes.Buffer
   216  	for _, cert := range certs {
   217  		buf.Write([]byte("\n"))
   218  		buf.Write(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}))
   219  	}
   220  
   221  	return certs[0], buf.Bytes(), nil
   222  }
   223  
   224  func main() {
   225  	listenAddr := flag.String("addr", "", "HTTP listen address override")
   226  	tlsAddr := flag.String("tls-addr", "", "HTTPS listen address override")
   227  	debugAddr := flag.String("debug-addr", "", "Debug server address override")
   228  	configFile := flag.String("config", "", "File path to the configuration file for this service")
   229  	flag.Parse()
   230  	if *configFile == "" {
   231  		flag.Usage()
   232  		os.Exit(1)
   233  	}
   234  
   235  	var c Config
   236  	err := cmd.ReadConfigFile(*configFile, &c)
   237  	cmd.FailOnError(err, "Reading JSON config file into config structure")
   238  
   239  	features.Set(c.WFE.Features)
   240  
   241  	if *listenAddr != "" {
   242  		c.WFE.ListenAddress = *listenAddr
   243  	}
   244  	if *tlsAddr != "" {
   245  		c.WFE.TLSListenAddress = *tlsAddr
   246  	}
   247  	if *debugAddr != "" {
   248  		c.WFE.DebugAddr = *debugAddr
   249  	}
   250  
   251  	certChains := map[issuance.NameID][][]byte{}
   252  	issuerCerts := map[issuance.NameID]*issuance.Certificate{}
   253  	for _, files := range c.WFE.Chains {
   254  		issuer, chain, err := loadChain(files)
   255  		cmd.FailOnError(err, "Failed to load chain")
   256  
   257  		id := issuer.NameID()
   258  		certChains[id] = append(certChains[id], chain)
   259  		// This may overwrite a previously-set issuerCert (e.g. if there are two
   260  		// chains for the same issuer, but with different versions of the same
   261  		// same intermediate issued by different roots). This is okay, as the
   262  		// only truly important content here is the public key to verify other
   263  		// certs.
   264  		issuerCerts[id] = issuer
   265  	}
   266  
   267  	stats, logger, oTelShutdown := cmd.StatsAndLogging(c.Syslog, c.OpenTelemetry, c.WFE.DebugAddr)
   268  	logger.Info(cmd.VersionString())
   269  
   270  	clk := clock.New()
   271  
   272  	var unpauseSigner unpause.JWTSigner
   273  	if features.Get().CheckIdentifiersPaused {
   274  		unpauseSigner, err = unpause.NewJWTSigner(c.WFE.Unpause.HMACKey)
   275  		cmd.FailOnError(err, "Failed to create unpause signer from HMACKey")
   276  	}
   277  
   278  	tlsConfig, err := c.WFE.TLS.Load(stats)
   279  	cmd.FailOnError(err, "TLS config")
   280  
   281  	raConn, err := bgrpc.ClientSetup(c.WFE.RAService, tlsConfig, stats, clk)
   282  	cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to RA")
   283  	rac := rapb.NewRegistrationAuthorityClient(raConn)
   284  
   285  	saConn, err := bgrpc.ClientSetup(c.WFE.SAService, tlsConfig, stats, clk)
   286  	cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA")
   287  	sac := sapb.NewStorageAuthorityReadOnlyClient(saConn)
   288  
   289  	var eec emailpb.ExporterClient
   290  	if c.WFE.EmailExporter != nil {
   291  		emailExporterConn, err := bgrpc.ClientSetup(c.WFE.EmailExporter, tlsConfig, stats, clk)
   292  		cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to email-exporter")
   293  		eec = emailpb.NewExporterClient(emailExporterConn)
   294  	}
   295  
   296  	if c.WFE.RedeemNonceService == nil {
   297  		cmd.Fail("'redeemNonceService' must be configured.")
   298  	}
   299  	if c.WFE.GetNonceService == nil {
   300  		cmd.Fail("'getNonceService' must be configured")
   301  	}
   302  
   303  	noncePrefixKey, err := c.WFE.NonceHMACKey.Load()
   304  	cmd.FailOnError(err, "Failed to load nonceHMACKey file")
   305  
   306  	getNonceConn, err := bgrpc.ClientSetup(c.WFE.GetNonceService, tlsConfig, stats, clk)
   307  	cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to get nonce service")
   308  	gnc := nonce.NewGetter(getNonceConn)
   309  
   310  	if c.WFE.RedeemNonceService.SRVResolver != noncebalancer.SRVResolverScheme {
   311  		cmd.Fail(fmt.Sprintf(
   312  			"'redeemNonceService.SRVResolver' must be set to %q", noncebalancer.SRVResolverScheme),
   313  		)
   314  	}
   315  	redeemNonceConn, err := bgrpc.ClientSetup(c.WFE.RedeemNonceService, tlsConfig, stats, clk)
   316  	cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to redeem nonce service")
   317  	rnc := nonce.NewRedeemer(redeemNonceConn)
   318  
   319  	kp, err := sagoodkey.NewPolicy(&c.WFE.GoodKey, sac.KeyBlocked)
   320  	cmd.FailOnError(err, "Unable to create key policy")
   321  
   322  	if c.WFE.StaleTimeout.Duration == 0 {
   323  		c.WFE.StaleTimeout.Duration = time.Minute * 10
   324  	}
   325  
   326  	if c.WFE.MaxContactsPerRegistration == 0 {
   327  		c.WFE.MaxContactsPerRegistration = 10
   328  	}
   329  
   330  	var limiter *ratelimits.Limiter
   331  	var txnBuilder *ratelimits.TransactionBuilder
   332  	var limiterRedis *bredis.Ring
   333  	overridesRefresherShutdown := func() {}
   334  	if c.WFE.Limiter.Defaults != "" {
   335  		// Setup rate limiting.
   336  		limiterRedis, err = bredis.NewRingFromConfig(*c.WFE.Limiter.Redis, stats, logger)
   337  		cmd.FailOnError(err, "Failed to create Redis ring")
   338  
   339  		source := ratelimits.NewRedisSource(limiterRedis.Ring, clk, stats)
   340  		limiter, err = ratelimits.NewLimiter(clk, source, stats)
   341  		cmd.FailOnError(err, "Failed to create rate limiter")
   342  		if c.WFE.Limiter.OverridesFromDB {
   343  			if c.WFE.Limiter.Overrides != "" {
   344  				cmd.Fail("OverridesFromDB and an overrides file were both defined, but are mutually exclusive")
   345  			}
   346  			txnBuilder, err = ratelimits.NewTransactionBuilderFromDatabase(c.WFE.Limiter.Defaults, sac.GetEnabledRateLimitOverrides, stats, logger)
   347  		} else {
   348  			txnBuilder, err = ratelimits.NewTransactionBuilderFromFiles(c.WFE.Limiter.Defaults, c.WFE.Limiter.Overrides, stats, logger)
   349  		}
   350  		cmd.FailOnError(err, "Failed to create rate limits transaction builder")
   351  
   352  		// The 30 minute period here must be kept in sync with the promise
   353  		// (successCommentBody) made to requesters in sfe/overridesimporter.go
   354  		overridesRefresherShutdown = txnBuilder.NewRefresher(30 * time.Minute)
   355  	}
   356  
   357  	var accountGetter wfe2.AccountGetter
   358  	if c.WFE.AccountCache != nil {
   359  		accountGetter = wfe2.NewAccountCache(sac,
   360  			c.WFE.AccountCache.Size,
   361  			c.WFE.AccountCache.TTL.Duration,
   362  			clk,
   363  			stats)
   364  	} else {
   365  		accountGetter = sac
   366  	}
   367  	wfe, err := wfe2.NewWebFrontEndImpl(
   368  		stats,
   369  		clk,
   370  		kp,
   371  		certChains,
   372  		issuerCerts,
   373  		logger,
   374  		c.WFE.Timeout.Duration,
   375  		c.WFE.StaleTimeout.Duration,
   376  		c.WFE.MaxContactsPerRegistration,
   377  		rac,
   378  		sac,
   379  		eec,
   380  		gnc,
   381  		rnc,
   382  		noncePrefixKey,
   383  		accountGetter,
   384  		limiter,
   385  		txnBuilder,
   386  		c.WFE.CertProfiles,
   387  		unpauseSigner,
   388  		c.WFE.Unpause.JWTLifetime.Duration,
   389  		c.WFE.Unpause.URL,
   390  	)
   391  	cmd.FailOnError(err, "Unable to create WFE")
   392  
   393  	wfe.SubscriberAgreementURL = c.WFE.SubscriberAgreementURL
   394  	wfe.AllowOrigins = c.WFE.AllowOrigins
   395  	wfe.DirectoryCAAIdentity = c.WFE.DirectoryCAAIdentity
   396  	wfe.DirectoryWebsite = c.WFE.DirectoryWebsite
   397  	wfe.LegacyKeyIDPrefix = c.WFE.LegacyKeyIDPrefix
   398  
   399  	if c.WFE.ListenAddress == "" {
   400  		cmd.Fail("HTTP listen address is not configured")
   401  	}
   402  
   403  	logger.Infof("Server running, listening on %s....", c.WFE.ListenAddress)
   404  	handler := wfe.Handler(stats, c.OpenTelemetryHTTPConfig.Options()...)
   405  
   406  	srv := web.NewServer(c.WFE.ListenAddress, handler, logger)
   407  	go func() {
   408  		err := srv.ListenAndServe()
   409  		if err != nil && err != http.ErrServerClosed {
   410  			cmd.FailOnError(err, "Running HTTP server")
   411  		}
   412  	}()
   413  
   414  	tlsSrv := web.NewServer(c.WFE.TLSListenAddress, handler, logger)
   415  	if tlsSrv.Addr != "" {
   416  		go func() {
   417  			logger.Infof("TLS server listening on %s", tlsSrv.Addr)
   418  			err := tlsSrv.ListenAndServeTLS(c.WFE.ServerCertificatePath, c.WFE.ServerKeyPath)
   419  			if err != nil && err != http.ErrServerClosed {
   420  				cmd.FailOnError(err, "Running TLS server")
   421  			}
   422  		}()
   423  	}
   424  
   425  	// When main is ready to exit (because it has received a shutdown signal),
   426  	// gracefully shutdown the servers. Calling these shutdown functions causes
   427  	// ListenAndServe() and ListenAndServeTLS() to immediately return, then waits
   428  	// for any lingering connection-handling goroutines to finish their work.
   429  	defer func() {
   430  		ctx, cancel := context.WithTimeout(context.Background(), c.WFE.ShutdownStopTimeout.Duration)
   431  		defer cancel()
   432  		overridesRefresherShutdown()
   433  		_ = srv.Shutdown(ctx)
   434  		_ = tlsSrv.Shutdown(ctx)
   435  		limiterRedis.StopLookups()
   436  		oTelShutdown(ctx)
   437  	}()
   438  
   439  	cmd.WaitForSignal()
   440  }
   441  
   442  func init() {
   443  	cmd.RegisterCommand("boulder-wfe2", main, &cmd.ConfigValidator{Config: &Config{}})
   444  }