418 lines
15 KiB
Go
418 lines
15 KiB
Go
package notmain
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/pem"
|
|
"flag"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/letsencrypt/boulder/cmd"
|
|
"github.com/letsencrypt/boulder/config"
|
|
emailpb "github.com/letsencrypt/boulder/email/proto"
|
|
"github.com/letsencrypt/boulder/features"
|
|
"github.com/letsencrypt/boulder/goodkey"
|
|
"github.com/letsencrypt/boulder/goodkey/sagoodkey"
|
|
bgrpc "github.com/letsencrypt/boulder/grpc"
|
|
"github.com/letsencrypt/boulder/grpc/noncebalancer"
|
|
"github.com/letsencrypt/boulder/issuance"
|
|
"github.com/letsencrypt/boulder/nonce"
|
|
rapb "github.com/letsencrypt/boulder/ra/proto"
|
|
"github.com/letsencrypt/boulder/ratelimits"
|
|
bredis "github.com/letsencrypt/boulder/redis"
|
|
sapb "github.com/letsencrypt/boulder/sa/proto"
|
|
"github.com/letsencrypt/boulder/unpause"
|
|
"github.com/letsencrypt/boulder/web"
|
|
"github.com/letsencrypt/boulder/wfe2"
|
|
)
|
|
|
|
type Config struct {
|
|
WFE struct {
|
|
DebugAddr string `validate:"omitempty,hostname_port"`
|
|
|
|
// ListenAddress is the address:port on which to listen for incoming
|
|
// HTTP requests. Defaults to ":80".
|
|
ListenAddress string `validate:"omitempty,hostname_port"`
|
|
|
|
// TLSListenAddress is the address:port on which to listen for incoming
|
|
// HTTPS requests. If none is provided the WFE will not listen for HTTPS
|
|
// requests.
|
|
TLSListenAddress string `validate:"omitempty,hostname_port"`
|
|
|
|
// Timeout is the per-request overall timeout. This should be slightly
|
|
// lower than the upstream's timeout when making requests to this service.
|
|
Timeout config.Duration `validate:"-"`
|
|
|
|
// ShutdownStopTimeout determines the maximum amount of time to wait
|
|
// for extant request handlers to complete before exiting. It should be
|
|
// greater than Timeout.
|
|
ShutdownStopTimeout config.Duration
|
|
|
|
ServerCertificatePath string `validate:"required_with=TLSListenAddress"`
|
|
ServerKeyPath string `validate:"required_with=TLSListenAddress"`
|
|
|
|
AllowOrigins []string
|
|
|
|
SubscriberAgreementURL string
|
|
|
|
TLS cmd.TLSConfig
|
|
|
|
RAService *cmd.GRPCClientConfig
|
|
SAService *cmd.GRPCClientConfig
|
|
EmailExporter *cmd.GRPCClientConfig
|
|
|
|
// GetNonceService is a gRPC config which contains a single SRV name
|
|
// used to lookup nonce-service instances used exclusively for nonce
|
|
// creation. In a multi-DC deployment this should refer to local
|
|
// nonce-service instances only.
|
|
GetNonceService *cmd.GRPCClientConfig `validate:"required"`
|
|
|
|
// RedeemNonceService is a gRPC config which contains a list of SRV
|
|
// names used to lookup nonce-service instances used exclusively for
|
|
// nonce redemption. In a multi-DC deployment this should contain both
|
|
// local and remote nonce-service instances.
|
|
RedeemNonceService *cmd.GRPCClientConfig `validate:"required"`
|
|
|
|
// NonceHMACKey is a path to a file containing an HMAC key which is a
|
|
// secret used for deriving the prefix of each nonce instance. It should
|
|
// contain 256 bits (32 bytes) of random data to be suitable as an
|
|
// HMAC-SHA256 key (e.g. the output of `openssl rand -hex 32`). In a
|
|
// multi-DC deployment this value should be the same across all
|
|
// boulder-wfe and nonce-service instances.
|
|
NonceHMACKey cmd.HMACKeyConfig `validate:"-"`
|
|
|
|
// Chains is a list of lists of certificate filenames. Each inner list is
|
|
// a chain (starting with the issuing intermediate, followed by one or
|
|
// more additional certificates, up to and including a root) which we are
|
|
// willing to serve. Chains that start with a given intermediate will only
|
|
// be offered for certificates which were issued by the key pair represented
|
|
// by that intermediate. The first chain representing any given issuing
|
|
// key pair will be the default for that issuer, served if the client does
|
|
// not request a specific chain.
|
|
Chains [][]string `validate:"required,min=1,dive,min=2,dive,required"`
|
|
|
|
Features features.Config
|
|
|
|
// DirectoryCAAIdentity is used for the /directory response's "meta"
|
|
// element's "caaIdentities" field. It should match the VA's "issuerDomain"
|
|
// configuration value (this value is the one used to enforce CAA)
|
|
DirectoryCAAIdentity string `validate:"required,fqdn"`
|
|
// DirectoryWebsite is used for the /directory response's "meta" element's
|
|
// "website" field.
|
|
DirectoryWebsite string `validate:"required,url"`
|
|
|
|
// ACMEv2 requests (outside some registration/revocation messages) use a JWS with
|
|
// a KeyID header containing the full account URL. For new accounts this
|
|
// will be a KeyID based on the HTTP request's Host header and the ACMEv2
|
|
// account path. For legacy ACMEv1 accounts we need to whitelist the account
|
|
// ID prefix that legacy accounts would have been using based on the Host
|
|
// header of the WFE1 instance and the legacy 'reg' path component. This
|
|
// will differ in configuration for production and staging.
|
|
LegacyKeyIDPrefix string `validate:"required,url"`
|
|
|
|
// GoodKey is an embedded config stanza for the goodkey library.
|
|
GoodKey goodkey.Config
|
|
|
|
// StaleTimeout determines how old should data be to be accessed via Boulder-specific GET-able APIs
|
|
StaleTimeout config.Duration `validate:"-"`
|
|
|
|
// AuthorizationLifetimeDays duplicates the RA's config of the same name.
|
|
// Deprecated: This field no longer has any effect.
|
|
AuthorizationLifetimeDays int `validate:"-"`
|
|
|
|
// PendingAuthorizationLifetimeDays duplicates the RA's config of the same name.
|
|
// Deprecated: This field no longer has any effect.
|
|
PendingAuthorizationLifetimeDays int `validate:"-"`
|
|
|
|
AccountCache *CacheConfig
|
|
|
|
Limiter struct {
|
|
// Redis contains the configuration necessary to connect to Redis
|
|
// for rate limiting. This field is required to enable rate
|
|
// limiting.
|
|
Redis *bredis.Config `validate:"required_with=Defaults"`
|
|
|
|
// Defaults is a path to a YAML file containing default rate limits.
|
|
// See: ratelimits/README.md for details. This field is required to
|
|
// enable rate limiting. If any individual rate limit is not set,
|
|
// that limit will be disabled. Failed Authorizations limits passed
|
|
// in this file must be identical to those in the RA.
|
|
Defaults string `validate:"required_with=Redis"`
|
|
|
|
// Overrides is a path to a YAML file containing overrides for the
|
|
// default rate limits. See: ratelimits/README.md for details. If
|
|
// this field is not set, all requesters will be subject to the
|
|
// default rate limits. Overrides for the Failed Authorizations
|
|
// overrides passed in this file must be identical to those in the
|
|
// RA.
|
|
Overrides string
|
|
}
|
|
|
|
// CertProfiles is a map of acceptable certificate profile names to
|
|
// descriptions (perhaps including URLs) of those profiles. NewOrder
|
|
// Requests with a profile name not present in this map will be rejected.
|
|
// This field is optional; if unset, no profile names are accepted.
|
|
CertProfiles map[string]string `validate:"omitempty,dive,keys,alphanum,min=1,max=32,endkeys"`
|
|
|
|
Unpause struct {
|
|
// HMACKey signs outgoing JWTs for redemption at the unpause
|
|
// endpoint. This key must match the one configured for all SFEs.
|
|
// This field is required to enable the pausing feature.
|
|
HMACKey cmd.HMACKeyConfig `validate:"required_with=JWTLifetime URL,structonly"`
|
|
|
|
// JWTLifetime is the lifetime of the unpause JWTs generated by the
|
|
// WFE for redemption at the SFE. The minimum value for this field
|
|
// is 336h (14 days). This field is required to enable the pausing
|
|
// feature.
|
|
JWTLifetime config.Duration `validate:"omitempty,required_with=HMACKey URL,min=336h"`
|
|
|
|
// URL is the URL of the Self-Service Frontend (SFE). This is used
|
|
// to build URLs sent to end-users in error messages. This field
|
|
// must be a URL with a scheme of 'https://' This field is required
|
|
// to enable the pausing feature.
|
|
URL string `validate:"omitempty,required_with=HMACKey JWTLifetime,url,startswith=https://,endsnotwith=/"`
|
|
}
|
|
}
|
|
|
|
Syslog cmd.SyslogConfig
|
|
OpenTelemetry cmd.OpenTelemetryConfig
|
|
|
|
// OpenTelemetryHTTPConfig configures tracing on incoming HTTP requests
|
|
OpenTelemetryHTTPConfig cmd.OpenTelemetryHTTPConfig
|
|
}
|
|
|
|
type CacheConfig struct {
|
|
Size int
|
|
TTL config.Duration
|
|
}
|
|
|
|
// loadChain takes a list of filenames containing pem-formatted certificates,
|
|
// and returns a chain representing all of those certificates in order. It
|
|
// ensures that the resulting chain is valid. The final file is expected to be
|
|
// a root certificate, which the chain will be verified against, but which will
|
|
// not be included in the resulting chain.
|
|
func loadChain(certFiles []string) (*issuance.Certificate, []byte, error) {
|
|
certs, err := issuance.LoadChain(certFiles)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Iterate over all certs appending their pem to the buf.
|
|
var buf bytes.Buffer
|
|
for _, cert := range certs {
|
|
buf.Write([]byte("\n"))
|
|
buf.Write(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}))
|
|
}
|
|
|
|
return certs[0], buf.Bytes(), nil
|
|
}
|
|
|
|
func main() {
|
|
listenAddr := flag.String("addr", "", "HTTP listen address override")
|
|
tlsAddr := flag.String("tls-addr", "", "HTTPS listen address override")
|
|
debugAddr := flag.String("debug-addr", "", "Debug server address override")
|
|
configFile := flag.String("config", "", "File path to the configuration file for this service")
|
|
flag.Parse()
|
|
if *configFile == "" {
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
var c Config
|
|
err := cmd.ReadConfigFile(*configFile, &c)
|
|
cmd.FailOnError(err, "Reading JSON config file into config structure")
|
|
|
|
features.Set(c.WFE.Features)
|
|
|
|
if *listenAddr != "" {
|
|
c.WFE.ListenAddress = *listenAddr
|
|
}
|
|
if *tlsAddr != "" {
|
|
c.WFE.TLSListenAddress = *tlsAddr
|
|
}
|
|
if *debugAddr != "" {
|
|
c.WFE.DebugAddr = *debugAddr
|
|
}
|
|
|
|
certChains := map[issuance.NameID][][]byte{}
|
|
issuerCerts := map[issuance.NameID]*issuance.Certificate{}
|
|
for _, files := range c.WFE.Chains {
|
|
issuer, chain, err := loadChain(files)
|
|
cmd.FailOnError(err, "Failed to load chain")
|
|
|
|
id := issuer.NameID()
|
|
certChains[id] = append(certChains[id], chain)
|
|
// This may overwrite a previously-set issuerCert (e.g. if there are two
|
|
// chains for the same issuer, but with different versions of the same
|
|
// same intermediate issued by different roots). This is okay, as the
|
|
// only truly important content here is the public key to verify other
|
|
// certs.
|
|
issuerCerts[id] = issuer
|
|
}
|
|
|
|
stats, logger, oTelShutdown := cmd.StatsAndLogging(c.Syslog, c.OpenTelemetry, c.WFE.DebugAddr)
|
|
logger.Info(cmd.VersionString())
|
|
|
|
clk := cmd.Clock()
|
|
|
|
var unpauseSigner unpause.JWTSigner
|
|
if features.Get().CheckIdentifiersPaused {
|
|
unpauseSigner, err = unpause.NewJWTSigner(c.WFE.Unpause.HMACKey)
|
|
cmd.FailOnError(err, "Failed to create unpause signer from HMACKey")
|
|
}
|
|
|
|
tlsConfig, err := c.WFE.TLS.Load(stats)
|
|
cmd.FailOnError(err, "TLS config")
|
|
|
|
raConn, err := bgrpc.ClientSetup(c.WFE.RAService, tlsConfig, stats, clk)
|
|
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to RA")
|
|
rac := rapb.NewRegistrationAuthorityClient(raConn)
|
|
|
|
saConn, err := bgrpc.ClientSetup(c.WFE.SAService, tlsConfig, stats, clk)
|
|
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA")
|
|
sac := sapb.NewStorageAuthorityReadOnlyClient(saConn)
|
|
|
|
var eec emailpb.ExporterClient
|
|
if c.WFE.EmailExporter != nil {
|
|
emailExporterConn, err := bgrpc.ClientSetup(c.WFE.EmailExporter, tlsConfig, stats, clk)
|
|
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to email-exporter")
|
|
eec = emailpb.NewExporterClient(emailExporterConn)
|
|
}
|
|
|
|
if c.WFE.RedeemNonceService == nil {
|
|
cmd.Fail("'redeemNonceService' must be configured.")
|
|
}
|
|
if c.WFE.GetNonceService == nil {
|
|
cmd.Fail("'getNonceService' must be configured")
|
|
}
|
|
|
|
noncePrefixKey, err := c.WFE.NonceHMACKey.Load()
|
|
cmd.FailOnError(err, "Failed to load nonceHMACKey file")
|
|
|
|
getNonceConn, err := bgrpc.ClientSetup(c.WFE.GetNonceService, tlsConfig, stats, clk)
|
|
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to get nonce service")
|
|
gnc := nonce.NewGetter(getNonceConn)
|
|
|
|
if c.WFE.RedeemNonceService.SRVResolver != noncebalancer.SRVResolverScheme {
|
|
cmd.Fail(fmt.Sprintf(
|
|
"'redeemNonceService.SRVResolver' must be set to %q", noncebalancer.SRVResolverScheme),
|
|
)
|
|
}
|
|
redeemNonceConn, err := bgrpc.ClientSetup(c.WFE.RedeemNonceService, tlsConfig, stats, clk)
|
|
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to redeem nonce service")
|
|
rnc := nonce.NewRedeemer(redeemNonceConn)
|
|
|
|
kp, err := sagoodkey.NewPolicy(&c.WFE.GoodKey, sac.KeyBlocked)
|
|
cmd.FailOnError(err, "Unable to create key policy")
|
|
|
|
if c.WFE.StaleTimeout.Duration == 0 {
|
|
c.WFE.StaleTimeout.Duration = time.Minute * 10
|
|
}
|
|
|
|
var limiter *ratelimits.Limiter
|
|
var txnBuilder *ratelimits.TransactionBuilder
|
|
var limiterRedis *bredis.Ring
|
|
if c.WFE.Limiter.Defaults != "" {
|
|
// Setup rate limiting.
|
|
limiterRedis, err = bredis.NewRingFromConfig(*c.WFE.Limiter.Redis, stats, logger)
|
|
cmd.FailOnError(err, "Failed to create Redis ring")
|
|
|
|
source := ratelimits.NewRedisSource(limiterRedis.Ring, clk, stats)
|
|
limiter, err = ratelimits.NewLimiter(clk, source, stats)
|
|
cmd.FailOnError(err, "Failed to create rate limiter")
|
|
txnBuilder, err = ratelimits.NewTransactionBuilderFromFiles(c.WFE.Limiter.Defaults, c.WFE.Limiter.Overrides)
|
|
cmd.FailOnError(err, "Failed to create rate limits transaction builder")
|
|
}
|
|
|
|
var accountGetter wfe2.AccountGetter
|
|
if c.WFE.AccountCache != nil {
|
|
accountGetter = wfe2.NewAccountCache(sac,
|
|
c.WFE.AccountCache.Size,
|
|
c.WFE.AccountCache.TTL.Duration,
|
|
clk,
|
|
stats)
|
|
} else {
|
|
accountGetter = sac
|
|
}
|
|
wfe, err := wfe2.NewWebFrontEndImpl(
|
|
stats,
|
|
clk,
|
|
kp,
|
|
certChains,
|
|
issuerCerts,
|
|
logger,
|
|
c.WFE.Timeout.Duration,
|
|
c.WFE.StaleTimeout.Duration,
|
|
rac,
|
|
sac,
|
|
eec,
|
|
gnc,
|
|
rnc,
|
|
noncePrefixKey,
|
|
accountGetter,
|
|
limiter,
|
|
txnBuilder,
|
|
c.WFE.CertProfiles,
|
|
unpauseSigner,
|
|
c.WFE.Unpause.JWTLifetime.Duration,
|
|
c.WFE.Unpause.URL,
|
|
)
|
|
cmd.FailOnError(err, "Unable to create WFE")
|
|
|
|
wfe.SubscriberAgreementURL = c.WFE.SubscriberAgreementURL
|
|
wfe.AllowOrigins = c.WFE.AllowOrigins
|
|
wfe.DirectoryCAAIdentity = c.WFE.DirectoryCAAIdentity
|
|
wfe.DirectoryWebsite = c.WFE.DirectoryWebsite
|
|
wfe.LegacyKeyIDPrefix = c.WFE.LegacyKeyIDPrefix
|
|
|
|
logger.Infof("WFE using key policy: %#v", kp)
|
|
|
|
if c.WFE.ListenAddress == "" {
|
|
cmd.Fail("HTTP listen address is not configured")
|
|
}
|
|
|
|
logger.Infof("Server running, listening on %s....", c.WFE.ListenAddress)
|
|
handler := wfe.Handler(stats, c.OpenTelemetryHTTPConfig.Options()...)
|
|
|
|
srv := web.NewServer(c.WFE.ListenAddress, handler, logger)
|
|
go func() {
|
|
err := srv.ListenAndServe()
|
|
if err != nil && err != http.ErrServerClosed {
|
|
cmd.FailOnError(err, "Running HTTP server")
|
|
}
|
|
}()
|
|
|
|
tlsSrv := web.NewServer(c.WFE.TLSListenAddress, handler, logger)
|
|
if tlsSrv.Addr != "" {
|
|
go func() {
|
|
logger.Infof("TLS server listening on %s", tlsSrv.Addr)
|
|
err := tlsSrv.ListenAndServeTLS(c.WFE.ServerCertificatePath, c.WFE.ServerKeyPath)
|
|
if err != nil && err != http.ErrServerClosed {
|
|
cmd.FailOnError(err, "Running TLS server")
|
|
}
|
|
}()
|
|
}
|
|
|
|
// When main is ready to exit (because it has received a shutdown signal),
|
|
// gracefully shutdown the servers. Calling these shutdown functions causes
|
|
// ListenAndServe() and ListenAndServeTLS() to immediately return, then waits
|
|
// for any lingering connection-handling goroutines to finish their work.
|
|
defer func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), c.WFE.ShutdownStopTimeout.Duration)
|
|
defer cancel()
|
|
_ = srv.Shutdown(ctx)
|
|
_ = tlsSrv.Shutdown(ctx)
|
|
limiterRedis.StopLookups()
|
|
oTelShutdown(ctx)
|
|
}()
|
|
|
|
cmd.WaitForSignal()
|
|
}
|
|
|
|
func init() {
|
|
cmd.RegisterCommand("boulder-wfe2", main, &cmd.ConfigValidator{Config: &Config{}})
|
|
}
|