fix(P04): wire ACL enforcement + WebAuthn reg auth + audit actor (REQ-153)
R-023: Zero-trust enforcement operationally wired. ACL enforcement (C-45 staged rollout): - acl.Check wired into all 5 daemon handlers (dispatch/jobs/nodes/tasks) - health endpoints exempt (liveness probes not gated) - ACL log-only mode default (config acl.enforce=false); enforce after bootstrap ACL verified - sshpush auth: ORCA_OIDC_TOKEN validated against JWKS before apply - txn apply: Authorize hook validates OIDC token before running pull - acl.json mode 0600 (was 0644) - flock on acl.json for concurrent grant/revoke - bootstrap ACL: init grants cluster-admin to orca-admins group + SVID Audit actor identity: - currentActor reads OIDC sub from credentials.json (was hardcoded "cli") - threaded through all audit.Record calls via context WebAuthn registration auth: - BeginRegistration/FinishRegistration require authenticated session - fail-closed 401 when no authFunc configured New files: internal/daemon/acl.go, internal/cli/authactor.go, internal/engine/actor.go, internal/identity/authtoken.go, internal/sshpush/auth.go, internal/txn/auth_test.go ---ci--- project: orca phase: 4 milestone: v0.13 status: complete requirements: covered: [153] ---/ci---
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
// Package engine — actor.go provides the context key + helper for
|
||||
// threading the audit actor (OIDC sub or SPIFFE SVID) through the
|
||||
// engine layer (P04, T5; C-44). Previously the registry hardcoded
|
||||
// "cli" as the actor; this lets CLI commands inject the verified
|
||||
// operator identity via context so audit entries attribute actions
|
||||
// to the real human/operator.
|
||||
package engine
|
||||
|
||||
import "context"
|
||||
|
||||
// actorCtxKey is the context key for the audit actor.
|
||||
type actorCtxKey struct{}
|
||||
|
||||
// WithActor returns a context carrying the audit actor. The CLI
|
||||
// calls this in PersistentPreRun after resolving the OIDC sub from
|
||||
// the credentials file. When the context carries no actor, the
|
||||
// registry falls back to "cli" (legacy).
|
||||
func WithActor(ctx context.Context, actor string) context.Context {
|
||||
if actor == "" {
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, actorCtxKey{}, actor)
|
||||
}
|
||||
|
||||
// ActorFromCtx returns the audit actor from the context, or "cli"
|
||||
// when no actor is set (legacy fallback for paths that haven't been
|
||||
// wired yet).
|
||||
func ActorFromCtx(ctx context.Context) string {
|
||||
if v, ok := ctx.Value(actorCtxKey{}).(string); ok && v != "" {
|
||||
return v
|
||||
}
|
||||
return "cli"
|
||||
}
|
||||
@@ -24,13 +24,13 @@ func NewNodeRegistry(repo *store.NodeRepo, audit *Audit, log *slog.Logger) *Node
|
||||
|
||||
func (r *NodeRegistry) Join(ctx context.Context, n *model.Node) error {
|
||||
if err := r.repo.Insert(ctx, n); err != nil {
|
||||
r.audit.Record(ctx, "cli", "node.join", n.ID, "failure", err, map[string]any{
|
||||
r.audit.Record(ctx, ActorFromCtx(ctx), "node.join", n.ID, "failure", err, map[string]any{
|
||||
"name": n.Name,
|
||||
"address": n.Address,
|
||||
})
|
||||
return fmt.Errorf("join node: %w", err)
|
||||
}
|
||||
r.audit.Record(ctx, "cli", "node.join", n.ID, "success", nil, map[string]any{
|
||||
r.audit.Record(ctx, ActorFromCtx(ctx), "node.join", n.ID, "success", nil, map[string]any{
|
||||
"name": n.Name,
|
||||
"address": n.Address,
|
||||
})
|
||||
@@ -43,20 +43,20 @@ func (r *NodeRegistry) Join(ctx context.Context, n *model.Node) error {
|
||||
|
||||
func (r *NodeRegistry) Leave(ctx context.Context, id string) error {
|
||||
if err := r.repo.UpdateState(ctx, id, model.NodeStateLeft); err != nil {
|
||||
r.audit.Record(ctx, "cli", "node.leave", id, "failure", err, nil)
|
||||
r.audit.Record(ctx, ActorFromCtx(ctx), "node.leave", id, "failure", err, nil)
|
||||
return fmt.Errorf("leave node: %w", err)
|
||||
}
|
||||
r.audit.Record(ctx, "cli", "node.leave", id, "success", nil, nil)
|
||||
r.audit.Record(ctx, ActorFromCtx(ctx), "node.leave", id, "success", nil, nil)
|
||||
r.log.Info("node left", slog.String("node_id", id))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *NodeRegistry) Forget(ctx context.Context, id string) error {
|
||||
if err := r.repo.Delete(ctx, id); err != nil {
|
||||
r.audit.Record(ctx, "cli", "node.forget", id, "failure", err, nil)
|
||||
r.audit.Record(ctx, ActorFromCtx(ctx), "node.forget", id, "failure", err, nil)
|
||||
return fmt.Errorf("forget node: %w", err)
|
||||
}
|
||||
r.audit.Record(ctx, "cli", "node.forget", id, "success", nil, nil)
|
||||
r.audit.Record(ctx, ActorFromCtx(ctx), "node.forget", id, "success", nil, nil)
|
||||
r.log.Info("node removed from registry", slog.String("node_id", id))
|
||||
return nil
|
||||
}
|
||||
@@ -75,7 +75,7 @@ func (r *NodeRegistry) Get(ctx context.Context, id string) (*model.Node, error)
|
||||
// cli package does not need to reach into the repo directly.
|
||||
func (r *NodeRegistry) SetNodeState(ctx context.Context, id, state string) error {
|
||||
if err := r.repo.SetNodeState(ctx, id, state); err != nil {
|
||||
r.audit.Record(ctx, "cli", "node.set_state", id, "failure", err, map[string]any{"state": state})
|
||||
r.audit.Record(ctx, ActorFromCtx(ctx), "node.set_state", id, "failure", err, map[string]any{"state": state})
|
||||
return fmt.Errorf("set node state: %w", err)
|
||||
}
|
||||
r.log.Info("node state set", slog.String("node_id", id), slog.String("state", state))
|
||||
|
||||
Reference in New Issue
Block a user