@marcel91200 / caddy Mirror · read-only / commits / bbad6931
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 13, 21:45 UTC

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

Francis Lavoie committed Mar 2, 2022 at 13:08 UTC bbad6931e30a2e74b3f53fff797d1115cc9dd491
9 files changed +483 -109
admin.go
+35 -15
@@ -92,6 +92,10 @@ type AdminConfig struct {
92 //
93 // EXPERIMENTAL: This feature is subject to change.
94 Remote *RemoteAdmin `json:"remote,omitempty"`
95 +
96 + // Holds onto the routers so that we can later provision them
97 + // if they require provisioning.
98 + routers []AdminRouter
99 }
100
101 // ConfigSettings configures the management of configuration.
@@ -190,7 +194,7 @@ type AdminPermissions struct {
194
195 // newAdminHandler reads admin's config and returns an http.Handler suitable
196 // for use in an admin endpoint server, which will be listening on listenAddr.
193 -func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler {
197 +func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler {
198 muxWrap := adminHandler{mux: http.NewServeMux()}
199
200 // secure the local or remote endpoint respectively
@@ -250,11 +254,32 @@ func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) admin
254 for _, route := range router.Routes() {
255 addRoute(route.Pattern, handlerLabel, route.Handler)
256 }
257 + admin.routers = append(admin.routers, router)
258 }
259
260 return muxWrap
261 }
262
263 +// provisionAdminRouters provisions all the router modules
264 +// in the admin.api namespace that need provisioning.
265 +func (admin AdminConfig) provisionAdminRouters(ctx Context) error {
266 + for _, router := range admin.routers {
267 + provisioner, ok := router.(Provisioner)
268 + if !ok {
269 + continue
270 + }
271 +
272 + err := provisioner.Provision(ctx)
273 + if err != nil {
274 + return err
275 + }
276 + }
277 +
278 + // We no longer need the routers once provisioned, allow for GC
279 + admin.routers = nil
280 + return nil
281 +}
282 +
283 // allowedOrigins returns a list of origins that are allowed.
284 // If admin.Origins is nil (null), the provided listen address
285 // will be used as the default origin. If admin.Origins is
@@ -332,25 +357,26 @@ func replaceLocalAdminServer(cfg *Config) error {
357 }
358 }()
359
335 - // always get a valid admin config
336 - adminConfig := DefaultAdminConfig
337 - if cfg != nil && cfg.Admin != nil {
338 - adminConfig = cfg.Admin
360 + // set a default if admin wasn't otherwise configured
361 + if cfg.Admin == nil {
362 + cfg.Admin = &AdminConfig{
363 + Listen: DefaultAdminListen,
364 + }
365 }
366
367 // if new admin endpoint is to be disabled, we're done
342 - if adminConfig.Disabled {
368 + if cfg.Admin.Disabled {
369 Log().Named("admin").Warn("admin endpoint disabled")
370 return nil
371 }
372
373 // extract a singular listener address
348 - addr, err := parseAdminListenAddr(adminConfig.Listen, DefaultAdminListen)
374 + addr, err := parseAdminListenAddr(cfg.Admin.Listen, DefaultAdminListen)
375 if err != nil {
376 return err
377 }
378
353 - handler := adminConfig.newAdminHandler(addr, false)
379 + handler := cfg.Admin.newAdminHandler(addr, false)
380
381 ln, err := Listen(addr.Network, addr.JoinHostPort(0))
382 if err != nil {
@@ -380,7 +406,7 @@ func replaceLocalAdminServer(cfg *Config) error {
406
407 adminLogger.Info("admin endpoint started",
408 zap.String("address", addr.String()),
383 - zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
409 + zap.Bool("enforce_origin", cfg.Admin.EnforceOrigin),
410 zap.Array("origins", loggableURLArray(handler.allowedOrigins)))
411
412 if !handler.enforceHost {
@@ -1244,12 +1270,6 @@ var (
1270 // (TLS-authenticated) admin listener, if enabled and not
1271 // specified otherwise.
1272 DefaultRemoteAdminListen = ":2021"
1247 -
1248 - // DefaultAdminConfig is the default configuration
1249 - // for the local administration endpoint.
1250 - DefaultAdminConfig = &AdminConfig{
1251 - Listen: DefaultAdminListen,
1252 - }
1273 )
1274
1275 // PIDFile writes a pidfile to the file at filename. It
caddy.go
+7
@@ -427,6 +427,13 @@ func run(newCfg *Config, start bool) error {
427 return nil
428 }
429
430 + // Provision any admin routers which may need to access
431 + // some of the other apps at runtime
432 + err = newCfg.Admin.provisionAdminRouters(ctx)
433 + if err != nil {
434 + return err
435 + }
436 +
437 // Start
438 err = func() error {
439 var started []string
cmd/commandfuncs.go
+74 -38
@@ -202,7 +202,7 @@ func cmdRun(fl Flags) (int, error) {
202 // we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
203 var configFile string
204 if !runCmdResumeFlag {
205 - config, configFile, err = loadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
205 + config, configFile, err = LoadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
206 if err != nil {
207 return caddy.ExitCodeFailedStartup, err
208 }
@@ -275,25 +275,33 @@ func cmdRun(fl Flags) (int, error) {
275 }
276
277 func cmdStop(fl Flags) (int, error) {
278 - stopCmdAddrFlag := fl.String("address")
278 + addrFlag := fl.String("address")
279 + configFlag := fl.String("config")
280 + configAdapterFlag := fl.String("adapter")
281
280 - err := apiRequest(stopCmdAddrFlag, http.MethodPost, "/stop", nil, nil)
282 + adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag)
283 + if err != nil {
284 + return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
285 + }
286 +
287 + resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/stop", nil, nil)
288 if err != nil {
289 caddy.Log().Warn("failed using API to stop instance", zap.Error(err))
290 return caddy.ExitCodeFailedStartup, err
291 }
292 + defer resp.Body.Close()
293
294 return caddy.ExitCodeSuccess, nil
295 }
296
297 func cmdReload(fl Flags) (int, error) {
290 - reloadCmdConfigFlag := fl.String("config")
291 - reloadCmdConfigAdapterFlag := fl.String("adapter")
292 - reloadCmdAddrFlag := fl.String("address")
293 - reloadCmdForceFlag := fl.Bool("force")
298 + configFlag := fl.String("config")
299 + configAdapterFlag := fl.String("adapter")
300 + addrFlag := fl.String("address")
301 + forceFlag := fl.Bool("force")
302
303 // get the config in caddy's native format
296 - config, configFile, err := loadConfig(reloadCmdConfigFlag, reloadCmdConfigAdapterFlag)
304 + config, configFile, err := LoadConfig(configFlag, configAdapterFlag)
305 if err != nil {
306 return caddy.ExitCodeFailedStartup, err
307 }
@@ -301,30 +309,22 @@ func cmdReload(fl Flags) (int, error) {
309 return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
310 }
311
304 - // get the address of the admin listener; use flag if specified
305 - adminAddr := reloadCmdAddrFlag
306 - if adminAddr == "" && len(config) > 0 {
307 - var tmpStruct struct {
308 - Admin caddy.AdminConfig `json:"admin"`
309 - }
310 - err = json.Unmarshal(config, &tmpStruct)
311 - if err != nil {
312 - return caddy.ExitCodeFailedStartup,
313 - fmt.Errorf("unmarshaling admin listener address from config: %v", err)
314 - }
315 - adminAddr = tmpStruct.Admin.Listen
312 + adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag)
313 + if err != nil {
314 + return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
315 }
316
317 // optionally force a config reload
318 headers := make(http.Header)
320 - if reloadCmdForceFlag {
319 + if forceFlag {
320 headers.Set("Cache-Control", "must-revalidate")
321 }
322
324 - err = apiRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config))
323 + resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config))
324 if err != nil {
325 return caddy.ExitCodeFailedStartup, fmt.Errorf("sending configuration to instance: %v", err)
326 }
327 + defer resp.Body.Close()
328
329 return caddy.ExitCodeSuccess, nil
330 }
@@ -518,7 +518,7 @@ func cmdValidateConfig(fl Flags) (int, error) {
518 validateCmdConfigFlag := fl.String("config")
519 validateCmdAdapterFlag := fl.String("adapter")
520
521 - input, _, err := loadConfig(validateCmdConfigFlag, validateCmdAdapterFlag)
521 + input, _, err := LoadConfig(validateCmdConfigFlag, validateCmdAdapterFlag)
522 if err != nil {
523 return caddy.ExitCodeFailedStartup, err
524 }
@@ -640,17 +640,15 @@ commands:
640 return caddy.ExitCodeSuccess, nil
641 }
642
643 -// apiRequest makes an API request to the endpoint adminAddr with the
644 -// given HTTP method and request URI. If body is non-nil, it will be
645 -// assumed to be Content-Type application/json.
646 -func apiRequest(adminAddr, method, uri string, headers http.Header, body io.Reader) error {
647 - // parse the admin address
648 - if adminAddr == "" {
649 - adminAddr = caddy.DefaultAdminListen
650 - }
643 +// AdminAPIRequest makes an API request according to the CLI flags given,
644 +// with the given HTTP method and request URI. If body is non-nil, it will
645 +// be assumed to be Content-Type application/json. The caller should close
646 +// the response body. Should only be used by Caddy CLI commands which
647 +// need to interact with a running instance of Caddy via the admin API.
648 +func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io.Reader) (*http.Response, error) {
649 parsedAddr, err := caddy.ParseNetworkAddress(adminAddr)
650 if err != nil || parsedAddr.PortRangeSize() > 1 {
653 - return fmt.Errorf("invalid admin address %s: %v", adminAddr, err)
651 + return nil, fmt.Errorf("invalid admin address %s: %v", adminAddr, err)
652 }
653 origin := parsedAddr.JoinHostPort(0)
654 if parsedAddr.IsUnixNetwork() {
@@ -660,7 +658,7 @@ func apiRequest(adminAddr, method, uri string, headers http.Header, body io.Read
658 // form the request
659 req, err := http.NewRequest(method, "http://"+origin+uri, body)
660 if err != nil {
663 - return fmt.Errorf("making request: %v", err)
661 + return nil, fmt.Errorf("making request: %v", err)
662 }
663 if parsedAddr.IsUnixNetwork() {
664 // 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
698
699 resp, err := client.Do(req)
700 if err != nil {
703 - return fmt.Errorf("performing request: %v", err)
701 + return nil, fmt.Errorf("performing request: %v", err)
702 }
705 - defer resp.Body.Close()
703
704 // if it didn't work, let the user know
705 if resp.StatusCode >= 400 {
706 respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1024*10))
707 if err != nil {
711 - return fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
708 + return nil, fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
709 + }
710 + return nil, fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
711 + }
712 +
713 + return resp, nil
714 +}
715 +
716 +// DetermineAdminAPIAddress determines which admin API endpoint address should
717 +// be used based on the inputs. By priority: if `address` is specified, then
718 +// it is returned; if `configFile` (and `configAdapter`) are specified, then that
719 +// config will be loaded to find the admin address; otherwise, the default
720 +// admin listen address will be returned.
721 +func DetermineAdminAPIAddress(address, configFile, configAdapter string) (string, error) {
722 + // Prefer the address if specified and non-empty
723 + if address != "" {
724 + return address, nil
725 + }
726 +
727 + // Try to load the config from file if specified, with the given adapter name
728 + if configFile != "" {
729 + // get the config in caddy's native format
730 + config, loadedConfigFile, err := LoadConfig(configFile, configAdapter)
731 + if err != nil {
732 + return "", err
733 + }
734 + if loadedConfigFile == "" {
735 + return "", fmt.Errorf("no config file to load")
736 + }
737 +
738 + // get the address of the admin listener
739 + if len(config) > 0 {
740 + var tmpStruct struct {
741 + Admin caddy.AdminConfig `json:"admin"`
742 + }
743 + err = json.Unmarshal(config, &tmpStruct)
744 + if err != nil {
745 + return "", fmt.Errorf("unmarshaling admin listener address from config: %v", err)
746 + }
747 + return tmpStruct.Admin.Listen, nil
748 }
713 - return fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
749 }
750
716 - return nil
751 + // Fallback to the default listen address otherwise
752 + return caddy.DefaultAdminListen, nil
753 }
754
755 type moduleInfo struct {
cmd/commands.go
+5 -2
@@ -156,16 +156,19 @@ development environment.`,
156 RegisterCommand(Command{
157 Name: "stop",
158 Func: cmdStop,
159 + Usage: "[--address <interface>] [--config <path> [--adapter <name>]]",
160 Short: "Gracefully stops a started Caddy process",
161 Long: `
162 Stops the background Caddy process as gracefully as possible.
163
164 It requires that the admin API is enabled and accessible, since it will
164 -use the API's /stop endpoint. The address of this request can be
165 -customized using the --address flag if it is not the default.`,
165 +use the API's /stop endpoint. The address of this request can be customized
166 +using the --address flag, or from the given --config, if not the default.`,
167 Flags: func() *flag.FlagSet {
168 fs := flag.NewFlagSet("stop", flag.ExitOnError)
169 fs.String("address", "", "The address to use to reach the admin API endpoint, if not the default")
170 + fs.String("config", "", "Configuration file to use to parse the admin address, if --address is not used")
171 + fs.String("adapter", "", "Name of config adapter to apply (when --config is used)")
172 return fs
173 }(),
174 })
cmd/main.go
+4 -4
@@ -103,15 +103,15 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
103 return nil
104 }
105
106 -// loadConfig loads the config from configFile and adapts it
106 +// LoadConfig loads the config from configFile and adapts it
107 // using adapterName. If adapterName is specified, configFile
108 // must be also. If no configFile is specified, it tries
109 // loading a default config file. The lack of a config file is
110 // not treated as an error, but false will be returned if
111 // there is no config available. It prints any warnings to stderr,
112 // and returns the resulting JSON config bytes along with
113 -// whether a config file was loaded or not.
114 -func loadConfig(configFile, adapterName string) ([]byte, string, error) {
113 +// the name of the loaded config file (if any).
114 +func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
115 // specifying an adapter without a config file is ambiguous
116 if adapterName != "" && configFile == "" {
117 return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)")
@@ -262,7 +262,7 @@ func watchConfigFile(filename, adapterName string) {
262 lastModified = info.ModTime()
263
264 // load the contents of the file
265 - config, _, err := loadConfig(filename, adapterName)
265 + config, _, err := LoadConfig(filename, adapterName)
266 if err != nil {
267 logger().Error("unable to load latest config", zap.Error(err))
268 continue
context.go
+11
@@ -423,6 +423,17 @@ func (ctx Context) App(name string) (interface{}, error) {
423 return modVal, nil
424 }
425
426 +// AppIsConfigured returns whether an app named name has been
427 +// configured. Can be called before calling App() to avoid
428 +// instantiating an empty app when that's not desirable.
429 +func (ctx Context) AppIsConfigured(name string) bool {
430 + if _, ok := ctx.cfg.apps[name]; ok {
431 + return true
432 + }
433 + appRaw := ctx.cfg.AppsRaw[name]
434 + return appRaw != nil
435 +}
436 +
437 // Storage returns the configured Caddy storage implementation.
438 func (ctx Context) Storage() certmagic.Storage {
439 return ctx.cfg.storage
modules/caddypki/adminpki.go new
+194
@@ -0,0 +1,194 @@
1 +// Copyright 2020 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package caddypki
16 +
17 +import (
18 + "encoding/json"
19 + "encoding/pem"
20 + "fmt"
21 + "net/http"
22 + "strings"
23 +
24 + "github.com/caddyserver/caddy/v2"
25 + "go.uber.org/zap"
26 +)
27 +
28 +func init() {
29 + caddy.RegisterModule(adminPKI{})
30 +}
31 +
32 +// adminPKI is a module that serves a PKI endpoint to retrieve
33 +// information about the CAs being managed by Caddy.
34 +type adminPKI struct {
35 + ctx caddy.Context
36 + log *zap.Logger
37 + pkiApp *PKI
38 +}
39 +
40 +// CaddyModule returns the Caddy module information.
41 +func (adminPKI) CaddyModule() caddy.ModuleInfo {
42 + return caddy.ModuleInfo{
43 + ID: "admin.api.pki",
44 + New: func() caddy.Module { return new(adminPKI) },
45 + }
46 +}
47 +
48 +// Provision sets up the adminPKI module.
49 +func (a *adminPKI) Provision(ctx caddy.Context) error {
50 + a.ctx = ctx
51 + a.log = ctx.Logger(a)
52 +
53 + // First check if the PKI app was configured, because
54 + // a.ctx.App() has the side effect of instantiating
55 + // and provisioning an app even if it wasn't configured.
56 + pkiAppConfigured := a.ctx.AppIsConfigured("pki")
57 + if !pkiAppConfigured {
58 + return nil
59 + }
60 +
61 + // Load the PKI app, so we can query it for information.
62 + appModule, err := a.ctx.App("pki")
63 + if err != nil {
64 + return err
65 + }
66 + a.pkiApp = appModule.(*PKI)
67 +
68 + return nil
69 +}
70 +
71 +// Routes returns the admin routes for the PKI app.
72 +func (a *adminPKI) Routes() []caddy.AdminRoute {
73 + return []caddy.AdminRoute{
74 + {
75 + Pattern: adminPKICertificatesEndpoint,
76 + Handler: caddy.AdminHandlerFunc(a.handleCertificates),
77 + },
78 + }
79 +}
80 +
81 +// handleCertificates returns certificate information about a particular
82 +// CA, by its ID. If the CA ID is the default, then the CA will be
83 +// provisioned if it has not already been. Other CA IDs will return an
84 +// error if they have not been previously provisioned.
85 +func (a *adminPKI) handleCertificates(w http.ResponseWriter, r *http.Request) error {
86 + if r.Method != http.MethodGet {
87 + return caddy.APIError{
88 + HTTPStatus: http.StatusMethodNotAllowed,
89 + Err: fmt.Errorf("method not allowed"),
90 + }
91 + }
92 +
93 + // Prep for a JSON response
94 + w.Header().Set("Content-Type", "application/json")
95 + enc := json.NewEncoder(w)
96 +
97 + idPath := r.URL.Path
98 +
99 + // Grab the CA ID from the request path, it should be the 4th segment
100 + parts := strings.Split(idPath, "/")
101 + if len(parts) < 4 || parts[3] == "" {
102 + return caddy.APIError{
103 + HTTPStatus: http.StatusBadRequest,
104 + Err: fmt.Errorf("request path is missing the CA ID"),
105 + }
106 + }
107 + if parts[0] != "" || parts[1] != "pki" || parts[2] != "certificates" {
108 + return caddy.APIError{
109 + HTTPStatus: http.StatusBadRequest,
110 + Err: fmt.Errorf("malformed object path"),
111 + }
112 + }
113 + id := parts[3]
114 +
115 + // Find the CA by ID, if PKI is configured
116 + var ca *CA
117 + ok := false
118 + if a.pkiApp != nil {
119 + ca, ok = a.pkiApp.CAs[id]
120 + }
121 +
122 + // If we didn't find the CA, and PKI is not configured
123 + // then we'll either error out if the CA ID is not the
124 + // default. If the CA ID is the default, then we'll
125 + // provision it, because the user probably aims to
126 + // change their config to enable PKI immediately after
127 + // if they actually requested the local CA ID.
128 + if !ok {
129 + if id != DefaultCAID {
130 + return caddy.APIError{
131 + HTTPStatus: http.StatusNotFound,
132 + Err: fmt.Errorf("no certificate authority configured with id: %s", id),
133 + }
134 + }
135 +
136 + // Provision the default CA, which generates and stores a root
137 + // certificate in storage, if one doesn't already exist.
138 + ca = new(CA)
139 + err := ca.Provision(a.ctx, id, a.log)
140 + if err != nil {
141 + return caddy.APIError{
142 + HTTPStatus: http.StatusInternalServerError,
143 + Err: fmt.Errorf("failed to provision CA %s, %w", id, err),
144 + }
145 + }
146 + }
147 +
148 + // Convert the root certificate to PEM
149 + rootPem := string(pem.EncodeToMemory(&pem.Block{
150 + Type: "CERTIFICATE",
151 + Bytes: ca.RootCertificate().Raw,
152 + }))
153 +
154 + // Convert the intermediate certificate to PEM
155 + interPem := string(pem.EncodeToMemory(&pem.Block{
156 + Type: "CERTIFICATE",
157 + Bytes: ca.IntermediateCertificate().Raw,
158 + }))
159 +
160 + // Build the response
161 + response := CAInfo{
162 + ID: ca.ID,
163 + Name: ca.Name,
164 + Root: rootPem,
165 + Intermediate: interPem,
166 + }
167 +
168 + // Encode and write the JSON response
169 + err := enc.Encode(response)
170 + if err != nil {
171 + return caddy.APIError{
172 + HTTPStatus: http.StatusInternalServerError,
173 + Err: err,
174 + }
175 + }
176 +
177 + return nil
178 +}
179 +
180 +// CAInfo is the response from the certificates API endpoint
181 +type CAInfo struct {
182 + ID string `json:"id"`
183 + Name string `json:"name"`
184 + Root string `json:"root"`
185 + Intermediate string `json:"intermediate"`
186 +}
187 +
188 +const adminPKICertificatesEndpoint = "/pki/certificates/"
189 +
190 +// Interface guards
191 +var (
192 + _ caddy.AdminRouter = (*adminPKI)(nil)
193 + _ caddy.Provisioner = (*adminPKI)(nil)
194 +)
modules/caddypki/command.go
+152 -49
@@ -15,11 +15,13 @@
15 package caddypki
16
17 import (
18 - "context"
18 + "crypto/x509"
19 + "encoding/json"
20 + "encoding/pem"
21 "flag"
22 "fmt"
23 + "net/http"
24 "os"
22 - "path/filepath"
25
26 "github.com/caddyserver/caddy/v2"
27 caddycmd "github.com/caddyserver/caddy/v2/cmd"
@@ -30,69 +32,110 @@ func init() {
32 caddycmd.RegisterCommand(caddycmd.Command{
33 Name: "trust",
34 Func: cmdTrust,
35 + Usage: "[--ca <id>] [--address <listen>] [--config <path> [--adapter <name>]]",
36 Short: "Installs a CA certificate into local trust stores",
37 Long: `
35 -Adds a root certificate into the local trust stores. Intended for
36 -development environments only.
37 -
38 -Since Caddy will install its root certificates into the local trust
39 -stores automatically when they are first generated, this command is
40 -only necessary if you need to pre-install the certificates before
41 -using them; for example, if you have elevated privileges at one
42 -point but not later, you will want to use this command so that a
43 -password prompt is not required later.
44 -
45 -This command installs the root certificate only for Caddy's
46 -default CA.`,
38 +Adds a root certificate into the local trust stores.
39 +
40 +Caddy will attempt to install its root certificates into the local
41 +trust stores automatically when they are first generated, but it
42 +might fail if Caddy doesn't have the appropriate permissions to
43 +write to the trust store. This command is necessary to pre-install
44 +the certificates before using them, if the server process runs as an
45 +unprivileged user (such as via systemd).
46 +
47 +By default, this command installs the root certificate for Caddy's
48 +default CA (i.e. 'local'). You may specify the ID of another CA
49 +with the --ca flag.
50 +
51 +Also, this command will attempt to connect to the Caddy's admin API
52 +running at '` + caddy.DefaultAdminListen + `' to fetch the root certificate. You may
53 +explicitly specify the --address, or use the --config flag to load
54 +the admin address from your config, if not using the default.`,
55 + Flags: func() *flag.FlagSet {
56 + fs := flag.NewFlagSet("trust", flag.ExitOnError)
57 + fs.String("ca", "", "The ID of the CA to trust (defaults to 'local')")
58 + fs.String("address", "", "Address of the administration API listener (if --config is not used)")
59 + fs.String("config", "", "Configuration file (if --address is not used)")
60 + fs.String("adapter", "", "Name of config adapter to apply (if --config is used)")
61 + return fs
62 + }(),
63 })
64
65 caddycmd.RegisterCommand(caddycmd.Command{
66 Name: "untrust",
67 Func: cmdUntrust,
52 - Usage: "[--ca <id> | --cert <path>]",
68 + Usage: "[--cert <path>] | [[--ca <id>] [--address <listen>] [--config <path> [--adapter <name>]]]",
69 Short: "Untrusts a locally-trusted CA certificate",
70 Long: `
55 -Untrusts a root certificate from the local trust store(s). Intended
56 -for development environments only.
71 +Untrusts a root certificate from the local trust store(s).
72
73 This command uninstalls trust; it does not necessarily delete the
74 root certificate from trust stores entirely. Thus, repeatedly
75 trusting and untrusting new certificates can fill up trust databases.
76
62 -This command does not delete or modify certificate files.
77 +This command does not delete or modify certificate files from Caddy's
78 +configured storage.
79
64 -Specify which certificate to untrust either by the ID of its CA with
65 -the --ca flag, or the direct path to the certificate file with the
66 ---cert flag. If the --ca flag is used, only the default storage paths
67 -are assumed (i.e. using --ca flag with custom storage backends or file
68 -paths will not work).
80 +This command can be used in one of two ways. Either by specifying
81 +which certificate to untrust by a direct path to the certificate
82 +file with the --cert flag, or by fetching the root certificate for
83 +the CA from the admin API (default behaviour).
84
70 -If no flags are specified, --ca=local is assumed.`,
85 +If the admin API is used, then the CA defaults to 'local'. You may
86 +specify the ID of another CA with the --ca flag. By default, this
87 +will attempt to connect to the Caddy's admin API running at
88 +'` + caddy.DefaultAdminListen + `' to fetch the root certificate.
89 +You may explicitly specify the --address, or use the --config flag
90 +to load the admin address from your config, if not using the default.`,
91 Flags: func() *flag.FlagSet {
92 fs := flag.NewFlagSet("untrust", flag.ExitOnError)
73 - fs.String("ca", "", "The ID of the CA to untrust")
93 fs.String("cert", "", "The path to the CA certificate to untrust")
94 + fs.String("ca", "", "The ID of the CA to untrust (defaults to 'local')")
95 + fs.String("address", "", "Address of the administration API listener (if --config is not used)")
96 + fs.String("config", "", "Configuration file (if --address is not used)")
97 + fs.String("adapter", "", "Name of config adapter to apply (if --config is used)")
98 return fs
99 }(),
100 })
101 }
102
80 -func cmdTrust(fs caddycmd.Flags) (int, error) {
81 - // we have to create a sort of dummy context so that
82 - // the CA can provision itself...
83 - ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
84 - defer cancel()
103 +func cmdTrust(fl caddycmd.Flags) (int, error) {
104 + caID := fl.String("ca")
105 + addrFlag := fl.String("address")
106 + configFlag := fl.String("config")
107 + configAdapterFlag := fl.String("adapter")
108
86 - // provision the CA, which generates and stores a root
87 - // certificate if one doesn't already exist in storage
88 - ca := CA{
89 - storage: caddy.DefaultStorage,
109 + // Prepare the URI to the admin endpoint
110 + if caID == "" {
111 + caID = DefaultCAID
112 }
91 - err := ca.Provision(ctx, DefaultCAID, caddy.Log())
113 +
114 + // Determine where we're sending the request to get the CA info
115 + adminAddr, err := caddycmd.DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag)
116 + if err != nil {
117 + return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
118 + }
119 +
120 + // Fetch the root cert from the admin API
121 + rootCert, err := rootCertFromAdmin(adminAddr, caID)
122 if err != nil {
123 return caddy.ExitCodeFailedStartup, err
124 }
125
126 + // Set up the CA struct; we only need to fill in the root
127 + // because we're only using it to make use of the installRoot()
128 + // function. Also needs a logger for warnings, and a "cert path"
129 + // for the root cert; since we're loading from the API and we
130 + // don't know the actual storage path via this flow, we'll just
131 + // pass through the admin API address instead.
132 + ca := CA{
133 + log: caddy.Log(),
134 + root: rootCert,
135 + rootCertPath: adminAddr + adminPKICertificatesEndpoint + caID,
136 + }
137 +
138 + // Install the cert!
139 err = ca.installRoot()
140 if err != nil {
141 return caddy.ExitCodeFailedStartup, err
@@ -101,33 +144,93 @@ func cmdTrust(fs caddycmd.Flags) (int, error) {
144 return caddy.ExitCodeSuccess, nil
145 }
146
104 -func cmdUntrust(fs caddycmd.Flags) (int, error) {
105 - ca := fs.String("ca")
106 - cert := fs.String("cert")
147 +func cmdUntrust(fl caddycmd.Flags) (int, error) {
148 + certFile := fl.String("cert")
149 + caID := fl.String("ca")
150 + addrFlag := fl.String("address")
151 + configFlag := fl.String("config")
152 + configAdapterFlag := fl.String("adapter")
153
108 - if ca != "" && cert != "" {
109 - return caddy.ExitCodeFailedStartup, fmt.Errorf("conflicting command line arguments")
154 + if certFile != "" && (caID != "" || addrFlag != "" || configFlag != "") {
155 + return caddy.ExitCodeFailedStartup, fmt.Errorf("conflicting command line arguments, cannot use --cert with other flags")
156 }
111 - if ca == "" && cert == "" {
112 - ca = DefaultCAID
157 +
158 + // If a file was specified, try to uninstall the cert matching that file
159 + if certFile != "" {
160 + // Sanity check, make sure cert file exists first
161 + _, err := os.Stat(certFile)
162 + if err != nil {
163 + return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing certificate file: %v", err)
164 + }
165 +
166 + // Uninstall the file!
167 + err = truststore.UninstallFile(certFile,
168 + truststore.WithDebug(),
169 + truststore.WithFirefox(),
170 + truststore.WithJava())
171 + if err != nil {
172 + return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to uninstall certificate file: %v", err)
173 + }
174 +
175 + return caddy.ExitCodeSuccess, nil
176 }
114 - if ca != "" {
115 - cert = filepath.Join(caddy.AppDataDir(), "pki", "authorities", ca, "root.crt")
177 +
178 + // Prepare the URI to the admin endpoint
179 + if caID == "" {
180 + caID = DefaultCAID
181 }
182
118 - // sanity check, make sure cert file exists first
119 - _, err := os.Stat(cert)
183 + // Determine where we're sending the request to get the CA info
184 + adminAddr, err := caddycmd.DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag)
185 if err != nil {
121 - return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing certificate file: %v", err)
186 + return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
187 }
188
124 - err = truststore.UninstallFile(cert,
189 + // Fetch the root cert from the admin API
190 + rootCert, err := rootCertFromAdmin(adminAddr, caID)
191 + if err != nil {
192 + return caddy.ExitCodeFailedStartup, err
193 + }
194 +
195 + // Uninstall the cert!
196 + err = truststore.Uninstall(rootCert,
197 truststore.WithDebug(),
198 truststore.WithFirefox(),
199 truststore.WithJava())
200 if err != nil {
129 - return caddy.ExitCodeFailedStartup, err
201 + return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to uninstall certificate file: %v", err)
202 }
203
204 return caddy.ExitCodeSuccess, nil
205 }
206 +
207 +// rootCertFromAdmin makes the API request to fetch the
208 +func rootCertFromAdmin(adminAddr string, caID string) (*x509.Certificate, error) {
209 + uri := adminPKICertificatesEndpoint + caID
210 +
211 + // Make the request to fetch the CA info
212 + resp, err := caddycmd.AdminAPIRequest(adminAddr, http.MethodGet, uri, make(http.Header), nil)
213 + if err != nil {
214 + return nil, fmt.Errorf("requesting CA info: %v", err)
215 + }
216 + defer resp.Body.Close()
217 +
218 + // Decode the resposne
219 + caInfo := new(CAInfo)
220 + err = json.NewDecoder(resp.Body).Decode(caInfo)
221 + if err != nil {
222 + return nil, fmt.Errorf("failed to decode JSON response: %v", err)
223 + }
224 +
225 + // Decode the root
226 + rootBlock, _ := pem.Decode([]byte(caInfo.Root))
227 + if rootBlock == nil {
228 + return nil, fmt.Errorf("failed to decode root certificate: %v", err)
229 + }
230 + rootCert, err := x509.ParseCertificate(rootBlock.Bytes)
231 + if err != nil {
232 + return nil, fmt.Errorf("failed to parse root certificate: %v", err)
233 + }
234 +
235 + return rootCert, nil
236 +}
modules/caddypki/pki.go
+1 -1
@@ -91,7 +91,7 @@ func (p *PKI) Start() error {
91 // install roots to trust store, if not disabled
92 for _, ca := range p.CAs {
93 if ca.InstallTrust != nil && !*ca.InstallTrust {
94 - ca.log.Warn("root certificate trust store installation disabled; unconfigured clients may show warnings",
94 + ca.log.Info("root certificate trust store installation disabled; unconfigured clients may show warnings",
95 zap.String("path", ca.rootCertPath))
96 continue
97 }