FerrisKeyDocs

Event types

SeaWatch has 29 event types, grouped below by subject. FerrisKey writes 27 of them. client_secret_rotated and realm_config_changed are valid values you can filter on, but nothing emits them. Every event has a status of success or failure.

Each table gives the actor_id and the target of the event. The actor is the account that made the request, and “none” means the field is null. The target is the target_type and target_id pair. The overview explains both, and why actor_type is mostly null.

Authentication and sessions

EventWritten whenactor_idTarget
login_successA user authenticated and a session was opened, resumed from an SSO session, or re-authenticatedThe usersession
login_failureA sign-in was refusedThe user, if the username matched an accountnone
session_createdA new session was opened. Written in the same statement as login_success, with the same timestampThe usersession
session_revokedA session ended: the user signed out, or the session was revoked through the APIThe session’s user on sign-out, otherwise whoever revoked it, the user or an administratorsession

login_success carries no details for a new session. When an existing session is reused, details.method says how: sso_session for a session resumed from its SSO cookie, reauthentication for a session that was authenticated again.

login_failure carries the reason in details:

{
  "reason": "invalid_credentials"
}
reasonMeaning
user_not_foundNo account matches the username. The event has no actor
user_disabledThe account is disabled
account_lockedThe account is locked after repeated failures
invalid_credentialsThe password is wrong
ldap_authentication_failedThe account’s LDAP provider rejected the password
provider_disabledThe account’s LDAP provider is disabled

The username is not stored. A wrong one-time code does not write login_failure: it shows up as a failed mfa_challenge step on the Compass flow, and it counts toward the account lockout.

The actor of a failed sign-in is the account the attempt named, not a person who proved who they are: nobody authenticated. Only a username that matches no account leaves actor_id null.

Token exchange

EventWritten whenactor_idTarget
token_exchangedA client tried to swap a subject token for a new access token (RFC 8693)The subject token’s user, when it could be readclient, with the client id in resource

The target is the client that made the exchange, and the status is failure when the exchange was refused. A request from a client that fails authentication is not recorded.

{
  "client_id": "orders-api",
  "audience": "billing-api",
  "policy_id": "01914b3c-9a10-7f5a-b456-00000000a001",
  "actor": null
}

policy_id is the delegation policy that applied to the exchange, and actor is the identity of the acting party when the request carried an actor token. Both are null otherwise. On a failure, details.error_code holds the OAuth error that was returned, for example unauthorized_client or invalid_scope.

Passwords

EventWritten whenactor_idTarget
password_resetAn administrator reset a user’s passwordThe administratoruser
password_reset_requestedA user asked for a password reset emailThe useruser
password_reset_completedA user finished the resetThe useruser
password_importedAn administrator imported a pre-hashed password for a userThe administratoruser

password_reset_requested and password_reset_completed bracket the self-service flow. A request with no matching completion is a user who gave up, or someone else asking for a reset on their behalf.

password_imported is written once, by POST /realms/{realm_name}/users/{user_id}/credentials/import, when the hash is stored. The later re-encoding of the hash to argon2id at the user’s first sign-in writes nothing.

Users

EventWritten whenactor_idTarget
user_createdAn account was created by an administrator, or registered itselfThe administrator, or the new user on self-registrationuser
user_email_verifiedA user confirmed their email addressThe useruser
user_deletedOne or several accounts were deletedThe administratoruser, none on a bulk delete

When actor_id and target_id of a user_created event are the same, the account registered itself. A bulk delete writes a single event with no target, and details.user_ids lists the accounts:

{
  "user_ids": [
    "01914b3c-5678-7f5a-b456-000000000002",
    "01914b3c-5678-7f5a-b456-000000000003"
  ]
}

Roles

EventWritten whenactor_idTarget
role_assignedA role was assigned to a userThe administratoruser, with the role name in resource
role_unassignedA role was removed from a userThe administratoruser, with the role name in resource
role_createdA realm role or a client role was createdThe administratornone
role_removedA role was deletedThe administratornone
role_updatedA role was editedThe administratornone
role_permission_updatedA role’s permission set changedThe administratornone

The four events without a target say that a role changed and who changed it, not which role. For an access review, role_assigned and role_unassigned are the ones that answer “who gave this user admin permissions, and when”.

Clients

EventWritten whenactor_idTarget
client_createdA client was registeredThe administratornone
client_deletedA client was removedThe administratorclient
client_secret_viewedA client’s secret was readThe administratorclient
client_secret_rotatedNever written

client_secret_viewed is the quiet one: nothing changed, but a credential left the system, which is the trail you want during an incident.

Maintenance

EventWritten whenactor_idTarget
client_maintenance_enabledMaintenance mode was turned on for a clientThe administratorclient
client_maintenance_disabledMaintenance mode was turned offThe administratorclient

Both carry the request that was applied. reason and session_strategy are null when the request left them out:

{
  "enabled": true,
  "reason": "Migration under way, back at 2pm.",
  "session_strategy": "expire"
}

Both transitions are recorded, so a window that stayed open longer than intended is visible afterwards.

Email

EventWritten whenactor_idTarget
email_sentThe SMTP server accepted a magic link or password reset emailThe recipientnone
email_not_sentThat email could not be sent. The status is failureThe recipientnone

Only magic link and password reset emails are recorded. details identifies the email:

{
  "email_type": "reset_password",
  "user_id": "01914b3c-5678-7f5a-b456-000000000002",
  "error_code": "SMTP_NOT_CONFIGURED",
  "reason": "SMTP not configured for realm my-app"
}

email_type is magic_link or reset_password. email_sent carries email_type, user_id and template_id. email_not_sent adds reason and an error_code: SMTP_SEND_FAILED (with the template_id), TEMPLATE_NOT_CONFIGURED or SMTP_NOT_CONFIGURED.

email_not_sent is the one to alert on. A password reset that never left the building looks identical to a user ignoring it, unless you watch this event.

Federation

EventWritten whenactor_idTarget
identity_provider_link_removedA federated identity was unlinked from a userThe administratoridentity_provider_link, with the provider alias in resource

details holds user_id, the account that lost the link, and identity_provider_id.

Webhooks

EventWritten whenactor_idTarget
webhook_delivery_exhaustedA webhook delivery ran out of attempts, or failed with an error that is not retried. The status is failurenonewebhook
{
  "delivery_id": "01914b3c-9a10-7f5a-b456-00000000d001",
  "event": "user.created",
  "error_code": "transport_error",
  "attempts": 5
}

event is the webhook trigger that produced the delivery, error_code is the error of the last attempt and attempts counts every attempt made. The delivery itself, with its history, is on the deliveries page.

Configuration

EventWritten whenactor_idTarget
realm_config_changedNever written

Changing a realm’s settings does not produce a SeaWatch event.

Unrecognised types

A stored row whose type this version does not know is returned as unknown, never as a guess. A row written by a newer version, or a corrupt one, can therefore not be misread as a successful login. unknown is never written, and you cannot filter on it: event_types=unknown returns 400.