crl: add cache control headers (#8011)

The crl-storer passes along Cache-Control and Expires from the
crl-updater (because the crl-updater knows the UpdatePeriod).

The crl-updater calculates the Expires header based on when it expects
to update the CRL, plus a margin of error.

Fixes #8004
This commit is contained in:
Jacob Hoffman-Andrews 2025-02-13 14:20:29 -08:00 committed by GitHub
parent a8b2fd6960
commit e0e5a17899
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 123 additions and 36 deletions

View File

@ -116,6 +116,37 @@ type Config struct {
// load of said run. The default is 1.
MaxAttempts int `validate:"omitempty,min=1"`
// ExpiresMargin adds a small increment to the CRL's HTTP Expires time.
//
// When uploading a CRL, its Expires field in S3 is set to the expected time
// the next CRL will be uploaded (by this instance). That allows our CDN
// instances to cache for that long. However, since the next update might be
// slow or delayed, we add a margin of error.
//
// Tradeoffs: A large ExpiresMargin reduces the chance that a CRL becomes
// uncacheable and floods S3 with traffic (which might result in 503s while
// S3 scales out).
//
// A small ExpiresMargin means revocations become visible sooner, including
// admin-invoked revocations that may have a time requirement.
ExpiresMargin config.Duration
// CacheControl is a string passed verbatim to the crl-storer to store on
// the S3 object.
//
// Note: if this header contains max-age, it will override
// Expires. https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-freshness-lifet
// Cache-Control: max-age has the disadvantage that it caches for a fixed
// amount of time, regardless of how close the CRL is to replacement. So
// if max-age is used, the worst-case time for a revocation to become visible
// is UpdatePeriod + the value of max age.
//
// The stale-if-error and stale-while-revalidate headers may be useful here:
// https://aws.amazon.com/about-aws/whats-new/2023/05/amazon-cloudfront-stale-while-revalidate-stale-if-error-cache-control-directives/
//
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
CacheControl string
Features features.Config
}
@ -189,6 +220,8 @@ func main() {
c.CRLUpdater.UpdateTimeout.Duration,
c.CRLUpdater.MaxParallelism,
c.CRLUpdater.MaxAttempts,
c.CRLUpdater.CacheControl,
c.CRLUpdater.ExpiresMargin.Duration,
c.CRLUpdater.TemporallyShardedSerialPrefixes,
sac,
cac,

View File

@ -10,6 +10,7 @@ import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
@ -107,9 +108,11 @@ type CRLMetadata struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
IssuerNameID int64 `protobuf:"varint,1,opt,name=issuerNameID,proto3" json:"issuerNameID,omitempty"`
Number int64 `protobuf:"varint,2,opt,name=number,proto3" json:"number,omitempty"`
ShardIdx int64 `protobuf:"varint,3,opt,name=shardIdx,proto3" json:"shardIdx,omitempty"`
IssuerNameID int64 `protobuf:"varint,1,opt,name=issuerNameID,proto3" json:"issuerNameID,omitempty"`
Number int64 `protobuf:"varint,2,opt,name=number,proto3" json:"number,omitempty"`
ShardIdx int64 `protobuf:"varint,3,opt,name=shardIdx,proto3" json:"shardIdx,omitempty"`
Expires *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires,proto3" json:"expires,omitempty"`
CacheControl string `protobuf:"bytes,5,opt,name=cacheControl,proto3" json:"cacheControl,omitempty"`
}
func (x *CRLMetadata) Reset() {
@ -165,35 +168,57 @@ func (x *CRLMetadata) GetShardIdx() int64 {
return 0
}
func (x *CRLMetadata) GetExpires() *timestamppb.Timestamp {
if x != nil {
return x.Expires
}
return nil
}
func (x *CRLMetadata) GetCacheControl() string {
if x != nil {
return x.CacheControl
}
return ""
}
var File_storer_proto protoreflect.FileDescriptor
var file_storer_proto_rawDesc = []byte{
0x0a, 0x0c, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06,
0x73, 0x74, 0x6f, 0x72, 0x65, 0x72, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x22, 0x6e, 0x0a, 0x10, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x52, 0x4c,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64,
0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x74, 0x6f, 0x72,
0x65, 0x72, 0x2e, 0x43, 0x52, 0x4c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x48, 0x00,
0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1c, 0x0a, 0x08, 0x63, 0x72,
0x6c, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x08,
0x63, 0x72, 0x6c, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c,
0x6f, 0x61, 0x64, 0x22, 0x65, 0x0a, 0x0b, 0x43, 0x52, 0x4c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61,
0x74, 0x61, 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65,
0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72,
0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72,
0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x1a,
0x0a, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
0x52, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x32, 0x4e, 0x0a, 0x09, 0x43, 0x52,
0x4c, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x09, 0x55, 0x70, 0x6c, 0x6f, 0x61,
0x64, 0x43, 0x52, 0x4c, 0x12, 0x18, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x72, 0x2e, 0x55, 0x70,
0x6c, 0x6f, 0x61, 0x64, 0x43, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x28, 0x01, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69,
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x65, 0x74, 0x73, 0x65, 0x6e, 0x63,
0x72, 0x79, 0x70, 0x74, 0x2f, 0x62, 0x6f, 0x75, 0x6c, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x72, 0x6c,
0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6e, 0x0a, 0x10, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x52,
0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61,
0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x74, 0x6f,
0x72, 0x65, 0x72, 0x2e, 0x43, 0x52, 0x4c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x48,
0x00, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1c, 0x0a, 0x08, 0x63,
0x72, 0x6c, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52,
0x08, 0x63, 0x72, 0x6c, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79,
0x6c, 0x6f, 0x61, 0x64, 0x22, 0xbf, 0x01, 0x0a, 0x0b, 0x43, 0x52, 0x4c, 0x4d, 0x65, 0x74, 0x61,
0x64, 0x61, 0x74, 0x61, 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61,
0x6d, 0x65, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x69, 0x73, 0x73, 0x75,
0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62,
0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72,
0x12, 0x1a, 0x0a, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, 0x03, 0x20, 0x01,
0x28, 0x03, 0x52, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x12, 0x34, 0x0a, 0x07,
0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72,
0x65, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x61, 0x63, 0x68, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x72,
0x6f, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x61, 0x63, 0x68, 0x65, 0x43,
0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x32, 0x4e, 0x0a, 0x09, 0x43, 0x52, 0x4c, 0x53, 0x74, 0x6f,
0x72, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x09, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x52, 0x4c,
0x12, 0x18, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x72, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64,
0x43, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
0x74, 0x79, 0x22, 0x00, 0x28, 0x01, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x65, 0x74, 0x73, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74,
0x2f, 0x62, 0x6f, 0x75, 0x6c, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x72, 0x6c, 0x2f, 0x73, 0x74, 0x6f,
0x72, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
}
var (
@ -210,19 +235,21 @@ func file_storer_proto_rawDescGZIP() []byte {
var file_storer_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_storer_proto_goTypes = []interface{}{
(*UploadCRLRequest)(nil), // 0: storer.UploadCRLRequest
(*CRLMetadata)(nil), // 1: storer.CRLMetadata
(*emptypb.Empty)(nil), // 2: google.protobuf.Empty
(*UploadCRLRequest)(nil), // 0: storer.UploadCRLRequest
(*CRLMetadata)(nil), // 1: storer.CRLMetadata
(*timestamppb.Timestamp)(nil), // 2: google.protobuf.Timestamp
(*emptypb.Empty)(nil), // 3: google.protobuf.Empty
}
var file_storer_proto_depIdxs = []int32{
1, // 0: storer.UploadCRLRequest.metadata:type_name -> storer.CRLMetadata
0, // 1: storer.CRLStorer.UploadCRL:input_type -> storer.UploadCRLRequest
2, // 2: storer.CRLStorer.UploadCRL:output_type -> google.protobuf.Empty
2, // [2:3] is the sub-list for method output_type
1, // [1:2] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
2, // 1: storer.CRLMetadata.expires:type_name -> google.protobuf.Timestamp
0, // 2: storer.CRLStorer.UploadCRL:input_type -> storer.UploadCRLRequest
3, // 3: storer.CRLStorer.UploadCRL:output_type -> google.protobuf.Empty
3, // [3:4] is the sub-list for method output_type
2, // [2:3] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_storer_proto_init() }

View File

@ -4,6 +4,7 @@ package storer;
option go_package = "github.com/letsencrypt/boulder/crl/storer/proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
service CRLStorer {
rpc UploadCRL(stream UploadCRLRequest) returns (google.protobuf.Empty) {}
@ -20,4 +21,6 @@ message CRLMetadata {
int64 issuerNameID = 1;
int64 number = 2;
int64 shardIdx = 3;
google.protobuf.Timestamp expires = 4;
string cacheControl = 5;
}

View File

@ -105,6 +105,8 @@ func (cs *crlStorer) UploadCRL(stream grpc.ClientStreamingServer[cspb.UploadCRLR
var shardIdx int64
var crlNumber *big.Int
crlBytes := make([]byte, 0)
var cacheControl string
var expires time.Time
// Read all of the messages from the input stream.
for {
@ -125,6 +127,9 @@ func (cs *crlStorer) UploadCRL(stream grpc.ClientStreamingServer[cspb.UploadCRLR
return errors.New("got incomplete metadata message")
}
cacheControl = payload.Metadata.CacheControl
expires = payload.Metadata.Expires.AsTime()
shardIdx = payload.Metadata.ShardIdx
crlNumber = crl.Number(time.Unix(0, payload.Metadata.Number))
@ -229,6 +234,8 @@ func (cs *crlStorer) UploadCRL(stream grpc.ClientStreamingServer[cspb.UploadCRLR
ChecksumSHA256: &checksumb64,
ContentType: &crlContentType,
Metadata: map[string]string{"crlNumber": crlNumber.String()},
Expires: &expires,
CacheControl: &cacheControl,
})
latency := cs.clk.Now().Sub(start)

View File

@ -26,6 +26,8 @@ func TestRunOnce(t *testing.T) {
[]*issuance.Certificate{e1, r3},
2, 18*time.Hour, 24*time.Hour,
6*time.Hour, time.Minute, 1, 1,
"stale-if-error=60",
5*time.Minute,
nil,
&fakeSAC{revokedCerts: revokedCertsStream{err: errors.New("db no worky")}, maxNotAfter: clk.Now().Add(90 * 24 * time.Hour)},
&fakeCA{gcc: generateCRLStream{}},

View File

@ -36,6 +36,9 @@ type crlUpdater struct {
maxParallelism int
maxAttempts int
cacheControl string
expiresMargin time.Duration
temporallyShardedPrefixes []string
sa sapb.StorageAuthorityClient
@ -58,6 +61,8 @@ func NewUpdater(
updateTimeout time.Duration,
maxParallelism int,
maxAttempts int,
cacheControl string,
expiresMargin time.Duration,
temporallyShardedPrefixes []string,
sa sapb.StorageAuthorityClient,
ca capb.CRLGeneratorClient,
@ -117,6 +122,8 @@ func NewUpdater(
updateTimeout,
maxParallelism,
maxAttempts,
cacheControl,
expiresMargin,
temporallyShardedPrefixes,
sa,
ca,
@ -392,6 +399,8 @@ func (cu *crlUpdater) updateShard(ctx context.Context, atTime time.Time, issuerN
IssuerNameID: int64(issuerNameID),
Number: atTime.UnixNano(),
ShardIdx: int64(shardIdx),
CacheControl: cu.cacheControl,
Expires: timestamppb.New(atTime.Add(cu.updatePeriod).Add(cu.expiresMargin)),
},
},
})

View File

@ -225,6 +225,8 @@ func TestUpdateShard(t *testing.T) {
[]*issuance.Certificate{e1, r3},
2, 18*time.Hour, 24*time.Hour,
6*time.Hour, time.Minute, 1, 1,
"stale-if-error=60",
5*time.Minute,
nil,
&fakeSAC{
revokedCerts: revokedCertsStream{},
@ -406,6 +408,8 @@ func TestUpdateShardWithRetry(t *testing.T) {
[]*issuance.Certificate{e1, r3},
2, 18*time.Hour, 24*time.Hour,
6*time.Hour, time.Minute, 1, 1,
"stale-if-error=60",
5*time.Minute,
nil,
&fakeSAC{revokedCerts: revokedCertsStream{err: sentinelErr}, maxNotAfter: clk.Now().Add(90 * 24 * time.Hour)},
&fakeCA{gcc: generateCRLStream{}},

View File

@ -48,6 +48,8 @@
"lookbackPeriod": "24h",
"updatePeriod": "10m",
"updateTimeout": "1m",
"expiresMargin": "5m",
"cacheControl": "stale-if-error=60",
"temporallyShardedSerialPrefixes": [
"7f"
],