Fix ECDH zeroization, add audit logging, and remediate high findings

- Fix #61: handleRotateKey and handleDeleteUser now zeroize stored
  privBytes instead of calling Bytes() (which returns a copy). New
  state populates privBytes; old references nil'd for GC.
- Add audit logging subsystem (internal/audit) with structured event
  recording for cryptographic operations.
- Add audit log engine spec (engines/auditlog.md).
- Add ValidateName checks across all engines for path traversal (#48).
- Update AUDIT.md: all High findings resolved (0 open).
- Add REMEDIATION.md with detailed remediation tracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 14:04:39 -07:00
parent b33d1f99a0
commit 5c5d7e184e
24 changed files with 1699 additions and 72 deletions

View File

@@ -11,6 +11,7 @@ import (
mcias "git.wntrmute.dev/kyle/mcias/clients/go"
"git.wntrmute.dev/kyle/metacrypt/internal/audit"
"git.wntrmute.dev/kyle/metacrypt/internal/auth"
"git.wntrmute.dev/kyle/metacrypt/internal/barrier"
"git.wntrmute.dev/kyle/metacrypt/internal/crypto"
@@ -286,6 +287,14 @@ func (s *Server) handleEngineMount(w http.ResponseWriter, r *http.Request) {
return
}
// Inject external_url into CA engine config if available and not already set.
if req.Config == nil {
req.Config = make(map[string]interface{})
}
if _, ok := req.Config["external_url"]; !ok && s.cfg.Server.ExternalURL != "" {
req.Config["external_url"] = s.cfg.Server.ExternalURL
}
if err := s.engines.Mount(r.Context(), req.Name, engine.EngineType(req.Type), req.Config); err != nil {
s.logger.Error("mount engine", "name", req.Name, "type", req.Type, "error", err)
writeJSONError(w, err.Error(), http.StatusBadRequest)
@@ -435,10 +444,16 @@ func (s *Server) handleEngineRequest(w http.ResponseWriter, r *http.Request) {
case strings.Contains(err.Error(), "not found"):
status = http.StatusNotFound
}
outcome := "error"
if status == http.StatusForbidden || status == http.StatusUnauthorized {
outcome = "denied"
}
s.auditOp(r, info, req.Operation, "", req.Mount, outcome, nil, err)
writeJSONError(w, err.Error(), status)
return
}
s.auditOp(r, info, req.Operation, "", req.Mount, "success", nil, nil)
writeJSON(w, http.StatusOK, resp.Data)
}
@@ -1317,6 +1332,24 @@ func writeJSONError(w http.ResponseWriter, msg string, status int) {
writeJSON(w, status, map[string]string{"error": msg})
}
// auditOp logs an audit event for a completed engine operation.
func (s *Server) auditOp(r *http.Request, info *auth.TokenInfo,
op, engineType, mount, outcome string, detail map[string]interface{}, err error) {
e := audit.Event{
Caller: info.Username,
Roles: info.Roles,
Operation: op,
Engine: engineType,
Mount: mount,
Outcome: outcome,
Detail: detail,
}
if err != nil {
e.Error = err.Error()
}
s.audit.Log(r.Context(), e)
}
// newPolicyChecker builds a PolicyChecker closure for a caller, used by typed
// REST handlers to pass service-level policy evaluation into the engine.
func (s *Server) newPolicyChecker(r *http.Request, info *auth.TokenInfo) engine.PolicyChecker {

View File

@@ -14,6 +14,7 @@ import (
"google.golang.org/grpc"
internacme "git.wntrmute.dev/kyle/metacrypt/internal/acme"
"git.wntrmute.dev/kyle/metacrypt/internal/audit"
"git.wntrmute.dev/kyle/metacrypt/internal/auth"
"git.wntrmute.dev/kyle/metacrypt/internal/config"
"git.wntrmute.dev/kyle/metacrypt/internal/engine"
@@ -28,6 +29,7 @@ type Server struct {
auth *auth.Authenticator
policy *policy.Engine
engines *engine.Registry
audit *audit.Logger
httpSrv *http.Server
grpcSrv *grpc.Server
logger *slog.Logger
@@ -38,13 +40,14 @@ type Server struct {
// New creates a new server.
func New(cfg *config.Config, sealMgr *seal.Manager, authenticator *auth.Authenticator,
policyEngine *policy.Engine, engineRegistry *engine.Registry, logger *slog.Logger, version string) *Server {
policyEngine *policy.Engine, engineRegistry *engine.Registry, auditLog *audit.Logger, logger *slog.Logger, version string) *Server {
s := &Server{
cfg: cfg,
seal: sealMgr,
auth: authenticator,
policy: policyEngine,
engines: engineRegistry,
audit: auditLog,
logger: logger,
version: version,
}

View File

@@ -36,7 +36,7 @@ func setupTestServer(t *testing.T) (*Server, *seal.Manager, chi.Router) {
_ = db.Migrate(database)
b := barrier.NewAESGCMBarrier(database)
sealMgr := seal.NewManager(database, b, slog.Default())
sealMgr := seal.NewManager(database, b, nil, slog.Default())
_ = sealMgr.CheckInitialized()
// Auth requires MCIAS client which we can't create in tests easily,
@@ -61,7 +61,7 @@ func setupTestServer(t *testing.T) (*Server, *seal.Manager, chi.Router) {
}
logger := slog.Default()
srv := New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger, "test")
srv := New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, nil, logger, "test")
r := chi.NewRouter()
srv.registerRoutes(r)