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:
parent
3dfe4efe5c
commit
79048cffba
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
27
sa/sa.go
27
sa/sa.go
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
de75ae663596735b90e461e5924f71a4c5f622ab
|
||||
Loading…
Reference in New Issue