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
| Event | Written when | actor_id | Target |
|---|---|---|---|
login_success | A user authenticated and a session was opened, resumed from an SSO session, or re-authenticated | The user | session |
login_failure | A sign-in was refused | The user, if the username matched an account | none |
session_created | A new session was opened. Written in the same statement as login_success, with the same timestamp | The user | session |
session_revoked | A session ended: the user signed out, or the session was revoked through the API | The session’s user on sign-out, otherwise whoever revoked it, the user or an administrator | session |
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"
}
reason | Meaning |
|---|---|
user_not_found | No account matches the username. The event has no actor |
user_disabled | The account is disabled |
account_locked | The account is locked after repeated failures |
invalid_credentials | The password is wrong |
ldap_authentication_failed | The account’s LDAP provider rejected the password |
provider_disabled | The 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
| Event | Written when | actor_id | Target |
|---|---|---|---|
token_exchanged | A client tried to swap a subject token for a new access token (RFC 8693) | The subject token’s user, when it could be read | client, 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
| Event | Written when | actor_id | Target |
|---|---|---|---|
password_reset | An administrator reset a user’s password | The administrator | user |
password_reset_requested | A user asked for a password reset email | The user | user |
password_reset_completed | A user finished the reset | The user | user |
password_imported | An administrator imported a pre-hashed password for a user | The administrator | user |
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
| Event | Written when | actor_id | Target |
|---|---|---|---|
user_created | An account was created by an administrator, or registered itself | The administrator, or the new user on self-registration | user |
user_email_verified | A user confirmed their email address | The user | user |
user_deleted | One or several accounts were deleted | The administrator | user, 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
| Event | Written when | actor_id | Target |
|---|---|---|---|
role_assigned | A role was assigned to a user | The administrator | user, with the role name in resource |
role_unassigned | A role was removed from a user | The administrator | user, with the role name in resource |
role_created | A realm role or a client role was created | The administrator | none |
role_removed | A role was deleted | The administrator | none |
role_updated | A role was edited | The administrator | none |
role_permission_updated | A role’s permission set changed | The administrator | none |
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
| Event | Written when | actor_id | Target |
|---|---|---|---|
client_created | A client was registered | The administrator | none |
client_deleted | A client was removed | The administrator | client |
client_secret_viewed | A client’s secret was read | The administrator | client |
client_secret_rotated | Never 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
| Event | Written when | actor_id | Target |
|---|---|---|---|
client_maintenance_enabled | Maintenance mode was turned on for a client | The administrator | client |
client_maintenance_disabled | Maintenance mode was turned off | The administrator | client |
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.
| Event | Written when | actor_id | Target |
|---|---|---|---|
email_sent | The SMTP server accepted a magic link or password reset email | The recipient | none |
email_not_sent | That email could not be sent. The status is failure | The recipient | none |
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
| Event | Written when | actor_id | Target |
|---|---|---|---|
identity_provider_link_removed | A federated identity was unlinked from a user | The administrator | identity_provider_link, with the provider alias in resource |
details holds user_id, the account that lost the link, and identity_provider_id.
Webhooks
| Event | Written when | actor_id | Target |
|---|---|---|---|
webhook_delivery_exhausted | A webhook delivery ran out of attempts, or failed with an error that is not retried. The status is failure | none | webhook |
{
"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
| Event | Written when | actor_id | Target |
|---|---|---|---|
realm_config_changed | Never 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.