Add CRL endpoint, sign-CSR web route, and policy-based issuance authorization
- Register handleSignCSR route in webserver (was dead code)
- Add GET /v1/pki/{mount}/issuer/{name}/crl REST endpoint and
PKIService.GetCRL gRPC RPC for DER-encoded CRL generation
- Replace admin-only gates on issue/renew/sign-csr with policy-based
access control: admins grant-all, authenticated users subject to
identifier ownership (CN/SANs not held by another user's active cert)
and optional policy overrides via ca/{mount}/id/{identifier} resources
- Add PolicyChecker to engine.Request and policy.Match() method to
distinguish matched rules from default deny
- Update and expand CA engine tests for ownership, revocation freeing,
and policy override scenarios
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,7 @@ func (s *Server) registerRoutes(r chi.Router) {
|
||||
r.Get("/v1/pki/{mount}/ca", s.requireUnseal(s.handlePKIRoot))
|
||||
r.Get("/v1/pki/{mount}/ca/chain", s.requireUnseal(s.handlePKIChain))
|
||||
r.Get("/v1/pki/{mount}/issuer/{name}", s.requireUnseal(s.handlePKIIssuer))
|
||||
r.Get("/v1/pki/{mount}/issuer/{name}/crl", s.requireUnseal(s.handlePKICRL))
|
||||
|
||||
r.HandleFunc("/v1/policy/rules", s.requireAuth(s.handlePolicyRules))
|
||||
r.HandleFunc("/v1/policy/rule", s.requireAuth(s.handlePolicyRule))
|
||||
@@ -288,6 +289,20 @@ func (s *Server) handleEngineRequest(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
policyChecker := func(resource, action string) (string, bool) {
|
||||
pReq := &policy.Request{
|
||||
Username: info.Username,
|
||||
Roles: info.Roles,
|
||||
Resource: resource,
|
||||
Action: action,
|
||||
}
|
||||
eff, matched, pErr := s.policy.Match(r.Context(), pReq)
|
||||
if pErr != nil {
|
||||
return string(policy.EffectDeny), false
|
||||
}
|
||||
return string(eff), matched
|
||||
}
|
||||
|
||||
engReq := &engine.Request{
|
||||
Operation: req.Operation,
|
||||
Path: req.Path,
|
||||
@@ -297,6 +312,7 @@ func (s *Server) handleEngineRequest(w http.ResponseWriter, r *http.Request) {
|
||||
Roles: info.Roles,
|
||||
IsAdmin: info.IsAdmin,
|
||||
},
|
||||
CheckPolicy: policyChecker,
|
||||
}
|
||||
|
||||
resp, err := s.engines.HandleRequest(r.Context(), req.Mount, engReq)
|
||||
@@ -305,6 +321,8 @@ func (s *Server) handleEngineRequest(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case errors.Is(err, engine.ErrMountNotFound):
|
||||
status = http.StatusNotFound
|
||||
case errors.Is(err, ca.ErrIdentifierInUse):
|
||||
status = http.StatusConflict
|
||||
case strings.Contains(err.Error(), "forbidden"):
|
||||
status = http.StatusForbidden
|
||||
case strings.Contains(err.Error(), "authentication required"):
|
||||
@@ -551,6 +569,30 @@ func (s *Server) handlePKIIssuer(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(certPEM) //nolint:gosec
|
||||
}
|
||||
|
||||
func (s *Server) handlePKICRL(w http.ResponseWriter, r *http.Request) {
|
||||
mountName := chi.URLParam(r, "mount")
|
||||
issuerName := chi.URLParam(r, "name")
|
||||
|
||||
caEng, err := s.getCAEngine(mountName)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
crlDER, err := caEng.GetCRLDER(r.Context(), issuerName)
|
||||
if err != nil {
|
||||
if errors.Is(err, ca.ErrIssuerNotFound) {
|
||||
http.Error(w, `{"error":"issuer not found"}`, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, `{"error":"sealed"}`, http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/pkix-crl")
|
||||
_, _ = w.Write(crlDER) //nolint:gosec
|
||||
}
|
||||
|
||||
func (s *Server) getCAEngine(mountName string) (*ca.CAEngine, error) {
|
||||
mount, err := s.engines.GetMount(mountName)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user