Audit
The audit service writes the audit log of OpenCloud: who did what to which object, when, from where and with which outcome. It records successful actions as well as failed and denied ones, for example logins with a wrong password or a user trying to give themselves the admin role.
The services emit audit events into the JetStream stream audit. The audit service reads that stream and writes every event as one line of JSON. Shipping the records to a SIEM or a log store is done by a log agent or the OpenTelemetry Collector, not by OpenCloud.
The design is described in ADR 0006.
Enabling the Audit Log
Two things are needed:
- The services must emit audit events. Set
OC_AUDIT_ENABLED=true. It is read by the proxy, graph, frontend, sharing and storage-users services, each of them also has its own variable, e.g.PROXY_AUDIT_ENABLED. - The audit service must run. It is not started automatically when running as single binary via
opencloud serveror as docker container. Add it withOC_ADD_RUN_SERVICES=audit.
Events emitted while the audit service is not running are kept in the stream and written when it starts, see AUDIT_STREAM_MAX_AGE.
Audit Records
Every record is one line of JSON. Field names follow the OpenTelemetry semantic conventions where they exist.
{"event.id":"019a3c1e-7b2f-7c4e-9a10-5d2f0e6b8a41","event.name":"opencloud.auth.login","event.code":"A001F","schema_version":"1.0","time":"2026-10-28T09:14:03.512Z","outcome":"failure","outcome.reason":"invalid_credentials","severity":"warning","tenant.id":"","actor.type":"user","actor.name":"alice","auth.method":"basic","client.address":"203.0.113.7","user_agent.original":"Mozilla/5.0 ...","target.type":"user","target.id":"alice","target.name":"alice","service.name":"proxy","service.version":"5.1.0","ocsf.class_uid":3002,"ocsf.activity_id":1}
| Field | Content |
|---|---|
event.id |
UUIDv7, unique per event |
event.name |
opencloud.<object>.<action>, see the event catalog |
event.code |
stable code per event and outcome: a letter for the category, a number and S, F or D for success, failure or denied |
schema_version |
1.0, changes are additive only |
time |
RFC 3339, UTC, milliseconds |
outcome, outcome.reason |
success, failure or denied, with a machine readable reason on failures |
severity |
info, notice, warning or alert; failures and denials are at least warning |
tenant.id |
empty on single-tenant installations |
actor.type, actor.id, actor.name, actor.impersonated_by |
type is user, service, public_link, guest, anonymous, system or cli |
auth.method, auth.client_id, session.id |
oidc, basic, app_token, public_link, guest_link or signed_url; the OIDC client and session |
client.address, user_agent.original |
address of the client, see Client Address |
target.type, target.id, target.space.id, target.name, target.path |
the object acted on |
change.before, change.after |
changed attributes of an update, never secrets |
trace_id, span_id |
OpenTelemetry trace correlation |
service.name, service.version, service.instance.id |
the service that emitted the event |
ocsf.class_uid, ocsf.activity_id |
mapping to OCSF 1.9.0, for SIEMs that normalize to OCSF |
Records contain no human readable message. Passwords, tokens and file contents are never written. Public links are identified by their share ID; when a link lookup fails there is no share ID, then a hash of the token is used.
Event Catalog
| Category | Object | Actions |
|---|---|---|
| Authentication | auth |
login, backchannel_logout |
| Credentials | app_token |
create, delete |
guest_token |
create, renew | |
user.password |
change, reset | |
| Files | file |
upload, download, preview, open_in_app, rename, move, copy, delete, restore, purge, lock, unlock |
file.version |
restore, delete | |
file.malware |
detected | |
folder |
create, download, rename, move, copy, delete, restore, purge | |
trash |
purge_all | |
| Sharing | share |
create, update, delete, accept, decline |
public_link |
create, update, delete, access | |
ocm.share |
create, update, delete | |
ocm.invite |
create, accept | |
| Spaces | space |
create, update, enable, disable, delete |
space.member |
add, update, remove | |
space.quota |
change | |
| Identity | user |
create, update, enable, disable, soft_delete, delete |
group |
create, update, delete | |
group.member |
add, remove | |
app_role |
assign, unassign | |
| Administration | app_role.definition |
change |
sharing_role |
enable, disable | |
settings |
change | |
config |
change | |
cli |
command | |
personal_data |
export | |
| Security | access |
denied |
The catalog is defined in reva's pkg/audit. Some of its events are not emitted yet: app and guest tokens, previews, open in app, copies, deleted versions, the malware finding, OCM, settings, configuration and CLI commands, app role and sharing role definitions, and access.denied. group.update is only recorded when it fails.
Some actions are left out on purpose:
- Thumbnails are not audited.
file.previewis opening a file in the preview app. - Uploads and downloads are recorded when the transfer is done. Uploads and downloads by OpenCloud itself, e.g. for a copy, thumbnails, search or an archive, are not recorded.
- A download that is resumed or seeked makes many range requests, only the one starting at the first byte counts.
auth.loginis recorded for a new OIDC session. Basic auth and app tokens authenticate every single request, for them only failed logins are recorded. Expired OIDC tokens are part of the normal token renewal and are not recorded as failed logins.public_link.accessis recorded once per link and client within 30 minutes, every failed attempt is recorded. The web client asking whether a link needs a password is not an attempt.
Outputs
The audit service writes to stdout, one record per line and nothing else, see AUDIT_OUTPUT_STDOUT. Any log agent can pick it up. When the audit service runs with other services in one process or container, their logs share the same stdout; run it in its own container to keep the audit log separate.
With AUDIT_OUTPUT_OTLP_ENDPOINT the audit service also sends the records to an OpenTelemetry Collector as OTLP logs over gRPC; use AUDIT_OUTPUT_OTLP_INSECURE for a Collector without TLS. Every record is an OpenTelemetry event: the event name is event.name, the fields are attributes and the body is the record as JSON. The Collector routes the records to the SIEM, e.g. with its Splunk HEC, Elasticsearch, OpenSearch or syslog exporters. Every record is sent on its own and acknowledged only after the Collector accepted it, so a Collector outage delays the records but loses none.
At least one output must be enabled.
Delivery
The services publish the events to the JetStream stream audit. The stream is a buffer, not the archive: it keeps the events for AUDIT_STREAM_MAX_AGE (default 7 days). Use AUDIT_STREAM_REPLICAS=3 in a clustered NATS.
The audit service acknowledges an event only after it was written. If it stops or an output fails, the event is delivered again. A record can therefore appear twice after a restart, with the same event.id.
OC_AUDIT_MODE decides what a service does when it can't store an event:
best_effort(default) publishes in the background and never slows down a request. Events that can't be stored are dropped and counted inopencloud_audit_events_dropped_total.strictwaits until the event is stored. In this version the events are emitted after the action, so a failed event is logged and counted, but the request still succeeds.
Client Address
The proxy determines the client address. It only uses the X-Forwarded-For header, or the header set in PROXY_CLIENT_IP_HEADER, when the request comes from a trusted reverse proxy, see PROXY_TRUSTED_PROXIES. The default trusts the loopback and the private network ranges.
Check the setting if your reverse proxy is not in a private network:
- Your reverse proxy reaches OpenCloud from a public address, e.g. Cloudflare, a load balancer of a cloud provider or a proxy on another server. Its headers are ignored with the default, every request then has the address of the proxy and a SIEM sees all logins coming from one address. Add the addresses of your proxy to
PROXY_TRUSTED_PROXIES, for Cloudflare its published IP ranges. - Clients can reach OpenCloud from a private network without passing your reverse proxy. With the default they can set their address themselves. Set
PROXY_TRUSTED_PROXIESto the addresses of your reverse proxies only.
PROXY_TRUSTED_PROXIES replaces the default, list the loopback and private ranges again if you still need them. See Trusted Reverse Proxies in the proxy service.
The proxy passes the address and the other request details to the services in the X-Audit-Context header and drops any value a client sent. The services trust this header like the other internal headers, so the services must not be reachable directly from outside.
Metrics
| Metric | Content |
|---|---|
opencloud_audit_events_emitted_total |
events handed to the emitter, by service, event and outcome |
opencloud_audit_events_dropped_total |
events not stored, by service and reason |
opencloud_audit_publish_duration_seconds |
time to store an event in the stream |
opencloud_audit_records_total |
records handled by the audit service, by result |
opencloud_audit_sink_errors_total |
failed writes to an output |
opencloud_audit_consumer_lag |
events in the stream the audit service has not read yet |
Alert on dropped events and on a growing consumer lag.
Legacy Audit Log
The audit log of earlier versions is still available with AUDIT_LEGACY_ENABLED=true and will be removed in a later version. It reads the domain events instead of the audit stream and uses the AUDIT_LOG_TO_CONSOLE, AUDIT_LOG_TO_FILE, AUDIT_FILEPATH and AUDIT_FORMAT settings. Its records have a different format:
{"RemoteAddr":"","User":"user_id","URL":"","Method":"","UserAgent":"","Time":"","App":"admin_audit","Message":"user 'user_id' trashed file 'item_id'","Action":"file_delete","CLI":false,"Level":1,"Path":"path","Owner":"user_id","FileID":"item_id"}