summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--admin.go50
-rw-r--r--caddy.go7
-rw-r--r--cmd/commandfuncs.go112
-rw-r--r--cmd/commands.go7
-rw-r--r--cmd/main.go8
-rw-r--r--context.go11
-rw-r--r--modules/caddypki/adminpki.go194
-rw-r--r--modules/caddypki/command.go201
-rw-r--r--modules/caddypki/pki.go2
9 files changed, 483 insertions, 109 deletions
diff --git a/admin.go b/admin.go
index 891f818..11efc68 100644
--- a/admin.go
+++ b/admin.go
@@ -92,6 +92,10 @@ type AdminConfig struct {
//
// EXPERIMENTAL: This feature is subject to change.
Remote *RemoteAdmin `json:"remote,omitempty"`
+
+ // Holds onto the routers so that we can later provision them
+ // if they require provisioning.
+ routers []AdminRouter
}
// ConfigSettings configures the management of configuration.
@@ -190,7 +194,7 @@ type AdminPermissions struct {
// newAdminHandler reads admin's config and returns an http.Handler suitable
// for use in an admin endpoint server, which will be listening on listenAddr.
-func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler {
+func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler {
muxWrap := adminHandler{mux: http.NewServeMux()}
// secure the local or remote endpoint respectively
@@ -250,11 +254,32 @@ func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) admin
for _, route := range router.Routes() {
addRoute(route.Pattern, handlerLabel, route.Handler)
}
+ admin.routers = append(admin.routers, router)
}
return muxWrap
}
+// provisionAdminRouters provisions all the router modules
+// in the admin.api namespace that need provisioning.
+func (admin AdminConfig) provisionAdminRouters(ctx Context) error {
+ for _, router := range admin.routers {
+ provisioner, ok := router.(Provisioner)
+ if !ok {
+ continue
+ }
+
+ err := provisioner.Provision(ctx)
+ if err != nil {
+ return err
+ }
+ }
+
+ // We no longer need the routers once provisioned, allow for GC
+ admin.routers = nil
+ return nil
+}
+
// allowedOrigins returns a list of origins that are allowed.
// If admin.Origins is nil (null), the provided listen address
// will be used as the default origin. If admin.Origins is
@@ -332,25 +357,26 @@ func replaceLocalAdminServer(cfg *Config) error {
}
}()
- // always get a valid admin config
- adminConfig := DefaultAdminConfig
- if cfg != nil && cfg.Admin != nil {
- adminConfig = cfg.Admin
+ // set a default if admin wasn't otherwise configured
+ if cfg.Admin == nil {
+ cfg.Admin = &AdminConfig{
+ Listen: DefaultAdminListen,
+ }
}
// if new admin endpoint is to be disabled, we're done
- if adminConfig.Disabled {
+ if cfg.Admin.Disabled {
Log().Named("admin").Warn("admin endpoint disabled")
return nil
}
// extract a singular listener address
- addr, err := parseAdminListenAddr(adminConfig.Listen, DefaultAdminListen)
+ addr, err := parseAdminListenAddr(cfg.Admin.Listen, DefaultAdminListen)
if err != nil {
return err
}
- handler := adminConfig.newAdminHandler(addr, false)
+ handler := cfg.Admin.newAdminHandler(addr, false)
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
if err != nil {
@@ -380,7 +406,7 @@ func replaceLocalAdminServer(cfg *Config) error {
adminLogger.Info("admin endpoint started",
zap.String("address", addr.String()),
- zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
+ zap.Bool("enforce_origin", cfg.Admin.EnforceOrigin),
zap.Array("origins", loggableURLArray(handler.allowedOrigins)))
if !handler.enforceHost {
@@ -1244,12 +1270,6 @@ var (
// (TLS-authenticated) admin listener, if enabled and not
// specified otherwise.
DefaultRemoteAdminListen = ":2021"
-
- // DefaultAdminConfig is the default configuration
- // for the local administration endpoint.
- DefaultAdminConfig = &AdminConfig{
- Listen: DefaultAdminListen,
- }
)
// PIDFile writes a pidfile to the file at filename. It
diff --git a/caddy.go b/caddy.go
index 127484d..3643924 100644
--- a/caddy.go
+++ b/caddy.go
@@ -427,6 +427,13 @@ func run(newCfg *Config, start bool) error {
return nil
}
+ // Provision any admin routers which may need to access
+ // some of the other apps at runtime
+ err = newCfg.Admin.provisionAdminRouters(ctx)
+ if err != nil {
+ return err
+ }
+
// Start
err = func() error {
var started []string
diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go
index ec56ab9..d308aeb 100644
--- a/cmd/commandfuncs.go
+++ b/cmd/commandfuncs.go
@@ -202,7 +202,7 @@ func cmdRun(fl Flags) (int, error) {
// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
var configFile string
if !runCmdResumeFlag {
- config, configFile, err = loadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
+ config, configFile, err = LoadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
@@ -275,25 +275,33 @@ func cmdRun(fl Flags) (int, error) {
}
func cmdStop(fl Flags) (int, error) {
- stopCmdAddrFlag := fl.String("address")
+ addrFlag := fl.String("address")
+ configFlag := fl.String("config")
+ configAdapterFlag := fl.String("adapter")
- err := apiRequest(stopCmdAddrFlag, http.MethodPost, "/stop", nil, nil)
+ adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
+ }
+
+ resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/stop", nil, nil)
if err != nil {
caddy.Log().Warn("failed using API to stop instance", zap.Error(err))
return caddy.ExitCodeFailedStartup, err
}
+ defer resp.Body.Close()
return caddy.ExitCodeSuccess, nil
}
func cmdReload(fl Flags) (int, error) {
- reloadCmdConfigFlag := fl.String("config")
- reloadCmdConfigAdapterFlag := fl.String("adapter")
- reloadCmdAddrFlag := fl.String("address")
- reloadCmdForceFlag := fl.Bool("force")
+ configFlag := fl.String("config")
+ configAdapterFlag := fl.String("adapter")
+ addrFlag := fl.String("address")
+ forceFlag := fl.Bool("force")
// get the config in caddy's native format
- config, configFile, err := loadConfig(reloadCmdConfigFlag, reloadCmdConfigAdapterFlag)
+ config, configFile, err := LoadConfig(configFlag, configAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
@@ -301,30 +309,22 @@ func cmdReload(fl Flags) (int, error) {
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
}
- // get the address of the admin listener; use flag if specified
- adminAddr := reloadCmdAddrFlag
- if adminAddr == "" && len(config) > 0 {
- var tmpStruct struct {
- Admin caddy.AdminConfig `json:"admin"`
- }
- err = json.Unmarshal(config, &tmpStruct)
- if err != nil {
- return caddy.ExitCodeFailedStartup,
- fmt.Errorf("unmarshaling admin listener address from config: %v", err)
- }
- adminAddr = tmpStruct.Admin.Listen
+ adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
}
// optionally force a config reload
headers := make(http.Header)
- if reloadCmdForceFlag {
+ if forceFlag {
headers.Set("Cache-Control", "must-revalidate")
}
- err = apiRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config))
+ resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config))
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("sending configuration to instance: %v", err)
}
+ defer resp.Body.Close()
return caddy.ExitCodeSuccess, nil
}
@@ -518,7 +518,7 @@ func cmdValidateConfig(fl Flags) (int, error) {
validateCmdConfigFlag := fl.String("config")
validateCmdAdapterFlag := fl.String("adapter")
- input, _, err := loadConfig(validateCmdConfigFlag, validateCmdAdapterFlag)
+ input, _, err := LoadConfig(validateCmdConfigFlag, validateCmdAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
@@ -640,17 +640,15 @@ commands:
return caddy.ExitCodeSuccess, nil
}
-// apiRequest makes an API request to the endpoint adminAddr with the
-// given HTTP method and request URI. If body is non-nil, it will be
-// assumed to be Content-Type application/json.
-func apiRequest(adminAddr, method, uri string, headers http.Header, body io.Reader) error {
- // parse the admin address
- if adminAddr == "" {
- adminAddr = caddy.DefaultAdminListen
- }
+// AdminAPIRequest makes an API request according to the CLI flags given,
+// with the given HTTP method and request URI. If body is non-nil, it will
+// be assumed to be Content-Type application/json. The caller should close
+// the response body. Should only be used by Caddy CLI commands which
+// need to interact with a running instance of Caddy via the admin API.
+func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io.Reader) (*http.Response, error) {
parsedAddr, err := caddy.ParseNetworkAddress(adminAddr)
if err != nil || parsedAddr.PortRangeSize() > 1 {
- return fmt.Errorf("invalid admin address %s: %v", adminAddr, err)
+ return nil, fmt.Errorf("invalid admin address %s: %v", adminAddr, err)
}
origin := parsedAddr.JoinHostPort(0)
if parsedAddr.IsUnixNetwork() {
@@ -660,7 +658,7 @@ func apiRequest(adminAddr, method, uri string, headers http.Header, body io.Read
// form the request
req, err := http.NewRequest(method, "http://"+origin+uri, body)
if err != nil {
- return fmt.Errorf("making request: %v", err)
+ return nil, fmt.Errorf("making request: %v", err)
}
if parsedAddr.IsUnixNetwork() {
// When listening on a unix socket, the admin endpoint doesn't
@@ -700,20 +698,58 @@ func apiRequest(adminAddr, method, uri string, headers http.Header, body io.Read
resp, err := client.Do(req)
if err != nil {
- return fmt.Errorf("performing request: %v", err)
+ return nil, fmt.Errorf("performing request: %v", err)
}
- defer resp.Body.Close()
// if it didn't work, let the user know
if resp.StatusCode >= 400 {
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1024*10))
if err != nil {
- return fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
+ return nil, fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
+ }
+ return nil, fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
+ }
+
+ return resp, nil
+}
+
+// DetermineAdminAPIAddress determines which admin API endpoint address should
+// be used based on the inputs. By priority: if `address` is specified, then
+// it is returned; if `configFile` (and `configAdapter`) are specified, then that
+// config will be loaded to find the admin address; otherwise, the default
+// admin listen address will be returned.
+func DetermineAdminAPIAddress(address, configFile, configAdapter string) (string, error) {
+ // Prefer the address if specified and non-empty
+ if address != "" {
+ return address, nil
+ }
+
+ // Try to load the config from file if specified, with the given adapter name
+ if configFile != "" {
+ // get the config in caddy's native format
+ config, loadedConfigFile, err := LoadConfig(configFile, configAdapter)
+ if err != nil {
+ return "", err
+ }
+ if loadedConfigFile == "" {
+ return "", fmt.Errorf("no config file to load")
+ }
+
+ // get the address of the admin listener
+ if len(config) > 0 {
+ var tmpStruct struct {
+ Admin caddy.AdminConfig `json:"admin"`
+ }
+ err = json.Unmarshal(config, &tmpStruct)
+ if err != nil {
+ return "", fmt.Errorf("unmarshaling admin listener address from config: %v", err)
+ }
+ return tmpStruct.Admin.Listen, nil
}
- return fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
}
- return nil
+ // Fallback to the default listen address otherwise
+ return caddy.DefaultAdminListen, nil
}
type moduleInfo struct {
diff --git a/cmd/commands.go b/cmd/commands.go
index 1e2c40d..0c68b7c 100644
--- a/cmd/commands.go
+++ b/cmd/commands.go
@@ -156,16 +156,19 @@ development environment.`,
RegisterCommand(Command{
Name: "stop",
Func: cmdStop,
+ Usage: "[--address <interface>] [--config <path> [--adapter <name>]]",
Short: "Gracefully stops a started Caddy process",
Long: `
Stops the background Caddy process as gracefully as possible.
It requires that the admin API is enabled and accessible, since it will
-use the API's /stop endpoint. The address of this request can be
-customized using the --address flag if it is not the default.`,
+use the API's /stop endpoint. The address of this request can be customized
+using the --address flag, or from the given --config, if not the default.`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("stop", flag.ExitOnError)
fs.String("address", "", "The address to use to reach the admin API endpoint, if not the default")
+ fs.String("config", "", "Configuration file to use to parse the admin address, if --address is not used")
+ fs.String("adapter", "", "Name of config adapter to apply (when --config is used)")
return fs
}(),
})
diff --git a/cmd/main.go b/cmd/main.go
index 7c33c55..f111ba4 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -103,15 +103,15 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
return nil
}
-// loadConfig loads the config from configFile and adapts it
+// LoadConfig loads the config from configFile and adapts it
// using adapterName. If adapterName is specified, configFile
// must be also. If no configFile is specified, it tries
// loading a default config file. The lack of a config file is
// not treated as an error, but false will be returned if
// there is no config available. It prints any warnings to stderr,
// and returns the resulting JSON config bytes along with
-// whether a config file was loaded or not.
-func loadConfig(configFile, adapterName string) ([]byte, string, error) {
+// the name of the loaded config file (if any).
+func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
// specifying an adapter without a config file is ambiguous
if adapterName != "" && configFile == "" {
return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)")
@@ -262,7 +262,7 @@ func watchConfigFile(filename, adapterName string) {
lastModified = info.ModTime()
// load the contents of the file
- config, _, err := loadConfig(filename, adapterName)
+ config, _, err := LoadConfig(filename, adapterName)
if err != nil {
logger().Error("unable to load latest config", zap.Error(err))
continue
diff --git a/context.go b/context.go
index a6386aa..2a6f514 100644
--- a/context.go
+++ b/context.go
@@ -423,6 +423,17 @@ func (ctx Context) App(name string) (interface{}, error) {
return modVal, nil
}
+// AppIsConfigured returns whether an app named name has been
+// configured. Can be called before calling App() to avoid
+// instantiating an empty app when that's not desirable.
+func (ctx Context) AppIsConfigured(name string) bool {
+ if _, ok := ctx.cfg.apps[name]; ok {
+ return true
+ }
+ appRaw := ctx.cfg.AppsRaw[name]
+ return appRaw != nil
+}
+
// Storage returns the configured Caddy storage implementation.
func (ctx Context) Storage() certmagic.Storage {
return ctx.cfg.storage
diff --git a/modules/caddypki/adminpki.go b/modules/caddypki/adminpki.go
new file mode 100644
index 0000000..5933bcd
--- /dev/null
+++ b/modules/caddypki/adminpki.go
@@ -0,0 +1,194 @@
+// Copyright 2020 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 caddypki
+
+import (
+ "encoding/json"
+ "encoding/pem"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/caddyserver/caddy/v2"
+ "go.uber.org/zap"
+)
+
+func init() {
+ caddy.RegisterModule(adminPKI{})
+}
+
+// adminPKI is a module that serves a PKI endpoint to retrieve
+// information about the CAs being managed by Caddy.
+type adminPKI struct {
+ ctx caddy.Context
+ log *zap.Logger
+ pkiApp *PKI
+}
+
+// CaddyModule returns the Caddy module information.
+func (adminPKI) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ ID: "admin.api.pki",
+ New: func() caddy.Module { return new(adminPKI) },
+ }
+}
+
+// Provision sets up the adminPKI module.
+func (a *adminPKI) Provision(ctx caddy.Context) error {
+ a.ctx = ctx
+ a.log = ctx.Logger(a)
+
+ // 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
+ }
+ a.pkiApp = appModule.(*PKI)
+
+ return nil
+}
+
+// Routes returns the admin routes for the PKI app.
+func (a *adminPKI) Routes() []caddy.AdminRoute {
+ return []caddy.AdminRoute{
+ {
+ Pattern: adminPKICertificatesEndpoint,
+ Handler: caddy.AdminHandlerFunc(a.handleCertificates),
+ },
+ }
+}
+
+// handleCertificates returns certificate information about a particular
+// CA, by its ID. If the CA ID is the default, then the CA will be
+// provisioned if it has not already been. Other CA IDs will return an
+// error if they have not been previously provisioned.
+func (a *adminPKI) handleCertificates(w http.ResponseWriter, r *http.Request) error {
+ if r.Method != http.MethodGet {
+ return caddy.APIError{
+ HTTPStatus: http.StatusMethodNotAllowed,
+ Err: fmt.Errorf("method not allowed"),
+ }
+ }
+
+ // Prep for a JSON response
+ w.Header().Set("Content-Type", "application/json")
+ enc := json.NewEncoder(w)
+
+ idPath := r.URL.Path
+
+ // Grab the CA ID from the request path, it should be the 4th segment
+ parts := strings.Split(idPath, "/")
+ if len(parts) < 4 || parts[3] == "" {
+ return caddy.APIError{
+ HTTPStatus: http.StatusBadRequest,
+ Err: fmt.Errorf("request path is missing the CA ID"),
+ }
+ }
+ if parts[0] != "" || parts[1] != "pki" || parts[2] != "certificates" {
+ return caddy.APIError{
+ HTTPStatus: http.StatusBadRequest,
+ Err: fmt.Errorf("malformed object path"),
+ }
+ }
+ id := parts[3]
+
+ // Find the CA by ID, if PKI is configured
+ var ca *CA
+ ok := false
+ if a.pkiApp != nil {
+ ca, ok = a.pkiApp.CAs[id]
+ }
+
+ // If we didn't find the CA, and PKI is not configured
+ // then we'll either error out if the CA ID is not the
+ // default. If the CA ID is the default, then we'll
+ // provision it, because the user probably aims to
+ // change their config to enable PKI immediately after
+ // if they actually requested the local CA ID.
+ if !ok {
+ if id != DefaultCAID {
+ return caddy.APIError{
+ HTTPStatus: http.StatusNotFound,
+ Err: fmt.Errorf("no certificate authority configured with id: %s", id),
+ }
+ }
+
+ // Provision the default CA, which generates and stores a root
+ // certificate in storage, if one doesn't already exist.
+ ca = new(CA)
+ err := ca.Provision(a.ctx, id, a.log)
+ if err != nil {
+ return caddy.APIError{
+ HTTPStatus: http.StatusInternalServerError,
+ Err: fmt.Errorf("failed to provision CA %s, %w", id, err),
+ }
+ }
+ }
+
+ // Convert the root certificate to PEM
+ rootPem := string(pem.EncodeToMemory(&pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: ca.RootCertificate().Raw,
+ }))
+
+ // Convert the intermediate certificate to PEM
+ interPem := string(pem.EncodeToMemory(&pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: ca.IntermediateCertificate().Raw,
+ }))
+
+ // Build the response
+ response := CAInfo{
+ ID: ca.ID,
+ Name: ca.Name,
+ Root: rootPem,
+ Intermediate: interPem,
+ }
+
+ // Encode and write the JSON response
+ err := enc.Encode(response)
+ if err != nil {
+ return caddy.APIError{
+ HTTPStatus: http.StatusInternalServerError,
+ Err: err,
+ }
+ }
+
+ return nil
+}
+
+// CAInfo is the response from the certificates API endpoint
+type CAInfo struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Root string `json:"root"`
+ Intermediate string `json:"intermediate"`
+}
+
+const adminPKICertificatesEndpoint = "/pki/certificates/"
+
+// Interface guards
+var (
+ _ caddy.AdminRouter = (*adminPKI)(nil)
+ _ caddy.Provisioner = (*adminPKI)(nil)
+)
diff --git a/modules/caddypki/command.go b/modules/caddypki/command.go
index 34daefa..fa37ab0 100644
--- a/modules/caddypki/command.go
+++ b/modules/caddypki/command.go
@@ -15,11 +15,13 @@
package caddypki
import (
- "context"
+ "crypto/x509"
+ "encoding/json"
+ "encoding/pem"
"flag"
"fmt"
+ "net/http"
"os"
- "path/filepath"
"github.com/caddyserver/caddy/v2"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
@@ -30,69 +32,110 @@ 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: `
-Adds a root certificate into the local trust stores. Intended for
-development environments only.
-
-Since Caddy will install its root certificates into the local trust
-stores automatically when they are first generated, this command is
-only necessary if you need to pre-install the certificates before
-using them; for example, if you have elevated privileges at one
-point but not later, you will want to use this command so that a
-password prompt is not required later.
-
-This command installs the root certificate only for Caddy's
-default CA.`,
+Adds a root certificate into the local trust stores.
+
+Caddy will attempt to install its root certificates into the local
+trust stores automatically when they are first generated, but it
+might fail if Caddy doesn't have the appropriate permissions to
+write to the trust store. This command is necessary to pre-install
+the certificates before using them, if the server process runs as an
+unprivileged user (such as via systemd).
+
+By default, this command installs the root certificate for Caddy's
+default CA (i.e. 'local'). You may specify the ID of another CA
+with the --ca flag.
+
+Also, this command 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("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
+ }(),
})
caddycmd.RegisterCommand(caddycmd.Command{
Name: "untrust",
Func: cmdUntrust,
- Usage: "[--ca <id> | --cert <path>]",
+ Usage: "[--cert <path>] | [[--ca <id>] [--address <listen>] [--config <path> [--adapter <name>]]]",
Short: "Untrusts a locally-trusted CA certificate",
Long: `
-Untrusts a root certificate from the local trust store(s). Intended
-for development environments only.
+Untrusts a root certificate from the local trust store(s).
This command uninstalls trust; it does not necessarily delete the
root certificate from trust stores entirely. Thus, repeatedly
trusting and untrusting new certificates can fill up trust databases.
-This command does not delete or modify certificate files.
+This command does not delete or modify certificate files from Caddy's
+configured storage.
-Specify which certificate to untrust either by the ID of its CA with
-the --ca flag, or the direct path to the certificate file with the
---cert flag. If the --ca flag is used, only the default storage paths
-are assumed (i.e. using --ca flag with custom storage backends or file
-paths will not work).
+This command can be used in one of two ways. Either by specifying
+which certificate to untrust by a direct path to the certificate
+file with the --cert flag, or by fetching the root certificate for
+the CA from the admin API (default behaviour).
-If no flags are specified, --ca=local is assumed.`,
+If the admin API is used, then the CA defaults to 'local'. You may
+specify the ID of another CA with the --ca flag. By default, this
+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("ca", "", "The ID of the CA to untrust")
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
}(),
})
}
-func cmdTrust(fs caddycmd.Flags) (int, error) {
- // we have to create a sort of dummy context so that
- // the CA can provision itself...
- ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
- defer cancel()
+func cmdTrust(fl caddycmd.Flags) (int, error) {
+ caID := fl.String("ca")
+ addrFlag := fl.String("address")
+ configFlag := fl.String("config")
+ configAdapterFlag := fl.String("adapter")
- // provision the CA, which generates and stores a root
- // certificate if one doesn't already exist in storage
- ca := CA{
- storage: caddy.DefaultStorage,
+ // Prepare the URI to the admin endpoint
+ if caID == "" {
+ caID = DefaultCAID
}
- err := ca.Provision(ctx, DefaultCAID, caddy.Log())
+
+ // Determine where we're sending the request to get the CA info
+ adminAddr, err := caddycmd.DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
+ }
+
+ // Fetch the root cert from the admin API
+ rootCert, err := rootCertFromAdmin(adminAddr, caID)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
+ // Set up the CA struct; we only need to fill in the root
+ // because we're only using it to make use of the installRoot()
+ // function. Also needs a logger for warnings, and a "cert path"
+ // for the root cert; since we're loading from the API and we
+ // don't know the actual storage path via this flow, we'll just
+ // pass through the admin API address instead.
+ ca := CA{
+ log: caddy.Log(),
+ root: rootCert,
+ rootCertPath: adminAddr + adminPKICertificatesEndpoint + caID,
+ }
+
+ // Install the cert!
err = ca.installRoot()
if err != nil {
return caddy.ExitCodeFailedStartup, err
@@ -101,33 +144,93 @@ func cmdTrust(fs caddycmd.Flags) (int, error) {
return caddy.ExitCodeSuccess, nil
}
-func cmdUntrust(fs caddycmd.Flags) (int, error) {
- ca := fs.String("ca")
- cert := fs.String("cert")
+func cmdUntrust(fl caddycmd.Flags) (int, error) {
+ certFile := fl.String("cert")
+ caID := fl.String("ca")
+ addrFlag := fl.String("address")
+ configFlag := fl.String("config")
+ configAdapterFlag := fl.String("adapter")
- if ca != "" && cert != "" {
- return caddy.ExitCodeFailedStartup, fmt.Errorf("conflicting command line arguments")
+ if certFile != "" && (caID != "" || addrFlag != "" || configFlag != "") {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("conflicting command line arguments, cannot use --cert with other flags")
}
- if ca == "" && cert == "" {
- ca = DefaultCAID
+
+ // If a file was specified, try to uninstall the cert matching that file
+ if certFile != "" {
+ // Sanity check, make sure cert file exists first
+ _, err := os.Stat(certFile)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing certificate file: %v", err)
+ }
+
+ // Uninstall the file!
+ err = truststore.UninstallFile(certFile,
+ truststore.WithDebug(),
+ truststore.WithFirefox(),
+ truststore.WithJava())
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to uninstall certificate file: %v", err)
+ }
+
+ return caddy.ExitCodeSuccess, nil
}
- if ca != "" {
- cert = filepath.Join(caddy.AppDataDir(), "pki", "authorities", ca, "root.crt")
+
+ // Prepare the URI to the admin endpoint
+ if caID == "" {
+ caID = DefaultCAID
}
- // sanity check, make sure cert file exists first
- _, err := os.Stat(cert)
+ // Determine where we're sending the request to get the CA info
+ adminAddr, err := caddycmd.DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag)
if err != nil {
- return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing certificate file: %v", err)
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
}
- err = truststore.UninstallFile(cert,
+ // Fetch the root cert from the admin API
+ rootCert, err := rootCertFromAdmin(adminAddr, caID)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+
+ // Uninstall the cert!
+ err = truststore.Uninstall(rootCert,
truststore.WithDebug(),
truststore.WithFirefox(),
truststore.WithJava())
if err != nil {
- return caddy.ExitCodeFailedStartup, err
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to uninstall certificate file: %v", err)
}
return caddy.ExitCodeSuccess, nil
}
+
+// rootCertFromAdmin makes the API request to fetch the
+func rootCertFromAdmin(adminAddr string, caID string) (*x509.Certificate, error) {
+ uri := adminPKICertificatesEndpoint + caID
+
+ // Make the request to fetch the CA info
+ resp, err := caddycmd.AdminAPIRequest(adminAddr, http.MethodGet, uri, make(http.Header), nil)
+ if err != nil {
+ return nil, fmt.Errorf("requesting CA info: %v", err)
+ }
+ defer resp.Body.Close()
+
+ // Decode the resposne
+ caInfo := new(CAInfo)
+ err = json.NewDecoder(resp.Body).Decode(caInfo)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode JSON response: %v", err)
+ }
+
+ // Decode the root
+ rootBlock, _ := pem.Decode([]byte(caInfo.Root))
+ if rootBlock == nil {
+ return nil, fmt.Errorf("failed to decode root certificate: %v", err)
+ }
+ rootCert, err := x509.ParseCertificate(rootBlock.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse root certificate: %v", err)
+ }
+
+ return rootCert, nil
+}
diff --git a/modules/caddypki/pki.go b/modules/caddypki/pki.go
index c19bd0f..4fd0bb5 100644
--- a/modules/caddypki/pki.go
+++ b/modules/caddypki/pki.go
@@ -91,7 +91,7 @@ func (p *PKI) Start() error {
// install roots to trust store, if not disabled
for _, ca := range p.CAs {
if ca.InstallTrust != nil && !*ca.InstallTrust {
- ca.log.Warn("root certificate trust store installation disabled; unconfigured clients may show warnings",
+ ca.log.Info("root certificate trust store installation disabled; unconfigured clients may show warnings",
zap.String("path", ca.rootCertPath))
continue
}