Files
cameleer-server/.claude/rules/security.md
hsiegeln 7f095dc26c feat(sse)!: always reject commands targeting agents without requireSignedCommands
BREAKING: removes cameleer.server.security.enforce-signed-commands flag — was
default false. Now always on. Agent team is coordinated; pre-Phase-4 agents
must upgrade to advertise requireSignedCommands=true before they can receive
operator-issued single-target or group commands. Broadcast path is unchanged
(best-effort fan-out; signed payloads silently ignored by pre-verification
agents until they upgrade).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 13:47:36 +02:00

2.7 KiB

Security invariants

Agent self-service endpoint ownership

Every endpoint under /api/v1/agents/{id}/... that is intended for agent self-service must enforce AgentOwnershipGuard.requireAgentOwnership(id, httpRequest) as its FIRST statement, before any registry lookup, audit log, or other side effect.

Currently enforced on: SSE events, heartbeat, deregister, command-ack. Currently NOT enforced (out of scope): register (bootstrap-token-gated), refresh, replayExchange. Adding the guard there is a follow-up.

RBAC revocation invariant

Any code path that mutates the effective role set of one or more users — including:

  • user_roles (direct role assignment)
  • user_groups (group membership)
  • group_roles (group's role list)
  • Role deletion
  • User deletion
  • Password reset

— MUST bump users.token_revoked_before for each affected user (use Instant.now().plusMillis(1) to guard against same-millisecond races with token issuance, since JWT iat is millisecond-quantized).

UiAuthController.refresh and JwtAuthenticationFilter.tryInternalToken both consult users.token_revoked_before; without the bump on RBAC mutation, refresh- token role-retention privesc is possible (Vuln 2 of the 2026-04-29 review).

ClaimMappingAdminController mutations are EXEMPT — they affect role assignment at the next OIDC login (via OidcAuthController.applyClaimMappings), not currently- issued tokens.

SSE command signing

Every command emitted via SseConnectionManager.onCommandReady is signed with Ed25519 by SsePayloadSigner. The signer THROWS on null/empty/blank/non-object input — there is no silent unsigned fallback.

Emit-site contract: build the payload via objectMapper.writeValueAsString(...) so the input is in Jackson's canonical (no-whitespace, insertion-order) form. The agent verifies by parsing the wire JSON, removing the signature field, re-serializing, and Ed25519-verifying against the resulting bytes — this requires byte-equivalence between the server-signed bytes and the agent-reconstructed bytes.

The requireSignedCommands capability bit on AgentInfo (set from AgentRegistrationRequest) is hard-enforced: AgentCommandController refuses (409 Conflict) any single-target or group command sent to an agent that has not advertised requireSignedCommands=true. The broadcast path (POST /api/v1/agents/commands) is exempt — blocking an ops broadcast because one old agent is still in the fleet is too disruptive; signed payloads are silently ignored by pre-verification agents until they upgrade. E4 warn + metric (recordCommandToNonCapableAgent) fire for all non-capable targets even when the 409 guard triggers, giving operators observability.