Add certificate revocation, deletion, and retrieval
Admins can now revoke or delete certificate records from the cert detail
page in the web UI. Revoked certificates display a [REVOKED] badge and
show revocation metadata (time and actor). Deletion redirects to the
issuer page.
The REST API gains three new authenticated endpoints that mirror the
gRPC surface:
GET /v1/ca/{mount}/cert/{serial} (auth required)
POST /v1/ca/{mount}/cert/{serial}/revoke (admin only)
DELETE /v1/ca/{mount}/cert/{serial} (admin only)
The CA engine stores revocation state (revoked, revoked_at, revoked_by)
directly in the existing CertRecord barrier entry. The proto CertRecord
message is extended with the same three fields (field numbers 10–12).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,8 @@ func (ws *WebServer) registerRoutes(r chi.Router) {
|
||||
r.Get("/issuer/{issuer}", ws.requireAuth(ws.handleIssuerDetail))
|
||||
r.Get("/cert/{serial}", ws.requireAuth(ws.handleCertDetail))
|
||||
r.Get("/cert/{serial}/download", ws.requireAuth(ws.handleCertDownload))
|
||||
r.Post("/cert/{serial}/revoke", ws.requireAuth(ws.handleCertRevoke))
|
||||
r.Post("/cert/{serial}/delete", ws.requireAuth(ws.handleCertDelete))
|
||||
r.Get("/{issuer}", ws.requireAuth(ws.handlePKIIssuer))
|
||||
})
|
||||
}
|
||||
@@ -531,6 +533,11 @@ func (ws *WebServer) handleIssueCert(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Stream a tgz archive containing the private key (PKCS8) and certificate.
|
||||
// Extend the write deadline before streaming so that slow gRPC backends
|
||||
// don't consume the server WriteTimeout before we start writing.
|
||||
rc := http.NewResponseController(w)
|
||||
_ = rc.SetWriteDeadline(time.Now().Add(60 * time.Second))
|
||||
|
||||
filename := issuedCert.Serial + ".tgz"
|
||||
w.Header().Set("Content-Type", "application/gzip")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
|
||||
@@ -621,6 +628,70 @@ func (ws *WebServer) handleCertDownload(w http.ResponseWriter, r *http.Request)
|
||||
_, _ = w.Write([]byte(cert.CertPEM))
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleCertRevoke(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
if !info.IsAdmin {
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
token := extractCookie(r)
|
||||
mountName, err := ws.findCAMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no CA engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
serial := chi.URLParam(r, "serial")
|
||||
if err := ws.vault.RevokeCert(r.Context(), token, mountName, serial); err != nil {
|
||||
st, _ := status.FromError(err)
|
||||
if st.Code() == codes.NotFound {
|
||||
http.Error(w, "certificate not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, grpcMessage(err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/pki/cert/"+serial, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleCertDelete(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
if !info.IsAdmin {
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
token := extractCookie(r)
|
||||
mountName, err := ws.findCAMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no CA engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
serial := chi.URLParam(r, "serial")
|
||||
|
||||
// Fetch the cert to get the issuer for the redirect.
|
||||
cert, certErr := ws.vault.GetCert(r.Context(), token, mountName, serial)
|
||||
|
||||
if err := ws.vault.DeleteCert(r.Context(), token, mountName, serial); err != nil {
|
||||
st, _ := status.FromError(err)
|
||||
if st.Code() == codes.NotFound {
|
||||
http.Error(w, "certificate not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, grpcMessage(err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if certErr == nil && cert != nil {
|
||||
http.Redirect(w, r, "/pki/issuer/"+cert.Issuer, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/pki", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleSignCSR(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
Reference in New Issue
Block a user