Skip to content

Commit 3dc9886

Browse files
sfe: Add per IP rate limits to the overrides request form submissions (#8366)
1 parent de03bdc commit 3dc9886

File tree

13 files changed

+231
-19
lines changed

13 files changed

+231
-19
lines changed

cmd/sfe/main.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"github.com/letsencrypt/boulder/features"
1414
bgrpc "github.com/letsencrypt/boulder/grpc"
1515
rapb "github.com/letsencrypt/boulder/ra/proto"
16+
"github.com/letsencrypt/boulder/ratelimits"
17+
bredis "github.com/letsencrypt/boulder/redis"
1618
sapb "github.com/letsencrypt/boulder/sa/proto"
1719
"github.com/letsencrypt/boulder/sfe"
1820
"github.com/letsencrypt/boulder/sfe/zendesk"
@@ -60,6 +62,20 @@ type Config struct {
6062
IPAddress int64 `validate:"required"`
6163
} `validate:"required,dive"`
6264
} `validate:"omitempty,dive"`
65+
66+
Limiter struct {
67+
// Redis contains the configuration necessary to connect to Redis
68+
// for rate limiting. This field is required to enable rate
69+
// limiting.
70+
Redis *bredis.Config `validate:"required_with=Defaults"`
71+
72+
// Defaults is a path to a YAML file containing default rate limits.
73+
// See: ratelimits/README.md for details. This field is required to
74+
// enable rate limiting. If any individual rate limit is not set,
75+
// that limit will be disabled. Failed Authorizations limits passed
76+
// in this file must be identical to those in the RA.
77+
Defaults string `validate:"required_with=Redis"`
78+
}
6379
Features features.Config
6480
}
6581

@@ -139,6 +155,20 @@ func main() {
139155
}
140156
}
141157

158+
var limiter *ratelimits.Limiter
159+
var txnBuilder *ratelimits.TransactionBuilder
160+
var limiterRedis *bredis.Ring
161+
if c.SFE.Limiter.Defaults != "" {
162+
limiterRedis, err = bredis.NewRingFromConfig(*c.SFE.Limiter.Redis, stats, logger)
163+
cmd.FailOnError(err, "Failed to create Redis ring")
164+
165+
source := ratelimits.NewRedisSource(limiterRedis.Ring, clk, stats)
166+
limiter, err = ratelimits.NewLimiter(clk, source, stats)
167+
cmd.FailOnError(err, "Failed to create rate limiter")
168+
txnBuilder, err = ratelimits.NewTransactionBuilderFromFiles(c.SFE.Limiter.Defaults, "")
169+
cmd.FailOnError(err, "Failed to create rate limits transaction builder")
170+
}
171+
142172
sfei, err := sfe.NewSelfServiceFrontEndImpl(
143173
stats,
144174
clk,
@@ -148,6 +178,8 @@ func main() {
148178
sac,
149179
unpauseHMACKey,
150180
zendeskClient,
181+
limiter,
182+
txnBuilder,
151183
)
152184
cmd.FailOnError(err, "Unable to create SFE")
153185

errors/errors.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,14 @@ func FailedAuthorizationsPerDomainPerAccountError(retryAfter time.Duration, msg
258258
}
259259
}
260260

261+
func LimitOverrideRequestsPerIPAddressError(retryAfter time.Duration, msg string, args ...any) error {
262+
return &BoulderError{
263+
Type: RateLimit,
264+
Detail: fmt.Sprintf(msg+": see https://letsencrypt.org/docs/rate-limits/#limit-override-requests-per-ip-address", args...),
265+
RetryAfter: retryAfter,
266+
}
267+
}
268+
261269
func RejectedIdentifierError(msg string, args ...any) error {
262270
return newf(RejectedIdentifier, msg, args...)
263271
}

ratelimits/limiter.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,15 @@ func (d *Decision) Result(now time.Time) error {
172172
retryAfterTs,
173173
)
174174

175+
case LimitOverrideRequestsPerIPAddress:
176+
return berrors.LimitOverrideRequestsPerIPAddressError(
177+
retryAfter,
178+
"too many override request form submissions (%d) from this IP address in the last %s, retry after %s",
179+
d.transaction.limit.Burst,
180+
d.transaction.limit.Period.Duration,
181+
retryAfterTs,
182+
)
183+
175184
default:
176185
return berrors.InternalServerError("cannot generate error for unknown rate limit")
177186
}

ratelimits/limiter_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,22 @@ func TestRateLimitError(t *testing.T) {
557557
expectedErr: "too many certificates (3) already issued for \"example.net\" in the last 1h0m0s, retry after 1970-01-01 00:00:20 UTC: see https://letsencrypt.org/docs/rate-limits/#new-certificates-per-registered-domain",
558558
expectedErrType: berrors.RateLimit,
559559
},
560+
{
561+
name: "LimitOverrideRequestsPerIPAddress limit reached",
562+
decision: &Decision{
563+
allowed: false,
564+
retryIn: 20 * time.Second,
565+
transaction: Transaction{
566+
limit: &Limit{
567+
Name: LimitOverrideRequestsPerIPAddress,
568+
Burst: 3,
569+
Period: config.Duration{Duration: time.Hour},
570+
},
571+
},
572+
},
573+
expectedErr: "too many override request form submissions (3) from this IP address in the last 1h0m0s, retry after 1970-01-01 00:00:20 UTC: see https://letsencrypt.org/docs/rate-limits/#limit-override-requests-per-ip-address",
574+
expectedErrType: berrors.RateLimit,
575+
},
560576
{
561577
name: "Unknown rate limit name",
562578
decision: &Decision{

ratelimits/names.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import (
1818
// IMPORTANT: If you add or remove a limit Name, you MUST update:
1919
// - the string representation of the Name in nameToString,
2020
// - the validators for that name in validateIdForName(),
21-
// - the transaction constructors for that name in bucket.go
22-
// - the Subscriber facing error message in ErrForDecision(), and
21+
// - the transaction constructors for that name in transaction.go
22+
// - the Subscriber facing error message in Decision.Result(), and
2323
// - the case in BuildBucketKey() for that name.
2424
type Name int
2525

@@ -99,6 +99,11 @@ const (
9999
// the account and identValue is the value of an identifier in the
100100
// certificate.
101101
FailedAuthorizationsForPausingPerDomainPerAccount
102+
103+
// LimitOverrideRequestsPerIPAddress is used to limit the number of requests
104+
// to the rate limit override request endpoint per IP address. It uses
105+
// bucket key 'enum:ipAddress'.
106+
LimitOverrideRequestsPerIPAddress
102107
)
103108

104109
// nameToString is a map of Name values to string names.
@@ -112,6 +117,7 @@ var nameToString = map[Name]string{
112117
CertificatesPerDomainPerAccount: "CertificatesPerDomainPerAccount",
113118
CertificatesPerFQDNSet: "CertificatesPerFQDNSet",
114119
FailedAuthorizationsForPausingPerDomainPerAccount: "FailedAuthorizationsForPausingPerDomainPerAccount",
120+
LimitOverrideRequestsPerIPAddress: "LimitOverrideRequestsPerIPAddress",
115121
}
116122

117123
// isValid returns true if the Name is a valid rate limit name.
@@ -278,7 +284,7 @@ func validateFQDNSet(id string) error {
278284

279285
func validateIdForName(name Name, id string) error {
280286
switch name {
281-
case NewRegistrationsPerIPAddress:
287+
case NewRegistrationsPerIPAddress, LimitOverrideRequestsPerIPAddress:
282288
// 'enum:ipaddress'
283289
return validIPAddress(id)
284290

@@ -361,7 +367,7 @@ func BuildBucketKey(name Name, regId int64, singleIdent identifier.ACMEIdentifie
361367
}
362368

363369
switch name {
364-
case NewRegistrationsPerIPAddress:
370+
case NewRegistrationsPerIPAddress, LimitOverrideRequestsPerIPAddress:
365371
if !subscriberIP.IsValid() {
366372
return "", makeMissingErr("subscriberIP")
367373
}

ratelimits/transaction.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,3 +572,18 @@ func (builder *TransactionBuilder) NewAccountLimitTransactions(ip netip.Addr) ([
572572
}
573573
return append(transactions, txn), nil
574574
}
575+
576+
// LimitOverrideRequestsPerIPAddressTransaction returns a Transaction for the
577+
// LimitOverrideRequestsPerIPAddress limit for the provided IP address. This
578+
// limit is used to rate limit requests to the SFE override request endpoint.
579+
func (builder *TransactionBuilder) LimitOverrideRequestsPerIPAddressTransaction(ip netip.Addr) (Transaction, error) {
580+
bucketKey := newIPAddressBucketKey(LimitOverrideRequestsPerIPAddress, ip)
581+
limit, err := builder.getLimit(LimitOverrideRequestsPerIPAddress, bucketKey)
582+
if err != nil {
583+
if errors.Is(err, errLimitDisabled) {
584+
return newAllowOnlyTransaction(), nil
585+
}
586+
return Transaction{}, err
587+
}
588+
return newTransaction(limit, bucketKey, 1)
589+
}

sfe/overrides.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package sfe
22

33
import (
44
"encoding/json"
5+
"errors"
56
"fmt"
67
"html/template"
78
"net/http"
@@ -10,11 +11,13 @@ import (
1011
"strconv"
1112
"strings"
1213

14+
berrors "github.com/letsencrypt/boulder/errors"
1315
"github.com/letsencrypt/boulder/iana"
1416
"github.com/letsencrypt/boulder/policy"
1517
rl "github.com/letsencrypt/boulder/ratelimits"
1618
"github.com/letsencrypt/boulder/sfe/forms"
1719
"github.com/letsencrypt/boulder/sfe/zendesk"
20+
"github.com/letsencrypt/boulder/web"
1821
)
1922

2023
const (
@@ -556,7 +559,54 @@ type overrideRequest struct {
556559
// either the form logic is flawed or the requester has bypassed the form and
557560
// submitting (malformed) requests directly to this endpoint.
558561
func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.ResponseWriter, r *http.Request) {
559-
// TODO(#8359): Check per-IP rate limits for this endpoint.
562+
var refundLimits func()
563+
var submissionSuccess bool
564+
if sfe.limiter != nil && sfe.txnBuilder != nil {
565+
requesterIP, err := web.ExtractRequesterIP(r)
566+
if err != nil {
567+
sfe.log.Errf("determining requester IP address: %s", err)
568+
http.Error(w, "failed to determine the IP address of the requester", http.StatusInternalServerError)
569+
return
570+
}
571+
572+
txns, err := sfe.txnBuilder.LimitOverrideRequestsPerIPAddressTransaction(requesterIP)
573+
if err != nil {
574+
sfe.log.Errf("building transaction for override request form limits: %s", err)
575+
http.Error(w, "failed to build transaction for override request form limits", http.StatusInternalServerError)
576+
return
577+
}
578+
579+
d, err := sfe.limiter.Spend(r.Context(), txns)
580+
if err != nil {
581+
sfe.log.Errf("spending transaction for override request form limits: %s", err)
582+
http.Error(w, "failed to spend transaction for override request form limits", http.StatusInternalServerError)
583+
return
584+
}
585+
586+
err = d.Result(sfe.clk.Now())
587+
if err != nil {
588+
var bErr *berrors.BoulderError
589+
if errors.As(err, &bErr) && bErr.Type == berrors.RateLimit {
590+
http.Error(w, bErr.Detail, http.StatusTooManyRequests)
591+
return
592+
}
593+
sfe.log.Errf("determining result of override request form limits transaction: %s", err)
594+
http.Error(w, "failed to determine result of override request form limits transaction", http.StatusInternalServerError)
595+
return
596+
}
597+
598+
refundLimits = func() {
599+
_, err := sfe.limiter.Refund(r.Context(), txns)
600+
if err != nil {
601+
sfe.log.Errf("refunding transaction for override request form limits: %s", err)
602+
}
603+
}
604+
}
605+
defer func() {
606+
if !submissionSuccess && refundLimits != nil {
607+
refundLimits()
608+
}
609+
}()
560610

561611
var req overrideRequest
562612
err := json.NewDecoder(http.MaxBytesReader(w, r.Body, submitOverrideRequestBodyLimit)).Decode(&req)
@@ -710,5 +760,6 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response
710760
// TODO(#8362): If FundraisingFieldName value is true, use the Salesforce
711761
// API to create a new Lead record with the provided information.
712762

763+
submissionSuccess = true
713764
w.WriteHeader(http.StatusOK)
714765
}

sfe/overrides_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,3 +411,49 @@ func TestValidateOverrideRequestField(t *testing.T) {
411411
})
412412
}
413413
}
414+
415+
func TestSubmitOverrideRequestHandlerRateLimited(t *testing.T) {
416+
t.Parallel()
417+
418+
sfe, _ := setupSFE(t)
419+
sfe.templatePages = minimalTemplates(t)
420+
client := createFakeZendeskClientServer(t)
421+
sfe.zendeskClient = client
422+
423+
for attempt := range 101 {
424+
reqObj := overrideRequest{
425+
RateLimit: rl.CertificatesPerDomainPerAccount.String(),
426+
Fields: map[string]string{
427+
subscriberAgreementFieldName: "true",
428+
privacyPolicyFieldName: "true",
429+
mailingListFieldName: "false",
430+
fundraisingFieldName: FundraisingOptions[0],
431+
emailAddressFieldName: "[email protected]",
432+
OrganizationFieldName: "Big Host Inc.",
433+
useCaseFieldName: strings.Repeat("x", 60),
434+
TierFieldName: certificatesPerDomainPerAccountTierOptions[0],
435+
AccountURIFieldName: "https://acme-v02.api.letsencrypt.org/acme/acct/67890",
436+
},
437+
}
438+
body, err := json.Marshal(reqObj)
439+
if err != nil {
440+
t.Errorf("Unexpected failure to marshal JSON overrideRequest: %s", err)
441+
}
442+
rec := httptest.NewRecorder()
443+
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
444+
445+
sfe.submitOverrideRequestHandler(rec, req)
446+
if attempt < 100 {
447+
if rec.Code != http.StatusOK {
448+
t.Errorf("Unexpected status=%d, expected status=200", rec.Code)
449+
}
450+
} else {
451+
if rec.Code != http.StatusTooManyRequests {
452+
t.Errorf("Unexpected status=%d, expected status=429", rec.Code)
453+
}
454+
if !strings.Contains(rec.Body.String(), "too many override request form submissions (100)") {
455+
t.Errorf("Expected rate limit error message, got: %s", rec.Body.String())
456+
}
457+
}
458+
}
459+
}

sfe/sfe.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ type SelfServiceFrontEndImpl struct {
6767

6868
templatePages *template.Template
6969
cop *http.CrossOriginProtection
70+
71+
limiter *rl.Limiter
72+
txnBuilder *rl.TransactionBuilder
7073
}
7174

7275
// NewSelfServiceFrontEndImpl constructs a web service for Boulder
@@ -79,6 +82,8 @@ func NewSelfServiceFrontEndImpl(
7982
sac sapb.StorageAuthorityReadOnlyClient,
8083
unpauseHMACKey []byte,
8184
zendeskClient *zendesk.Client,
85+
limiter *rl.Limiter,
86+
txnBuilder *rl.TransactionBuilder,
8287
) (SelfServiceFrontEndImpl, error) {
8388

8489
// Parse the files once at startup to avoid each request causing the server
@@ -99,6 +104,8 @@ func NewSelfServiceFrontEndImpl(
99104
zendeskClient: zendeskClient,
100105
templatePages: tmplPages,
101106
cop: http.NewCrossOriginProtection(),
107+
limiter: limiter,
108+
txnBuilder: txnBuilder,
102109
}
103110

104111
return sfe, nil

sfe/sfe_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/letsencrypt/boulder/metrics"
1919
"github.com/letsencrypt/boulder/mocks"
2020
"github.com/letsencrypt/boulder/must"
21+
"github.com/letsencrypt/boulder/ratelimits"
2122
"github.com/letsencrypt/boulder/test"
2223
"github.com/letsencrypt/boulder/unpause"
2324

@@ -51,6 +52,11 @@ func setupSFE(t *testing.T) (SelfServiceFrontEndImpl, clock.FakeClock) {
5152
key, err := hmacKey.Load()
5253
test.AssertNotError(t, err, "Unable to load HMAC key")
5354

55+
limiter, err := ratelimits.NewLimiter(fc, ratelimits.NewInmemSource(), stats)
56+
test.AssertNotError(t, err, "making limiter")
57+
txnBuilder, err := ratelimits.NewTransactionBuilderFromFiles("../test/config-next/sfe-ratelimit-defaults.yml", "")
58+
test.AssertNotError(t, err, "making transaction composer")
59+
5460
sfe, err := NewSelfServiceFrontEndImpl(
5561
stats,
5662
fc,
@@ -60,6 +66,8 @@ func setupSFE(t *testing.T) (SelfServiceFrontEndImpl, clock.FakeClock) {
6066
mockSA,
6167
key,
6268
nil,
69+
limiter,
70+
txnBuilder,
6371
)
6472
test.AssertNotError(t, err, "Unable to create SFE")
6573

0 commit comments

Comments
 (0)