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:
@@ -29,6 +29,14 @@ func (es *engineServer) Mount(ctx context.Context, req *pb.MountRequest) (*pb.Mo
|
||||
}
|
||||
}
|
||||
|
||||
// Inject external_url into engine config if available and not already set.
|
||||
if config == nil {
|
||||
config = make(map[string]interface{})
|
||||
}
|
||||
if _, ok := config["external_url"]; !ok && es.s.cfg.Server.ExternalURL != "" {
|
||||
config["external_url"] = es.s.cfg.Server.ExternalURL
|
||||
}
|
||||
|
||||
if err := es.s.engines.Mount(ctx, req.Name, engine.EngineType(req.Type), config); err != nil {
|
||||
es.s.logger.Error("grpc: mount engine", "name", req.Name, "type", req.Type, "error", err)
|
||||
switch {
|
||||
|
||||
@@ -71,7 +71,7 @@ func newTestGRPCServer(t *testing.T) (*GRPCServer, func()) {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
b := barrier.NewAESGCMBarrier(database)
|
||||
sealMgr := seal.NewManager(database, b, slog.Default())
|
||||
sealMgr := seal.NewManager(database, b, nil, slog.Default())
|
||||
policyEngine := policy.NewEngine(b)
|
||||
reg := newTestRegistry()
|
||||
authenticator := auth.NewAuthenticator(nil, slog.Default())
|
||||
@@ -82,7 +82,7 @@ func newTestGRPCServer(t *testing.T) (*GRPCServer, func()) {
|
||||
Argon2Threads: 1,
|
||||
},
|
||||
}
|
||||
srv := New(cfg, sealMgr, authenticator, policyEngine, reg, slog.Default())
|
||||
srv := New(cfg, sealMgr, authenticator, policyEngine, reg, nil, slog.Default())
|
||||
return srv, func() { _ = database.Close() }
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package grpcserver
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"git.wntrmute.dev/kyle/metacrypt/internal/audit"
|
||||
"git.wntrmute.dev/kyle/metacrypt/internal/auth"
|
||||
"git.wntrmute.dev/kyle/metacrypt/internal/seal"
|
||||
)
|
||||
@@ -97,6 +99,46 @@ func chainInterceptors(interceptors ...grpc.UnaryServerInterceptor) grpc.UnarySe
|
||||
}
|
||||
}
|
||||
|
||||
// auditInterceptor logs an audit event after each RPC completes. Must run
|
||||
// after authInterceptor so that caller info is available in the context.
|
||||
func auditInterceptor(auditLog *audit.Logger) grpc.UnaryServerInterceptor {
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
resp, err := handler(ctx, req)
|
||||
|
||||
caller := "anonymous"
|
||||
var roles []string
|
||||
if ti := tokenInfoFromContext(ctx); ti != nil {
|
||||
caller = ti.Username
|
||||
roles = ti.Roles
|
||||
}
|
||||
|
||||
outcome := "success"
|
||||
var errMsg string
|
||||
if err != nil {
|
||||
outcome = "error"
|
||||
if st, ok := status.FromError(err); ok {
|
||||
if st.Code() == codes.PermissionDenied || st.Code() == codes.Unauthenticated {
|
||||
outcome = "denied"
|
||||
}
|
||||
errMsg = st.Message()
|
||||
} else {
|
||||
errMsg = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
auditLog.Log(ctx, audit.Event{
|
||||
Caller: caller,
|
||||
Roles: roles,
|
||||
Operation: path.Base(info.FullMethod),
|
||||
Resource: info.FullMethod,
|
||||
Outcome: outcome,
|
||||
Error: errMsg,
|
||||
})
|
||||
|
||||
return resp, err
|
||||
}
|
||||
}
|
||||
|
||||
func extractToken(ctx context.Context) string {
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
pb "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v2"
|
||||
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"
|
||||
@@ -27,6 +28,7 @@ type GRPCServer struct {
|
||||
auth *auth.Authenticator
|
||||
policy *policy.Engine
|
||||
engines *engine.Registry
|
||||
audit *audit.Logger
|
||||
logger *slog.Logger
|
||||
srv *grpc.Server
|
||||
acmeHandlers map[string]*internacme.Handler
|
||||
@@ -35,13 +37,14 @@ type GRPCServer struct {
|
||||
|
||||
// New creates a new GRPCServer.
|
||||
func New(cfg *config.Config, sealMgr *seal.Manager, authenticator *auth.Authenticator,
|
||||
policyEngine *policy.Engine, engineRegistry *engine.Registry, logger *slog.Logger) *GRPCServer {
|
||||
policyEngine *policy.Engine, engineRegistry *engine.Registry, auditLog *audit.Logger, logger *slog.Logger) *GRPCServer {
|
||||
return &GRPCServer{
|
||||
cfg: cfg,
|
||||
sealMgr: sealMgr,
|
||||
auth: authenticator,
|
||||
policy: policyEngine,
|
||||
engines: engineRegistry,
|
||||
audit: auditLog,
|
||||
logger: logger,
|
||||
acmeHandlers: make(map[string]*internacme.Handler),
|
||||
}
|
||||
@@ -68,6 +71,7 @@ func (s *GRPCServer) Start() error {
|
||||
sealInterceptor(s.sealMgr, s.logger, sealRequiredMethods()),
|
||||
authInterceptor(s.auth, s.logger, authRequiredMethods()),
|
||||
adminInterceptor(s.logger, adminRequiredMethods()),
|
||||
auditInterceptor(s.audit),
|
||||
)
|
||||
|
||||
s.srv = grpc.NewServer(
|
||||
|
||||
Reference in New Issue
Block a user