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:
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user