Audit log
The audit log captures every administrative action against your tenant: who did what, when, from which IP, against which resource. Retention is two-tier: a queryable hot tier with full event payload (actor, action, fields_json) for 90 / 180 / 365 / 730 days — defaulting to 90 (standard plan) or 365 (enterprise plans), and configurable per tenant at Authentication policy → Audit log retention; and a 3-year cryptographic chain archive in T Cloud Public OBS WORM that retains hash-chain anchor objects after the hot-tier payload has been redacted. Compliance exports cover the queryable window.
For the cryptographic chain of custody (anchor-chain, OBS-backed merkle commitments), see Audit chain integrity.
What’s recorded
| Event class | Examples |
|---|---|
| Identity | Sign-in, SSO step-up, MFA reset, SCIM provisioning, role change |
| Repository | Repo created, transferred, archived, deleted; branch protection changed |
| Secrets | Secret created, rotated, deleted (name only, never the value) |
| Webhooks | Destination added, rotated, removed; delivery replayed |
| Policy | Push-protection rule added or override applied; malware-scan override; SAST dismissal |
| Billing | Plan change, payment method update, cap change |
| Data export | Export queued, downloaded, expired |
Notably absent: ordinary reads (clone, view, file open). The audit log is an administrative-action log, not an access log. The exception is read-only auditors, whose reads of your settings are recorded (see Auditor activity). Package and container registry access (every download, upload and delete, with actor and client IP) is in the separate registry access log, which you can read through the API, forward to your SIEM and take in the data export. Git clone and fetch are not logged per request yet.
Package and release events
Publishing, tagging, deleting and refusing internal artefacts is in the audit log, hash-chained like every other event and on your SIEM stream. Two views write it:
- The version view, from the registry itself: a version appeared or went away.
- The request view, from the upload proxy in front of every registry write: one event per accepted or refused write, with the job behind a job token and, for tags, what the tag pointed at before.
| Action | When |
|---|---|
package.published | A package version appeared (its first file landed). |
package.deleted | A package version and its files are gone. |
package.upload.accepted | A registry write succeeded: a file, an npm publish, a further file in an existing version, a container manifest pushed by digest. |
package.upload.refused | fremforge refused a write: malware, the scanner unavailable, too large to scan, a container layer gate. |
package.delete.accepted | A delete succeeded (a file, a version, a container manifest by digest). |
package.delete.refused | A delete was refused (other than a registry robot’s, below). |
package.tag.created | A container tag or npm dist-tag that did not exist now names a digest. |
package.tag.moved | A container tag now names a different digest. previous_digest is the one it named before. |
package.tag.set | A tag was written and fremforge could not tell what it named before, or it named the same digest (unchanged: true). npm dist-tags are always set. |
package.tag.deleted | A tag was deleted; digest is what it named. |
package.immutable.refused | An overwrite of a published version was refused (immutable releases); both sha256s. |
registry_robot.delete_refused | A registry robot tried to delete; robots publish but never delete. |
package.files.recorded, package.files.removed | The files of a version with their sha256 (package policies orgs). |
forgejo.release.*, forgejo.ref.create, forgejo.ref.delete | A release published, edited or deleted (with its id, target, draft and pre-release flags and assets); a git tag or branch created (with its commit) or deleted. |
Every package event carries:
| Field | Meaning |
|---|---|
org, package_type, package_name, package_version, file | What was written. A request names what its path names; the version view names the version. |
digest | sha256:<hex>: a container manifest’s digest, or the sha256 of a raw upload body (the file’s own sha256 for every type except npm and Cargo, whose upload bodies wrap the file). |
actor_kind | human, robot (a registry robot), job (a Forgejo Actions job token), bot (a fremforge system bot), system or anonymous. |
actor_login, actor_id | Who the registry authenticated. |
job_repo, job_run_id, job_run_number, job_id | For a job token: the repository and the run (/<owner>/<repo>/actions/runs/<job_run_number>). |
decision, reason | On a refusal, or a write a gate in observation mode let through. |
access_event_id | The matching row of the registry access log, which also holds the client IP. |
Request-view events exist from 2026-10-08. Promotion between repositories is not a registry concept in fremforge, so there is no promotion event.
Package access
Per-package access and protected names write:
| Action | When |
|---|---|
package_access.settings.updated | The organisation default changed (default_mode from and to). |
package_access.package.updated | A package’s access setting was created or changed (mode, inherit_repo, changed). |
package_access.package.removed | A package’s setting and grants were removed (grants_removed). |
package_access.grant.set | A team or Actions grant was added or changed (grantee_kind, grantee_name, permission, previous_permission). |
package_access.grant.removed | A grant was removed. |
package_protection.rule.created, .updated, .deleted | A protected-name rule changed (package_type, name_pattern, push, delete). |
package_access.denied | A request was refused by per-package access or a protected name (access, needed, permission, reason, and the actor fields above). At most once an hour for the same actor, package and access. |
package_access.would_deny | The same, while fremforge ran the check in observation mode and served the request. |
Model scanning
Model scanning writes:
| Action | When |
|---|---|
security_policy.model_scan.updated | The organisation’s model policy changed (changed, and diff with each setting’s from and to). |
security_policy.model_scan.dismissed | A finding was dismissed for one file, or its dismissal renewed (sha256, verdict, reason, expires_at, replaced). |
security_policy.model_scan.dismissal_revoked | A dismissal was revoked (sha256). |
A pull the model policy refuses is a registry.pull.blocked event with gate: model, below.
Container pull gates
The registry’s image gates write their decisions to the audit log, so they reach your SIEM like any other event:
| Action | When |
|---|---|
registry.pull.blocked | A container pull was refused |
registry.pull.would_block | A pull was served that a gate would have refused: your image-scan block action is warn, or the gate runs in observation mode |
The actor is system:registry-pull-gate. The fields are:
| Field | Meaning |
|---|---|
gate | image_scan (vulnerability findings at or above your block threshold), image_malware (malicious packages), registry_scan (a layer of the image ClamAV found infected or could not scan), registry_blob (the same, for a layer fetched directly by digest) signature (image signatures: reason is unsigned, untrusted or invalid) or model (model scanning: reason is model-malicious, model-suspicious or model-unscannable) |
image, ref, digest | The image, the tag or digest that was asked for, and the digest that was served |
reason | What the gate found, for example layer-infected or image-malware |
mode | enforce, warn (your block action) or shadow (observation mode) |
block_severity, finding_count | image_scan only: the threshold and the number of open findings at or above it |
window_start | The hour this event stands for |
previous_window_start, previous_window_count | The last earlier hour with an event for the same key, and how many decisions it had in total; null for the first |
One event per hour. A CI job that pulls a blocked image in a loop would otherwise fill the log. So there is one event per image digest, gate and decision per UTC hour, written at the first decision of the hour. Further decisions in that hour are counted, and the count is reported on the next event for the same key as previous_window_count. An hour’s count therefore reaches your SIEM with the next event for that digest, not at the end of the hour; the count of the last hour with any pulls is not sent. A blocked and a would_block for the same digest, or two gates, are separate keys.
A registry.pull.blocked event also triggers your security.pull.blocked alert webhook, if you subscribed one. Every pull, allowed or not, is in the registry access log.
Auditor activity
Members of your org’s Auditors team can read your org settings without changing them. Their reads are recorded, so you can see what an auditor looked at:
| Action | When |
|---|---|
tenant.auditor.read | An auditor opened an admin page or called a read endpoint of the API. At most one event per auditor and route every 10 minutes |
tenant.auditor.write_refused | An auditor sent a request that would change something, and it was refused. Every attempt |
tenant.auditor.read_refused | An auditor asked for a page or endpoint that is closed to auditors, such as the data export, and it was refused. Every attempt |
compliance.evidence.fetched | Anyone, auditor or not, fetched the signed compliance evidence bundle through the API. Every fetch |
On the three tenant.auditor.* events the actor is the auditor’s username, and the fields carry the route (the route pattern, such as /:slug/_admin/members, never the query string), the method, the status on tenant.auditor.read, via (portal, oauth or token) and, for an API token, its token_id.
Reading the log
Tenant admin → Reports → Audit log. Filter by actor (<username>), action (secret.created, role.changed, …), time range, or affected resource. The default view is the last 7 days.
Click any row to expand the JSON payload. Sensitive fields (secret values, OIDC tokens, PII attachments) are redacted at the write boundary, so an expanded row never shows a value you wouldn’t want a customer’s auditor to read.
CSV export
Click Export CSV on any filtered view to get a snapshot. The CSV contains the rows currently filtered, capped at 50,000 rows per export. For larger windows, queue a full Data export from Reports → Data export.
Via REST API
The audit log is available on the public API surface. Use this when streaming events into your own analytics pipeline, building a compliance dashboard, or driving audit queries from a script.
- Query the log
GET /_app/api/v1/orgs/:slug/audit(filter with?actor_kind=and?since=) - Verify chain integrity
GET /_app/api/v1/orgs/:slug/audit/integrity - Change retention window
PATCH /_app/api/v1/orgs/:slug/auth-policybody:{ "audit_full_payload_retention_days": 365 }
Requires a Personal Access Token (PAT) with the audit:read scope (or policy:write to change the retention window):
curl -H "Authorization: Bearer ${FREMFORGE_PAT}" \
"https://frem.sh/_app/api/v1/orgs/acme/audit?actor_kind=human&since=2026-01-01T00:00:00Z"See the public REST API reference for the full OpenAPI specification.
SIEM forwarding
Every audit event is also emitted to a per-tenant HTTPS sink if you configure SIEM forwarding. The sink receives events in real time (within seconds of the action) in a compact JSON shape; the format is stable and versioned.
Retention
Audit records are kept in two tiers:
- Queryable payload tier — the actor name and
fields_jsonpayload of every event are queryable in the admin UI / CSV export / SIEM stream for the retention window below. - Tamper-evident chain tier — each event’s cryptographic hash + chain link stays for 3 years (WORM-anchored to OBS object lock). This is what makes the chain provably untampered for audit-readiness; it lives intact even after the payload tier redaction stamps a row.
Queryable payload retention
| Plan | Default | Customer-tunable presets |
|---|---|---|
| fremforge / starter | 90 days | 90 / 180 / 365 / 730 |
| enterprise / enterprise_trial | 365 days | 90 / 180 / 365 / 730 |
The cap of 730 days (≈2 years) keeps the live audit_events table bounded. Anything beyond that is the WORM tier’s job; if you need queryable history past 730 days, queue a Data export quarterly and archive it in your SIEM.
The cutoff applies at the actor + fields_json columns: rows older than the window keep their hash, timestamp, and action name (so SOC 2 / DORA reporting still works) but the actor is pseudonymised to redacted and the payload is replaced with {}. The cryptographic chain is not broken by this; redaction is an in-place pseudonymisation, not a delete.
The queryable window is the audit_payload class of evidence retention, where the other evidence classes are set too. A legal hold suspends the redaction.
Changing the queryable window
Tenant admin → Security → Authentication policy → Audit log retention card. Pick one of 90 / 180 / 365 / 730 days. Behaviour notes:
- Extending the window (e.g. 90 → 365): newly-emitted rows stay queryable for longer. Rows that were already redacted under the previous window stay redacted — fremforge does not un-redact.
- Shortening the window (e.g. 365 → 90): the next daily redaction sweep redacts every row older than the new window. Redaction cannot be undone once applied; the WORM chain is unaffected.
Audit-log action recorded against this setting: tenant.auth_policy.audit_retention.updated (with previous and next day counts).
Compliance commitments
DPA Annex A.7 commits to the 90-day baseline + 3-year WORM. Enterprise SLA commits to 365-day queryable + 3-year WORM. Going below the plan default is allowed in the UI but would breach the DPA — fremforge does not enforce the floor at the column, the contract does. A failure of the daily redaction sweep itself (separate from a customer changing retention) is monitored and alerted continuously.