Skip to content

Audit Log

The Audit log at Admin → Settings → Audit log (/admin/settings?tab=audit-log) answers the two questions a security review asks of an instance that holds a map of the whole IT landscape: what changed in it, and who signed in and how. It is split into two sub-tabs:

Sub-tab URL What it records
Changes /admin/settings?tab=audit-log Every write to the landscape — cards, relations, risks, tags, stakeholder roles, draft decisions — grouped into mutation batches, with a per-event diff and a Rollback
Security & admin /admin/settings?tab=audit-log&view=security Sign-ins and sign-outs, failed attempts, lockouts, SSO and proxy sessions, password events, role impersonation and every administrative action

The two are kept apart on purpose. Changes is a working tool — it exists so you can see and undo a bad write — and it keeps a short history. Security & admin is an evidence trail: its entries cannot be edited or deleted, and it is kept for a year by default.

Who can see it

Both sub-tabs require the admin.events permission ("View audit trail and event stream"), which only the Admin role holds by default. To give a security or compliance officer read access without making them an administrator, create a role that carries admin.events under Users & Roles and assign it to them.

Changes

The Changes sub-tab is the mutation-batch ledger. Every mutating write lands here as a batch, together with who made it and where it came from:

Origin Meaning
Web The Turbo EA web interface
API A direct call to the REST API (a script, an integration)
AI tool An AI assistant connected through the MCP server
Extension An installed extension, recorded as an ext:<key> batch

Click a batch to open its drawer: the per-event diff shows each field's old and new value, and Rollback reverts the card, relation, risk, stakeholder-role, tag and draft-decision writes it made.

Batches are kept for MUTATION_BATCH_RETENTION_DAYS days (an environment variable, default 15) and then purged automatically. The events themselves stay on each card's History tab after the batch is gone.

Security & admin

Security & admin log

The Security & admin sub-tab lists one entry per security-relevant action, newest first:

  • sign-ins and sign-outs, with password, SSO or a trusted reverse proxy;
  • failed sign-in attempts, each with a reason code saying why it failed;
  • account lockouts after repeated failures;
  • registrations, and passwords being set, reset or changed;
  • role impersonation — an administrator starting and stopping View as role…;
  • administrative actions — changes to users, roles, settings, the metamodel, tags, calculations, extensions, imports, integrations, public portals and published diagrams, surveys and operator access (see Administrative actions);
  • data exports — the card CSV and JSON exports, workspace exports and exports for another EA platform (see Data exports) — and exports of this log itself.

Columns

Column Description
When Date and time the action happened
Category auth (authentication), admin (administrative actions) or access (exports and other data access)
Action What happened, for example auth.login.failed — see the action catalogue
Outcome Success or Failure
Actor The signed-in user who performed the action. Empty when nobody was signed in — a failed sign-in, for instance
Subject The account the action was about — the email address typed into a failed sign-in, or the user whose password was reset
Target The object acted on, when there is one — for example the role chosen when starting an impersonation
IP The client's IP address (see Client IP address)
Origin Where the request came from: web, API, AI tool, extension, ops or system
Impersonated role When an administrator was viewing the app as another role, that role. The real administrator stays the actor

Columns can be shown, hidden, reordered and frozen like on every other data table; When and Action are always shown.

Filters

The filter sidebar on the left narrows the list by category, outcome, action, actor, IP address and a date range. Every section header shows how many values are selected, and the collapsed sidebar shows the number of active filters. The Columns tab of the same sidebar shows, hides and freezes columns. Your filters and column layout are remembered in this browser.

A search for an email address matches the actor, the subject and the target, so typing one address finds both what that person did and what was attempted against their account.

Reading an entry

Click a row to open the entry drawer. It shows every field of the entry — when, who, from which IP address and browser — and, under Details, what the entry recorded, written out in plain language rather than as stored keys:

  • Changes — one sentence per change. Editing a card type reads, for example, Field “Business criticality” (Single select) added to section “Governance” or Option “High” on field “Business criticality” renamed to “Critical”. Fields, sections, options, roles and portals are named by their labels, permissions by their description and languages by their name.
  • Before / After — a table of the values that changed, one row per field, each field named by its label. Values are formatted: Yes or No, dates in the workspace date format, colours as a swatch, file sizes in KB or MB. Secrets are never stored: a password, token or client secret is replaced before the entry is written and is shown as Secret value.
  • Context — facts that frame a change without being part of it: a portal's current address and whether it is published, whether a connection's credentials were replaced, how many rows an import touched.

A failed sign-in shows its Reason above the details — a wrong password, an unknown account, an account that is temporarily locked.

Entries written before version 2.161.0 show their details as they were recorded: old and new values still appear in the table, and anything the drawer cannot read is shown as raw JSON.

The drawer has no actions. Entries cannot be edited or deleted.

Export CSV

Export CSV in the toolbar downloads the entries matching the current filters as a CSV file, up to 50,000 rows. The export is itself recorded, as access.audit_log.exported, so an export of the security log always leaves a trace in it.

Action catalogue

Action Recorded when
auth.login.succeeded A user signs in with email and password
auth.login.failed A password sign-in is refused. The reason code says why (see below)
auth.login.locked The fifth failed attempt in a row locks the account for 15 minutes. The details carry the time the lock ends
auth.logout A signed-in user signs out. A sign-out without a valid session records nothing
auth.sso.succeeded A user signs in through an SSO provider. The details carry the provider, the identity provider's subject and whether the account was created by this sign-in
auth.sso.failed An SSO sign-in is refused — a domain that is not allowed, a missing email address, a local account with the same email, an inactive account. The details carry the provider and the reason
auth.proxy_session.succeeded A user signs in through a trusted reverse proxy
auth.proxy_session.failed A reverse-proxy sign-in is refused
auth.register A user creates an account through self-registration
auth.password.set An invited user sets their first password
auth.password.reset_requested Someone asks for a password-reset email. Always recorded as a success with the typed address, so the response reveals nothing; the details say whether an account matched and whether an email was sent
auth.password.reset A password is changed through a reset link
auth.password.changed A signed-in user changes their own password
auth.impersonation.started An administrator starts View as role…. The target is the role chosen
auth.impersonation.stopped The administrator returns to their own role

Reason codes on auth.login.failed:

Reason Meaning
unknown_user No account has that email address
sso_account The account signs in through SSO and has no password
no_password The account has no password set yet — an invitation that was never accepted
locked The account is locked after repeated failures
bad_password The password is wrong
inactive The account is deactivated

Reason codes on auth.sso.failed, auth.proxy_session.failed, auth.password.set and auth.password.reset:

Reason Meaning
expired The account's time-limited access has ended
hd_mismatch The Google account belongs to a domain that is not allowed
no_email The identity provider returned no email address
no_subject The identity provider returned no subject identifier
local_account A local password account already uses that email
no_account No account exists and accounts are not created automatically
exchange_failed The identity provider did not complete the sign-in
unauthenticated The reverse proxy did not authenticate the request
identity_rejected The forwarded identity was rejected (domain or unverified email)
invalid_token The invitation or reset link was invalid or has expired

A burst of unknown_user failures from one IP address is what password spraying looks like; a run of bad_password against one account followed by auth.login.locked is a guessing attempt against that account.

Administrative actions

Every change an administrator makes to how the instance is configured is recorded in the admin category. The actor is the administrator, the target is the object changed — a user, a role, a setting, a card type — with its name as it was at that moment, and the details list what changed as old and new values, limited to the fields that actually changed. Two rules hold for every entry:

  • Secrets are masked. An SMTP password, an SSO client secret, an AI provider key, a ServiceNow credential or any other secret value is written as ***: the entry shows that it changed, never what it was or what it became.
  • No license text or credential is ever stored. An extension license is identified, not copied, and a password an administrator sets for a user is recorded as changed, never with its value.

A save that changes nothing is not recorded, and neither is a user editing their own name or language in their profile. Large configuration — a card type's fields, translations or layout, a portal's filters — is summarised by its size rather than copied; when the size did not change, a short fingerprint of the content is added, so the entry still shows that something changed. A card type's per-role permission overrides are the exception: each cell that changed is listed with its old and new value, an empty value meaning the role's own permission applies.

Area Actions What is recorded
Users & roles admin.user.created, .updated, .deleted, .bulk_updated, .bulk_deleted, .unlocked; admin.user.invitation.resent, .deleted; admin.role.created, .updated, .archived, .restored; admin.stakeholder_role.created, .updated, .deleted, .archived, .restored Accounts created, edited, activated or deactivated, deleted (one by one or in bulk) and unlocked, with the changed fields — role, email, name, active state; invitations re-sent or revoked; application roles and per-type stakeholder roles, with the permissions granted or removed
Settings admin.settings.updated One entry per setting saved — email, authentication and SSO, registration, AI, MCP, languages, module toggles, branding, logo and favicon, retention periods. The target is the setting; the details are its old and new value, with secrets masked
Metamodel admin.metamodel.type.* (including .references_generated), admin.metamodel.relation_type.*, admin.metamodel.resource_type.*, admin.metamodel.category.* (including .reordered), admin.principle.* (including .imported), admin.regulation.* Card types, relation types and resource types created, changed, deleted or restored, with the changed properties; card references generated for a type; EA principles, including those imported from the catalogue; compliance regulations; card type categories (architecture layers) created, renamed, recolored, reordered or deleted
Tags & calculations admin.tag_group.*, admin.tag.*, admin.calculation.* Tag groups and tags; calculated fields created, changed, deleted, activated or deactivated, with the old and new formula, and a calculation run over all cards (.recalculated)
Extensions admin.extension.install.uploaded, .applied, .discarded; admin.extension.store.claimed, .refreshed, .installed; admin.extension.enabled, .disabled, .uninstalled; admin.extension.license.updated, .removed Bundles uploaded, installed or discarded; licenses and downloads from the Extension Store; extensions enabled, disabled and uninstalled; the license replaced or removed — with the extension and its version, never the license text
Workspace transfer & migration admin.workspace.import.uploaded, .applied, .completed, .discarded; admin.migration.uploaded, .field_mappings.updated, .applied, .deleted A workspace bundle uploaded, its import started, finished or discarded; a platform migration uploaded, its field mappings edited, applied or deleted
Integrations admin.servicenow.connection.*, admin.servicenow.mapping.*, admin.servicenow.sync.pulled, .pushed, .applied ServiceNow connections, with credentials masked, and mappings; every pull, push and apply of staged records
Public exposure admin.web_portal.created, .updated, .deleted; admin.diagram.published, .unpublished Web portals and published diagrams — everything that makes data reachable without an account
Surveys admin.survey.sent, admin.survey.closed A survey sent to its recipients, or closed
Operator access admin.ops.rescue_access.granted, .revoked Rescue access granted or revoked through the operations API, recorded alongside the existing ops.* events

Data exports

Every time data leaves the instance in bulk, an access entry records who took it, how much and in which form. The entry is written before the file is sent, so an export whose download was never finished is still on the record.

Action Recorded when Details
access.export.cards Cards are exported through the API's CSV or JSON export (/cards/export/csv, /cards/export/json), as scripts, integrations and AI assistants do format (csv or json), type (the card type or types) and rows (the number of cards). The target is the card type when the export covered exactly one
access.export.workspace A full workspace bundle is downloaded — from Workspace transfer, or by a control plane through the operations API, which records no actor and requested_by: ops include_archived and size_bytes
access.export.migration The workspace is exported for another EA platform under Admin → Settings → Migration (see Platform migration) The target platform, include_archived and size_bytes
access.audit_log.exported Someone exports the security log to CSV rows

What is not recorded

  • Token refresh. The web app renews its session on a timer, so every open tab would add an entry and none of them would mean anything.
  • Requests rejected for an expired or invalid session token. These happen every time a session ends naturally.
  • OData feed reads. Excel and Power BI poll a saved view's OData feed on a schedule, so every refresh would add an entry.
  • Spreadsheets and decks built in the browser. The inventory's and the reports' Excel and PowerPoint downloads are made from data the page has already loaded, so they are reads, not exports from the server.
  • Plain read access — opening a card, running a report, searching the inventory.

Retention

Entries are kept for 365 days by default. Change it under Admin → Settings → General → Data management → Security log retention: any number of days up to 3650, or 0 to keep entries forever. A job deletes the entries older than the window once a night, and re-reads the setting each time, so a change takes effect without a restart.

The retention job is the only thing that ever deletes an entry. There is no edit or delete action in the interface or the API, for any role.

The log is deliberately not part of Workspace Transfer: it is the security history of this instance, not workspace content, and cloning an instance does not copy it.

The JSON audit stream

Every entry is also written by the backend as one JSON line on the logger turbo_ea.audit, at the same moment it is stored. The backend logs to standard output, so the stream reaches whatever already collects your container logs — docker compose logs backend, Kubernetes log collection, or any SIEM shipper — with no configuration in Turbo EA:

docker compose logs -f backend | grep turbo_ea.audit

The line is written when the entry is recorded. If the request's transaction fails afterwards, the line exists without a row, so the table is the record and the stream is the live feed.

Each line carries the same fields as the entry:

Field Content
audit_id, created_at The entry's id and the moment it was written
category, action, outcome What happened and whether it succeeded
actor_user_id, actor_email Who did it
subject_email The account it was about
target_type, target_id, target_label What it acted on
ip, user_agent The client
origin web, api, mcp, ext, ops or system
impersonator_user_id, impersonated_role Set while an administrator is viewing the app as another role
details Up to four keys, secrets already masked: reason (a failure code), changes ({field: {old, new}}), items (semantic changes, {kind, …} — e.g. field.added) and context (plain facts)

Shipping the stream to a central log store keeps a copy outside the instance, out of reach of anyone who can administer it — which is what an insider-threat review needs. See Logging and the audit stream for how to collect it.

Client IP address

The IP column records the address of the client, not of the proxy in front of the backend. The bundled edge nginx sets the X-Real-IP header to the address it received the connection from, and the backend reads that header because the published backend image sets TRUST_PROXY_HEADERS=true. A value that is not a valid IP address is dropped rather than recorded.

  • Leave TRUST_PROXY_HEADERS on as long as every request reaches the backend through the bundled nginx — the default stack, the Helm chart and the managed container templates all work that way.
  • Turn it off (TRUST_PROXY_HEADERS=false) if you expose the backend container directly, without the bundled nginx in front of it: a client could then send its own X-Real-IP and choose the address that gets recorded.
  • Behind a load balancer, an ingress controller or a CDN, the bundled nginx receives every connection from that hop, so on its own it records the load balancer's address for every user. Set NGINX_TRUSTED_PROXIES on the nginx container, as described below.

Behind a load balancer

NGINX_TRUSTED_PROXIES lists the addresses of the hops in front of the bundled nginx, as IP addresses or CIDR ranges separated by commas or spaces. nginx then reads the client address from the X-Forwarded-For header those hops add — but only on a connection that comes from one of the listed addresses — and skips every listed address in that header, so a chain of proxies resolves to the first address that is not one of yours. That address replaces the connection's, and the X-Real-IP header the backend reads carries it unchanged. Empty, the default, trusts no one.

To find the value, read the edge nginx's access log (docker compose logs nginx, kubectl logs deployment/turbo-ea-nginx, or the platform's log viewer): the address at the start of each line is the hop to trust, and the last quoted field is the X-Forwarded-For header it sent.

Deployment Where to set it Typical value
Docker Compose, browsers reach nginx directly — Leave empty
Docker Compose behind your own reverse proxy NGINX_TRUSTED_PROXIES in .env The reverse proxy's address, as nginx sees it
Kubernetes behind an ingress controller Chart value nginx.trustedProxies The cluster's pod CIDR, for example 10.244.0.0/16
AWS ECS Fargate NginxTrustedProxies / Terraform nginx_trusted_proxies The CIDR ranges of the load balancer's subnets
Azure Container Apps nginxTrustedProxies / Terraform nginx_trusted_proxies The environment's infrastructure subnet
Google Cloud Run NGINX_TRUSTED_PROXIES in the manifest / Terraform nginx_trusted_proxies The addresses the access log shows

Keep the list as narrow as the hops themselves. Every listed address is believed when it names the client, so a range your users connect from — all of 10.0.0.0/8 on an internal deployment, say — would let any of them write a different address into the log. With nothing set, the column shows the load balancer: not useful, but never forged.

The sign-in rate limit counts attempts per client address from the same header, so behind a load balancer this setting is also what keeps one person's failed attempts from slowing everyone else down. This applies to sign-ins that reach the backend through the edge nginx; the MCP server signs in to the backend directly, so its own sign-in calls still share the MCP container's address. The per-client key trusts that header the way the audit log does: it assumes sign-ins reach the backend only through the edge nginx, which the bundled compose file and the Helm chart guarantee by not exposing the backend. A caller that could reach the backend directly could choose its own bucket by setting the header itself; keep the backend unexposed, or turn TRUST_PROXY_HEADERS off.