From bf50d7010a26468791f4397c0f0c4f9a8ed1d6a2 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 2 Feb 2021 17:23:52 -0700 Subject: [PATCH] acmeserver: Support custom CAs from Caddyfile The HTTP Caddyfile adapter can now configure the PKI app, and the acme_server directive can now be used to specify a custom CA used for issuing certificates. More customization options can follow later as needed. --- caddyconfig/httpcaddyfile/httptype.go | 10 +++++ caddyconfig/httpcaddyfile/pkiapp.go | 41 +++++++++++++++++++++ modules/caddypki/acmeserver/caddyfile.go | 47 +++++++++++++++++++++--- modules/caddypki/ca.go | 16 ++++---- modules/caddypki/maintain.go | 4 +- modules/caddypki/pki.go | 12 ++++-- modules/caddytls/internalissuer.go | 2 +- 7 files changed, 111 insertions(+), 21 deletions(-) create mode 100644 caddyconfig/httpcaddyfile/pkiapp.go diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 0a5149fc..a32acabb 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -28,6 +28,7 @@ import ( "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/caddy/v2/modules/caddypki" "github.com/caddyserver/caddy/v2/modules/caddytls" ) @@ -219,6 +220,12 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock, return nil, warnings, err } + // then make the PKI app + pkiApp, warnings, err := st.buildPKIApp(pairings, options, warnings) + if err != nil { + return nil, warnings, err + } + // extract any custom logs, and enforce configured levels var customLogs []namedCustomLog var hasDefaultLog bool @@ -259,6 +266,9 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock, if !reflect.DeepEqual(tlsApp, &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}) { cfg.AppsRaw["tls"] = caddyconfig.JSON(tlsApp, &warnings) } + if !reflect.DeepEqual(pkiApp, &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}) { + cfg.AppsRaw["pki"] = caddyconfig.JSON(pkiApp, &warnings) + } if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok { cfg.StorageRaw = caddyconfig.JSONModuleObject(storageCvtr, "module", diff --git a/caddyconfig/httpcaddyfile/pkiapp.go b/caddyconfig/httpcaddyfile/pkiapp.go new file mode 100644 index 00000000..3abcc6bb --- /dev/null +++ b/caddyconfig/httpcaddyfile/pkiapp.go @@ -0,0 +1,41 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httpcaddyfile + +import ( + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/modules/caddypki" +) + +func (st ServerType) buildPKIApp( + pairings []sbAddrAssociation, + options map[string]interface{}, + warnings []caddyconfig.Warning, +) (*caddypki.PKI, []caddyconfig.Warning, error) { + + pkiApp := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)} + + for _, p := range pairings { + for _, sblock := range p.serverBlocks { + // find all the CAs that were defined and add them to the app config + for _, caCfgValue := range sblock.pile["pki.ca"] { + ca := caCfgValue.Value.(*caddypki.CA) + pkiApp.CAs[ca.ID] = ca + } + } + } + + return pkiApp, warnings, nil +} diff --git a/modules/caddypki/acmeserver/caddyfile.go b/modules/caddypki/acmeserver/caddyfile.go index 66874604..9ac0bb20 100644 --- a/modules/caddypki/acmeserver/caddyfile.go +++ b/modules/caddypki/acmeserver/caddyfile.go @@ -16,23 +16,58 @@ package acmeserver import ( "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" - "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/caddy/v2/modules/caddypki" ) func init() { - httpcaddyfile.RegisterHandlerDirective("acme_server", parseACMEServer) + httpcaddyfile.RegisterDirective("acme_server", parseACMEServer) } // parseACMEServer sets up an ACME server handler from Caddyfile tokens. // -// acme_server [] +// acme_server [] { +// ca +// } // -func parseACMEServer(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { - var as Handler +func parseACMEServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { + if !h.Next() { + return nil, h.ArgErr() + } + + matcherSet, err := h.ExtractMatcherSet() + if err != nil { + return nil, err + } + + var acmeServer Handler + var ca *caddypki.CA + for h.Next() { if h.NextArg() { return nil, h.ArgErr() } + for h.NextBlock(0) { + switch h.Val() { + case "ca": + if !h.AllArgs(&acmeServer.CA) { + return nil, h.ArgErr() + } + if ca == nil { + ca = new(caddypki.CA) + } + ca.ID = acmeServer.CA + } + } } - return as, nil + + configVals := h.NewRoute(matcherSet, acmeServer) + + if ca == nil { + return configVals, nil + } + + return append(configVals, httpcaddyfile.ConfigValue{ + Class: "pki.ca", + Value: ca, + }), nil } diff --git a/modules/caddypki/ca.go b/modules/caddypki/ca.go index 5e766767..957f0760 100644 --- a/modules/caddypki/ca.go +++ b/modules/caddypki/ca.go @@ -63,7 +63,12 @@ type CA struct { // separate location from your leaf certificates. StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` - id string + // The unique config-facing ID of the certificate authority. + // Since the ID is set in JSON config via object key, this + // field is exported only for purposes of config generation + // and module provisioning. + ID string `json:"-"` + storage certmagic.Storage root, inter *x509.Certificate interKey interface{} // TODO: should we just store these as crypto.Signer? @@ -82,7 +87,7 @@ func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error { return fmt.Errorf("CA ID is required (use 'local' for the default CA)") } ca.mu.Lock() - ca.id = id + ca.ID = id ca.mu.Unlock() if ca.StorageRaw != nil { @@ -142,11 +147,6 @@ func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error { return nil } -// ID returns the CA's ID, as given by the user in the config. -func (ca CA) ID() string { - return ca.id -} - // RootCertificate returns the CA's root certificate (public key). func (ca CA) RootCertificate() *x509.Certificate { ca.mu.RLock() @@ -338,7 +338,7 @@ func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey interface{}) (i } func (ca CA) storageKeyCAPrefix() string { - return path.Join("pki", "authorities", certmagic.StorageKeys.Safe(ca.id)) + return path.Join("pki", "authorities", certmagic.StorageKeys.Safe(ca.ID)) } func (ca CA) storageKeyRootCert() string { return path.Join(ca.storageKeyCAPrefix(), "root.crt") diff --git a/modules/caddypki/maintain.go b/modules/caddypki/maintain.go index c0b277d4..31e453ff 100644 --- a/modules/caddypki/maintain.go +++ b/modules/caddypki/maintain.go @@ -50,7 +50,7 @@ func (p *PKI) renewCerts() { if err != nil { p.log.Error("renewing intermediate certificates", zap.Error(err), - zap.String("ca", ca.id)) + zap.String("ca", ca.ID)) } } } @@ -59,7 +59,7 @@ func (p *PKI) renewCertsForCA(ca *CA) error { ca.mu.Lock() defer ca.mu.Unlock() - log := p.log.With(zap.String("ca", ca.id)) + log := p.log.With(zap.String("ca", ca.ID)) // only maintain the root if it's not manually provided in the config if ca.Root == nil { diff --git a/modules/caddypki/pki.go b/modules/caddypki/pki.go index 77370797..b6f08b18 100644 --- a/modules/caddypki/pki.go +++ b/modules/caddypki/pki.go @@ -49,10 +49,14 @@ func (p *PKI) Provision(ctx caddy.Context) error { p.ctx = ctx p.log = ctx.Logger(p) - // if this app is initialized at all, ensure there's - // at least a default CA that can be used - if len(p.CAs) == 0 { - p.CAs = map[string]*CA{DefaultCAID: new(CA)} + // if this app is initialized at all, ensure there's at + // least a default CA that can be used: the standard CA + // which is used implicitly for signing local-use certs + if p.CAs == nil { + p.CAs = make(map[string]*CA) + } + if _, ok := p.CAs[DefaultCAID]; !ok { + p.CAs[DefaultCAID] = new(CA) } for caID, ca := range p.CAs { diff --git a/modules/caddytls/internalissuer.go b/modules/caddytls/internalissuer.go index 416369f5..a6ae587a 100644 --- a/modules/caddytls/internalissuer.go +++ b/modules/caddytls/internalissuer.go @@ -94,7 +94,7 @@ func (iss *InternalIssuer) Provision(ctx caddy.Context) error { // IssuerKey returns the unique issuer key for the // confgured CA endpoint. func (iss InternalIssuer) IssuerKey() string { - return iss.ca.ID() + return iss.ca.ID } // Issue issues a certificate to satisfy the CSR.