diff options
Diffstat (limited to 'modules/caddypki')
-rw-r--r-- | modules/caddypki/acmeserver/acmeserver.go | 130 | ||||
-rw-r--r-- | modules/caddypki/acmeserver/caddyfile.go | 11 | ||||
-rw-r--r-- | modules/caddypki/adminapi.go | 19 | ||||
-rw-r--r-- | modules/caddypki/ca.go | 7 | ||||
-rw-r--r-- | modules/caddypki/command.go | 42 | ||||
-rw-r--r-- | modules/caddypki/pki.go | 3 |
6 files changed, 149 insertions, 63 deletions
diff --git a/modules/caddypki/acmeserver/acmeserver.go b/modules/caddypki/acmeserver/acmeserver.go index 6ecdfdc..8689615 100644 --- a/modules/caddypki/acmeserver/acmeserver.go +++ b/modules/caddypki/acmeserver/acmeserver.go @@ -15,7 +15,10 @@ package acmeserver import ( + "context" "fmt" + weakrand "math/rand" + "net" "net/http" "os" "path/filepath" @@ -23,18 +26,19 @@ import ( "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/acme/api" acmeNoSQL "github.com/smallstep/certificates/acme/db/nosql" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" "github.com/smallstep/nosql" "go.uber.org/zap" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/caddy/v2/modules/caddypki" ) func init() { @@ -76,8 +80,26 @@ type Handler struct { // changed or removed in the future. SignWithRoot bool `json:"sign_with_root,omitempty"` + // The addresses of DNS resolvers to use when looking up + // the TXT records for solving DNS challenges. + // It accepts [network addresses](/docs/conventions#network-addresses) + // with port range of only 1. If the host is an IP address, + // it will be dialed directly to resolve the upstream server. + // If the host is not an IP address, the addresses are resolved + // using the [name resolution convention](https://golang.org/pkg/net/#hdr-Name_Resolution) + // of the Go standard library. If the array contains more + // than 1 resolver address, one is chosen at random. + Resolvers []string `json:"resolvers,omitempty"` + + logger *zap.Logger + resolvers []caddy.NetworkAddress + ctx caddy.Context + + acmeDB acme.DB + acmeAuth *authority.Authority + acmeClient acme.Client + acmeLinker acme.Linker acmeEndpoints http.Handler - logger *zap.Logger } // CaddyModule returns the Caddy module information. @@ -90,7 +112,9 @@ func (Handler) CaddyModule() caddy.ModuleInfo { // Provision sets up the ACME server handler. func (ash *Handler) Provision(ctx caddy.Context) error { + ash.ctx = ctx ash.logger = ctx.Logger() + // set some defaults if ash.CA == "" { ash.CA = caddypki.DefaultCAID @@ -142,31 +166,30 @@ func (ash *Handler) Provision(ctx caddy.Context) error { DB: database, } - auth, err := ca.NewAuthority(authorityConfig) + ash.acmeAuth, err = ca.NewAuthority(authorityConfig) if err != nil { return err } - var acmeDB acme.DB - if authorityConfig.DB != nil { - acmeDB, err = acmeNoSQL.New(auth.GetDatabase().(nosql.DB)) - if err != nil { - return fmt.Errorf("configuring ACME DB: %v", err) - } + ash.acmeDB, err = acmeNoSQL.New(ash.acmeAuth.GetDatabase().(nosql.DB)) + if err != nil { + return fmt.Errorf("configuring ACME DB: %v", err) } - // create the router for the ACME endpoints - acmeRouterHandler := acmeAPI.NewHandler(acmeAPI.HandlerOptions{ - CA: auth, - DB: acmeDB, // stores all the server state - DNS: ash.Host, // used for directory links - Prefix: strings.Trim(ash.PathPrefix, "/"), // used for directory links - }) + ash.acmeClient, err = ash.makeClient() + if err != nil { + return err + } + + ash.acmeLinker = acme.NewLinker( + ash.Host, + strings.Trim(ash.PathPrefix, "/"), + ) // extract its http.Handler so we can use it directly r := chi.NewRouter() r.Route(ash.PathPrefix, func(r chi.Router) { - acmeRouterHandler.Route(r) + api.Route(r) }) ash.acmeEndpoints = r @@ -175,6 +198,16 @@ func (ash *Handler) Provision(ctx caddy.Context) error { func (ash Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { if strings.HasPrefix(r.URL.Path, ash.PathPrefix) { + acmeCtx := acme.NewContext( + r.Context(), + ash.acmeDB, + ash.acmeClient, + ash.acmeLinker, + nil, + ) + acmeCtx = authority.NewContext(acmeCtx, ash.acmeAuth) + r = r.WithContext(acmeCtx) + ash.acmeEndpoints.ServeHTTP(w, r) return nil } @@ -207,7 +240,7 @@ func (ash Handler) openDatabase() (*db.AuthDB, error) { dbFolder := filepath.Join(caddy.AppDataDir(), "acme_server", key) dbPath := filepath.Join(dbFolder, "db") - err := os.MkdirAll(dbFolder, 0755) + err := os.MkdirAll(dbFolder, 0o755) if err != nil { return nil, fmt.Errorf("making folder for CA database: %v", err) } @@ -227,10 +260,61 @@ func (ash Handler) openDatabase() (*db.AuthDB, error) { return database.(databaseCloser).DB, err } +// makeClient creates an ACME client which will use a custom +// resolver instead of net.DefaultResolver. +func (ash Handler) makeClient() (acme.Client, error) { + for _, v := range ash.Resolvers { + addr, err := caddy.ParseNetworkAddressWithDefaults(v, "udp", 53) + if err != nil { + return nil, err + } + if addr.PortRangeSize() != 1 { + return nil, fmt.Errorf("resolver address must have exactly one address; cannot call %v", addr) + } + ash.resolvers = append(ash.resolvers, addr) + } + + var resolver *net.Resolver + if len(ash.resolvers) != 0 { + dialer := &net.Dialer{ + Timeout: 2 * time.Second, + } + resolver = &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + //nolint:gosec + addr := ash.resolvers[weakrand.Intn(len(ash.resolvers))] + return dialer.DialContext(ctx, addr.Network, addr.JoinHostPort(0)) + }, + } + } else { + resolver = net.DefaultResolver + } + + return resolverClient{ + Client: acme.NewClient(), + resolver: resolver, + ctx: ash.ctx, + }, nil +} + +type resolverClient struct { + acme.Client + + resolver *net.Resolver + ctx context.Context +} + +func (c resolverClient) LookupTxt(name string) ([]string, error) { + return c.resolver.LookupTXT(c.ctx, name) +} + const defaultPathPrefix = "/acme/" -var keyCleaner = regexp.MustCompile(`[^\w.-_]`) -var databasePool = caddy.NewUsagePool() +var ( + keyCleaner = regexp.MustCompile(`[^\w.-_]`) + databasePool = caddy.NewUsagePool() +) type databaseCloser struct { DB *db.AuthDB diff --git a/modules/caddypki/acmeserver/caddyfile.go b/modules/caddypki/acmeserver/caddyfile.go index ae2d8ef..3b52113 100644 --- a/modules/caddypki/acmeserver/caddyfile.go +++ b/modules/caddypki/acmeserver/caddyfile.go @@ -29,8 +29,9 @@ func init() { // parseACMEServer sets up an ACME server handler from Caddyfile tokens. // // acme_server [<matcher>] { -// ca <id> -// lifetime <duration> +// ca <id> +// lifetime <duration> +// resolvers <addresses...> // } func parseACMEServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { if !h.Next() { @@ -74,6 +75,12 @@ func parseACMEServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error } acmeServer.Lifetime = caddy.Duration(dur) + + case "resolvers": + acmeServer.Resolvers = h.RemainingArgs() + if len(acmeServer.Resolvers) == 0 { + return nil, h.Errf("must specify at least one resolver address") + } } } } diff --git a/modules/caddypki/adminapi.go b/modules/caddypki/adminapi.go index f03c6b6..435af34 100644 --- a/modules/caddypki/adminapi.go +++ b/modules/caddypki/adminapi.go @@ -20,8 +20,9 @@ import ( "net/http" "strings" - "github.com/caddyserver/caddy/v2" "go.uber.org/zap" + + "github.com/caddyserver/caddy/v2" ) func init() { @@ -49,20 +50,10 @@ func (a *adminAPI) Provision(ctx caddy.Context) error { a.ctx = ctx a.log = ctx.Logger(a) // TODO: passing in 'a' is a hack until the admin API is officially extensible (see #5032) - // First check if the PKI app was configured, because - // a.ctx.App() has the side effect of instantiating - // and provisioning an app even if it wasn't configured. - pkiAppConfigured := a.ctx.AppIsConfigured("pki") - if !pkiAppConfigured { - return nil - } - - // Load the PKI app, so we can query it for information. - appModule, err := a.ctx.App("pki") - if err != nil { - return err + // Avoid initializing PKI if it wasn't configured + if pkiApp := a.ctx.AppIfConfigured("pki"); pkiApp != nil { + a.pkiApp = pkiApp.(*PKI) } - a.pkiApp = appModule.(*PKI) return nil } diff --git a/modules/caddypki/ca.go b/modules/caddypki/ca.go index 1ba0890..6c48da6 100644 --- a/modules/caddypki/ca.go +++ b/modules/caddypki/ca.go @@ -25,12 +25,13 @@ import ( "sync" "time" - "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" + + "github.com/caddyserver/caddy/v2" ) // CA describes a certificate authority, which consists of @@ -376,15 +377,19 @@ func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey crypto.Signer) func (ca CA) storageKeyCAPrefix() string { return path.Join("pki", "authorities", certmagic.StorageKeys.Safe(ca.ID)) } + func (ca CA) storageKeyRootCert() string { return path.Join(ca.storageKeyCAPrefix(), "root.crt") } + func (ca CA) storageKeyRootKey() string { return path.Join(ca.storageKeyCAPrefix(), "root.key") } + func (ca CA) storageKeyIntermediateCert() string { return path.Join(ca.storageKeyCAPrefix(), "intermediate.crt") } + func (ca CA) storageKeyIntermediateKey() string { return path.Join(ca.storageKeyCAPrefix(), "intermediate.key") } diff --git a/modules/caddypki/command.go b/modules/caddypki/command.go index cb86c93..b7fa1bb 100644 --- a/modules/caddypki/command.go +++ b/modules/caddypki/command.go @@ -18,21 +18,22 @@ import ( "crypto/x509" "encoding/json" "encoding/pem" - "flag" "fmt" "net/http" "os" "path" - "github.com/caddyserver/caddy/v2" - caddycmd "github.com/caddyserver/caddy/v2/cmd" "github.com/smallstep/truststore" + "github.com/spf13/cobra" + + caddycmd "github.com/caddyserver/caddy/v2/cmd" + + "github.com/caddyserver/caddy/v2" ) func init() { caddycmd.RegisterCommand(caddycmd.Command{ Name: "trust", - Func: cmdTrust, Usage: "[--ca <id>] [--address <listen>] [--config <path> [--adapter <name>]]", Short: "Installs a CA certificate into local trust stores", Long: ` @@ -53,19 +54,17 @@ This command will attempt to connect to Caddy's admin API running at '` + caddy.DefaultAdminListen + `' to fetch the root certificate. You may explicitly specify the --address, or use the --config flag to load the admin address from your config, if not using the default.`, - Flags: func() *flag.FlagSet { - fs := flag.NewFlagSet("trust", flag.ExitOnError) - fs.String("ca", "", "The ID of the CA to trust (defaults to 'local')") - fs.String("address", "", "Address of the administration API listener (if --config is not used)") - fs.String("config", "", "Configuration file (if --address is not used)") - fs.String("adapter", "", "Name of config adapter to apply (if --config is used)") - return fs - }(), + CobraFunc: func(cmd *cobra.Command) { + cmd.Flags().StringP("ca", "", "", "The ID of the CA to trust (defaults to 'local')") + cmd.Flags().StringP("address", "", "", "Address of the administration API listener (if --config is not used)") + cmd.Flags().StringP("config", "c", "", "Configuration file (if --address is not used)") + cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply (if --config is used)") + cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdTrust) + }, }) caddycmd.RegisterCommand(caddycmd.Command{ Name: "untrust", - Func: cmdUntrust, Usage: "[--cert <path>] | [[--ca <id>] [--address <listen>] [--config <path> [--adapter <name>]]]", Short: "Untrusts a locally-trusted CA certificate", Long: ` @@ -89,15 +88,14 @@ will attempt to connect to the Caddy's admin API running at '` + caddy.DefaultAdminListen + `' to fetch the root certificate. You may explicitly specify the --address, or use the --config flag to load the admin address from your config, if not using the default.`, - Flags: func() *flag.FlagSet { - fs := flag.NewFlagSet("untrust", flag.ExitOnError) - fs.String("cert", "", "The path to the CA certificate to untrust") - fs.String("ca", "", "The ID of the CA to untrust (defaults to 'local')") - fs.String("address", "", "Address of the administration API listener (if --config is not used)") - fs.String("config", "", "Configuration file (if --address is not used)") - fs.String("adapter", "", "Name of config adapter to apply (if --config is used)") - return fs - }(), + CobraFunc: func(cmd *cobra.Command) { + cmd.Flags().StringP("cert", "p", "", "The path to the CA certificate to untrust") + cmd.Flags().StringP("ca", "", "", "The ID of the CA to untrust (defaults to 'local')") + cmd.Flags().StringP("address", "", "", "Address of the administration API listener (if --config is not used)") + cmd.Flags().StringP("config", "c", "", "Configuration file (if --address is not used)") + cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply (if --config is used)") + cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdUntrust) + }, }) } diff --git a/modules/caddypki/pki.go b/modules/caddypki/pki.go index 2caeb2b..9f974a9 100644 --- a/modules/caddypki/pki.go +++ b/modules/caddypki/pki.go @@ -17,8 +17,9 @@ package caddypki import ( "fmt" - "github.com/caddyserver/caddy/v2" "go.uber.org/zap" + + "github.com/caddyserver/caddy/v2" ) func init() { |