A confession before we start: the title undersells it. When we locked this topic on the editorial calendar it said “three audit sinks” — database, SIEM, webhook. By the time you read the code, the fan-out in Stylus SFTP Server actually drives five: the server log, the database, syslog, webhooks, and email notification rules. This post walks through what one audit event looks like as it lands in each of them, at the same time.
The scenario: user alice finishes uploading data.csv over SFTP. The server emits one immutable AuditEvent — timestamp, event type UPLOAD_COMPLETE, username, remote IP, filename, byte count, success flag. That single event is handed to a compound sink that delivers it to every enabled destination in order, with error isolation: if the syslog send throws, the webhook and the database still get the event. One sink's outage never blinds the others.
Sink 1: the database — your queryable system of record
The JDBC sink writes to the sftp_audit table — but not on the transfer thread. Events go onto an in-memory queue (default capacity 10,000, configurable via <queue-capacity>) and a dedicated audit-writer thread drains it in batches of up to 100 rows per insert. Your SFTP throughput never waits on a database insert. The table is what the Web Admin audit views query, what retention policies prune, and what the email sink (below) reads its rules against.
Sink 2: syslog — the SIEM feed
The syslog sink emits RFC 5424 structured messages over UDP. Here is the actual wire format for alice's upload:
<134>1 2026-07-28T13:32:45.123456Z sftp01 StylusSFTPServer 5621 UPLOAD_COMPLETE
[sss@0 event="UPLOAD_COMPLETE" user="alice" ip="192.168.1.100"
file="/data.csv" bytes="5000" ok="1"]
Upload complete: data.csv (5000 bytes) by alice
Two details matter for your SIEM team. First, the structured-data block [sss@0 ...] means Splunk, Elastic, QRadar, and friends extract fields without writing a custom parser — it's standard RFC 5424 section 6.3, not a proprietary log line. Second, severity encodes outcome: successes go out as Informational (6), failures as Warning (4), so a facility-level filter for warnings surfaces every failed login and rejected upload with zero query logic. Facility is configurable (LOCAL0–LOCAL7) so you can route SFTP audit separately from everything else on the collector.
Sink 3: webhooks — the integration hook
The webhook sink POSTs each event as JSON to any HTTPS endpoint, asynchronously, with a 30-second timeout:
{
"event": "UPLOAD_COMPLETE",
"timestamp": "2026-07-28T13:32:45.123456Z",
"username": "alice",
"remoteIp": "192.168.1.100",
"filename": "/data.csv",
"bytes": 5000,
"success": true
}
Three things distinguish it from a naive HTTP logger. You can filter per webhook — <events>UPLOAD_COMPLETE,AUTH_FAILURE</events> sends only those two types, an empty list sends everything. You can sign it: configure a <secret> and every request carries an X-SSS-Signature header with an HMAC-SHA256 digest of the payload, so the receiver can verify the event actually came from your server. And there is a text format that wraps a rendered template in a {"text": "..."} envelope — which is exactly the shape Slack and Teams incoming webhooks expect, so “post failed logins to the security channel” is configuration, not code.
Sink 4: email rules — the human alert
The notification sink is the only one that thinks before it sends. Rules live in the <notifications> block and come in three shapes:
- Simple — one email per event, with a CSV attachment of the audit row.
- Threshold —
threshold="5" window="PT5M"fires only when fiveAUTH_FAILUREevents land within five minutes. One email, all five rows attached — a brute-force alert, not five separate pings. - Batch —
batch-window="PT1H"accumulates uploads per user and sends one hourly manifest: file list, sizes, totals.
Because this sink works off the sftp_audit table rather than in-memory state, it also catches up: events that occurred while the server was down are evaluated against the rules at startup. Delivery state is tracked per event and rule in sftp_notification_log, so restarts don't double-send. The full rule syntax is in the Notifications chapter of the User's Guide.
The config that turns all of it on
Everything above is one block in sftp-server.xml:
<audit>
<log enabled="true"/>
<jdbc enabled="true">
<queue-capacity>10000</queue-capacity>
</jdbc>
<syslog enabled="true">
<host>siem.internal</host>
<port>514</port>
<facility>LOCAL0</facility>
</syslog>
<webhooks>
<webhook enabled="true">
<url>https://hooks.example.com/sss-audit</url>
<events>UPLOAD_COMPLETE,AUTH_FAILURE</events>
<format>json</format>
<secret>change-me</secret>
</webhook>
</webhooks>
</audit>
<notifications>
<rules>
<rule event="AUTH_FAILURE" to="security@example.com"
threshold="5" window="PT5M"/>
<rule event="UPLOAD_COMPLETE" to="ops@example.com"
batch-window="PT1H"/>
</rules>
</notifications>
And it hot-reloads: edit the block, and the syslog, webhook, and notification sinks are swapped in place without dropping a session or restarting the server. The database sink keeps its connection pool across the swap.
Why parallel, not either/or
Each sink serves a different consumer with a different failure mode. The database is queryable history — but nobody watches a table. Syslog feeds the SOC — but UDP is fire-and-forget. The webhook drives automation — but the receiving endpoint is someone else's uptime problem. Email reaches a human — but only for the events a rule says matter. Running them in parallel means each audience gets the feed shaped for it, and losing one channel loses one audience, not the audit trail. The event fields, types, and sink reference are in the Audit chapter of the User's Guide, and the broader security stack is on the features page.