...

Source file src/github.com/letsencrypt/boulder/cmd/ocsp-responder/main.go

Documentation: github.com/letsencrypt/boulder/cmd/ocsp-responder

     1  package notmain
     2  
     3  import (
     4  	"context"
     5  	"flag"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"os"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/prometheus/client_golang/prometheus"
    14  	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    15  
    16  	"github.com/letsencrypt/boulder/cmd"
    17  	"github.com/letsencrypt/boulder/config"
    18  	"github.com/letsencrypt/boulder/db"
    19  	"github.com/letsencrypt/boulder/features"
    20  	bgrpc "github.com/letsencrypt/boulder/grpc"
    21  	"github.com/letsencrypt/boulder/issuance"
    22  	blog "github.com/letsencrypt/boulder/log"
    23  	"github.com/letsencrypt/boulder/metrics/measured_http"
    24  	"github.com/letsencrypt/boulder/ocsp/responder"
    25  	"github.com/letsencrypt/boulder/ocsp/responder/live"
    26  	redis_responder "github.com/letsencrypt/boulder/ocsp/responder/redis"
    27  	rapb "github.com/letsencrypt/boulder/ra/proto"
    28  	rocsp_config "github.com/letsencrypt/boulder/rocsp/config"
    29  	"github.com/letsencrypt/boulder/sa"
    30  	sapb "github.com/letsencrypt/boulder/sa/proto"
    31  )
    32  
    33  type Config struct {
    34  	OCSPResponder struct {
    35  		DebugAddr string       `validate:"hostname_port"`
    36  		DB        cmd.DBConfig `validate:"required_without_all=Source SAService,structonly"`
    37  
    38  		// Source indicates the source of pre-signed OCSP responses to be used. It
    39  		// can be a DBConnect string or a file URL. The file URL style is used
    40  		// when responding from a static file for intermediates and roots.
    41  		// If DBConfig has non-empty fields, it takes precedence over this.
    42  		Source string `validate:"required_without_all=DB.DBConnectFile SAService Redis"`
    43  
    44  		// The list of issuer certificates, against which OCSP requests/responses
    45  		// are checked to ensure we're not responding for anyone else's certs.
    46  		IssuerCerts []string `validate:"min=1,dive,required"`
    47  
    48  		Path string
    49  
    50  		// ListenAddress is the address:port on which to listen for incoming
    51  		// OCSP requests. This has a default value of ":80".
    52  		ListenAddress string `validate:"omitempty,hostname_port"`
    53  
    54  		// When to timeout a request. This should be slightly lower than the
    55  		// upstream's timeout when making request to ocsp-responder.
    56  		Timeout config.Duration `validate:"-"`
    57  
    58  		// How often a response should be signed when using Redis/live-signing
    59  		// path. This has a default value of 60h.
    60  		LiveSigningPeriod config.Duration `validate:"-"`
    61  
    62  		// A limit on how many requests to the RA (and onwards to the CA) will
    63  		// be made to sign responses that are not fresh in the cache. This
    64  		// should be set to somewhat less than
    65  		// (HSM signing capacity) / (number of ocsp-responders).
    66  		// Requests that would exceed this limit will block until capacity is
    67  		// available and eventually serve an HTTP 500 Internal Server Error.
    68  		// This has a default value of 1000.
    69  		MaxInflightSignings int `validate:"min=0"`
    70  
    71  		// A limit on how many goroutines can be waiting for a signing slot at
    72  		// a time. When this limit is exceeded, additional signing requests
    73  		// will immediately serve an HTTP 500 Internal Server Error until
    74  		// we are back below the limit. This provides load shedding for when
    75  		// inbound requests arrive faster than our ability to sign them.
    76  		// The default of 0 means "no limit." A good value for this is the
    77  		// longest queue we can expect to process before a timeout. For
    78  		// instance, if the timeout is 5 seconds, and a signing takes 20ms,
    79  		// and we have MaxInflightSignings = 40, we can expect to process
    80  		// 40 * 5 / 0.02 = 10,000 requests before the oldest request times out.
    81  		MaxSigningWaiters int `validate:"min=0"`
    82  
    83  		ShutdownStopTimeout config.Duration
    84  
    85  		RequiredSerialPrefixes []string `validate:"omitempty,dive,hexadecimal"`
    86  
    87  		Features map[string]bool
    88  
    89  		// Configuration for using Redis as a cache. This configuration should
    90  		// allow for both read and write access.
    91  		Redis *rocsp_config.RedisConfig `validate:"required_without=Source"`
    92  
    93  		// TLS client certificate, private key, and trusted root bundle.
    94  		TLS cmd.TLSConfig `validate:"required_without=Source,structonly"`
    95  
    96  		// RAService configures how to communicate with the RA when it is necessary
    97  		// to generate a fresh OCSP response.
    98  		RAService *cmd.GRPCClientConfig
    99  
   100  		// SAService configures how to communicate with the SA to look up
   101  		// certificate status metadata used to confirm/deny that the response from
   102  		// Redis is up-to-date.
   103  		SAService *cmd.GRPCClientConfig `validate:"required_without_all=DB.DBConnectFile Source"`
   104  
   105  		// LogSampleRate sets how frequently error logs should be emitted. This
   106  		// avoids flooding the logs during outages. 1 out of N log lines will be emitted.
   107  		// If LogSampleRate is 0, no logs will be emitted.
   108  		LogSampleRate int `validate:"min=0"`
   109  	}
   110  
   111  	Syslog        cmd.SyslogConfig
   112  	OpenTelemetry cmd.OpenTelemetryConfig
   113  
   114  	// OpenTelemetryHTTPConfig configures tracing on incoming HTTP requests
   115  	OpenTelemetryHTTPConfig cmd.OpenTelemetryHTTPConfig
   116  }
   117  
   118  func main() {
   119  	configFile := flag.String("config", "", "File path to the configuration file for this service")
   120  	flag.Parse()
   121  
   122  	if *configFile == "" {
   123  		fmt.Fprintf(os.Stderr, `Usage of %s:
   124  Config JSON should contain either a DBConnectFile or a Source value containing a file: URL.
   125  If Source is a file: URL, the file should contain a list of OCSP responses in base64-encoded DER,
   126  as generated by Boulder's ceremony command.
   127  `, os.Args[0])
   128  		flag.PrintDefaults()
   129  		os.Exit(1)
   130  	}
   131  
   132  	var c Config
   133  	err := cmd.ReadConfigFile(*configFile, &c)
   134  	cmd.FailOnError(err, "Reading JSON config file into config structure")
   135  	err = features.Set(c.OCSPResponder.Features)
   136  	cmd.FailOnError(err, "Failed to set feature flags")
   137  
   138  	scope, logger, oTelShutdown := cmd.StatsAndLogging(c.Syslog, c.OpenTelemetry, c.OCSPResponder.DebugAddr)
   139  	logger.Info(cmd.VersionString())
   140  
   141  	clk := cmd.Clock()
   142  
   143  	var source responder.Source
   144  
   145  	if strings.HasPrefix(c.OCSPResponder.Source, "file:") {
   146  		url, err := url.Parse(c.OCSPResponder.Source)
   147  		cmd.FailOnError(err, "Source was not a URL")
   148  		filename := url.Path
   149  		// Go interprets cwd-relative file urls (file:test/foo.txt) as having the
   150  		// relative part of the path in the 'Opaque' field.
   151  		if filename == "" {
   152  			filename = url.Opaque
   153  		}
   154  		source, err = responder.NewMemorySourceFromFile(filename, logger)
   155  		cmd.FailOnError(err, fmt.Sprintf("Couldn't read file: %s", url.Path))
   156  	} else {
   157  		// Set up the redis source and the combined multiplex source.
   158  		rocspRWClient, err := rocsp_config.MakeClient(c.OCSPResponder.Redis, clk, scope)
   159  		cmd.FailOnError(err, "Could not make redis client")
   160  
   161  		err = rocspRWClient.Ping(context.Background())
   162  		cmd.FailOnError(err, "pinging Redis")
   163  
   164  		liveSigningPeriod := c.OCSPResponder.LiveSigningPeriod.Duration
   165  		if liveSigningPeriod == 0 {
   166  			liveSigningPeriod = 60 * time.Hour
   167  		}
   168  
   169  		tlsConfig, err := c.OCSPResponder.TLS.Load(scope)
   170  		cmd.FailOnError(err, "TLS config")
   171  
   172  		raConn, err := bgrpc.ClientSetup(c.OCSPResponder.RAService, tlsConfig, scope, clk)
   173  		cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to RA")
   174  		rac := rapb.NewRegistrationAuthorityClient(raConn)
   175  
   176  		maxInflight := c.OCSPResponder.MaxInflightSignings
   177  		if maxInflight == 0 {
   178  			maxInflight = 1000
   179  		}
   180  		liveSource := live.New(rac, int64(maxInflight), c.OCSPResponder.MaxSigningWaiters)
   181  
   182  		rocspSource, err := redis_responder.NewRedisSource(rocspRWClient, liveSource, liveSigningPeriod, clk, scope, logger, c.OCSPResponder.LogSampleRate)
   183  		cmd.FailOnError(err, "Could not create redis source")
   184  
   185  		var dbMap *db.WrappedMap
   186  		if c.OCSPResponder.DB != (cmd.DBConfig{}) {
   187  			dbMap, err = sa.InitWrappedDb(c.OCSPResponder.DB, scope, logger)
   188  			cmd.FailOnError(err, "While initializing dbMap")
   189  		}
   190  
   191  		var sac sapb.StorageAuthorityReadOnlyClient
   192  		if c.OCSPResponder.SAService != nil {
   193  			saConn, err := bgrpc.ClientSetup(c.OCSPResponder.SAService, tlsConfig, scope, clk)
   194  			cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA")
   195  			sac = sapb.NewStorageAuthorityReadOnlyClient(saConn)
   196  		}
   197  
   198  		source, err = redis_responder.NewCheckedRedisSource(rocspSource, dbMap, sac, scope, logger)
   199  		cmd.FailOnError(err, "Could not create checkedRedis source")
   200  	}
   201  
   202  	// Load the certificate from the file path.
   203  	issuerCerts := make([]*issuance.Certificate, len(c.OCSPResponder.IssuerCerts))
   204  	for i, issuerFile := range c.OCSPResponder.IssuerCerts {
   205  		issuerCert, err := issuance.LoadCertificate(issuerFile)
   206  		cmd.FailOnError(err, "Could not load issuer cert")
   207  		issuerCerts[i] = issuerCert
   208  	}
   209  
   210  	source, err = responder.NewFilterSource(
   211  		issuerCerts,
   212  		c.OCSPResponder.RequiredSerialPrefixes,
   213  		source,
   214  		scope,
   215  		logger,
   216  		clk,
   217  	)
   218  	cmd.FailOnError(err, "Could not create filtered source")
   219  
   220  	m := mux(c.OCSPResponder.Path, source, c.OCSPResponder.Timeout.Duration, scope, c.OpenTelemetryHTTPConfig.Options(), logger, c.OCSPResponder.LogSampleRate)
   221  
   222  	srv := &http.Server{
   223  		ReadTimeout:  30 * time.Second,
   224  		WriteTimeout: 120 * time.Second,
   225  		IdleTimeout:  120 * time.Second,
   226  		Addr:         c.OCSPResponder.ListenAddress,
   227  		Handler:      m,
   228  	}
   229  
   230  	err = srv.ListenAndServe()
   231  	if err != nil && err != http.ErrServerClosed {
   232  		cmd.FailOnError(err, "Running HTTP server")
   233  	}
   234  
   235  	// When main is ready to exit (because it has received a shutdown signal),
   236  	// gracefully shutdown the servers. Calling these shutdown functions causes
   237  	// ListenAndServe() to immediately return, cleaning up the server goroutines
   238  	// as well, then waits for any lingering connection-handing goroutines to
   239  	// finish and clean themselves up.
   240  	defer func() {
   241  		ctx, cancel := context.WithTimeout(context.Background(),
   242  			c.OCSPResponder.ShutdownStopTimeout.Duration)
   243  		defer cancel()
   244  		_ = srv.Shutdown(ctx)
   245  		oTelShutdown(ctx)
   246  	}()
   247  
   248  	cmd.WaitForSignal()
   249  }
   250  
   251  // ocspMux partially implements the interface defined for http.ServeMux but doesn't implement
   252  // the path cleaning its Handler method does. Notably http.ServeMux will collapse repeated
   253  // slashes into a single slash which breaks the base64 encoding that is used in OCSP GET
   254  // requests. ocsp.Responder explicitly recommends against using http.ServeMux
   255  // for this reason.
   256  type ocspMux struct {
   257  	handler http.Handler
   258  }
   259  
   260  func (om *ocspMux) Handler(_ *http.Request) (http.Handler, string) {
   261  	return om.handler, "/"
   262  }
   263  
   264  func mux(responderPath string, source responder.Source, timeout time.Duration, stats prometheus.Registerer, oTelHTTPOptions []otelhttp.Option, logger blog.Logger, sampleRate int) http.Handler {
   265  	stripPrefix := http.StripPrefix(responderPath, responder.NewResponder(source, timeout, stats, logger, sampleRate))
   266  	h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   267  		if r.Method == "GET" && r.URL.Path == "/" {
   268  			w.Header().Set("Cache-Control", "max-age=43200") // Cache for 12 hours
   269  			w.WriteHeader(200)
   270  			return
   271  		}
   272  		stripPrefix.ServeHTTP(w, r)
   273  	})
   274  	return measured_http.New(&ocspMux{h}, cmd.Clock(), stats, oTelHTTPOptions...)
   275  }
   276  
   277  func init() {
   278  	cmd.RegisterCommand("ocsp-responder", main, &cmd.ConfigValidator{Config: &Config{}})
   279  }
   280  

View as plain text