mirror of https://github.com/caddyserver/caddy.git
caddypki: Refactor /pki/ admin endpoints
Remove /pki/certificates/<ca> endpoint and split into two endpoints: - GET /pki/ca/<id> to get CA info and certs in JSON format - GET /pki/ca/<id>/certificates to get cert in PEM chain
This commit is contained in:
parent
de490c7cad
commit
78e381b29f
|
@ -16,7 +16,6 @@ package caddypki
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -26,27 +25,27 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(adminPKI{})
|
caddy.RegisterModule(adminAPI{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// adminPKI is a module that serves a PKI endpoint to retrieve
|
// adminAPI is a module that serves PKI endpoints to retrieve
|
||||||
// information about the CAs being managed by Caddy.
|
// information about the CAs being managed by Caddy.
|
||||||
type adminPKI struct {
|
type adminAPI struct {
|
||||||
ctx caddy.Context
|
ctx caddy.Context
|
||||||
log *zap.Logger
|
log *zap.Logger
|
||||||
pkiApp *PKI
|
pkiApp *PKI
|
||||||
}
|
}
|
||||||
|
|
||||||
// CaddyModule returns the Caddy module information.
|
// CaddyModule returns the Caddy module information.
|
||||||
func (adminPKI) CaddyModule() caddy.ModuleInfo {
|
func (adminAPI) CaddyModule() caddy.ModuleInfo {
|
||||||
return caddy.ModuleInfo{
|
return caddy.ModuleInfo{
|
||||||
ID: "admin.api.pki",
|
ID: "admin.api.pki",
|
||||||
New: func() caddy.Module { return new(adminPKI) },
|
New: func() caddy.Module { return new(adminAPI) },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision sets up the adminPKI module.
|
// Provision sets up the adminAPI module.
|
||||||
func (a *adminPKI) Provision(ctx caddy.Context) error {
|
func (a *adminAPI) Provision(ctx caddy.Context) error {
|
||||||
a.ctx = ctx
|
a.ctx = ctx
|
||||||
a.log = ctx.Logger(a)
|
a.log = ctx.Logger(a)
|
||||||
|
|
||||||
|
@ -69,52 +68,128 @@ func (a *adminPKI) Provision(ctx caddy.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Routes returns the admin routes for the PKI app.
|
// Routes returns the admin routes for the PKI app.
|
||||||
func (a *adminPKI) Routes() []caddy.AdminRoute {
|
func (a *adminAPI) Routes() []caddy.AdminRoute {
|
||||||
return []caddy.AdminRoute{
|
return []caddy.AdminRoute{
|
||||||
{
|
{
|
||||||
Pattern: adminPKICertificatesEndpoint,
|
Pattern: adminPKIEndpointBase,
|
||||||
Handler: caddy.AdminHandlerFunc(a.handleCertificates),
|
Handler: caddy.AdminHandlerFunc(a.handleAPIEndpoints),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleCertificates returns certificate information about a particular
|
// handleAPIEndpoints routes API requests within adminPKIEndpointBase.
|
||||||
// CA, by its ID. If the CA ID is the default, then the CA will be
|
func (a *adminAPI) handleAPIEndpoints(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
uri := strings.TrimPrefix(r.URL.Path, "/pki/")
|
||||||
|
parts := strings.Split(uri, "/")
|
||||||
|
switch {
|
||||||
|
case len(parts) == 2 && parts[0] == "ca" && parts[1] != "":
|
||||||
|
return a.handleCAInfo(w, r)
|
||||||
|
case len(parts) == 3 && parts[0] == "ca" && parts[1] != "" && parts[2] == "certificates":
|
||||||
|
return a.handleCACerts(w, r)
|
||||||
|
}
|
||||||
|
return caddy.APIError{
|
||||||
|
HTTPStatus: http.StatusNotFound,
|
||||||
|
Err: fmt.Errorf("resource not found: %v", r.URL.Path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCAInfo returns cinformation 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
|
// provisioned if it has not already been. Other CA IDs will return an
|
||||||
// error if they have not been previously provisioned.
|
// error if they have not been previously provisioned.
|
||||||
func (a *adminPKI) handleCertificates(w http.ResponseWriter, r *http.Request) error {
|
func (a *adminAPI) handleCAInfo(w http.ResponseWriter, r *http.Request) error {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
return caddy.APIError{
|
return caddy.APIError{
|
||||||
HTTPStatus: http.StatusMethodNotAllowed,
|
HTTPStatus: http.StatusMethodNotAllowed,
|
||||||
Err: fmt.Errorf("method not allowed"),
|
Err: fmt.Errorf("method not allowed: %v", r.Method),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ca, err := a.getCAFromAPIRequestPath(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCert, interCert, err := rootAndIntermediatePEM(ca)
|
||||||
|
if err != nil {
|
||||||
|
return caddy.APIError{
|
||||||
|
HTTPStatus: http.StatusInternalServerError,
|
||||||
|
Err: fmt.Errorf("failed to get root and intermediate cert for CA %s: %v", ca.ID, err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repl := ca.newReplacer()
|
||||||
|
|
||||||
|
response := caInfo{
|
||||||
|
ID: ca.ID,
|
||||||
|
Name: ca.Name,
|
||||||
|
RootCN: repl.ReplaceAll(ca.RootCommonName, ""),
|
||||||
|
IntermediateCN: repl.ReplaceAll(ca.IntermediateCommonName, ""),
|
||||||
|
RootCert: string(rootCert),
|
||||||
|
IntermediateCert: string(interCert),
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
return caddy.APIError{
|
||||||
|
HTTPStatus: http.StatusInternalServerError,
|
||||||
|
Err: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prep for a JSON response
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
enc := json.NewEncoder(w)
|
w.Write(encoded)
|
||||||
|
|
||||||
idPath := r.URL.Path
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Grab the CA ID from the request path, it should be the 4th segment
|
// handleCACerts returns cinformation about a particular
|
||||||
parts := strings.Split(idPath, "/")
|
// CA by its ID. If the CA ID is the default, then the CA will be
|
||||||
if len(parts) < 4 || parts[3] == "" {
|
// provisioned if it has not already been. Other CA IDs will return an
|
||||||
|
// error if they have not been previously provisioned.
|
||||||
|
func (a *adminAPI) handleCACerts(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
return caddy.APIError{
|
return caddy.APIError{
|
||||||
HTTPStatus: http.StatusBadRequest,
|
HTTPStatus: http.StatusMethodNotAllowed,
|
||||||
Err: fmt.Errorf("request path is missing the CA ID"),
|
Err: fmt.Errorf("method not allowed: %v", r.Method),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if parts[0] != "" || parts[1] != "pki" || parts[2] != "certificates" {
|
|
||||||
|
ca, err := a.getCAFromAPIRequestPath(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCert, interCert, err := rootAndIntermediatePEM(ca)
|
||||||
|
if err != nil {
|
||||||
return caddy.APIError{
|
return caddy.APIError{
|
||||||
HTTPStatus: http.StatusBadRequest,
|
HTTPStatus: http.StatusInternalServerError,
|
||||||
Err: fmt.Errorf("malformed object path"),
|
Err: fmt.Errorf("failed to get root and intermediate cert for CA %s: %v", ca.ID, err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/pem-certificate-chain")
|
||||||
|
_, err = w.Write(interCert)
|
||||||
|
if err == nil {
|
||||||
|
w.Write(rootCert)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adminAPI) getCAFromAPIRequestPath(r *http.Request) (*CA, error) {
|
||||||
|
// Grab the CA ID from the request path, it should be the 4th segment (/pki/ca/<ca>)
|
||||||
|
id := strings.Split(r.URL.Path, "/")[3]
|
||||||
|
if id == "" {
|
||||||
|
return nil, caddy.APIError{
|
||||||
|
HTTPStatus: http.StatusBadRequest,
|
||||||
|
Err: fmt.Errorf("missing CA in path"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
id := parts[3]
|
|
||||||
|
|
||||||
// Find the CA by ID, if PKI is configured
|
// Find the CA by ID, if PKI is configured
|
||||||
var ca *CA
|
var ca *CA
|
||||||
ok := false
|
var ok bool
|
||||||
if a.pkiApp != nil {
|
if a.pkiApp != nil {
|
||||||
ca, ok = a.pkiApp.CAs[id]
|
ca, ok = a.pkiApp.CAs[id]
|
||||||
}
|
}
|
||||||
|
@ -127,7 +202,7 @@ func (a *adminPKI) handleCertificates(w http.ResponseWriter, r *http.Request) er
|
||||||
// if they actually requested the local CA ID.
|
// if they actually requested the local CA ID.
|
||||||
if !ok {
|
if !ok {
|
||||||
if id != DefaultCAID {
|
if id != DefaultCAID {
|
||||||
return caddy.APIError{
|
return nil, caddy.APIError{
|
||||||
HTTPStatus: http.StatusNotFound,
|
HTTPStatus: http.StatusNotFound,
|
||||||
Err: fmt.Errorf("no certificate authority configured with id: %s", id),
|
Err: fmt.Errorf("no certificate authority configured with id: %s", id),
|
||||||
}
|
}
|
||||||
|
@ -138,57 +213,43 @@ func (a *adminPKI) handleCertificates(w http.ResponseWriter, r *http.Request) er
|
||||||
ca = new(CA)
|
ca = new(CA)
|
||||||
err := ca.Provision(a.ctx, id, a.log)
|
err := ca.Provision(a.ctx, id, a.log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.APIError{
|
return nil, caddy.APIError{
|
||||||
HTTPStatus: http.StatusInternalServerError,
|
HTTPStatus: http.StatusInternalServerError,
|
||||||
Err: fmt.Errorf("failed to provision CA %s, %w", id, err),
|
Err: fmt.Errorf("failed to provision CA %s, %w", id, err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the root certificate to PEM
|
return ca, nil
|
||||||
rootPem := string(pem.EncodeToMemory(&pem.Block{
|
}
|
||||||
Type: "CERTIFICATE",
|
|
||||||
Bytes: ca.RootCertificate().Raw,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Convert the intermediate certificate to PEM
|
func rootAndIntermediatePEM(ca *CA) (root, inter []byte, err error) {
|
||||||
interPem := string(pem.EncodeToMemory(&pem.Block{
|
root, err = pemEncodeCert(ca.RootCertificate().Raw)
|
||||||
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 {
|
if err != nil {
|
||||||
return caddy.APIError{
|
return
|
||||||
HTTPStatus: http.StatusInternalServerError,
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
inter, err = pemEncodeCert(ca.IntermediateCertificate().Raw)
|
||||||
return nil
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// CAInfo is the response from the certificates API endpoint
|
// caInfo is the response structure for the CA info API endpoint.
|
||||||
type CAInfo struct {
|
type caInfo struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Root string `json:"root"`
|
RootCN string `json:"root_common_name"`
|
||||||
Intermediate string `json:"intermediate"`
|
IntermediateCN string `json:"intermediate_common_name"`
|
||||||
|
RootCert string `json:"root_certificate"`
|
||||||
|
IntermediateCert string `json:"intermediate_certificate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminPKICertificatesEndpoint = "/pki/certificates/"
|
// adminPKIEndpointBase is the base admin endpoint under which all PKI admin endpoints exist.
|
||||||
|
const adminPKIEndpointBase = "/pki/"
|
||||||
|
|
||||||
// Interface guards
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ caddy.AdminRouter = (*adminPKI)(nil)
|
_ caddy.AdminRouter = (*adminAPI)(nil)
|
||||||
_ caddy.Provisioner = (*adminPKI)(nil)
|
_ caddy.Provisioner = (*adminAPI)(nil)
|
||||||
)
|
)
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||||
|
@ -132,7 +133,7 @@ func cmdTrust(fl caddycmd.Flags) (int, error) {
|
||||||
ca := CA{
|
ca := CA{
|
||||||
log: caddy.Log(),
|
log: caddy.Log(),
|
||||||
root: rootCert,
|
root: rootCert,
|
||||||
rootCertPath: adminAddr + adminPKICertificatesEndpoint + caID,
|
rootCertPath: adminAddr + path.Join(adminPKIEndpointBase, caID, "certificates"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install the cert!
|
// Install the cert!
|
||||||
|
@ -204,9 +205,9 @@ func cmdUntrust(fl caddycmd.Flags) (int, error) {
|
||||||
return caddy.ExitCodeSuccess, nil
|
return caddy.ExitCodeSuccess, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// rootCertFromAdmin makes the API request to fetch the
|
// rootCertFromAdmin makes the API request to fetch the root certificate for the named CA via admin API.
|
||||||
func rootCertFromAdmin(adminAddr string, caID string) (*x509.Certificate, error) {
|
func rootCertFromAdmin(adminAddr string, caID string) (*x509.Certificate, error) {
|
||||||
uri := adminPKICertificatesEndpoint + caID
|
uri := path.Join(adminPKIEndpointBase, caID, "certificates")
|
||||||
|
|
||||||
// Make the request to fetch the CA info
|
// Make the request to fetch the CA info
|
||||||
resp, err := caddycmd.AdminAPIRequest(adminAddr, http.MethodGet, uri, make(http.Header), nil)
|
resp, err := caddycmd.AdminAPIRequest(adminAddr, http.MethodGet, uri, make(http.Header), nil)
|
||||||
|
@ -216,14 +217,14 @@ func rootCertFromAdmin(adminAddr string, caID string) (*x509.Certificate, error)
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// Decode the resposne
|
// Decode the resposne
|
||||||
caInfo := new(CAInfo)
|
caInfo := new(caInfo)
|
||||||
err = json.NewDecoder(resp.Body).Decode(caInfo)
|
err = json.NewDecoder(resp.Body).Decode(caInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to decode JSON response: %v", err)
|
return nil, fmt.Errorf("failed to decode JSON response: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode the root
|
// Decode the root cert
|
||||||
rootBlock, _ := pem.Decode([]byte(caInfo.Root))
|
rootBlock, _ := pem.Decode([]byte(caInfo.RootCert))
|
||||||
if rootBlock == nil {
|
if rootBlock == nil {
|
||||||
return nil, fmt.Errorf("failed to decode root certificate: %v", err)
|
return nil, fmt.Errorf("failed to decode root certificate: %v", err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue