#Three places a detection can go
A detection can leave the machine by three routes. They serve different purposes, so you set them up separately.
| Route | What it is for | Settings |
|---|---|---|
| Emergency notifications (Telegram, LINE, Webhook) | Tell a person at once, on a phone or in chat | webhook_url and others. Covered in Installing Linux Server and first settings |
| Event forwarding (SIEM, syslog, Webhook) | Keep a machine-readable copy in a SIEM or log platform | Keys that start with forward_ |
| Notification history | Look back, on this machine, at the alerts it raised | Nothing to set |
This lesson covers event forwarding and the notification history. Event forwarding is opt-in and off by default. The note in the settings file says nothing is sent unless forward_enabled=true and at least one destination is set.
sudo roamswitch forward statusEvent forwarding (SIEM/syslog/webhook) - opt-in, default OFF
enabled: no
the daemon is not forwarding (disabled or not configured)#Set three destinations and send a test event
There are three kinds of destination: syslog, a JSON Lines file, and a webhook. For practice, you run receivers on the same machine and try all three at once.
This edits the settings file and restarts the service (it changes the machine). Try it in a practice environment. The steps to undo it are in a later section.
Start two receivers. The webhook one listens on port 8099 (TCP) and records the signature header and the body. The syslog one listens on port 5514 (UDP).
cat > /tmp/recv.py <<'PY'
import http.server
class H(http.server.BaseHTTPRequestHandler):
def do_POST(self):
n = int(self.headers.get('content-length', 0))
body = self.rfile.read(n)
sig = self.headers.get('X-RoamSwitch-Signature', '')
with open('/tmp/fwd.log', 'a') as f:
f.write(sig + '\n' + body.decode() + '\n')
open('/tmp/fwd.body', 'wb').write(body)
open('/tmp/fwd.sig', 'w').write(sig)
self.send_response(200); self.end_headers()
def log_message(self, *a): pass
http.server.HTTPServer(('127.0.0.1', 8099), H).serve_forever()
PY
cat > /tmp/syslog.py <<'PY'
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('127.0.0.1', 5514))
while True:
data, _ = s.recvfrom(65535)
with open('/tmp/syslog.log', 'a') as f:
f.write(data.decode(errors='replace') + '\n')
PY
nohup python3 /tmp/recv.py >/dev/null 2>&1 &
nohup python3 /tmp/syslog.py >/dev/null 2>&1 &Put the secret that signs the webhook body in a file only root can read. labsecret123 is a practice dummy. The receivers are on the same machine (127.0.0.1). Sending to internal addresses is refused by default, so allow it by writing 127.0.0.1/32 in forward_webhook_allow_addresses. This works the same on a server updated from an earlier version (the migration of the old default false is in the decision section). Alert text follows language, so set it to en. Then set the destinations, restart, and send a test event.
sudo sh -c 'umask 077; echo labsecret123 > /etc/roamswitch/fwd.secret'
sudo roamswitch server config set language en
sudo roamswitch server config set forward_webhook_url http://127.0.0.1:8099/events
sudo roamswitch server config set forward_webhook_secret_file /etc/roamswitch/fwd.secret
sudo roamswitch server config set forward_syslog_url udp://127.0.0.1:5514
sudo roamswitch server config set forward_file_path /var/log/roamswitch/events.jsonl
sudo roamswitch server config set forward_webhook_allow_addresses 127.0.0.1/32
sudo roamswitch server config set forward_enabled true
sudo roamswitch server restart
sleep 5
sudo roamswitch forward status
sudo roamswitch forward testEvent forwarding (SIEM/syslog/webhook) - opt-in, default OFF
enabled: yes
running: submitted=0 dropped(oldest)=0 retry queue=0 items / 0 bytes
- syslog_udp: ok=0 failed=0 last error=-
- file: ok=0 failed=0 last error=-
- webhook: ok=0 failed=0 last error=-
OK syslog_udp
OK file
OK webhookThe forward_ keys take effect only after a restart. When forward status lists all three destinations, the daemon has loaded them. The test event reports OK or FAIL for each destination.
Right after a restart, forward status can say the daemon has not started forwarding yet. In this check it said "running" right after the restart both times. The sleep 5 is there to be safe.
Look at what reached the three places.
cat /tmp/syslog.log
sudo cat /var/log/roamswitch/events.jsonl
cat /tmp/fwd.log<38>1 2026-11-12T01:54:30.070Z ubuntu roamswitch 1442 forward_test [roamswitch@32473 event_id="1a1cd76b036-5a2-0" rule="FWD-TEST" action="none" severity="info"] RoamSwitch event-forwarding test message
{"host":"ubuntu","event_id":"1a1cd76b036-5a2-0","ts":"2026-11-12T01:54:30.070Z","source":"forward_test","severity":"info","rule":"FWD-TEST","action":"none","pid":null,"proc":null,"summary":"RoamSwitch event-forwarding test message"}
sha256=845116342ad6a9d9e925a66a0f6dd79bea84c435d78a6e52f13c7abfeaa8f7e1
{"host":"ubuntu","event_id":"1a1cd76b036-5a2-0","ts":"2026-11-12T01:54:30.070Z","source":"forward_test","severity":"info","rule":"FWD-TEST","action":"none","pid":null,"proc":null,"summary":"RoamSwitch event-forwarding test message"}One event with the same event_id reached all three. Read it like this.
- syslog is RFC 5424. The leading
<38>is facility 4 (auth) × 8 + severity 6 (info) - The file and the webhook carry the same JSON: host, ID, time, source, severity, rule, action, PID, process name and summary
tsis UTC (the trailingZ).01:54:30Zis 10:54:30 on November 12 in Japan time- The file
/var/log/roamswitch/events.jsonlis-rw-------, readable by root only. To let a SIEM agent that is not root read it, name a group inforward_file_group
The webhook receiver can use the signature to check that a body came from RoamSwitch. The X-RoamSwitch-Signature header is sha256= followed by the HMAC-SHA256 of the body.
cat > /tmp/verify.py <<'PY'
import hmac, hashlib
key = open('/etc/roamswitch/fwd.secret', 'rb').read().strip()
body = open('/tmp/fwd.body', 'rb').read()
got = open('/tmp/fwd.sig').read().strip()
want = 'sha256=' + hmac.new(key, body, hashlib.sha256).hexdigest()
print('match' if hmac.compare_digest(got, want) else 'MISMATCH')
PY
sudo python3 /tmp/verify.py
python3 -c "import hmac,hashlib;print(hmac.compare_digest(open('/tmp/fwd.sig').read().strip(),'sha256='+hmac.new(b'wrongsecret',open('/tmp/fwd.body','rb').read(),hashlib.sha256).hexdigest()))"match
FalseThe right secret matches and a wrong one does not. On the receiving side, compute over the exact bytes of the body you received. If you parse the JSON and write it out again, spacing or order can change and the check fails. Compare with a constant-time function such as hmac.compare_digest.
- Prefer
forward_webhook_secret_file(a 0600 file) or theROAMSWITCH_FORWARD_SECRETenvironment variable over writingforward_webhook_secretitself (a note in the settings file) - The practice used
http://. On a real machine, usehttps://. The body carries process names and summaries
#Forward a real detection
A test event that arrives does not show that a real detection will. Cause a real detection with a harmless change.
Add one comment line to /root/.ssh/authorized_keys (it changes the machine). A comment line does not work as a login key. You put the file back within this lesson.
: > /tmp/syslog.log
sudo sh -c 'echo "# lab comment" >> /root/.ssh/authorized_keys'
sleep 10
cat /tmp/syslog.log<36>1 2026-11-12T01:54:31.685Z ubuntu roamswitch 1209 timeline.fim [roamswitch@32473 event_id="18e72960a9fffc06-1209" rule="" action="notified" severity="medium" pid="1461" proc="dash"] /root/.ssh/authorized_keys: modified (no SSH login found: service tart-guest-agent.service)
<37>1 2026-11-12T01:54:31.731Z ubuntu roamswitch 1209 notification [roamswitch@32473 event_id="1a1cd76b6b3-4b9-1" rule="" action="notify" severity="low"] Critical Path FIM Tampering Detected (1 violation): Host: ubuntu (IP: 192.168.64.24) / Integrity violations in critical system files: / /root/.ssh/authorized_keys: modified / Process: root / /usr/bin/dash / command: sh -c echo "# lab comment" >> /root/.ssh/authorized_keys (pid 1461) (The process had already exited, so this comes from the exec record made at 2026-11-12 01:54:31.) / No SSH login was found (it is a process of the systemd service tart-guest-agent.service) / Recommended Action: Run `sudo roamswitch fim verify` to inspect changes. / Timestamp: 2026-11-12 01:54:31One change produced two events.
timeline.fim: the detection itself. Its severity ismedium, and it names the writing process (dash, PID 1461). The summary is a fixed English textnotification: a copy of the alert that was raised. Alerts carry no severity, so they are always sent aslow(a note in the code). The summary is the whole alert text, with/between its lines, in the language of the server'slanguagesetting
Put the file back. This writes the old contents into the same file instead of replacing it. The contents match the baseline again, so no new detection comes out. Adding the same line again after that brought a new detection and alert copy, with no restart.
sudo sh -c 'grep -v "^# lab comment$" /root/.ssh/authorized_keys > /tmp/ak; cat /tmp/ak > /root/.ssh/authorized_keys'Next, raise the lowest severity that is sent, forward_min_severity (default low), to medium, and make the same change.
sudo roamswitch server config set forward_min_severity medium
sudo roamswitch server restart
sleep 5
: > /tmp/syslog.log
sudo sh -c 'echo "# lab comment" >> /root/.ssh/authorized_keys'
sleep 10
cat /tmp/syslog.log
sudo roamswitch forward status<36>1 2026-11-12T01:55:09.267Z ubuntu roamswitch 1634 timeline.fim [roamswitch@32473 event_id="18e7296969615736-1634" rule="" action="notified" severity="medium" pid="1860" proc="dash"] /root/.ssh/authorized_keys: modified (login path not determined: proc)
Event forwarding (SIEM/syslog/webhook) - opt-in, default OFF
enabled: yes
running: submitted=1 dropped(oldest)=0 retry queue=0 items / 0 bytes
- syslog_udp: ok=1 failed=0 last error=-
- file: ok=1 failed=0 last error=-
- webhook: ok=1 failed=0 last error=-Only the timeline.fim event arrived. The low copy of the alert was not sent, and submitted is 1: events below the floor are not counted. The alert that was not forwarded still went into the notification history. When you are done, put the file back with the same command as above.
There are five severities. They map to syslog and CEF values like this (the mapping table in the code).
| Severity | syslog | CEF |
|---|---|---|
critical | 2 (crit) | 10 |
high | 3 (err) | 8 |
medium | 4 (warning) | 5 |
low | 5 (notice) | 3 |
info | 6 (info) | 1 |
- The default,
low: both detections and alert copies arrive. The SIEM can also track whether an alert was raised mediumor higher: alert copies do not arrive. Use it to cut what the SIEM ingests- The test event from
forward testisinfo, yet it arrived with the floor atlow. The test is there to check that a destination can be reached
#When a destination is down, and the retry queue
A destination that is down does not easily make you lose events. What cannot be sent to a TCP, TLS or webhook destination goes to a retry queue on disk.
Put the floor back to low, then stop the webhook receiver. Find the listener's PID with ss and stop it.
sudo roamswitch server config set forward_min_severity low
sudo roamswitch server restart
sleep 5
P=$(sudo ss -ltnpH 'sport = :8099' | sed -n 's/.*pid=\([0-9]*\).*/\1/p'); kill $P
sudo roamswitch forward test; echo "exit=$?"
sudo roamswitch forward queue OK syslog_udp
OK file
FAIL webhook: HTTP 0
exit=2
retry queue: 0 (0 bytes) /var/lib/roamswitch/forward_queue.jsonlHTTP 0 means no response came back. When any destination fails, forward test exits with code 2 (with no destination at all, 1). The test event does not go into the retry queue. The queue is for real events, so cause a real detection.
sudo sh -c 'echo "# lab comment" >> /root/.ssh/authorized_keys'
sleep 10
sudo roamswitch forward status
sudo roamswitch forward queueEvent forwarding (SIEM/syslog/webhook) - opt-in, default OFF
enabled: yes
running: submitted=2 dropped(oldest)=0 retry queue=2 items / 1040 bytes
- syslog_udp: ok=2 failed=0 last error=-
- file: ok=2 failed=0 last error=-
- webhook: ok=0 failed=4 last error=HTTP 0
retry queue: 2 (1040 bytes) /var/lib/roamswitch/forward_queue.jsonl
2026-11-12T01:55:31.151Z webhook /root/.ssh/authorized_keys: modified (no SSH login found: service tart-guest-agent.service)
2026-11-12T01:55:31.209Z webhook Critical Path FIM Tampering Detected (1 violation): Host: ubuntu (IP: 192.168.64.24) / Integrity violations in critical system files: / /root/.ssh/authorized_keys: modified / Process: root / /usr/bin/dash / command: sh -c echo "# lab comment" >> /root/.ssh/authorized_keys (pid 2198) / No SSH login was found (it is a process of the systemd service tart-guest-agent.service) / Recommended Action: Run `sudo roamswitch fim verify` to inspect changes. / Timestamp: 2026-11-12 01:55:31syslog and the file got both events. Only the two webhook events stayed in the queue. Start the receiver again.
: > /tmp/fwd.log
nohup python3 /tmp/recv.py >/dev/null 2>&1 &
sleep 20
sudo roamswitch forward queue
sudo roamswitch forward statusretry queue: 0 (0 bytes) /var/lib/roamswitch/forward_queue.jsonl
Event forwarding (SIEM/syslog/webhook) - opt-in, default OFF
enabled: yes
running: submitted=2 dropped(oldest)=0 retry queue=0 items / 0 bytes
- syslog_udp: ok=2 failed=0 last error=-
- file: ok=2 failed=0 last error=-
- webhook: ok=2 failed=4 last error=-Within 20 seconds the queue was empty. /tmp/fwd.log holds the two events from while it was down, with their original ts. Once the retry succeeded, last error went back to -. The failed count keeps the failures from while it was down. When you are done, put the file back.
The queue has limits. The note in the settings file says that anything over forward_queue_max_mb (default 20 MiB) or older than forward_queue_max_age_hours (default 72 hours) is not kept. If a destination stays down for more than three days, recover those events from the file destination.
forward_webhook_block_private_addresses is true by default. Sending to loopback, private, link-local (cloud metadata) and similar addresses is refused, for both the webhook and syslog. It stops a wrong setting from sending events to an internal service. In this course the receivers are on the same machine, so 127.0.0.1/32 went into forward_webhook_allow_addresses at the start.
If you send to an internal destination, such as your own SIEM or syslog server, list only its CIDR in forward_webhook_allow_addresses. An internal address that is not on the allow list gets nothing, and forward status and forward test give the reason for each destination. The reason is in the screen language. The other destinations still get the event.
With false, sending to internal addresses is not refused either. When you can add the destination to the allow list, keep it true.
On a server updated from an earlier version, a forward_webhook_block_private_addresses=false left in server.conf as the old default is migrated once at the first start after the update. The internal addresses the current destinations use are added to forward_webhook_allow_addresses, the switch becomes true, and this is reported once in the notification history. When a destination name cannot be looked up, the switch stays false, one notice goes out, and the next start tries again. Destinations written as addresses are migrated even while forwarding is off. A false after the migration, and a switch set with server config set, stay as they are. A host that uses server.json is not migrated.
- Reading
forward statuswithoutsudo: an ordinary user cannot read the settings file (-rw-------). No state is shown. It prints "Cannot read /etc/roamswitch/server.conf (only root can), so the forwarding settings and state cannot be checked. Run sudo roamswitch forward status". It exits with code 1, andforward testprints the same and exits with code 1 too. A script that checks the exit code sees a failure. Read the state withsudo - Putting the file destination in
/tmpor similar: the service runs withProtectSystem=strict, so it can write in few places. Use a path under/var/log/roamswitch/or/var/lib/roamswitch/
sudo roamswitch server config set forward_file_path /tmp/events.jsonl
sudo roamswitch server restart
sleep 5
sudo roamswitch forward status ! forward_file_path /tmp/events.jsonl is not used: the service's sandbox (ProtectSystem=strict) keeps that location read-only. Put it under /var/log/roamswitch/ or /var/lib/roamswitch/, or add ReadWritePaths=<directory> in a drop-in (systemctl edit roamswitch-server)
running: submitted=0 dropped(oldest)=0 retry queue=0 items / 0 bytes
- syslog_udp: ok=0 failed=0 last error=-
- webhook: ok=0 failed=0 last error=-file is gone from the list of destinations. forward test then printed a FAIL file: line with the same reason. After you change a setting, also read the ! lines of forward status. When you are done, set forward_file_path back to /var/log/roamswitch/events.jsonl and restart.
#Read the notification history
Notification history is the record of the alerts RoamSwitch raised. roamswitch notifications shows the past week. There is nothing to set.
sudo roamswitch notifications=====================================================
🔔 Notification History (past 7 days)
=====================================================
2026-11-12T01:55:31.209488464+00:00
Critical Path FIM Tampering Detected (1 violation)
Host: ubuntu (IP: 192.168.64.24)
Integrity violations in critical system files:
/root/.ssh/authorized_keys: modified
Process: root / /usr/bin/dash / command: sh -c echo "# lab comment" >> /root/.ssh/authorized_keys (pid 2198)
No SSH login was found (it is a process of the systemd service tart-guest-agent.service)
Recommended Action: Run `sudo roamswitch fim verify` to inspect changes.
Timestamp: 2026-11-12 01:55:31
Total: 4 notifications
This is the record of the notifications RoamSwitch raised. It does not show whether they arrived (they are recorded even when no channel is set up). To check delivery: sudo roamswitch server test-notifyRead it like this.
- The leading time is UTC (
+00:00). In Japan time, add nine hours: 10:55 on November 12 - It names the writing process, the command, and whether an SSH login was involved. When the process had already exited, a note says the details come from the exec record
- As the last two lines say, it does not show whether an alert arrived. Alerts are recorded even with no channel set up. Check delivery with
test-notify
The text is shown in the reader's language. In this check, the alerts on this machine were recorded with language set to en, and reading them with LANG=ja_JP.UTF-8 showed them in Japanese. Addresses, paths and command lines stay as recorded. The forwarded copies and the text sent to alert channels use the language in effect when they were sent.
To hand the history to another tool, take it out as JSON or JSON Lines.
sudo roamswitch notifications --json | python3 -c 'import json,sys; print(sorted(json.load(sys.stdin)[0]))'
sudo roamswitch notifications --export /tmp/notif.jsonl
ls -l /tmp/notif.jsonl['body', 'text', 'timestamp', 'title']
Exported 4 records to /tmp/notif.jsonl as JSON Lines.
-rw------- 1 admin admin 6542 Nov 12 01:56 /tmp/notif.jsonlEach record has four fields: timestamp, title, body and text. title and body are text in the reader's language, and text is a language-neutral form (message keys and values). There is no severity field. The exported file is -rw------- and belongs to the user who ran sudo (admin here). The body includes command lines, so check the contents before you hand the file to anyone.
#Undo the changes
Undo what this lesson changed (it changes the machine). This assumes you put authorized_keys back after each lab.
sudo roamswitch server config set forward_enabled false
for k in forward_webhook_url forward_webhook_secret_file forward_syslog_url forward_file_path forward_webhook_allow_addresses; do
sudo roamswitch server config set $k ""
done
sudo roamswitch server config set forward_min_severity low
sudo rm -f /etc/roamswitch/fwd.secret
sudo roamswitch server restart
sleep 5
sudo grep -E '^forward_(enabled|webhook_url|syslog_url|file_path)=' /etc/roamswitch/server.conf
sudo roamswitch fim verify
sudo roamswitch forward statusforward_enabled=false
forward_syslog_url=
forward_file_path=
forward_webhook_url=
✓ No tampering detected. All files verified healthy. (91 files monitored)
✓ No SUID / SGID file was added or changed. (20 files checked)
Event forwarding (SIEM/syslog/webhook) - opt-in, default OFF
enabled: no
the daemon is not forwarding (disabled or not configured)After forwarding is turned off and the service restarts, forward status says the daemon is not forwarding again. Stop the two receivers by finding their PIDs with ss (port 8099 is TCP, port 5514 is UDP).
#What it cannot see
In the range checked on a real machine (a disposable Ubuntu 24.04 VM):
- A test event that arrives does not prove that real events will. This lesson checked by causing a FIM detection. Check the destination's ingest rules with real events too
- UDP syslog is not retried. In the code, only TCP, TLS and webhook destinations use the retry queue. If you cannot afford to lose events, use
tcp://ortls:// - syslog can also be sent as CEF (
forward_syslog_format=cef). In this check, lines starting withCEF:0|Lafine|RoamSwitch|…arrived.server config set forward_syslog_format ""sets it back to the default,rfc5424. The file switches to a new one when it reachesforward_file_max_mb(default 50 MiB), and keeps up toforward_file_keep(default 5) old files (checked in the code)
forward test printed OK for all three destinations. What does that tell you
Show the explanation
The test event only checks that each destination can be reached. Its severity is
info, it is sent even though that is below thelowfloor, and it never enters the retry queue. Real events are filtered by severity and queued when a send fails. To see how they arrive, cause a real detection with a harmless change.
#What to learn next
- Practice the decisions after an alert in A containment fired: the first hour
- Telling a false alarm from a real one is covered in "An alert fired: false alarm or real?" (coming soon)
- Every setting is listed in the Linux Server operations manual
Verified on 2026-11-12. Target: Linux 1.17.39. On disposable Ubuntu 24.04 VMs with 1.17.39 from the public apt repository, with a webhook and a UDP syslog receiver on the same machine, every lab (forward test and its exit codes, status without sudo, a real FIM detection, forward_min_severity, the retry queue, destination limits, CEF and resetting it, reading and exporting the history) was run on one VM in Japanese and on another with language=en. Added 2026-11-12: that refusing internal addresses is now on by default and also covers syslog was checked in webhook_guard.rs, event_forward.rs and server_config.rs on roamswitch-linux main (885c3cee); the lab now sets forward_webhook_allow_addresses 127.0.0.1/32, and the decision section and the clean-up were updated. This addition was not run on a VM. Second addition the same day: the migration of the old default false was checked in egress_guard_migration.rs on roamswitch-linux main (5073d7a3, fb00c636) and added to the decision section. This addition was not run on a VM either.
Command output varies with the product version and the environment. If yours differs, check the version first.