Skip to content

Commit

Permalink
pki: Embedded ACME server (#3198)
Browse files Browse the repository at this point in the history
* pki: Initial commit of embedded ACME server (#3021)

* reverseproxy: Support auto-managed TLS client certificates (#3021)

* A little cleanup after today's review session
  • Loading branch information
mholt authored May 5, 2020
1 parent 1e8c976 commit 184e8e9
Show file tree
Hide file tree
Showing 9 changed files with 281 additions and 167 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/caddyserver/certmagic v0.10.12
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
github.com/go-acme/lego/v3 v3.6.0
github.com/go-chi/chi v4.0.2+incompatible
github.com/gogo/protobuf v1.3.1
github.com/google/cel-go v0.4.1
github.com/jsternberg/zap-logfmt v1.2.0
Expand All @@ -20,6 +21,7 @@ require (
github.com/naoina/toml v0.1.1
github.com/smallstep/certificates v0.14.2
github.com/smallstep/cli v0.14.2
github.com/smallstep/nosql v0.2.0
github.com/smallstep/truststore v0.9.5
github.com/yuin/goldmark v1.1.28
github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691
Expand Down
124 changes: 2 additions & 122 deletions go.sum

Large diffs are not rendered by default.

37 changes: 31 additions & 6 deletions modules/caddyhttp/reverseproxy/httptransport.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"time"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"golang.org/x/net/http2"
)

Expand Down Expand Up @@ -140,9 +141,8 @@ func (h *HTTPTransport) Provision(ctx caddy.Context) error {
return nil
}

// NewTransport builds a standard-lib-compatible
// http.Transport value from h.
func (h *HTTPTransport) NewTransport(_ caddy.Context) (*http.Transport, error) {
// NewTransport builds a standard-lib-compatible http.Transport value from h.
func (h *HTTPTransport) NewTransport(ctx caddy.Context) (*http.Transport, error) {
dialer := &net.Dialer{
Timeout: time.Duration(h.DialTimeout),
FallbackDelay: time.Duration(h.FallbackDelay),
Expand Down Expand Up @@ -175,9 +175,8 @@ func (h *HTTPTransport) NewTransport(_ caddy.Context) (*http.Transport, error) {

if h.TLS != nil {
rt.TLSHandshakeTimeout = time.Duration(h.TLS.HandshakeTimeout)

var err error
rt.TLSClientConfig, err = h.TLS.MakeTLSClientConfig()
rt.TLSClientConfig, err = h.TLS.MakeTLSClientConfig(ctx)
if err != nil {
return nil, fmt.Errorf("making TLS client config: %v", err)
}
Expand Down Expand Up @@ -267,6 +266,10 @@ type TLSConfig struct {
// PEM-encoded key to use with the client certificate.
ClientCertificateKeyFile string `json:"client_certificate_key_file,omitempty"`

// If specified, Caddy will use and automate a client certificate
// with this subject name.
ClientCertificateAutomate string `json:"client_certificate_automate,omitempty"`

// If true, TLS verification of server certificates will be disabled.
// This is insecure and may be removed in the future. Do not use this
// option except in testing or local development environments.
Expand All @@ -281,7 +284,7 @@ type TLSConfig struct {

// MakeTLSClientConfig returns a tls.Config usable by a client to a backend.
// If there is no custom TLS configuration, a nil config may be returned.
func (t TLSConfig) MakeTLSClientConfig() (*tls.Config, error) {
func (t TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) {
cfg := new(tls.Config)

// client auth
Expand All @@ -298,6 +301,28 @@ func (t TLSConfig) MakeTLSClientConfig() (*tls.Config, error) {
}
cfg.Certificates = []tls.Certificate{cert}
}
if t.ClientCertificateAutomate != "" {
tlsAppIface, err := ctx.App("tls")
if err != nil {
return nil, fmt.Errorf("getting tls app: %v", err)
}
tlsApp := tlsAppIface.(*caddytls.TLS)
err = tlsApp.Manage([]string{t.ClientCertificateAutomate})
if err != nil {
return nil, fmt.Errorf("managing client certificate: %v", err)
}
cfg.GetClientCertificate = func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) {
certs := tlsApp.AllMatchingCertificates(t.ClientCertificateAutomate)
var err error
for _, cert := range certs {
err = cri.SupportsCertificate(&cert.Certificate)
if err == nil {
return &cert.Certificate, nil
}
}
return nil, err
}
}

// trusted root CAs
if len(t.RootCAPool) > 0 || len(t.RootCAPEMFiles) > 0 {
Expand Down
165 changes: 165 additions & 0 deletions modules/caddypki/acmeserver/acmeserver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// 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 acmeserver

import (
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddypki"
"github.com/go-chi/chi"
"github.com/smallstep/certificates/acme"
acmeAPI "github.com/smallstep/certificates/acme/api"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/db"
"github.com/smallstep/nosql"
)

func init() {
caddy.RegisterModule(Handler{})
}

// Handler is an ACME server handler.
type Handler struct {
// The ID of the CA to use for signing. This refers to
// the ID given to the CA in the `pki` app. If omitted,
// the default ID is "local".
CA string `json:"ca,omitempty"`

// The hostname or IP address by which ACME clients
// will access the server. This is used to populate
// the ACME directory endpoint. Default: localhost.
// TODO: this is probably not needed - check with smallstep
Host string `json:"host,omitempty"`

// The path prefix under which to serve all ACME
// endpoints. All other requests will not be served
// by this handler and will be passed through to
// the next one. Default: "/acme/"
PathPrefix string `json:"path_prefix,omitempty"`

acmeEndpoints http.Handler
}

// CaddyModule returns the Caddy module information.
func (Handler) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.acme_server",
New: func() caddy.Module { return new(Handler) },
}
}

// Provision sets up the ACME server handler.
func (ash *Handler) Provision(ctx caddy.Context) error {
// set some defaults
if ash.CA == "" {
ash.CA = caddypki.DefaultCAID
}
if ash.Host == "" {
ash.Host = defaultHost
}
if ash.PathPrefix == "" {
ash.PathPrefix = defaultPathPrefix
}

// get a reference to the configured CA
appModule, err := ctx.App("pki")
if err != nil {
return err
}
pkiApp := appModule.(*caddypki.PKI)
ca, ok := pkiApp.CAs[ash.CA]
if !ok {
return fmt.Errorf("no certificate authority configured with id: %s", ash.CA)
}

dbFolder := filepath.Join(caddy.AppDataDir(), "acme_server", "db")

// TODO: See https://github.com/smallstep/nosql/issues/7
err = os.MkdirAll(dbFolder, 0755)
if err != nil {
return fmt.Errorf("making folder for ACME server database: %v", err)
}

authorityConfig := caddypki.AuthorityConfig{
AuthConfig: &authority.AuthConfig{
Provisioners: provisioner.List{
&provisioner.ACME{
Name: ash.CA,
Type: provisioner.TypeACME.String(),
Claims: &provisioner.Claims{
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute},
MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour * 365},
DefaultTLSDur: &provisioner.Duration{Duration: 12 * time.Hour},
},
},
},
},
DB: &db.Config{
Type: "badger",
DataSource: dbFolder,
},
}

auth, err := ca.NewAuthority(authorityConfig)
if err != nil {
return err
}

acmeAuth, err := acme.NewAuthority(
auth.GetDatabase().(nosql.DB), // stores all the server state
ash.Host, // used for directory links; TODO: not needed
strings.Trim(ash.PathPrefix, "/"), // used for directory links
auth) // configures the signing authority
if err != nil {
return err
}

// create the router for the ACME endpoints
acmeRouterHandler := acmeAPI.New(acmeAuth)
r := chi.NewRouter()
r.Route(ash.PathPrefix, func(r chi.Router) {
acmeRouterHandler.Route(r)
})
ash.acmeEndpoints = r

return nil
}

func (ash Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
if strings.HasPrefix(r.URL.Path, ash.PathPrefix) {
ash.acmeEndpoints.ServeHTTP(w, r)
return nil
}
return next.ServeHTTP(w, r)
}

const (
defaultHost = "localhost"
defaultPathPrefix = "/acme/"
)

// Interface guards
var (
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
_ caddy.Provisioner = (*Handler)(nil)
)
63 changes: 62 additions & 1 deletion modules/caddypki/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package caddypki

import (
"crypto"
"crypto/x509"
"encoding/json"
"fmt"
Expand All @@ -24,6 +25,8 @@ import (

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/certmagic"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/db"
"github.com/smallstep/truststore"
"go.uber.org/zap"
)
Expand Down Expand Up @@ -171,6 +174,52 @@ func (ca CA) IntermediateKey() interface{} {
return ca.interKey
}

// NewAuthority returns a new Smallstep-powered signing authority for this CA.
func (ca CA) NewAuthority(authorityConfig AuthorityConfig) (*authority.Authority, error) {
cfg := &authority.Config{
// TODO: eliminate these placeholders / needless values
// see https://github.com/smallstep/certificates/issues/218
Address: "placeholder_Address:1",
Root: []string{"placeholder_Root"},
IntermediateCert: "placeholder_IntermediateCert",
IntermediateKey: "placeholder_IntermediateKey",
DNSNames: []string{"placeholder_DNSNames"},

AuthorityConfig: authorityConfig.AuthConfig,
DB: authorityConfig.DB,
}
// TODO: this also seems unnecessary, see above issue
if cfg.AuthorityConfig == nil {
cfg.AuthorityConfig = new(authority.AuthConfig)
}

// get the root certificate and the issuer cert+key
rootCert := ca.RootCertificate()
var issuerCert *x509.Certificate
var issuerKey interface{}
if authorityConfig.SignWithRoot {
issuerCert = rootCert
var err error
issuerKey, err = ca.RootKey()
if err != nil {
return nil, fmt.Errorf("loading signing key: %v", err)
}
} else {
issuerCert = ca.IntermediateCertificate()
issuerKey = ca.IntermediateKey()
}

auth, err := authority.New(cfg,
authority.WithX509Signer(issuerCert, issuerKey.(crypto.Signer)),
authority.WithX509RootCerts(rootCert),
)
if err != nil {
return nil, fmt.Errorf("initializing certificate authority: %v", err)
}

return auth, nil
}

func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey interface{}, err error) {
rootCertPEM, err := ca.storage.Load(ca.storageKeyRootCert())
if err != nil {
Expand Down Expand Up @@ -345,8 +394,20 @@ func (ca CA) installRoot() error {
)
}

// AuthorityConfig is used to help a CA configure
// the underlying signing authority.
type AuthorityConfig struct {
SignWithRoot bool

// TODO: should we just embed the underlying authority.Config struct type?
DB *db.Config
AuthConfig *authority.AuthConfig
}

const (
defaultCAID = "local"
// DefaultCAID is the default CA ID.
DefaultCAID = "local"

defaultCAName = "Caddy Local Authority"
defaultRootCommonName = "{pki.ca.name} - {time.now.year} ECC Root"
defaultIntermediateCommonName = "{pki.ca.name} - ECC Intermediate"
Expand Down
4 changes: 2 additions & 2 deletions modules/caddypki/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func cmdTrust(fs caddycmd.Flags) (int, error) {
ca := CA{
storage: caddy.DefaultStorage,
}
err := ca.Provision(ctx, defaultCAID, caddy.Log())
err := ca.Provision(ctx, DefaultCAID, caddy.Log())
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
Expand All @@ -109,7 +109,7 @@ func cmdUntrust(fs caddycmd.Flags) (int, error) {
return caddy.ExitCodeFailedStartup, fmt.Errorf("conflicting command line arguments")
}
if ca == "" && cert == "" {
ca = defaultCAID
ca = DefaultCAID
}
if ca != "" {
cert = filepath.Join(caddy.AppDataDir(), "pki", "authorities", ca, "root.crt")
Expand Down
2 changes: 1 addition & 1 deletion modules/caddypki/pki.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (p *PKI) Provision(ctx caddy.Context) error {
// 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)}
p.CAs = map[string]*CA{DefaultCAID: new(CA)}
}

for caID, ca := range p.CAs {
Expand Down
Loading

0 comments on commit 184e8e9

Please sign in to comment.