From 0bece2df7d8c0d19ce385c374c0cd369f4af3f62 Mon Sep 17 00:00:00 2001 From: Dave Marquard Date: Fri, 29 Jan 2021 18:44:40 -0800 Subject: [PATCH] Database backed LetsEncrypt certificate cache (#993) --- config/config_test.go | 35 ----------------- config/options.go | 9 ----- config/parser.go | 2 - database/migrations.go | 10 +++++ miniflux.1 | 5 --- packaging/systemd/miniflux.service | 2 +- service/httpd/httpd.go | 9 ++--- storage/cache.go | 63 ++++++++++++++++++++++++++++++ 8 files changed, 78 insertions(+), 57 deletions(-) create mode 100644 storage/cache.go diff --git a/config/config_test.go b/config/config_test.go index e5734e50..0141be4e 100644 --- a/config/config_test.go +++ b/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() diff --git a/config/options.go b/config/options.go index 8448455b..c423a3b0 100644 --- a/config/options.go +++ b/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, diff --git a/config/parser.go b/config/parser.go index c7a422af..f6e351e0 100644 --- a/config/parser.go +++ b/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": diff --git a/database/migrations.go b/database/migrations.go index bb0543e8..7ddd39a2 100644 --- a/database/migrations.go +++ b/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 + }, } diff --git a/miniflux.1 b/miniflux.1 index 303d3c81..c8c710f7 100644 --- a/miniflux.1 +++ b/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 diff --git a/packaging/systemd/miniflux.service b/packaging/systemd/miniflux.service index 7123465a..0e0bb1f9 100644 --- a/packaging/systemd/miniflux.service +++ b/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 diff --git a/service/httpd/httpd.go b/service/httpd/httpd.go index 3022231d..0482e619 100644 --- a/service/httpd/httpd.go +++ b/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), } diff --git a/storage/cache.go b/storage/cache.go new file mode 100644 index 00000000..5b8c8d0b --- /dev/null +++ b/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 +}