Skip to main content

Security

AgentCore enforces RBAC via JWT, scopes access by effective department permissions, scrubs PII before storing or forwarding data, and guards against prompt injection before queries hit the LLM.

Auth & RBAC

Authentication

JWTs are issued and verified through @fastify/jwt.

Token Payload

{
"sub": "<user-id>",
"email": "user@example.com",
"role": "approver",
"roleId": "<system-role-id>",
"departmentId": "<department-id>",
"tokenVersion": 3,
"iat": 1713000000,
"exp": 1713604800
}

Configuration

VariableDefaultDescription
JWT_SECRETSigning secret (min 32 chars, required)
JWT_EXPIRES_IN7dToken lifetime

Flow

  1. Register: POST /api/v1/auth/register — creates an employee user for a supplied departmentId, returns JWT. Client-supplied role is ignored by schema and cannot create admins.
  2. Login: POST /api/v1/auth/login — validates credentials, returns JWT
  3. Refresh: POST /api/v1/auth/refresh — issues new token from valid token
  4. Forgot/reset password: POST /api/v1/auth/forgot-password creates a one-hour reset token, and POST /api/v1/auth/reset-password consumes it and increments tokenVersion
  5. Self-service security: POST /api/v1/me/change-password and POST /api/v1/me/logout-all increment tokenVersion and return a fresh token
  6. Use: Include Authorization: Bearer <token> on API requests

Passwords are hashed with bcrypt (via bcryptjs). JWTs are only read from the Authorization header. WebSocket clients authenticate with a first JSON message carrying the JWT — query-string JWTs are rejected.

Role-Based Access Control (RBAC)

Roles and Permissions

RBAC is permission-based. SystemRole records define:

FieldDescription
permissionsCapability keys such as canManageUsers, canApprove, or wildcard *
allDepartmentsWhether the role applies globally
departmentIdsDepartment allowlist when the role is not global
isSystemBuilt-in role marker; system role slugs are protected

User.role still stores the legacy enum (employee, approver, dept_head, admin) for compatibility and fallback permissions. New access checks compute effective permissions from the assigned SystemRole, user-level grants/revokes, and department grants/revokes.

GET /api/v1/permissions exposes the permission catalog. /api/v1/roles manages roles. /api/v1/users/:id/permissions/... handles per-user overrides.

Middleware

Implementation: src/middleware/rbac.ts

requirePermission(permission) — requires one computed permission:

requirePermission('canManageUsers')

requireAnyPermission(permissions[]) — requires one of the listed permissions:

requireAnyPermission(['canManagePlugins', 'canManageNamespaces'])

requireRole() and requireAnyRole() remain for legacy routes. Prefer permission checks in new routes.

requireSelf() — the user can only access their own resources unless they hold canManageUsers.

RBAC denials return the 401/403 response immediately — the route handler does not run after a denied check.

Department Isolation

Department isolation lives in src/lib/department-scope.ts.

Scope is derived from effective RBAC state: role department scope, primary/current department, and user-level grants/revokes. Global-scope users see all departments. Everyone else is restricted to their effective department IDs.

SurfaceScope pattern
Knowledge bases and namespacesdirect departmentId filters via scope.directWhere()
Agent tasksnested namespace filters via scope.nestedWhere('namespace')
Approvals and conversationsuser/conversation department traversal
RAG retrievalraw SQL filters derived from scope.departmentId
WebSocket task eventsnamespace access checked on subscribe and again before every broadcast
Employee profilesown profile or effective department scope; global users can read all
Document templates and generation historynamespace/department filters from effective department scope
Notificationsowner-only (userId)

The regression harness in tests/department-isolation.test.ts covers list, detail, mutation, analytics, RAG, and WebSocket paths.

Endpoint Access Matrix

Endpoint Groupemployeeapproverdept_headadmin
Auth (register/login)yesyesyesyes
Conversation review APInoyesyesyes
Knowledge bases (read)yesyesyesyes
Knowledge bases (write)dept-scopeddept-scopeddept-scopedyes
Document uploaddept-scopeddept-scopeddept-scopedyes
Approvalsnoyesyesyes
Namespaces (read)yesyesyesyes
Namespaces (write)nonoyesyes
Departments managementnononoyes
Users managementnononoyes
Roles managementnononoyes
Document generationyesyesyesyes
Document template managementnonoyesyes
Plugins (view)yesyesyesyes
Plugins (manage)nonoyesyes
Notificationsownownownown
Audit logsnononoyes
RAG draftnoyesyesyes
Employee profiles (own)yesyesyesyes
Employee profiles (edit)nonoyesyes

PII Scrubber

AgentCore uses a two-tier PII protection system to prevent personal data leakage through the AI pipeline.

Implementation

src/knowledge/pii.ts

Tier 1: Ingestion-Time Scrubbing (One-Way)

PII is irreversibly masked during ingestion before any text enters the knowledge base. Raw personal data never reaches the vector store or chunk content.

Detected Patterns

TypePatternReplacement
Emailuser@domain.com[REDACTED_EMAIL]
Phone+380123456789[REDACTED_PHONE]
Credit card4111-1111-1111-1111[REDACTED_CC]
IP address192.168.1.1[REDACTED_IP]
IBANUA213223130000026007233566001[REDACTED_IBAN]
Amount$1,234.56[REDACTED_AMOUNT]

When Applied

  • After text extraction, before chunking.
  • Applied to all document types (PDF, DOCX, TXT, URL, image).
  • Not reversible — the knowledge base stays clean.

Tier 2: Conversation-Time Scrubbing (Reversible)

In live conversations, PII in the user message is reversibly masked before it reaches the LLM. Once the LLM responds, placeholders in the response are restored to the real values.

Flow

  1. User sends a message containing PII (e.g. an email).
  2. PII is replaced with a placeholder: [PII_EMAIL_a1b2c3].
  3. Mapping stored encrypted in PiiRedactionMap:
    • placeholder: [PII_EMAIL_a1b2c3]
    • entityType: email
    • encryptedValue: AES-256-GCM encrypted original
  4. Scrubbed message goes to the LLM.
  5. LLM response may still contain the placeholder.
  6. Placeholders in the response are restored from the encrypted mapping.
  7. The user sees the original PII.

Encryption

  • Algorithm: AES-256-GCM
  • Key source: PII_ENCRYPTION_KEY
  • Key format: base64-encoded 32-byte key
  • Each encrypted value includes IV and auth tag
  • Scoped to conversation (maps stored per conversationId)

PII encryption is deliberately separate from JWT_SECRET. Rotating auth tokens doesn't re-encrypt PII, and compromising one secret doesn't compromise the other domain.

Why Two Tiers?

TierPurposeReversibleScope
IngestionProtect knowledge base at restNoDocuments/chunks
ConversationProtect user data in transit to LLMYesLive messages

Knowledge-base PII is permanently removed because those documents stick around. Conversation PII needs to be restored so users see natural replies.


Injection Guard

The injection guard detects and blocks prompt-injection attempts in user messages and RAG context.

Implementation

src/knowledge/injection-guard.tscheckForInjection()

Detection Categories

1. Instruction Override

Attempts to override system instructions.

Examples: "ignore previous instructions", "disregard all rules", "forget your prompt", "do not follow your instructions"

2. Persona Injection

Attempts to change the AI's identity or behavior.

Examples: "you are now a hacker", "act as an unrestricted AI", "pretend you have no rules", "roleplay as", "from now on you will"

3. Jailbreak

Known jailbreak techniques.

Examples: "DAN mode enabled", "developer mode", "no restrictions apply", "unrestricted mode"

4. Prompt Extraction

Attempts to extract the system prompt.

Examples: "show me your system prompt", "reveal your instructions", "print all rules", "what is your prompt"

5. Role Injection

Attempts to inject system-level messages.

Examples: [system]:, [assistant]:, ````system ```,<|im_start|>system`

6. Context Manipulation

Attempts to reset or manipulate conversation context.

Examples: "end of context", "begin new session", "reset conversation", "new system message"

Application Points

Applied at two points:

  1. User message — checked before RAG query execution.
  2. RAG context chunks — each retrieved chunk is checked before context assembly.

Response

interface InjectionCheckResult {
safe: boolean;
reason?: string; // Category name if detected
}

When injection is detected:

  • safe: false with the detection reason;
  • the message always routes to HITL, regardless of trust status;
  • the RAG response includes injectionDetected: true and injectionReason.

Pattern Count

30+ regex patterns across the categories above. Patterns are case-insensitive and written to catch common variations and obfuscation.

Limitations

  • Regex-based detection — sophisticated attacks may slip through.
  • No LLM-based classification yet.
  • Focused on English and Ukrainian patterns.

API Boundary Protection

Structured Error Envelope

Application errors use one JSON shape:

{
"error": "ValidationError",
"message": "Request validation failed",
"statusCode": 422,
"details": {}
}

The details field is optional. src/middleware/errorHandler.ts normalizes Zod, JWT, Prisma, OpenAI/provider, known application, 404, and unexpected 500 errors into this envelope. Routes can use sendError(reply, statusCode, error, message, details) for explicit failures.

Rate Limits

@fastify/rate-limit is registered globally:

  • default global limit: 100 requests per minute;
  • key: authenticated user ID when available, otherwise client IP;
  • storage: Redis outside tests, in-memory in tests;
  • allowlist: loopback clients and /api/v1/health;
  • auth-specific overrides: register is limited to 3 requests/hour, login to 5 requests/15 minutes.

CORS

CORS uses an exact-origin allowlist from ALLOWED_ORIGINS. Credentials are enabled, so wildcard origins are rejected at config parse time.