pki: Implement API endpoints for certs and caddy trust (#4443)

* admin: Implement /pki/certificates/<id> API

* pki: Lower "skip_install_trust" log level to INFO

See https://github.com/caddyserver/caddy/issues/4058#issuecomment-976132935

It's not necessary to warn about this, because this was an option explicitly configured by the user. Still useful to log, but we don't need to be so loud about it.

* cmd: Export functions needed for PKI app, return API response to caller

* pki: Rewrite `caddy trust` command to use new admin endpoint instead

* pki: Rewrite `caddy untrust` command to support using admin endpoint

* Refactor cmd and pki packages for determining admin API endpoint
This commit is contained in:
Francis Lavoie 2022-03-02 13:08:36 -05:00 committed by GitHub
parent 5bd96a6ac2
commit bbad6931e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 485 additions and 111 deletions

View File

@ -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

View File

@ -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

View File

@ -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)
adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("unmarshaling admin listener address from config: %v", err)
}
adminAddr = tmpStruct.Admin.Listen
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 fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
return nil, fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
}
return nil
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
}
}
// Fallback to the default listen address otherwise
return caddy.DefaultAdminListen, nil
}
type moduleInfo struct {

View File

@ -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
}(),
})

View File

@ -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

View File

@ -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

View File

@ -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)
)

View File

@ -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.
Adds a root certificate into the local trust stores.
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.
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).
This command installs the root certificate only for Caddy's
default CA.`,
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 ca == "" && cert == "" {
ca = DefaultCAID
}
if ca != "" {
cert = filepath.Join(caddy.AppDataDir(), "pki", "authorities", ca, "root.crt")
if certFile != "" && (caID != "" || addrFlag != "" || configFlag != "") {
return caddy.ExitCodeFailedStartup, fmt.Errorf("conflicting command line arguments, cannot use --cert with other flags")
}
// sanity check, make sure cert file exists first
_, err := os.Stat(cert)
// 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)
}
err = truststore.UninstallFile(cert,
// Uninstall the file!
err = truststore.UninstallFile(certFile,
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
}
// Prepare the URI to the admin endpoint
if caID == "" {
caID = DefaultCAID
}
// 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
}
// Uninstall the cert!
err = truststore.Uninstall(rootCert,
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
}
// 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
}

View File

@ -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
}