Database backed LetsEncrypt certificate cache (#993)
This commit is contained in:
parent
4464802947
commit
0bece2df7d
8 changed files with 78 additions and 57 deletions
|
@ -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) {
|
func TestDefaultCleanupFrequencyHoursValue(t *testing.T) {
|
||||||
os.Clearenv()
|
os.Clearenv()
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,6 @@ const (
|
||||||
defaultCertFile = ""
|
defaultCertFile = ""
|
||||||
defaultKeyFile = ""
|
defaultKeyFile = ""
|
||||||
defaultCertDomain = ""
|
defaultCertDomain = ""
|
||||||
defaultCertCache = "/tmp/cert_cache"
|
|
||||||
defaultCleanupFrequencyHours = 24
|
defaultCleanupFrequencyHours = 24
|
||||||
defaultCleanupArchiveReadDays = 60
|
defaultCleanupArchiveReadDays = 60
|
||||||
defaultCleanupArchiveUnreadDays = 180
|
defaultCleanupArchiveUnreadDays = 180
|
||||||
|
@ -93,7 +92,6 @@ type Options struct {
|
||||||
listenAddr string
|
listenAddr string
|
||||||
certFile string
|
certFile string
|
||||||
certDomain string
|
certDomain string
|
||||||
certCache string
|
|
||||||
certKeyFile string
|
certKeyFile string
|
||||||
cleanupFrequencyHours int
|
cleanupFrequencyHours int
|
||||||
cleanupArchiveReadDays int
|
cleanupArchiveReadDays int
|
||||||
|
@ -150,7 +148,6 @@ func NewOptions() *Options {
|
||||||
listenAddr: defaultListenAddr,
|
listenAddr: defaultListenAddr,
|
||||||
certFile: defaultCertFile,
|
certFile: defaultCertFile,
|
||||||
certDomain: defaultCertDomain,
|
certDomain: defaultCertDomain,
|
||||||
certCache: defaultCertCache,
|
|
||||||
certKeyFile: defaultKeyFile,
|
certKeyFile: defaultKeyFile,
|
||||||
cleanupFrequencyHours: defaultCleanupFrequencyHours,
|
cleanupFrequencyHours: defaultCleanupFrequencyHours,
|
||||||
cleanupArchiveReadDays: defaultCleanupArchiveReadDays,
|
cleanupArchiveReadDays: defaultCleanupArchiveReadDays,
|
||||||
|
@ -266,11 +263,6 @@ func (o *Options) CertDomain() string {
|
||||||
return o.certDomain
|
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.
|
// CleanupFrequencyHours returns the interval in hours for cleanup jobs.
|
||||||
func (o *Options) CleanupFrequencyHours() int {
|
func (o *Options) CleanupFrequencyHours() int {
|
||||||
return o.cleanupFrequencyHours
|
return o.cleanupFrequencyHours
|
||||||
|
@ -466,7 +458,6 @@ func (o *Options) SortedOptions() []*Option {
|
||||||
"BASE_PATH": o.basePath,
|
"BASE_PATH": o.basePath,
|
||||||
"BASE_URL": o.baseURL,
|
"BASE_URL": o.baseURL,
|
||||||
"BATCH_SIZE": o.batchSize,
|
"BATCH_SIZE": o.batchSize,
|
||||||
"CERT_CACHE": o.certCache,
|
|
||||||
"CERT_DOMAIN": o.certDomain,
|
"CERT_DOMAIN": o.certDomain,
|
||||||
"CERT_FILE": o.certFile,
|
"CERT_FILE": o.certFile,
|
||||||
"CLEANUP_ARCHIVE_READ_DAYS": o.cleanupArchiveReadDays,
|
"CLEANUP_ARCHIVE_READ_DAYS": o.cleanupArchiveReadDays,
|
||||||
|
|
|
@ -112,8 +112,6 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||||
p.opts.certKeyFile = parseString(value, defaultKeyFile)
|
p.opts.certKeyFile = parseString(value, defaultKeyFile)
|
||||||
case "CERT_DOMAIN":
|
case "CERT_DOMAIN":
|
||||||
p.opts.certDomain = parseString(value, defaultCertDomain)
|
p.opts.certDomain = parseString(value, defaultCertDomain)
|
||||||
case "CERT_CACHE":
|
|
||||||
p.opts.certCache = parseString(value, defaultCertCache)
|
|
||||||
case "CLEANUP_FREQUENCY_HOURS":
|
case "CLEANUP_FREQUENCY_HOURS":
|
||||||
p.opts.cleanupFrequencyHours = parseInt(value, defaultCleanupFrequencyHours)
|
p.opts.cleanupFrequencyHours = parseInt(value, defaultCleanupFrequencyHours)
|
||||||
case "CLEANUP_ARCHIVE_READ_DAYS":
|
case "CLEANUP_ARCHIVE_READ_DAYS":
|
||||||
|
|
|
@ -504,4 +504,14 @@ var migrations = []func(tx *sql.Tx) error{
|
||||||
`)
|
`)
|
||||||
return err
|
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
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -245,11 +245,6 @@ Use Let's Encrypt to get automatically a certificate for this domain\&.
|
||||||
.br
|
.br
|
||||||
Default is empty\&.
|
Default is empty\&.
|
||||||
.TP
|
.TP
|
||||||
.B CERT_CACHE
|
|
||||||
Let's Encrypt cache directory\&.
|
|
||||||
.br
|
|
||||||
Default is /tmp/cert_cache\&.
|
|
||||||
.TP
|
|
||||||
.B METRICS_COLLECTOR
|
.B METRICS_COLLECTOR
|
||||||
Set to 1 to enable metrics collector. Expose a /metrics endpoint for Prometheus.
|
Set to 1 to enable metrics collector. Expose a /metrics endpoint for Prometheus.
|
||||||
.br
|
.br
|
||||||
|
|
|
@ -48,7 +48,7 @@ ReadWritePaths=/run
|
||||||
# https://www.freedesktop.org/software/systemd/man/systemd.exec.html#AmbientCapabilities=
|
# https://www.freedesktop.org/software/systemd/man/systemd.exec.html#AmbientCapabilities=
|
||||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
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=
|
# https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateTmp=
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,6 @@ func Serve(store *storage.Storage, pool *worker.Pool) *http.Server {
|
||||||
certFile := config.Opts.CertFile()
|
certFile := config.Opts.CertFile()
|
||||||
keyFile := config.Opts.CertKeyFile()
|
keyFile := config.Opts.CertKeyFile()
|
||||||
certDomain := config.Opts.CertDomain()
|
certDomain := config.Opts.CertDomain()
|
||||||
certCache := config.Opts.CertCache()
|
|
||||||
listenAddr := config.Opts.ListenAddr()
|
listenAddr := config.Opts.ListenAddr()
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
ReadTimeout: 300 * time.Second,
|
ReadTimeout: 300 * time.Second,
|
||||||
|
@ -47,9 +46,9 @@ func Serve(store *storage.Storage, pool *worker.Pool) *http.Server {
|
||||||
startSystemdSocketServer(server)
|
startSystemdSocketServer(server)
|
||||||
case strings.HasPrefix(listenAddr, "/"):
|
case strings.HasPrefix(listenAddr, "/"):
|
||||||
startUnixSocketServer(server, listenAddr)
|
startUnixSocketServer(server, listenAddr)
|
||||||
case certDomain != "" && certCache != "":
|
case certDomain != "":
|
||||||
config.Opts.HTTPS = true
|
config.Opts.HTTPS = true
|
||||||
startAutoCertTLSServer(server, certDomain, certCache)
|
startAutoCertTLSServer(server, certDomain, store)
|
||||||
case certFile != "" && keyFile != "":
|
case certFile != "" && keyFile != "":
|
||||||
config.Opts.HTTPS = true
|
config.Opts.HTTPS = true
|
||||||
server.Addr = listenAddr
|
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"
|
server.Addr = ":https"
|
||||||
certManager := autocert.Manager{
|
certManager := autocert.Manager{
|
||||||
Cache: autocert.DirCache(certCache),
|
Cache: storage.NewCache(store),
|
||||||
Prompt: autocert.AcceptTOS,
|
Prompt: autocert.AcceptTOS,
|
||||||
HostPolicy: autocert.HostWhitelist(certDomain),
|
HostPolicy: autocert.HostWhitelist(certDomain),
|
||||||
}
|
}
|
||||||
|
|
63
storage/cache.go
Normal file
63
storage/cache.go
Normal file
|
@ -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
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue