github.com/letsencrypt/boulder@v0.20251208.0/cmd/crl-updater/main.go (about)

     1  package notmain
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"flag"
     7  	"os"
     8  	"time"
     9  
    10  	"github.com/jmhodges/clock"
    11  
    12  	capb "github.com/letsencrypt/boulder/ca/proto"
    13  	"github.com/letsencrypt/boulder/cmd"
    14  	"github.com/letsencrypt/boulder/config"
    15  	cspb "github.com/letsencrypt/boulder/crl/storer/proto"
    16  	"github.com/letsencrypt/boulder/crl/updater"
    17  	"github.com/letsencrypt/boulder/features"
    18  	bgrpc "github.com/letsencrypt/boulder/grpc"
    19  	"github.com/letsencrypt/boulder/issuance"
    20  	sapb "github.com/letsencrypt/boulder/sa/proto"
    21  )
    22  
    23  type Config struct {
    24  	CRLUpdater struct {
    25  		DebugAddr string `validate:"omitempty,hostname_port"`
    26  
    27  		// TLS client certificate, private key, and trusted root bundle.
    28  		TLS cmd.TLSConfig
    29  
    30  		SAService           *cmd.GRPCClientConfig
    31  		CRLGeneratorService *cmd.GRPCClientConfig
    32  		CRLStorerService    *cmd.GRPCClientConfig
    33  
    34  		// IssuerCerts is a list of paths to issuer certificates on disk. This
    35  		// controls the set of CRLs which will be published by this updater: it will
    36  		// publish one set of NumShards CRL shards for each issuer in this list.
    37  		IssuerCerts []string `validate:"min=1,dive,required"`
    38  
    39  		// NumShards is the number of shards into which each issuer's "full and
    40  		// complete" CRL will be split.
    41  		// WARNING: When this number is changed, the "JSON Array of CRL URLs" field
    42  		// in CCADB MUST be updated.
    43  		NumShards int `validate:"min=1"`
    44  
    45  		// ShardWidth is the amount of time (width on a timeline) that a single
    46  		// shard should cover. Ideally, NumShards*ShardWidth should be an amount of
    47  		// time noticeably larger than the current longest certificate lifetime,
    48  		// but the updater will continue to work if this is not the case (albeit
    49  		// with more confusing mappings of serials to shards).
    50  		// WARNING: When this number is changed, revocation entries will move
    51  		// between shards.
    52  		ShardWidth config.Duration `validate:"-"`
    53  
    54  		// LookbackPeriod is how far back the updater should look for revoked expired
    55  		// certificates. We are required to include every revoked cert in at least
    56  		// one CRL, even if it is revoked seconds before it expires, so this must
    57  		// always be greater than the UpdatePeriod, and should be increased when
    58  		// recovering from an outage to ensure continuity of coverage.
    59  		LookbackPeriod config.Duration `validate:"-"`
    60  
    61  		// UpdatePeriod controls how frequently the crl-updater runs and publishes
    62  		// new versions of every CRL shard. The Baseline Requirements, Section 4.9.7:
    63  		// "MUST update and publish a new CRL within twenty‐four (24) hours after
    64  		// recording a Certificate as revoked."
    65  		UpdatePeriod config.Duration
    66  
    67  		// UpdateTimeout controls how long a single CRL shard is allowed to attempt
    68  		// to update before being timed out. The total CRL updating process may take
    69  		// significantly longer, since a full update cycle may consist of updating
    70  		// many shards with varying degrees of parallelism. This value must be
    71  		// strictly less than the UpdatePeriod. Defaults to 10 minutes, one order
    72  		// of magnitude greater than our p99 update latency.
    73  		UpdateTimeout config.Duration `validate:"-"`
    74  
    75  		// MaxParallelism controls how many workers may be running in parallel.
    76  		// A higher value reduces the total time necessary to update all CRL shards
    77  		// that this updater is responsible for, but also increases the memory used
    78  		// by this updater. Only relevant in -runOnce mode.
    79  		MaxParallelism int `validate:"min=0"`
    80  
    81  		// MaxAttempts control how many times the updater will attempt to generate
    82  		// a single CRL shard. A higher number increases the likelihood of a fully
    83  		// successful run, but also increases the worst-case runtime and db/network
    84  		// load of said run. The default is 1.
    85  		MaxAttempts int `validate:"omitempty,min=1"`
    86  
    87  		// ExpiresMargin adds a small increment to the CRL's HTTP Expires time.
    88  		//
    89  		// When uploading a CRL, its Expires field in S3 is set to the expected time
    90  		// the next CRL will be uploaded (by this instance). That allows our CDN
    91  		// instances to cache for that long. However, since the next update might be
    92  		// slow or delayed, we add a margin of error.
    93  		//
    94  		// Tradeoffs: A large ExpiresMargin reduces the chance that a CRL becomes
    95  		// uncacheable and floods S3 with traffic (which might result in 503s while
    96  		// S3 scales out).
    97  		//
    98  		// A small ExpiresMargin means revocations become visible sooner, including
    99  		// admin-invoked revocations that may have a time requirement.
   100  		ExpiresMargin config.Duration
   101  
   102  		// CacheControl is a string passed verbatim to the crl-storer to store on
   103  		// the S3 object.
   104  		//
   105  		// Note: if this header contains max-age, it will override
   106  		// Expires. https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-freshness-lifet
   107  		// Cache-Control: max-age has the disadvantage that it caches for a fixed
   108  		// amount of time, regardless of how close the CRL is to replacement. So
   109  		// if max-age is used, the worst-case time for a revocation to become visible
   110  		// is UpdatePeriod + the value of max age.
   111  		//
   112  		// The stale-if-error and stale-while-revalidate headers may be useful here:
   113  		// https://aws.amazon.com/about-aws/whats-new/2023/05/amazon-cloudfront-stale-while-revalidate-stale-if-error-cache-control-directives/
   114  		//
   115  		// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
   116  		CacheControl string
   117  
   118  		Features features.Config
   119  	}
   120  
   121  	Syslog        cmd.SyslogConfig
   122  	OpenTelemetry cmd.OpenTelemetryConfig
   123  }
   124  
   125  func main() {
   126  	configFile := flag.String("config", "", "File path to the configuration file for this service")
   127  	debugAddr := flag.String("debug-addr", "", "Debug server address override")
   128  	runOnce := flag.Bool("runOnce", false, "If true, run once immediately and then exit")
   129  	flag.Parse()
   130  	if *configFile == "" {
   131  		flag.Usage()
   132  		os.Exit(1)
   133  	}
   134  
   135  	var c Config
   136  	err := cmd.ReadConfigFile(*configFile, &c)
   137  	cmd.FailOnError(err, "Reading JSON config file into config structure")
   138  
   139  	if *debugAddr != "" {
   140  		c.CRLUpdater.DebugAddr = *debugAddr
   141  	}
   142  
   143  	features.Set(c.CRLUpdater.Features)
   144  
   145  	scope, logger, oTelShutdown := cmd.StatsAndLogging(c.Syslog, c.OpenTelemetry, c.CRLUpdater.DebugAddr)
   146  	defer oTelShutdown(context.Background())
   147  	logger.Info(cmd.VersionString())
   148  	clk := clock.New()
   149  
   150  	tlsConfig, err := c.CRLUpdater.TLS.Load(scope)
   151  	cmd.FailOnError(err, "TLS config")
   152  
   153  	issuers := make([]*issuance.Certificate, 0, len(c.CRLUpdater.IssuerCerts))
   154  	for _, filepath := range c.CRLUpdater.IssuerCerts {
   155  		cert, err := issuance.LoadCertificate(filepath)
   156  		cmd.FailOnError(err, "Failed to load issuer cert")
   157  		issuers = append(issuers, cert)
   158  	}
   159  
   160  	if c.CRLUpdater.ShardWidth.Duration == 0 {
   161  		c.CRLUpdater.ShardWidth.Duration = 16 * time.Hour
   162  	}
   163  	if c.CRLUpdater.LookbackPeriod.Duration == 0 {
   164  		c.CRLUpdater.LookbackPeriod.Duration = 24 * time.Hour
   165  	}
   166  	if c.CRLUpdater.UpdateTimeout.Duration == 0 {
   167  		c.CRLUpdater.UpdateTimeout.Duration = 10 * time.Minute
   168  	}
   169  
   170  	saConn, err := bgrpc.ClientSetup(c.CRLUpdater.SAService, tlsConfig, scope, clk)
   171  	cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA")
   172  	sac := sapb.NewStorageAuthorityClient(saConn)
   173  
   174  	caConn, err := bgrpc.ClientSetup(c.CRLUpdater.CRLGeneratorService, tlsConfig, scope, clk)
   175  	cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to CRLGenerator")
   176  	cac := capb.NewCRLGeneratorClient(caConn)
   177  
   178  	csConn, err := bgrpc.ClientSetup(c.CRLUpdater.CRLStorerService, tlsConfig, scope, clk)
   179  	cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to CRLStorer")
   180  	csc := cspb.NewCRLStorerClient(csConn)
   181  
   182  	u, err := updater.NewUpdater(
   183  		issuers,
   184  		c.CRLUpdater.NumShards,
   185  		c.CRLUpdater.ShardWidth.Duration,
   186  		c.CRLUpdater.LookbackPeriod.Duration,
   187  		c.CRLUpdater.UpdatePeriod.Duration,
   188  		c.CRLUpdater.UpdateTimeout.Duration,
   189  		c.CRLUpdater.MaxParallelism,
   190  		c.CRLUpdater.MaxAttempts,
   191  		c.CRLUpdater.CacheControl,
   192  		c.CRLUpdater.ExpiresMargin.Duration,
   193  		sac,
   194  		cac,
   195  		csc,
   196  		scope,
   197  		logger,
   198  		clk,
   199  	)
   200  	cmd.FailOnError(err, "Failed to create crl-updater")
   201  
   202  	ctx, cancel := context.WithCancel(context.Background())
   203  	go cmd.CatchSignals(cancel)
   204  
   205  	if *runOnce {
   206  		err = u.RunOnce(ctx)
   207  		if err != nil && !errors.Is(err, context.Canceled) {
   208  			cmd.FailOnError(err, "")
   209  		}
   210  	} else {
   211  		err = u.Run(ctx)
   212  		if err != nil && !errors.Is(err, context.Canceled) {
   213  			cmd.FailOnError(err, "")
   214  		}
   215  	}
   216  }
   217  
   218  func init() {
   219  	cmd.RegisterCommand("crl-updater", main, &cmd.ConfigValidator{Config: &Config{}})
   220  }