acme_server: fix reload of acme database (#3874)

* acme_server: Refactor database creation apart from authority creation

This is a WIP commit that doesn't really offer anything other than
setting us up for using a UsagePool to gracefully reload acme_server
configs.

* Implement UsagePool

* Remove unused context

* Fix initializing non-ACME CA

This will handle cases where a DB is not provided

* Sanitize acme db path and clean debug logs

* Move regex to package level to prevent recompiling
This commit is contained in:
Ian 2020-11-23 12:58:26 -08:00 committed by GitHub
parent 06ba006f9b
commit c5197f5999
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 71 additions and 27 deletions

View file

@ -19,6 +19,7 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"time" "time"
@ -66,6 +67,7 @@ type Handler struct {
PathPrefix string `json:"path_prefix,omitempty"` PathPrefix string `json:"path_prefix,omitempty"`
acmeEndpoints http.Handler acmeEndpoints http.Handler
logger *zap.Logger
} }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
@ -78,7 +80,7 @@ func (Handler) CaddyModule() caddy.ModuleInfo {
// Provision sets up the ACME server handler. // Provision sets up the ACME server handler.
func (ash *Handler) Provision(ctx caddy.Context) error { func (ash *Handler) Provision(ctx caddy.Context) error {
logger := ctx.Logger(ash) ash.logger = ctx.Logger(ash)
// set some defaults // set some defaults
if ash.CA == "" { if ash.CA == "" {
ash.CA = caddypki.DefaultCAID ash.CA = caddypki.DefaultCAID
@ -101,25 +103,9 @@ func (ash *Handler) Provision(ctx caddy.Context) error {
return fmt.Errorf("no certificate authority configured with id: %s", ash.CA) return fmt.Errorf("no certificate authority configured with id: %s", ash.CA)
} }
dbFolder := filepath.Join(caddy.AppDataDir(), "acme_server") database, err := ash.openDatabase()
dbPath := filepath.Join(dbFolder, "db")
// TODO: See https://github.com/smallstep/nosql/issues/7
err = os.MkdirAll(dbFolder, 0755)
if err != nil { if err != nil {
return fmt.Errorf("making folder for ACME server database: %v", err) return err
}
// Check to see if previous db exists
var stat os.FileInfo
stat, err = os.Stat(dbPath)
if stat != nil && err == nil {
// A badger db is found and should be removed
if stat.IsDir() {
logger.Warn("Found an old badger database and removing it",
zap.String("path", dbPath))
_ = os.RemoveAll(dbPath)
}
} }
authorityConfig := caddypki.AuthorityConfig{ authorityConfig := caddypki.AuthorityConfig{
@ -136,10 +122,7 @@ func (ash *Handler) Provision(ctx caddy.Context) error {
}, },
}, },
}, },
DB: &db.Config{ DB: database,
Type: "bbolt",
DataSource: dbPath,
},
} }
auth, err := ca.NewAuthority(authorityConfig) auth, err := ca.NewAuthority(authorityConfig)
@ -175,11 +158,68 @@ func (ash Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
return next.ServeHTTP(w, r) return next.ServeHTTP(w, r)
} }
func (ash Handler) getDatabaseKey() string {
key := ash.CA
key = strings.ToLower(key)
key = strings.TrimSpace(key)
return keyCleaner.ReplaceAllLiteralString(key, "")
}
// Cleanup implements caddy.CleanerUpper and closes any idle databases.
func (ash Handler) Cleanup() error {
key := ash.getDatabaseKey()
deleted, err := databasePool.Delete(key)
if deleted {
ash.logger.Debug("unloading unused CA database", zap.String("db_key", key))
}
if err != nil {
ash.logger.Error("closing CA database", zap.String("db_key", key), zap.Error(err))
}
return err
}
func (ash Handler) openDatabase() (*db.AuthDB, error) {
key := ash.getDatabaseKey()
database, loaded, err := databasePool.LoadOrNew(key, func() (caddy.Destructor, error) {
dbFolder := filepath.Join(caddy.AppDataDir(), "acme_server", key)
dbPath := filepath.Join(dbFolder, "db")
err := os.MkdirAll(dbFolder, 0755)
if err != nil {
return nil, fmt.Errorf("making folder for CA database: %v", err)
}
dbConfig := &db.Config{
Type: "bbolt",
DataSource: dbPath,
}
database, err := db.New(dbConfig)
return databaseCloser{&database}, err
})
if loaded {
ash.logger.Debug("loaded preexisting CA database", zap.String("db_key", key))
}
return database.(databaseCloser).DB, err
}
const ( const (
defaultHost = "localhost" defaultHost = "localhost"
defaultPathPrefix = "/acme/" defaultPathPrefix = "/acme/"
) )
var keyCleaner = regexp.MustCompile(`[^\w.-_]`)
var databasePool = caddy.NewUsagePool()
type databaseCloser struct {
DB *db.AuthDB
}
func (closer databaseCloser) Destruct() error {
return (*closer.DB).Shutdown()
}
// Interface guards // Interface guards
var ( var (
_ caddyhttp.MiddlewareHandler = (*Handler)(nil) _ caddyhttp.MiddlewareHandler = (*Handler)(nil)

View file

@ -195,14 +195,18 @@ func (ca CA) NewAuthority(authorityConfig AuthorityConfig) (*authority.Authority
issuerKey = ca.IntermediateKey() issuerKey = ca.IntermediateKey()
} }
auth, err := authority.NewEmbedded( opts := []authority.Option{
authority.WithConfig(&authority.Config{ authority.WithConfig(&authority.Config{
AuthorityConfig: authorityConfig.AuthConfig, AuthorityConfig: authorityConfig.AuthConfig,
DB: authorityConfig.DB,
}), }),
authority.WithX509Signer(issuerCert, issuerKey.(crypto.Signer)), authority.WithX509Signer(issuerCert, issuerKey.(crypto.Signer)),
authority.WithX509RootCerts(rootCert), authority.WithX509RootCerts(rootCert),
) }
// Add a database if we have one
if authorityConfig.DB != nil {
opts = append(opts, authority.WithDatabase(*authorityConfig.DB))
}
auth, err := authority.NewEmbedded(opts...)
if err != nil { if err != nil {
return nil, fmt.Errorf("initializing certificate authority: %v", err) return nil, fmt.Errorf("initializing certificate authority: %v", err)
} }
@ -382,7 +386,7 @@ type AuthorityConfig struct {
SignWithRoot bool SignWithRoot bool
// TODO: should we just embed the underlying authority.Config struct type? // TODO: should we just embed the underlying authority.Config struct type?
DB *db.Config DB *db.AuthDB
AuthConfig *authority.AuthConfig AuthConfig *authority.AuthConfig
} }