Security monitoring
Investigate sign-ins and platform activity using security views, dashboards, and alerts
How Nexus records sign-ins and platform activity for a security operations team, the read-only views those records are exposed through, and the dashboards and alerts built on them.
Three stores carry security-relevant data. Each keeps its own privacy boundary:
| Store | Contents | Identity | Read through |
|---|---|---|---|
Audit database (nexus_audit) |
Sealed audit events: sign-ins, changes, data access, completed gateway requests | Full: user, email, source address, user agent, org and project, login | soc_* views |
Identity and tenancy databases (nexus_auth, nexus_control) |
Current users, logins, keys, memberships, orgs, projects | Names and owners; no hashes or secrets | soc_* views |
| Request analytics (ClickHouse) | Request rows, sessions, nexus-property values, agent traces |
Tenant scoped | Query API, console, operator SQL |
| Operator logs (Loki) | Service log events, HTTP access records, gateway completions | None: tenant identifiers are rejected | LogQL |
What is recorded
Section titled “What is recorded”Sign-in lifecycle
Section titled “Sign-in lifecycle”| Event | When | Attributes |
|---|---|---|
user.signed_up |
Password registration completes | email, source_ip, user_agent, auth_session_id |
user.signed_in |
Password sign-in succeeds | email, source_ip, user_agent, auth_session_id |
user.sign_in_failed |
Password sign-in fails | email (as typed, lowercased), reason, method, source_ip, user_agent |
auth.session.refreshed |
A console session is refreshed | email, source_ip, user_agent, auth_session_id |
user.signed_out |
A console session is signed out | email, source_ip, user_agent, auth_session_id |
auth.oidc.login_succeeded |
Federated sign-in succeeds | provider, email, source_ip, user_agent, auth_session_id |
auth.oidc.login_denied |
Federated sign-in is refused | provider, email when known, reason, source_ip, user_agent |
admin.session.signed_in, admin.session.refreshed, admin.session.signed_out |
Administrator console session lifecycle | email, source_ip, user_agent, auth_session_id |
admin.sign_in_failed |
Administrator password sign-in fails | as user.sign_in_failed |
admin.session.denied |
A valid credential without an administrator assignment signs in to the administrator console | email, source_ip, user_agent |
auth.identity.linked |
A federated identity is linked to an existing account | provider, email |
reason on a failed password sign-in is one of bad_password, unknown_user, invalid_email (the typed value has no @), disabled, no_password (the account signs in only through an identity provider), account_unavailable, password_login_disabled, or error.
The caller always receives the same error; only the audit record distinguishes the cases.
Passwords are never recorded.
source_ip is the first address of the X-Forwarded-For header the console received, falling back to X-Real-IP; values that are not IP addresses are dropped and user agents are truncated to 512 characters, on both the audit event and the session row.
The console sets source_ip and user_agent from the request it received and overwrites any values in the browser-supplied body.
The first forwarded hop is whatever the client or an intermediate proxy put there, so the recorded address is reliable only when the edge in front of the console replaces inbound X-Forwarded-For with the connecting address.
Login correlation
Section titled “Login correlation”auth_session_id identifies one interactive login.
It is created at sign-in and kept when the session is refreshed, so every refresh and the sign-out carry the same value.
It is not a token and cannot authenticate.
Audit events emitted by the auth, control plane, administrator control plane, query, and audit services while handling a console request carry the auth_session_id of the login that made the request.
That includes credential creation and revocation, membership and role changes, classification and limit changes, tenancy lifecycle events, and data_access.* reads.
Gateway traffic authenticates with API keys rather than logins; the api_key.created event records the login that created the key.
Platform activity
Section titled “Platform activity”All other audit events (see ../architecture/traceability-auditability.md) are available through the same views.
For a completed gateway request, gateway.request.completed records the API key, org and project, session_id, session_path, session_name, model, provider, status, latency, tokens, cost, and the nexus-property values in reporting_dimensions.
Request and response content is never in the audit log.
Rejected gateway credentials are counted, not audited, in the Prometheus counter nexus_gateway_auth_failures_total{reason} with reason of missing, malformed, invalid, revoked, expired, or other.
Operator logs
Section titled “Operator logs”When an OTLP endpoint is configured, every Rust service exports tracing events at info and above as OTLP log records (filter: OTLP_LOG_FILTER, default info), in addition to stderr.
Each HTTP request other than /healthz, /readyz, and /metrics produces one http request completed record with the method, route template, status, status class, and latency to the response head (for a streamed response, the gateway request completed record carries the full duration).
The gateway also writes one gateway request completed record per model request with the request id, route, provider, model, status, latency, fallback count, and token counts.
The telemetry collector drops any log record carrying a tenant identifier; see ../security/telemetry-tiering.md.
Security views
Section titled “Security views”Service migrations create the views below.
A monitoring role granted SELECT on them can read nothing else: no tables, audit signing keys, password hashes, refresh-token digests, or credential hashes.
nexus_audit
Section titled “nexus_audit”Every audit view has event_id, tenant_id, occurred_at, and sealed (true once the event is in a signed batch).
| View | Rows | Columns |
|---|---|---|
soc_auth_events |
Sign-in lifecycle events above | kind, outcome (success, failure, session), surface (tenant, admin), method (password, oidc, session), user_id, email, reason, source_ip, user_agent, identity_provider, auth_session_id |
soc_admin_changes |
Credential, membership, invitation, registration, tenancy, wallet, connection, and user-account events | kind, category, action, actor_kind, actor_id, subject_kind, subject_id, workspace_id, org_id, project_id, credential_prefix, role, email, provider, auth_session_id, detail (all attributes) |
soc_data_access |
data_access.* reads |
kind, accessed, actor_kind, actor_id, workspace_id, org_id, project_id, request_id, session_id, trace_id, viewed_event_id, view, payload_depth, auth_session_id |
soc_usage_events |
gateway.request.completed |
request_id, api_key_id, auth_type, workspace_id, org_id, project_id, session_id, session_path, session_name, model, resolved_model, provider, endpoint_key, classification, status, latency_ms, fallback_count, prompt_tokens, completion_tokens, reasoning_tokens, cost_usd, policy_effect, properties (nexus-property values), property_sources |
soc_policy_denials |
policy.decision that did not allow |
effect, action, principal_kind, principal_id, resource_kind, resource_id, reason_code, policy_id, classification |
soc_event_timeline |
Every audit event | kind, actor and subject, scope, email, source_ip, auth_session_id, session_id, api_key_id, classification |
nexus_auth
Section titled “nexus_auth”| View | Rows | Columns |
|---|---|---|
soc_users |
Accounts | user_id, email, name, created_at, email_verified_at, disabled_at, active, has_password, identity_issuers, platform_admin |
soc_login_sessions |
One row per login | auth_session_id, user_id, email, kind (user, admin), started_at, last_used_at, expires_at, refresh_count (rotations after sign-in), first_source_ip, last_source_ip, last_user_agent, active (not revoked and not past its hard expiry; the inactivity limit is not applied) |
soc_developer_tokens |
Personal developer keys | token_id, user_id, email, name, prefix, scope ids, scopes, created_at, last_used_at, expires_at, revoked_at |
nexus_control
Section titled “nexus_control”| View | Columns |
|---|---|
soc_workspaces, soc_orgs, soc_projects |
ids, slug, name, kind and owner where applicable, trace_logging_enabled (orgs and projects), created_at, deleted_at |
soc_api_keys |
api_key_id, scope ids, name, prefix, created_by, scopes, trace_logging_enabled, created_at, last_used_at, expires_at, revoked_at, active |
soc_trace_ingest_keys |
trace_ingest_key_id, scope ids, name, prefix, created_by, created_at, rotated_at, revoked_at |
soc_memberships |
scope, user_id, workspace_id, org_id, project_id, role, created_at, revoked_at |
Example queries
Section titled “Example queries”Sign-in history for one user:
SELECT occurred_at, kind, outcome, method, source_ip, user_agent, reason, auth_session_idFROM soc_auth_eventsWHERE email = lower('someone@example.com')ORDER BY occurred_at DESCLIMIT 200;Everything done during one login:
SELECT occurred_at, kind, subject_kind, subject_id, org_id, project_idFROM soc_event_timelineWHERE auth_session_id = 'sess_01J...'ORDER BY occurred_at;Requests from one external application session carrying a property:
SELECT occurred_at, request_id, api_key_id, model, status, cost_usdFROM soc_usage_eventsWHERE session_id = 'checkout-4821' AND properties->>'customer_id' = 'acme'ORDER BY occurred_at;The same filters in ClickHouse request analytics:
SELECT completed_at, request_id, api_key_id, model, status_code, total_cost_usdFROM vortex.requestsWHERE session_id = 'checkout-4821' AND reporting_dimensions['customer_id'] = 'acme'ORDER BY completed_at;Grafana
Section titled “Grafana”The Nexus Helm chart provisions, when security monitoring is enabled:
- Read-only datasources
Nexus Audit,Nexus Identity, andNexus Tenancythat connect as a role limited to thesoc_*views, with read-only transactions and a 60-second statement limit. - A
Nexus Securityfolder with these dashboards:- Nexus Security - Authentication: successful and failed sign-ins, failure reasons, top failing emails and source addresses, administrator sign-ins and denials, per-user sign-in history, and active logins.
- Nexus Security - Changes and Access: credential lifecycle, membership and role changes, tenancy and policy changes, policy denials, and who viewed audit records, requests, and payloads.
- Nexus Usage - Adoption: active users and keys, sign-ups, new orgs, projects, and keys, and requests, tokens, and cost by model, provider, org, API key, and property value.
- Nexus Investigation: filters for user, login, API key, session, request, source address, and up to three property key/value pairs, combined with AND, with drill-down links between them.
- Alert rules for repeated failed sign-ins per account and per source address, administrator sign-in failures, platform provider credential changes, disabled accounts and manager grants, gateway credential rejection bursts, a stalled audit archive, and an audit event backlog.
Request and session rows link to the console request or session page.
Prompt and response content is read there, which enforces payload permissions and records a data_access event; the dashboards never display it.
Chart settings, secrets, and access control are described in the nexus-iac chart documentation.
Guidance for external applications
Section titled “Guidance for external applications”Correlation metadata sent by applications calling the gateway is what makes per-application investigation possible:
- Send
nexus-session-id(and optionallynexus-session-pathandnexus-session-name) consistently for every request in one user conversation or job. - Send
nexus-property-<key>: <value>for application identifiers such as a customer, tenant, or workflow. A workspace manager must first define<key>as a reporting dimension; requests that send an undefined key are rejected. - Never put secrets, credentials, or message content in session names or property values. They are stored in request analytics and the audit log.
Limits
Section titled “Limits”- The audit service builds the view indexes on
audit_eventsconcurrently at startup, one per migration, so replicas already serving keep ingesting. If the service is stopped during a build, Postgres keeps an invalid index under that name and the migration skips it on restart; runDROP INDEX CONCURRENTLY <name>for anyaudit_events_*_idxwhosepg_index.indisvalidis false, then restart. - Reading the views directly, including through Grafana, does not create
data_access.audit_viewedevents. Restrict who can sign in to Grafana and use the database’s own logging if direct reads must be audited. - Older audit records can lack
auth_session_idand sign-in client context; filters on those columns exclude them. - Gateway API-key rejections are counted per reason without the key or caller, because request volume makes per-attempt records impractical.
