Skip to content

Commit d73125d

Browse files
WFE: Add custom balancer implementation which routes nonce redemption RPCs by prefix (#6618)
Assign nonce prefixes for each nonce-service by taking the first eight characters of the the base64url encoded HMAC-SHA256 hash of the RPC listening address using a provided key. The provided key must be same across all boulder-wfe and nonce-service instances. - Add a custom `grpc-go` load balancer implementation (`nonce`) which can route nonce redemption RPC messages by matching the prefix to the derived prefix of the nonce-service instance which created it. - Modify the RPC client constructor to allow the operator to override the default load balancer implementation (`round_robin`). - Modify the `srv` RPC resolver to accept a comma separated list of targets to be resolved. - Remove unused nonce-service `-prefix` flag. Fixes #6404
1 parent e57c788 commit d73125d

File tree

19 files changed

+757
-131
lines changed

19 files changed

+757
-131
lines changed

cmd/boulder-wfe2/main.go

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ import (
1818
"github.com/letsencrypt/boulder/features"
1919
"github.com/letsencrypt/boulder/goodkey"
2020
bgrpc "github.com/letsencrypt/boulder/grpc"
21+
"github.com/letsencrypt/boulder/grpc/noncebalancer"
2122
"github.com/letsencrypt/boulder/issuance"
2223
blog "github.com/letsencrypt/boulder/log"
23-
noncepb "github.com/letsencrypt/boulder/nonce/proto"
24+
"github.com/letsencrypt/boulder/nonce"
2425
rapb "github.com/letsencrypt/boulder/ra/proto"
2526
sapb "github.com/letsencrypt/boulder/sa/proto"
2627
"github.com/letsencrypt/boulder/wfe2"
@@ -50,16 +51,36 @@ type Config struct {
5051

5152
RAService *cmd.GRPCClientConfig
5253
SAService *cmd.GRPCClientConfig
53-
// GetNonceService contains a gRPC config for any nonce-service instances
54-
// which we want to retrieve nonces from. In a multi-DC deployment this
55-
// should refer to any local nonce-service instances.
54+
55+
// GetNonceService is a gRPC config which contains a single SRV name
56+
// used to lookup nonce-service instances used exclusively for nonce
57+
// creation. In a multi-DC deployment this should refer to local
58+
// nonce-service instances only.
5659
GetNonceService *cmd.GRPCClientConfig
60+
5761
// RedeemNonceServices contains a map of nonce-service prefixes to
5862
// gRPC configs we want to use to redeem nonces. In a multi-DC deployment
5963
// this should contain all nonce-services from all DCs as we want to be
6064
// able to redeem nonces generated at any DC.
65+
//
66+
// DEPRECATED: See RedeemNonceService, below.
67+
// TODO (#6610) Remove this after all configs have migrated to
68+
// `RedeemNonceService`.
6169
RedeemNonceServices map[string]cmd.GRPCClientConfig
6270

71+
// RedeemNonceService is a gRPC config which contains a list of SRV
72+
// names used to lookup nonce-service instances used exclusively for
73+
// nonce redemption. In a multi-DC deployment this should contain both
74+
// local and remote nonce-service instances.
75+
RedeemNonceService *cmd.GRPCClientConfig
76+
77+
// NoncePrefixKey is a secret used for deriving the prefix of each nonce
78+
// instance. It should contain 256 bits of random data to be suitable as
79+
// an HMAC-SHA256 key (e.g. the output of `openssl rand -hex 32`). In a
80+
// multi-DC deployment this value should be the same across all
81+
// boulder-wfe and nonce-service instances.
82+
NoncePrefixKey cmd.PasswordConfig
83+
6384
// CertificateChains maps AIA issuer URLs to certificate filenames.
6485
// Certificates are read into the chain in the order they are defined in the
6586
// slice of filenames.
@@ -280,7 +301,7 @@ func loadChain(certFiles []string) (*issuance.Certificate, []byte, error) {
280301
return certs[0], buf.Bytes(), nil
281302
}
282303

283-
func setupWFE(c Config, scope prometheus.Registerer, clk clock.Clock) (rapb.RegistrationAuthorityClient, sapb.StorageAuthorityReadOnlyClient, noncepb.NonceServiceClient, map[string]noncepb.NonceServiceClient) {
304+
func setupWFE(c Config, scope prometheus.Registerer, clk clock.Clock) (rapb.RegistrationAuthorityClient, sapb.StorageAuthorityReadOnlyClient, nonce.Getter, map[string]nonce.Redeemer, nonce.Redeemer, string) {
284305
tlsConfig, err := c.WFE.TLS.Load()
285306
cmd.FailOnError(err, "TLS config")
286307

@@ -292,21 +313,56 @@ func setupWFE(c Config, scope prometheus.Registerer, clk clock.Clock) (rapb.Regi
292313
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA")
293314
sac := sapb.NewStorageAuthorityReadOnlyClient(saConn)
294315

295-
var rns noncepb.NonceServiceClient
296-
npm := map[string]noncepb.NonceServiceClient{}
297-
if c.WFE.GetNonceService != nil {
298-
rnsConn, err := bgrpc.ClientSetup(c.WFE.GetNonceService, tlsConfig, scope, clk)
299-
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to get nonce service")
300-
rns = noncepb.NewNonceServiceClient(rnsConn)
316+
// TODO(#6610) Refactor these checks.
317+
if c.WFE.RedeemNonceService != nil && c.WFE.RedeemNonceServices != nil {
318+
cmd.Fail("Only one of 'redeemNonceService' or 'redeemNonceServices' should be configured.")
319+
}
320+
if c.WFE.RedeemNonceService == nil && c.WFE.RedeemNonceServices == nil {
321+
cmd.Fail("One of 'redeemNonceService' or 'redeemNonceServices' must be configured.")
322+
}
323+
if c.WFE.RedeemNonceService != nil && c.WFE.NoncePrefixKey.PasswordFile == "" {
324+
cmd.Fail("'noncePrefixKey' must be configured if 'redeemNonceService' is configured.")
325+
}
326+
if c.WFE.GetNonceService == nil {
327+
cmd.Fail("'getNonceService' must be configured")
328+
}
329+
330+
var rncKey string
331+
if c.WFE.NoncePrefixKey.PasswordFile != "" {
332+
rncKey, err = c.WFE.NoncePrefixKey.Pass()
333+
cmd.FailOnError(err, "Failed to load noncePrefixKey")
334+
}
335+
336+
getNonceConn, err := bgrpc.ClientSetup(c.WFE.GetNonceService, tlsConfig, scope, clk)
337+
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to get nonce service")
338+
gnc := nonce.NewGetter(getNonceConn)
339+
340+
var rnc nonce.Redeemer
341+
var npm map[string]nonce.Redeemer
342+
if c.WFE.RedeemNonceService != nil {
343+
// Dispatch nonce redemption RPCs dynamically.
344+
if c.WFE.RedeemNonceService.SRVResolver != noncebalancer.SRVResolverScheme {
345+
cmd.Fail(fmt.Sprintf(
346+
"'redeemNonceService.SRVResolver' must be set to %q", noncebalancer.SRVResolverScheme),
347+
)
348+
}
349+
redeemNonceConn, err := bgrpc.ClientSetup(c.WFE.RedeemNonceService, tlsConfig, scope, clk)
350+
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to redeem nonce service")
351+
rnc = nonce.NewRedeemer(redeemNonceConn)
352+
} else {
353+
// Dispatch nonce redpemption RPCs using a static mapping.
354+
//
355+
// TODO(#6610) Remove code below and the `npm` mapping.
356+
npm = make(map[string]nonce.Redeemer)
301357
for prefix, serviceConfig := range c.WFE.RedeemNonceServices {
302358
serviceConfig := serviceConfig
303359
conn, err := bgrpc.ClientSetup(&serviceConfig, tlsConfig, scope, clk)
304360
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to redeem nonce service")
305-
npm[prefix] = noncepb.NewNonceServiceClient(conn)
361+
npm[prefix] = nonce.NewRedeemer(conn)
306362
}
307363
}
308364

309-
return rac, sac, rns, npm
365+
return rac, sac, gnc, npm, rnc, rncKey
310366
}
311367

312368
type errorWriter struct {
@@ -391,7 +447,7 @@ func main() {
391447

392448
clk := cmd.Clock()
393449

394-
rac, sac, rns, npm := setupWFE(c, stats, clk)
450+
rac, sac, gnc, npm, rnc, npKey := setupWFE(c, stats, clk)
395451

396452
kp, err := goodkey.NewKeyPolicy(&c.WFE.GoodKey, sac.KeyBlocked)
397453
cmd.FailOnError(err, "Unable to create key policy")
@@ -434,15 +490,17 @@ func main() {
434490
kp,
435491
allCertChains,
436492
issuerCerts,
437-
rns,
438-
npm,
439493
logger,
440494
c.WFE.Timeout.Duration,
441495
c.WFE.StaleTimeout.Duration,
442496
authorizationLifetime,
443497
pendingAuthorizationLifetime,
444498
rac,
445499
sac,
500+
gnc,
501+
npm,
502+
rnc,
503+
npKey,
446504
accountGetter,
447505
)
448506
cmd.FailOnError(err, "Unable to create WFE")

cmd/config.go

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/go-sql-driver/mysql"
1717
"github.com/honeycombio/beeline-go"
1818
"github.com/letsencrypt/boulder/core"
19+
"google.golang.org/grpc/resolver"
1920
)
2021

2122
// PasswordConfig contains a path to a file containing a password.
@@ -245,12 +246,20 @@ func (d *ConfigDuration) UnmarshalYAML(unmarshal func(interface{}) error) error
245246
return nil
246247
}
247248

249+
// ServiceDomain contains the service and domain name the gRPC client will use
250+
// to construct a SRV DNS query to lookup backends.
251+
type ServiceDomain struct {
252+
Service string
253+
Domain string
254+
}
255+
248256
// GRPCClientConfig contains the information necessary to setup a gRPC client
249257
// connection. The following field combinations are allowed:
250258
//
251259
// ServerIPAddresses, [Timeout]
252260
// ServerAddress, [Timeout], [DNSAuthority], [HostOverride]
253-
// SRVLookup, [Timeout], [DNSAuthority], [HostOverride]
261+
// SRVLookup, [Timeout], [DNSAuthority], [HostOverride], [SRVResolver]
262+
// SRVLookups, [Timeout], [DNSAuthority], [HostOverride], [SRVResolver]
254263
type GRPCClientConfig struct {
255264
// DNSAuthority is a single <hostname|IPv4|[IPv6]>:<port> of the DNS server
256265
// to be used for resolution of gRPC backends. If the address contains a
@@ -290,10 +299,21 @@ type GRPCClientConfig struct {
290299
// $ dig @10.55.55.10 -t SRV _foo._tcp.service.consul +short
291300
// 1 1 8080 0a585858.addr.dc1.consul.
292301
// 1 1 8080 0a4d4d4d.addr.dc1.consul.
293-
SRVLookup *struct {
294-
Service string
295-
Domain string
296-
}
302+
SRVLookup *ServiceDomain
303+
304+
// SRVLookups allows you to pass multiple SRV records to the gRPC client.
305+
// The gRPC client will resolves each SRV record and use the results to
306+
// construct a list of backends to connect to. For more details, see the
307+
// documentation for the SRVLookup field. Note: while you can pass multiple
308+
// targets to the gRPC client using this field, all of the targets will use
309+
// the same HostOverride and TLS configuration.
310+
SRVLookups []*ServiceDomain
311+
312+
// SRVResolver is an optional override to indicate that a specific
313+
// implementation of the SRV resolver should be used. The default is 'srv'
314+
// For more details, see the documentation in:
315+
// grpc/internal/resolver/dns/dns_resolver.go.
316+
SRVResolver string
297317

298318
// ServerAddress is a single <hostname|IPv4|[IPv6]>:<port> or `:<port>` that
299319
// the gRPC client will, if necessary, resolve via DNS and then connect to.
@@ -359,6 +379,10 @@ func (c *GRPCClientConfig) MakeTargetAndHostOverride() (string, string, error) {
359379
return fmt.Sprintf("dns://%s/%s", c.DNSAuthority, c.ServerAddress), hostOverride, nil
360380

361381
} else if c.SRVLookup != nil {
382+
scheme, err := c.makeSRVScheme()
383+
if err != nil {
384+
return "", "", err
385+
}
362386
if c.ServerIPAddresses != nil {
363387
return "", "", errors.New(
364388
"both 'SRVLookup' and 'serverIPAddresses' in gRPC client config. Only one should be provided",
@@ -371,19 +395,53 @@ func (c *GRPCClientConfig) MakeTargetAndHostOverride() (string, string, error) {
371395
if c.HostOverride != "" {
372396
hostOverride = c.HostOverride
373397
}
374-
return fmt.Sprintf("srv://%s/%s", c.DNSAuthority, targetHost), hostOverride, nil
398+
return fmt.Sprintf("%s://%s/%s", scheme, c.DNSAuthority, targetHost), hostOverride, nil
399+
400+
} else if c.SRVLookups != nil {
401+
scheme, err := c.makeSRVScheme()
402+
if err != nil {
403+
return "", "", err
404+
}
405+
if c.ServerIPAddresses != nil {
406+
return "", "", errors.New(
407+
"both 'SRVLookups' and 'serverIPAddresses' in gRPC client config. Only one should be provided",
408+
)
409+
}
410+
// Lookup backends using multiple DNS SRV records.
411+
var targetHosts []string
412+
for _, s := range c.SRVLookups {
413+
targetHosts = append(targetHosts, s.Service+"."+s.Domain)
414+
}
415+
if c.HostOverride != "" {
416+
hostOverride = c.HostOverride
417+
}
418+
return fmt.Sprintf("%s://%s/%s", scheme, c.DNSAuthority, strings.Join(targetHosts, ",")), hostOverride, nil
375419

376420
} else {
377421
if c.ServerIPAddresses == nil {
378422
return "", "", errors.New(
379-
"neither 'serverAddress', 'SRVLookup' nor 'serverIPAddresses' in gRPC client config. One should be provided",
423+
"neither 'serverAddress', 'SRVLookup', 'SRVLookups' nor 'serverIPAddresses' in gRPC client config. One should be provided",
380424
)
381425
}
382426
// Specify backends as a list of IP addresses.
383427
return "static:///" + strings.Join(c.ServerIPAddresses, ","), "", nil
384428
}
385429
}
386430

431+
// makeSRVScheme returns the scheme to use for SRV lookups. If the SRVResolver
432+
// field is empty, it returns "srv". Otherwise it checks that the specified
433+
// SRVResolver is registered with the gRPC runtime and returns it.
434+
func (c *GRPCClientConfig) makeSRVScheme() (string, error) {
435+
if c.SRVResolver == "" {
436+
return "srv", nil
437+
}
438+
rb := resolver.Get(c.SRVResolver)
439+
if rb == nil {
440+
return "", fmt.Errorf("resolver %q is not registered", c.SRVResolver)
441+
}
442+
return c.SRVResolver, nil
443+
}
444+
387445
// GRPCServerConfig contains the information needed to start a gRPC server.
388446
type GRPCServerConfig struct {
389447
Address string `json:"address"`

cmd/nonce-service/main.go

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package notmain
22

33
import (
44
"flag"
5+
"fmt"
6+
"net"
57

68
"github.com/honeycombio/beeline-go"
79

@@ -15,18 +17,53 @@ type Config struct {
1517
NonceService struct {
1618
cmd.ServiceConfig
1719

18-
MaxUsed int
20+
MaxUsed int
21+
// TODO(#6610): Remove once we've moved to derivable prefixes by
22+
// default.
1923
NoncePrefix string
2024

25+
// UseDerivablePrefix indicates whether to use a nonce prefix derived
26+
// from the gRPC listening address. If this is false, the nonce prefix
27+
// will be the value of the NoncePrefix field. If this is true, the
28+
// NoncePrefixKey field is required.
29+
//
30+
// TODO(#6610): Remove once we've moved to derivable prefixes by
31+
// default.
32+
UseDerivablePrefix bool
33+
34+
// NoncePrefixKey is a secret used for deriving the prefix of each nonce
35+
// instance. It should contain 256 bits (32 bytes) of random data to be
36+
// suitable as an HMAC-SHA256 key (e.g. the output of `openssl rand -hex
37+
// 32`). In a multi-DC deployment this value should be the same across
38+
// all boulder-wfe and nonce-service instances. This is only used if
39+
// UseDerivablePrefix is true.
40+
//
41+
// TODO(#6610): Edit this comment once we've moved to derivable prefixes
42+
// by default.
43+
NoncePrefixKey cmd.PasswordConfig
44+
2145
Syslog cmd.SyslogConfig
2246
Beeline cmd.BeelineConfig
2347
}
2448
}
2549

50+
func derivePrefix(key string, grpcAddr string) (string, error) {
51+
host, port, err := net.SplitHostPort(grpcAddr)
52+
if err != nil {
53+
return "", fmt.Errorf("parsing gRPC listen address: %w", err)
54+
}
55+
if host != "" && port != "" {
56+
hostIP := net.ParseIP(host)
57+
if hostIP == nil {
58+
return "", fmt.Errorf("parsing IP from gRPC listen address: %w", err)
59+
}
60+
}
61+
return nonce.DerivePrefix(grpcAddr, key), nil
62+
}
63+
2664
func main() {
2765
grpcAddr := flag.String("addr", "", "gRPC listen address override")
2866
debugAddr := flag.String("debug-addr", "", "Debug server address override")
29-
prefixOverride := flag.String("prefix", "", "Override the configured nonce prefix")
3067
configFile := flag.String("config", "", "File path to the configuration file for this service")
3168
flag.Parse()
3269

@@ -40,8 +77,22 @@ func main() {
4077
if *debugAddr != "" {
4178
c.NonceService.DebugAddr = *debugAddr
4279
}
43-
if *prefixOverride != "" {
44-
c.NonceService.NoncePrefix = *prefixOverride
80+
81+
// TODO(#6610): Remove once we've moved to derivable prefixes by default.
82+
if c.NonceService.NoncePrefix != "" && c.NonceService.UseDerivablePrefix {
83+
cmd.Fail("Cannot set both 'noncePrefix' and 'useDerivablePrefix'")
84+
}
85+
86+
// TODO(#6610): Remove once we've moved to derivable prefixes by default.
87+
if c.NonceService.UseDerivablePrefix && c.NonceService.NoncePrefixKey.PasswordFile == "" {
88+
cmd.Fail("Cannot set 'noncePrefixKey' without 'useDerivablePrefix'")
89+
}
90+
91+
if c.NonceService.UseDerivablePrefix && c.NonceService.NoncePrefixKey.PasswordFile != "" {
92+
key, err := c.NonceService.NoncePrefixKey.Pass()
93+
cmd.FailOnError(err, "Failed to load 'noncePrefixKey' file.")
94+
c.NonceService.NoncePrefix, err = derivePrefix(key, c.NonceService.GRPC.Address)
95+
cmd.FailOnError(err, "Failed to derive nonce prefix")
4596
}
4697

4798
bc, err := c.NonceService.Beeline.Load()

0 commit comments

Comments
 (0)