Răsfoiți Sursa

Database backed LetsEncrypt certificate cache (#993)

Dave Marquard 5 ani în urmă
părinte
comite
0bece2df7d

+ 0 - 35
config/config_test.go

@@ -409,41 +409,6 @@ func TestDefaultCertDomainValue(t *testing.T) {
 	}
 }
 
-func TestCertCache(t *testing.T) {
-	os.Clearenv()
-	os.Setenv("CERT_CACHE", "foobar")
-
-	parser := NewParser()
-	opts, err := parser.ParseEnvironmentVariables()
-	if err != nil {
-		t.Fatalf(`Parsing failure: %v`, err)
-	}
-
-	expected := "foobar"
-	result := opts.CertCache()
-
-	if result != expected {
-		t.Fatalf(`Unexpected CERT_CACHE value, got %q instead of %q`, result, expected)
-	}
-}
-
-func TestDefaultCertCacheValue(t *testing.T) {
-	os.Clearenv()
-
-	parser := NewParser()
-	opts, err := parser.ParseEnvironmentVariables()
-	if err != nil {
-		t.Fatalf(`Parsing failure: %v`, err)
-	}
-
-	expected := defaultCertCache
-	result := opts.CertCache()
-
-	if result != expected {
-		t.Fatalf(`Unexpected CERT_CACHE value, got %q instead of %q`, result, expected)
-	}
-}
-
 func TestDefaultCleanupFrequencyHoursValue(t *testing.T) {
 	os.Clearenv()
 

+ 0 - 9
config/options.go

@@ -38,7 +38,6 @@ const (
 	defaultCertFile                           = ""
 	defaultKeyFile                            = ""
 	defaultCertDomain                         = ""
-	defaultCertCache                          = "/tmp/cert_cache"
 	defaultCleanupFrequencyHours              = 24
 	defaultCleanupArchiveReadDays             = 60
 	defaultCleanupArchiveUnreadDays           = 180
@@ -93,7 +92,6 @@ type Options struct {
 	listenAddr                         string
 	certFile                           string
 	certDomain                         string
-	certCache                          string
 	certKeyFile                        string
 	cleanupFrequencyHours              int
 	cleanupArchiveReadDays             int
@@ -150,7 +148,6 @@ func NewOptions() *Options {
 		listenAddr:                         defaultListenAddr,
 		certFile:                           defaultCertFile,
 		certDomain:                         defaultCertDomain,
-		certCache:                          defaultCertCache,
 		certKeyFile:                        defaultKeyFile,
 		cleanupFrequencyHours:              defaultCleanupFrequencyHours,
 		cleanupArchiveReadDays:             defaultCleanupArchiveReadDays,
@@ -266,11 +263,6 @@ func (o *Options) CertDomain() string {
 	return o.certDomain
 }
 
-// CertCache returns the directory to use for Let's Encrypt session cache.
-func (o *Options) CertCache() string {
-	return o.certCache
-}
-
 // CleanupFrequencyHours returns the interval in hours for cleanup jobs.
 func (o *Options) CleanupFrequencyHours() int {
 	return o.cleanupFrequencyHours
@@ -466,7 +458,6 @@ func (o *Options) SortedOptions() []*Option {
 		"BASE_PATH":                              o.basePath,
 		"BASE_URL":                               o.baseURL,
 		"BATCH_SIZE":                             o.batchSize,
-		"CERT_CACHE":                             o.certCache,
 		"CERT_DOMAIN":                            o.certDomain,
 		"CERT_FILE":                              o.certFile,
 		"CLEANUP_ARCHIVE_READ_DAYS":              o.cleanupArchiveReadDays,

+ 0 - 2
config/parser.go

@@ -112,8 +112,6 @@ func (p *Parser) parseLines(lines []string) (err error) {
 			p.opts.certKeyFile = parseString(value, defaultKeyFile)
 		case "CERT_DOMAIN":
 			p.opts.certDomain = parseString(value, defaultCertDomain)
-		case "CERT_CACHE":
-			p.opts.certCache = parseString(value, defaultCertCache)
 		case "CLEANUP_FREQUENCY_HOURS":
 			p.opts.cleanupFrequencyHours = parseInt(value, defaultCleanupFrequencyHours)
 		case "CLEANUP_ARCHIVE_READ_DAYS":

+ 10 - 0
database/migrations.go

@@ -504,4 +504,14 @@ var migrations = []func(tx *sql.Tx) error{
 		`)
 		return err
 	},
+	func(tx *sql.Tx) (err error) {
+		_, err = tx.Exec(`
+			CREATE TABLE acme_cache (
+				key varchar(400) not null primary key,
+				data bytea not null,
+				updated_at timestamptz not null
+			);
+		`)
+		return err
+	},
 }

+ 0 - 5
miniflux.1

@@ -245,11 +245,6 @@ Use Let's Encrypt to get automatically a certificate for this domain\&.
 .br
 Default is empty\&.
 .TP
-.B CERT_CACHE
-Let's Encrypt cache directory\&.
-.br
-Default is /tmp/cert_cache\&.
-.TP
 .B METRICS_COLLECTOR
 Set to 1 to enable metrics collector. Expose a /metrics endpoint for Prometheus.
 .br

+ 1 - 1
packaging/systemd/miniflux.service

@@ -48,7 +48,7 @@ ReadWritePaths=/run
 # https://www.freedesktop.org/software/systemd/man/systemd.exec.html#AmbientCapabilities=
 AmbientCapabilities=CAP_NET_BIND_SERVICE
 
-# Provide a private /tmp for CERT_CACHE (required when using Let's Encrypt)
+# Provide a private /tmp
 # https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateTmp=
 PrivateTmp=true
 

+ 4 - 5
service/httpd/httpd.go

@@ -33,7 +33,6 @@ func Serve(store *storage.Storage, pool *worker.Pool) *http.Server {
 	certFile := config.Opts.CertFile()
 	keyFile := config.Opts.CertKeyFile()
 	certDomain := config.Opts.CertDomain()
-	certCache := config.Opts.CertCache()
 	listenAddr := config.Opts.ListenAddr()
 	server := &http.Server{
 		ReadTimeout:  300 * time.Second,
@@ -47,9 +46,9 @@ func Serve(store *storage.Storage, pool *worker.Pool) *http.Server {
 		startSystemdSocketServer(server)
 	case strings.HasPrefix(listenAddr, "/"):
 		startUnixSocketServer(server, listenAddr)
-	case certDomain != "" && certCache != "":
+	case certDomain != "":
 		config.Opts.HTTPS = true
-		startAutoCertTLSServer(server, certDomain, certCache)
+		startAutoCertTLSServer(server, certDomain, store)
 	case certFile != "" && keyFile != "":
 		config.Opts.HTTPS = true
 		server.Addr = listenAddr
@@ -119,10 +118,10 @@ func tlsConfig() *tls.Config {
 	}
 }
 
-func startAutoCertTLSServer(server *http.Server, certDomain, certCache string) {
+func startAutoCertTLSServer(server *http.Server, certDomain string, store *storage.Storage) {
 	server.Addr = ":https"
 	certManager := autocert.Manager{
-		Cache:      autocert.DirCache(certCache),
+		Cache:      storage.NewCache(store),
 		Prompt:     autocert.AcceptTOS,
 		HostPolicy: autocert.HostWhitelist(certDomain),
 	}

+ 63 - 0
storage/cache.go

@@ -0,0 +1,63 @@
+// Copyright 2020 Dave Marquard. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package storage // import "miniflux.app/storage"
+
+import (
+	"context"
+	"database/sql"
+
+	"golang.org/x/crypto/acme/autocert"
+)
+
+// Making sure that we're adhering to the autocert.Cache interface.
+var _ autocert.Cache = (*Cache)(nil)
+
+// Cache provides a SQL backend to the autocert cache.
+type Cache struct {
+	storage *Storage
+}
+
+// NewCache creates an cache instance that can be used with autocert.Cache.
+// It returns any errors that could happen while connecting to SQL.
+func NewCache(storage *Storage) *Cache {
+	return &Cache{
+		storage: storage,
+	}
+}
+
+// Get returns a certificate data for the specified key.
+// If there's no such key, Get returns ErrCacheMiss.
+func (c *Cache) Get(ctx context.Context, key string) ([]byte, error) {
+	query := `SELECT data::bytea FROM acme_cache WHERE key = $1`
+	var data []byte
+	err := c.storage.db.QueryRowContext(ctx, query, key).Scan(&data)
+	if err == sql.ErrNoRows {
+		return nil, autocert.ErrCacheMiss
+	}
+
+	return data, err
+}
+
+// Put stores the data in the cache under the specified key.
+func (c *Cache) Put(ctx context.Context, key string, data []byte) error {
+	query := `INSERT INTO acme_cache (key, data, updated_at) VALUES($1, $2::bytea, now())
+	          ON CONFLICT (key) DO UPDATE SET data = $2::bytea, updated_at = now()`
+	_, err := c.storage.db.ExecContext(ctx, query, key, data)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// Delete removes a certificate data from the cache under the specified key.
+// If there's no such key in the cache, Delete returns nil.
+func (c *Cache) Delete(ctx context.Context, key string) error {
+	query := `DELETE FROM acme_cache WHERE key = $1`
+	_, err := c.storage.db.ExecContext(ctx, query, key)
+	if err != nil {
+		return err
+	}
+	return nil
+}