/
main.go
489 lines (416 loc) · 18.3 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
package notmain
import (
"bytes"
"context"
"encoding/pem"
"flag"
"fmt"
"log"
"net/http"
"os"
"time"
"github.com/jmhodges/clock"
"github.com/prometheus/client_golang/prometheus"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/config"
"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"
blog "github.com/letsencrypt/boulder/log"
"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/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 request to the WFE.
Timeout config.Duration `validate:"-"`
ServerCertificatePath string `validate:"required_with=TLSListenAddress"`
ServerKeyPath string `validate:"required_with=TLSListenAddress"`
AllowOrigins []string
ShutdownStopTimeout config.Duration
SubscriberAgreementURL string
TLS cmd.TLSConfig
RAService *cmd.GRPCClientConfig
SAService *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
// RedeemNonceServices contains a map of nonce-service prefixes to
// gRPC configs we want to use to redeem nonces. In a multi-DC deployment
// this should contain all nonce-services from all DCs as we want to be
// able to redeem nonces generated at any DC.
//
// Deprecated: See RedeemNonceService, below.
// TODO (#6610) Remove this after all configs have migrated to
// `RedeemNonceService`.
RedeemNonceServices map[string]cmd.GRPCClientConfig `validate:"required_without=RedeemNonceService,omitempty,min=1,dive"`
// 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_without=RedeemNonceServices"`
// NoncePrefixKey is a secret used for deriving the prefix of each nonce
// instance. It should contain 256 bits 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.
NoncePrefixKey cmd.PasswordConfig `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 defines how long authorizations will be
// considered valid for. The WFE uses this to find the creation date of
// authorizations by subtracing this value from the expiry. It should match
// the value configured in the RA.
AuthorizationLifetimeDays int `validate:"required,min=1,max=397"`
// PendingAuthorizationLifetimeDays defines how long authorizations may be in
// the pending state before expiry. The WFE uses this to find the creation
// date of pending authorizations by subtracting this value from the expiry.
// It should match the value configured in the RA.
PendingAuthorizationLifetimeDays int `validate:"required,min=1,max=29"`
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
}
// MaxNames is the maximum number of subjectAltNames in a single cert.
// The value supplied SHOULD be greater than 0 and no more than 100,
// defaults to 100. These limits are per section 7.1 of our combined
// CP/CPS, under "DV-SSL Subscriber Certificate". The value must match
// the CA and RA configurations.
MaxNames int `validate:"min=0,max=100"`
// CertificateProfileNames is the list of acceptable certificate profile
// names for newOrder requests. Requests with a profile name not in this
// list will be rejected. This field is optional; if unset, no profile
// names are accepted.
CertificateProfileNames []string `validate:"omitempty,dive,alphanum,min=1,max=32"`
}
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 setupWFE(c Config, scope prometheus.Registerer, clk clock.Clock) (rapb.RegistrationAuthorityClient, sapb.StorageAuthorityReadOnlyClient, nonce.Getter, map[string]nonce.Redeemer, nonce.Redeemer, string) {
tlsConfig, err := c.WFE.TLS.Load(scope)
cmd.FailOnError(err, "TLS config")
raConn, err := bgrpc.ClientSetup(c.WFE.RAService, tlsConfig, scope, 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, scope, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA")
sac := sapb.NewStorageAuthorityReadOnlyClient(saConn)
// TODO(#6610) Refactor these checks.
if c.WFE.RedeemNonceService != nil && c.WFE.RedeemNonceServices != nil {
cmd.Fail("Only one of 'redeemNonceService' or 'redeemNonceServices' should be configured.")
}
if c.WFE.RedeemNonceService == nil && c.WFE.RedeemNonceServices == nil {
cmd.Fail("One of 'redeemNonceService' or 'redeemNonceServices' must be configured.")
}
if c.WFE.RedeemNonceService != nil && c.WFE.NoncePrefixKey.PasswordFile == "" {
cmd.Fail("'noncePrefixKey' must be configured if 'redeemNonceService' is configured.")
}
if c.WFE.GetNonceService == nil {
cmd.Fail("'getNonceService' must be configured")
}
var rncKey string
if c.WFE.NoncePrefixKey.PasswordFile != "" {
rncKey, err = c.WFE.NoncePrefixKey.Pass()
cmd.FailOnError(err, "Failed to load noncePrefixKey")
}
getNonceConn, err := bgrpc.ClientSetup(c.WFE.GetNonceService, tlsConfig, scope, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to get nonce service")
gnc := nonce.NewGetter(getNonceConn)
var rnc nonce.Redeemer
var npm map[string]nonce.Redeemer
if c.WFE.RedeemNonceService != nil {
// Dispatch nonce redemption RPCs dynamically.
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, scope, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to redeem nonce service")
rnc = nonce.NewRedeemer(redeemNonceConn)
} else {
// Dispatch nonce redpemption RPCs using a static mapping.
//
// TODO(#6610) Remove code below and the `npm` mapping.
npm = make(map[string]nonce.Redeemer)
for prefix, serviceConfig := range c.WFE.RedeemNonceServices {
serviceConfig := serviceConfig
conn, err := bgrpc.ClientSetup(&serviceConfig, tlsConfig, scope, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to redeem nonce service")
npm[prefix] = nonce.NewRedeemer(conn)
}
}
return rac, sac, gnc, npm, rnc, rncKey
}
type errorWriter struct {
blog.Logger
}
func (ew errorWriter) Write(p []byte) (n int, err error) {
// log.Logger will append a newline to all messages before calling
// Write. Our log checksum checker doesn't like newlines, because
// syslog will strip them out so the calculated checksums will
// differ. So that we don't hit this corner case for every line
// logged from inside net/http.Server we strip the newline before
// we get to the checksum generator.
p = bytes.TrimRight(p, "\n")
ew.Logger.Err(fmt.Sprintf("net/http.Server: %s", string(p)))
return
}
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
}
maxNames := c.WFE.MaxNames
if maxNames == 0 {
// Default to 100 names per cert.
maxNames = 100
}
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()
rac, sac, gnc, npm, rnc, npKey := setupWFE(c, stats, clk)
kp, err := sagoodkey.NewKeyPolicy(&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
}
// Baseline Requirements v1.8.1 section 4.2.1: "any reused data, document,
// or completed validation MUST be obtained no more than 398 days prior
// to issuing the Certificate". If unconfigured or the configured value is
// greater than 397 days, bail out.
if c.WFE.AuthorizationLifetimeDays <= 0 || c.WFE.AuthorizationLifetimeDays > 397 {
cmd.Fail("authorizationLifetimeDays value must be greater than 0 and less than 398")
}
authorizationLifetime := time.Duration(c.WFE.AuthorizationLifetimeDays) * 24 * time.Hour
// The Baseline Requirements v1.8.1 state that validation tokens "MUST
// NOT be used for more than 30 days from its creation". If unconfigured
// or the configured value pendingAuthorizationLifetimeDays is greater
// than 29 days, bail out.
if c.WFE.PendingAuthorizationLifetimeDays <= 0 || c.WFE.PendingAuthorizationLifetimeDays > 29 {
cmd.Fail("pendingAuthorizationLifetimeDays value must be greater than 0 and less than 30")
}
pendingAuthorizationLifetime := time.Duration(c.WFE.PendingAuthorizationLifetimeDays) * 24 * time.Hour
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.NewTransactionBuilder(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,
authorizationLifetime,
pendingAuthorizationLifetime,
rac,
sac,
gnc,
npm,
rnc,
npKey,
accountGetter,
limiter,
txnBuilder,
maxNames,
c.WFE.CertificateProfileNames,
)
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 := http.Server{
ReadTimeout: 30 * time.Second,
WriteTimeout: 120 * time.Second,
IdleTimeout: 120 * time.Second,
Addr: c.WFE.ListenAddress,
ErrorLog: log.New(errorWriter{logger}, "", 0),
Handler: handler,
}
go func() {
err := srv.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
cmd.FailOnError(err, "Running HTTP server")
}
}()
tlsSrv := http.Server{
ReadTimeout: 30 * time.Second,
WriteTimeout: 120 * time.Second,
IdleTimeout: 120 * time.Second,
Addr: c.WFE.TLSListenAddress,
ErrorLog: log.New(errorWriter{logger}, "", 0),
Handler: handler,
}
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{}})
}