Skip to content

Commit 0bece2d

Browse files
authored
Database backed LetsEncrypt certificate cache (#993)
1 parent 4464802 commit 0bece2d

File tree

8 files changed

+78
-57
lines changed

8 files changed

+78
-57
lines changed

config/config_test.go

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -409,41 +409,6 @@ func TestDefaultCertDomainValue(t *testing.T) {
409409
}
410410
}
411411

412-
func TestCertCache(t *testing.T) {
413-
os.Clearenv()
414-
os.Setenv("CERT_CACHE", "foobar")
415-
416-
parser := NewParser()
417-
opts, err := parser.ParseEnvironmentVariables()
418-
if err != nil {
419-
t.Fatalf(`Parsing failure: %v`, err)
420-
}
421-
422-
expected := "foobar"
423-
result := opts.CertCache()
424-
425-
if result != expected {
426-
t.Fatalf(`Unexpected CERT_CACHE value, got %q instead of %q`, result, expected)
427-
}
428-
}
429-
430-
func TestDefaultCertCacheValue(t *testing.T) {
431-
os.Clearenv()
432-
433-
parser := NewParser()
434-
opts, err := parser.ParseEnvironmentVariables()
435-
if err != nil {
436-
t.Fatalf(`Parsing failure: %v`, err)
437-
}
438-
439-
expected := defaultCertCache
440-
result := opts.CertCache()
441-
442-
if result != expected {
443-
t.Fatalf(`Unexpected CERT_CACHE value, got %q instead of %q`, result, expected)
444-
}
445-
}
446-
447412
func TestDefaultCleanupFrequencyHoursValue(t *testing.T) {
448413
os.Clearenv()
449414

config/options.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ const (
3838
defaultCertFile = ""
3939
defaultKeyFile = ""
4040
defaultCertDomain = ""
41-
defaultCertCache = "/tmp/cert_cache"
4241
defaultCleanupFrequencyHours = 24
4342
defaultCleanupArchiveReadDays = 60
4443
defaultCleanupArchiveUnreadDays = 180
@@ -93,7 +92,6 @@ type Options struct {
9392
listenAddr string
9493
certFile string
9594
certDomain string
96-
certCache string
9795
certKeyFile string
9896
cleanupFrequencyHours int
9997
cleanupArchiveReadDays int
@@ -150,7 +148,6 @@ func NewOptions() *Options {
150148
listenAddr: defaultListenAddr,
151149
certFile: defaultCertFile,
152150
certDomain: defaultCertDomain,
153-
certCache: defaultCertCache,
154151
certKeyFile: defaultKeyFile,
155152
cleanupFrequencyHours: defaultCleanupFrequencyHours,
156153
cleanupArchiveReadDays: defaultCleanupArchiveReadDays,
@@ -266,11 +263,6 @@ func (o *Options) CertDomain() string {
266263
return o.certDomain
267264
}
268265

269-
// CertCache returns the directory to use for Let's Encrypt session cache.
270-
func (o *Options) CertCache() string {
271-
return o.certCache
272-
}
273-
274266
// CleanupFrequencyHours returns the interval in hours for cleanup jobs.
275267
func (o *Options) CleanupFrequencyHours() int {
276268
return o.cleanupFrequencyHours
@@ -466,7 +458,6 @@ func (o *Options) SortedOptions() []*Option {
466458
"BASE_PATH": o.basePath,
467459
"BASE_URL": o.baseURL,
468460
"BATCH_SIZE": o.batchSize,
469-
"CERT_CACHE": o.certCache,
470461
"CERT_DOMAIN": o.certDomain,
471462
"CERT_FILE": o.certFile,
472463
"CLEANUP_ARCHIVE_READ_DAYS": o.cleanupArchiveReadDays,

config/parser.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,6 @@ func (p *Parser) parseLines(lines []string) (err error) {
112112
p.opts.certKeyFile = parseString(value, defaultKeyFile)
113113
case "CERT_DOMAIN":
114114
p.opts.certDomain = parseString(value, defaultCertDomain)
115-
case "CERT_CACHE":
116-
p.opts.certCache = parseString(value, defaultCertCache)
117115
case "CLEANUP_FREQUENCY_HOURS":
118116
p.opts.cleanupFrequencyHours = parseInt(value, defaultCleanupFrequencyHours)
119117
case "CLEANUP_ARCHIVE_READ_DAYS":

database/migrations.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,4 +504,14 @@ var migrations = []func(tx *sql.Tx) error{
504504
`)
505505
return err
506506
},
507+
func(tx *sql.Tx) (err error) {
508+
_, err = tx.Exec(`
509+
CREATE TABLE acme_cache (
510+
key varchar(400) not null primary key,
511+
data bytea not null,
512+
updated_at timestamptz not null
513+
);
514+
`)
515+
return err
516+
},
507517
}

miniflux.1

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -245,11 +245,6 @@ Use Let's Encrypt to get automatically a certificate for this domain\&.
245245
.br
246246
Default is empty\&.
247247
.TP
248-
.B CERT_CACHE
249-
Let's Encrypt cache directory\&.
250-
.br
251-
Default is /tmp/cert_cache\&.
252-
.TP
253248
.B METRICS_COLLECTOR
254249
Set to 1 to enable metrics collector. Expose a /metrics endpoint for Prometheus.
255250
.br

packaging/systemd/miniflux.service

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ ReadWritePaths=/run
4848
# https://www.freedesktop.org/software/systemd/man/systemd.exec.html#AmbientCapabilities=
4949
AmbientCapabilities=CAP_NET_BIND_SERVICE
5050

51-
# Provide a private /tmp for CERT_CACHE (required when using Let's Encrypt)
51+
# Provide a private /tmp
5252
# https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateTmp=
5353
PrivateTmp=true
5454

service/httpd/httpd.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ func Serve(store *storage.Storage, pool *worker.Pool) *http.Server {
3333
certFile := config.Opts.CertFile()
3434
keyFile := config.Opts.CertKeyFile()
3535
certDomain := config.Opts.CertDomain()
36-
certCache := config.Opts.CertCache()
3736
listenAddr := config.Opts.ListenAddr()
3837
server := &http.Server{
3938
ReadTimeout: 300 * time.Second,
@@ -47,9 +46,9 @@ func Serve(store *storage.Storage, pool *worker.Pool) *http.Server {
4746
startSystemdSocketServer(server)
4847
case strings.HasPrefix(listenAddr, "/"):
4948
startUnixSocketServer(server, listenAddr)
50-
case certDomain != "" && certCache != "":
49+
case certDomain != "":
5150
config.Opts.HTTPS = true
52-
startAutoCertTLSServer(server, certDomain, certCache)
51+
startAutoCertTLSServer(server, certDomain, store)
5352
case certFile != "" && keyFile != "":
5453
config.Opts.HTTPS = true
5554
server.Addr = listenAddr
@@ -119,10 +118,10 @@ func tlsConfig() *tls.Config {
119118
}
120119
}
121120

122-
func startAutoCertTLSServer(server *http.Server, certDomain, certCache string) {
121+
func startAutoCertTLSServer(server *http.Server, certDomain string, store *storage.Storage) {
123122
server.Addr = ":https"
124123
certManager := autocert.Manager{
125-
Cache: autocert.DirCache(certCache),
124+
Cache: storage.NewCache(store),
126125
Prompt: autocert.AcceptTOS,
127126
HostPolicy: autocert.HostWhitelist(certDomain),
128127
}

storage/cache.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright 2020 Dave Marquard. All rights reserved.
2+
// Use of this source code is governed by the Apache 2.0
3+
// license that can be found in the LICENSE file.
4+
5+
package storage // import "miniflux.app/storage"
6+
7+
import (
8+
"context"
9+
"database/sql"
10+
11+
"golang.org/x/crypto/acme/autocert"
12+
)
13+
14+
// Making sure that we're adhering to the autocert.Cache interface.
15+
var _ autocert.Cache = (*Cache)(nil)
16+
17+
// Cache provides a SQL backend to the autocert cache.
18+
type Cache struct {
19+
storage *Storage
20+
}
21+
22+
// NewCache creates an cache instance that can be used with autocert.Cache.
23+
// It returns any errors that could happen while connecting to SQL.
24+
func NewCache(storage *Storage) *Cache {
25+
return &Cache{
26+
storage: storage,
27+
}
28+
}
29+
30+
// Get returns a certificate data for the specified key.
31+
// If there's no such key, Get returns ErrCacheMiss.
32+
func (c *Cache) Get(ctx context.Context, key string) ([]byte, error) {
33+
query := `SELECT data::bytea FROM acme_cache WHERE key = $1`
34+
var data []byte
35+
err := c.storage.db.QueryRowContext(ctx, query, key).Scan(&data)
36+
if err == sql.ErrNoRows {
37+
return nil, autocert.ErrCacheMiss
38+
}
39+
40+
return data, err
41+
}
42+
43+
// Put stores the data in the cache under the specified key.
44+
func (c *Cache) Put(ctx context.Context, key string, data []byte) error {
45+
query := `INSERT INTO acme_cache (key, data, updated_at) VALUES($1, $2::bytea, now())
46+
ON CONFLICT (key) DO UPDATE SET data = $2::bytea, updated_at = now()`
47+
_, err := c.storage.db.ExecContext(ctx, query, key, data)
48+
if err != nil {
49+
return err
50+
}
51+
return nil
52+
}
53+
54+
// Delete removes a certificate data from the cache under the specified key.
55+
// If there's no such key in the cache, Delete returns nil.
56+
func (c *Cache) Delete(ctx context.Context, key string) error {
57+
query := `DELETE FROM acme_cache WHERE key = $1`
58+
_, err := c.storage.db.ExecContext(ctx, query, key)
59+
if err != nil {
60+
return err
61+
}
62+
return nil
63+
}

0 commit comments

Comments
 (0)