Support writing initial OCSP response to redis (#5958)

Adds a rocsp redis client to the sa if cluster information is provided in the
sa config. If a redis cluster is configured, all new certificate OCSP
responses added with sa.AddPrecertificate will attempt to be written to
the redis cluster, but will not block or fail on errors.

Fixes: #5871
This commit is contained in:
Andrew Gabbitas 2022-03-21 20:33:12 -06:00 committed by GitHub
parent 3dfe4efe5c
commit 79048cffba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 206 additions and 13 deletions

View File

@ -27,6 +27,8 @@ import (
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/mocks"
"github.com/letsencrypt/boulder/ra"
"github.com/letsencrypt/boulder/rocsp"
rocsp_config "github.com/letsencrypt/boulder/rocsp/config"
"github.com/letsencrypt/boulder/sa"
sapb "github.com/letsencrypt/boulder/sa/proto"
"github.com/letsencrypt/boulder/sa/satest"
@ -61,7 +63,11 @@ func TestRevokeBatch(t *testing.T) {
if err != nil {
t.Fatalf("Failed to create dbMap: %s", err)
}
ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, fc, log, metrics.NoopRegisterer, 1)
rocspIssuers, err := rocsp_config.LoadIssuers(map[string]int{
"../../test/hierarchy/int-r3.cert.pem": 102,
})
test.AssertNotError(t, err, "error loading issuers")
ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, rocsp.NewMockWriteSucceedClient(), rocspIssuers, fc, log, metrics.NoopRegisterer, 1)
if err != nil {
t.Fatalf("Failed to create SA: %s", err)
}
@ -503,8 +509,11 @@ func setup(t *testing.T) testCtx {
if err != nil {
t.Fatalf("Failed to create dbMap: %s", err)
}
ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, fc, log, metrics.NoopRegisterer, 1)
rocspIssuers, err := rocsp_config.LoadIssuers(map[string]int{
"../../test/hierarchy/int-r3.cert.pem": 102,
})
test.AssertNotError(t, err, "error loading issuers")
ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, rocsp.NewMockWriteSucceedClient(), rocspIssuers, fc, log, metrics.NoopRegisterer, 1)
if err != nil {
t.Fatalf("Failed to create SA: %s", err)
}

View File

@ -12,6 +12,8 @@ import (
"github.com/letsencrypt/boulder/db"
"github.com/letsencrypt/boulder/features"
bgrpc "github.com/letsencrypt/boulder/grpc"
"github.com/letsencrypt/boulder/rocsp"
rocsp_config "github.com/letsencrypt/boulder/rocsp/config"
"github.com/letsencrypt/boulder/sa"
sapb "github.com/letsencrypt/boulder/sa/proto"
)
@ -21,6 +23,8 @@ type Config struct {
cmd.ServiceConfig
DB cmd.DBConfig
ReadOnlyDB cmd.DBConfig
Redis *rocsp_config.RedisConfig
Issuers map[string]int
Features map[string]bool
@ -81,11 +85,20 @@ func main() {
clk := cmd.Clock()
redisConf := c.SA.Redis
var rocspWriteClient *rocsp.WritingClient
if redisConf != nil {
rocspWriteClient, err = rocsp_config.MakeClient(redisConf, clk, scope)
cmd.FailOnError(err, "making Redis client")
}
shortIssuers, err := rocsp_config.LoadIssuers(c.SA.Issuers)
cmd.FailOnError(err, "loading issuers")
parallel := c.SA.ParallelismPerRPC
if parallel < 1 {
parallel = 1
}
sai, err := sa.NewSQLStorageAuthority(dbMap, dbReadOnlyMap, clk, logger, scope, parallel)
sai, err := sa.NewSQLStorageAuthority(dbMap, dbReadOnlyMap, rocspWriteClient, shortIssuers, clk, logger, scope, parallel)
cmd.FailOnError(err, "Failed to create SA impl")
tls, err := c.SA.TLS.Load()

View File

@ -329,7 +329,7 @@ func TestGetAndProcessCerts(t *testing.T) {
fc.Set(fc.Now().Add(time.Hour))
checker := newChecker(saDbMap, fc, pa, kp, time.Hour, testValidityDurations)
sa, err := sa.NewSQLStorageAuthority(saDbMap, saDbMap, fc, blog.NewMock(), metrics.NoopRegisterer, 1)
sa, err := sa.NewSQLStorageAuthority(saDbMap, saDbMap, nil, nil, fc, blog.NewMock(), metrics.NoopRegisterer, 1)
test.AssertNotError(t, err, "Couldn't create SA to insert certificates")
saCleanUp := test.ResetSATestDatabase(t)
defer func() {

View File

@ -351,7 +351,7 @@ func setup(t *testing.T) testCtx {
t.Fatalf("Couldn't connect to the database: %s", err)
}
ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, clock.New(), log, metrics.NoopRegisterer, 1)
ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, nil, clock.New(), log, metrics.NoopRegisterer, 1)
if err != nil {
t.Fatalf("unable to create SQLStorageAuthority: %s", err)
}

View File

@ -801,7 +801,7 @@ func setup(t *testing.T, nagTimes []time.Duration) *testCtx {
}
fc := newFakeClock(t)
ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, fc, log, metrics.NoopRegisterer, 1)
ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, nil, fc, log, metrics.NoopRegisterer, 1)
if err != nil {
t.Fatalf("unable to create SQLStorageAuthority: %s", err)
}

View File

@ -449,7 +449,7 @@ func setup(t *testing.T) testCtx {
cleanUp := test.ResetSATestDatabase(t)
fc := newFakeClock(t)
ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, fc, log, metrics.NoopRegisterer, 1)
ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, nil, fc, log, metrics.NoopRegisterer, 1)
if err != nil {
t.Fatalf("unable to create SQLStorageAuthority: %s", err)
}

View File

@ -62,7 +62,7 @@ func setup(t *testing.T) (*OCSPUpdater, sapb.StorageAuthorityClient, *db.Wrapped
fc := clock.NewFake()
fc.Add(1 * time.Hour)
sa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, fc, log, metrics.NoopRegisterer, 1)
sa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, nil, fc, log, metrics.NoopRegisterer, 1)
test.AssertNotError(t, err, "Failed to create SA")
updater, err := New(

View File

@ -309,7 +309,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho
if err != nil {
t.Fatalf("Failed to create dbMap: %s", err)
}
ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, fc, log, metrics.NoopRegisterer, 1)
ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, nil, fc, log, metrics.NoopRegisterer, 1)
if err != nil {
t.Fatalf("Failed to create SA: %s", err)
}

30
rocsp/mocks.go Normal file
View File

@ -0,0 +1,30 @@
package rocsp
import (
"context"
"fmt"
"time"
)
// MockWriteClient is a mock
type MockWriteClient struct {
StoreReponseReturnError error
}
// StoreResponse mocks a rocsp.StoreResponse method and returns nil or an
// error depending on the desired state.
func (r MockWriteClient) StoreResponse(ctx context.Context, respBytes []byte, shortIssuerID byte, ttl time.Duration) error {
return r.StoreReponseReturnError
}
// NewMockWriteSucceedClient returns a mock MockWriteClient with a
// StoreResponse method that will always succeed.
func NewMockWriteSucceedClient() MockWriteClient {
return MockWriteClient{nil}
}
// NewMockWriteFailClient returns a mock MockWriteClient with a
// StoreResponse method that will always fail.
func NewMockWriteFailClient() MockWriteClient {
return MockWriteClient{StoreReponseReturnError: fmt.Errorf("could not store response")}
}

View File

@ -152,6 +152,24 @@ func (ssa *SQLStorageAuthority) AddPrecertificate(ctx context.Context, req *sapb
if overallError != nil {
return nil, overallError
}
// Store the OCSP response in Redis (if configured) on a best effort
// basis. We don't want to fail on an error here while mysql is the
// source of truth.
if ssa.rocspWriteClient != nil {
// Use a new context for the goroutine. We aren't going to wait on
// the goroutine to complete, so we don't want it to be canceled
// when the parent function ends. The rocsp client has a
// configurable timeout that can be set during creation.
rocspCtx := context.Background()
rocspTTL := parsed.NotAfter.Sub(ssa.clk.Now())
// Send the response off to redis in a goroutine.
go func() {
err = ssa.storeOCSPRedis(rocspCtx, req.Ocsp, req.IssuerID, rocspTTL)
ssa.log.Debugf("failed to store OCSP response in redis: %v", err)
}()
}
return &emptypb.Empty{}, nil
}

28
sa/rocsp_sa.go Normal file
View File

@ -0,0 +1,28 @@
package sa
import (
"context"
"time"
rocsp_config "github.com/letsencrypt/boulder/rocsp/config"
)
type rocspWriter interface {
StoreResponse(ctx context.Context, respBytes []byte, shortIssuerID byte, ttl time.Duration) error
}
// storeOCSPRedis stores an OCSP response in a redis cluster.
func (ssa *SQLStorageAuthority) storeOCSPRedis(ctx context.Context, resp []byte, issuerID int64, ttl time.Duration) error {
shortIssuerID, err := rocsp_config.FindIssuerByID(issuerID, ssa.shortIssuers)
if err != nil {
ssa.redisStoreResponse.WithLabelValues("find_issuer_error").Inc()
return err
}
err = ssa.rocspWriteClient.StoreResponse(ctx, resp, shortIssuerID.ShortID(), ttl)
if err != nil {
ssa.redisStoreResponse.WithLabelValues("store_response_error").Inc()
return err
}
ssa.redisStoreResponse.WithLabelValues("success").Inc()
return nil
}

39
sa/rocsp_sa_test.go Normal file
View File

@ -0,0 +1,39 @@
package sa
import (
"context"
"testing"
"time"
"github.com/letsencrypt/boulder/rocsp"
"github.com/letsencrypt/boulder/test"
)
func TestStoreOCSPRedis(t *testing.T) {
sa, _, cleanUp := initSA(t)
defer cleanUp()
response := []byte{0, 0, 1}
ctx := context.Background()
err := sa.storeOCSPRedis(ctx, response, 58923463773186183, time.Hour)
test.AssertNotError(t, err, "unexpected error")
}
func TestStoreOCSPRedisInvalidIssuer(t *testing.T) {
sa, _, cleanUp := initSA(t)
defer cleanUp()
response := []byte{0, 0, 1}
ctx := context.Background()
// 1234 is expected to not be a valid issuerID
err := sa.storeOCSPRedis(ctx, response, 1234, time.Hour)
test.AssertContains(t, err.Error(), "no issuer found for an ID in certificateStatus: 1234")
}
func TestStoreOCSPRedisFail(t *testing.T) {
sa, _, cleanUp := initSA(t)
defer cleanUp()
sa.rocspWriteClient = rocsp.NewMockWriteFailClient()
response := []byte{0, 0, 1}
ctx := context.Background()
err := sa.storeOCSPRedis(ctx, response, 58923463773186183, time.Hour)
test.AssertContains(t, err.Error(), "could not store response")
}

View File

@ -28,6 +28,7 @@ import (
"github.com/letsencrypt/boulder/identifier"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/revocation"
rocsp_config "github.com/letsencrypt/boulder/rocsp/config"
sapb "github.com/letsencrypt/boulder/sa/proto"
)
@ -40,8 +41,15 @@ type SQLStorageAuthority struct {
sapb.UnimplementedStorageAuthorityServer
dbMap *db.WrappedMap
dbReadOnlyMap *db.WrappedMap
clk clock.Clock
log blog.Logger
// Redis client for storing OCSP responses in Redis.
rocspWriteClient rocspWriter
// Short issuer map used by rocsp.
shortIssuers []rocsp_config.ShortIDIssuer
clk clock.Clock
log blog.Logger
// For RPCs that generate multiple, parallelizable SQL queries, this is the
// max parallelism they will use (to avoid consuming too many MariaDB
@ -58,6 +66,10 @@ type SQLStorageAuthority struct {
// transactions fail and so use this stat to maintain visibility into the rate
// this occurs.
rateLimitWriteErrors prometheus.Counter
// redisStoreResponse is a counter of OCSP responses written to redis by
// result.
redisStoreResponse *prometheus.CounterVec
}
// orderFQDNSet contains the SHA256 hash of the lowercased, comma joined names
@ -77,6 +89,8 @@ type orderFQDNSet struct {
func NewSQLStorageAuthority(
dbMap *db.WrappedMap,
dbReadOnlyMap *db.WrappedMap,
rocspWriteClient rocspWriter,
shortIssuers []rocsp_config.ShortIDIssuer,
clk clock.Clock,
logger blog.Logger,
stats prometheus.Registerer,
@ -90,13 +104,22 @@ func NewSQLStorageAuthority(
})
stats.MustRegister(rateLimitWriteErrors)
redisStoreResponse := prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "redis_store_response",
Help: "Count of OCSP Response writes to redis",
}, []string{"result"})
stats.MustRegister(redisStoreResponse)
ssa := &SQLStorageAuthority{
dbMap: dbMap,
dbReadOnlyMap: dbReadOnlyMap,
rocspWriteClient: rocspWriteClient,
shortIssuers: shortIssuers,
clk: clk,
log: logger,
parallelismPerRPC: parallelismPerRPC,
rateLimitWriteErrors: rateLimitWriteErrors,
redisStoreResponse: redisStoreResponse,
}
ssa.countCertificatesByName = ssa.countCertificates

View File

@ -29,6 +29,8 @@ import (
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/probs"
"github.com/letsencrypt/boulder/rocsp"
rocsp_config "github.com/letsencrypt/boulder/rocsp/config"
sapb "github.com/letsencrypt/boulder/sa/proto"
"github.com/letsencrypt/boulder/test"
"github.com/letsencrypt/boulder/test/vars"
@ -65,7 +67,17 @@ func initSA(t *testing.T) (*SQLStorageAuthority, clock.FakeClock, func()) {
fc := clock.NewFake()
fc.Set(time.Date(2015, 3, 4, 5, 0, 0, 0, time.UTC))
sa, err := NewSQLStorageAuthority(dbMap, dbMap, fc, log, metrics.NoopRegisterer, 1)
// Load the standard list of signing certificates from the hierarchy.
rocspIssuers, err := rocsp_config.LoadIssuers(map[string]int{
"../test/hierarchy/int-e1.cert.pem": 100,
"../test/hierarchy/int-e2.cert.pem": 101,
"../test/hierarchy/int-r3.cert.pem": 102,
"../test/hierarchy/int-r4.cert.pem": 103,
})
if err != nil {
t.Fatalf("failed to load issuers: %s", err)
}
sa, err := NewSQLStorageAuthority(dbMap, dbMap, rocsp.NewMockWriteSucceedClient(), rocspIssuers, fc, log, metrics.NoopRegisterer, 1)
if err != nil {
t.Fatalf("Failed to create SA: %s", err)
}

View File

@ -29,6 +29,25 @@
"wfe.boulder"
]
},
"redis": {
"username": "boulder-sa",
"passwordFile": "test/secrets/sa_redis_password",
"addrs": [
"10.33.33.7:4218"
],
"timeout": "5s",
"tls": {
"caCertFile": "test/redis-tls/minica.pem",
"certFile": "test/redis-tls/boulder/cert.pem",
"keyFile": "test/redis-tls/boulder/key.pem"
}
},
"issuers": {
".hierarchy/intermediate-cert-ecdsa-a.pem": 1,
".hierarchy/intermediate-cert-ecdsa-b.pem": 2,
".hierarchy/intermediate-cert-rsa-a.pem": 3,
".hierarchy/intermediate-cert-rsa-b.pem": 4
},
"features": {
"FasterNewOrdersRateLimit": true,
"StoreRevokerInfo": true,

View File

@ -22,6 +22,7 @@ rename-command SREM ""
user default off
user ocsp-updater on +@all ~* >e4e9ce7845cb6adbbc44fb1d9deb05e6b4dc1386
user ocsp-responder on +@all ~* >0e5a4c8b5faaf3194c8ad83c3dd9a0dd8a75982b
user boulder-sa on +@all ~* >de75ae663596735b90e461e5924f71a4c5f622ab
user boulder-ra on +@all ~* >b3b2fcbbf46fe39fd522c395a51f84d93a98ff2f
user replication-user on +@all ~* >435e9c4225f08813ef3af7c725f0d30d263b9cd3
user unittest-rw on +@all ~* >824968fa490f4ecec1e52d5e34916bdb60d45f8d

View File

@ -0,0 +1 @@
de75ae663596735b90e461e5924f71a4c5f622ab