RoamSwitch Server Edition Operations Manual
Official installation, deployment, and operational reference for Cloud VPS (AWS, GCP, Azure, Linode, DigitalOcean), bare-metal data centers, and container hosts.
1. Overview & Requirements
RoamSwitch Server Edition is a headless (zero GUI dependencies) autonomous defense and integrity monitoring suite designed for Linux servers exposed to the public Internet.
Unlike the client edition which dynamically switches profiles based on Wi-Fi state, the server edition enforces Inbound Default Drop, critical path File Integrity Monitoring (FIM), preventive kernel LPE mitigation (Frag Gap), eBPF runtime threat detection, SSH lockout prevention, and automated multi-channel emergency alerts (Telegram / LINE / Webhook).
cve_kernel_map_updates_enabled, on by default). That data is the kernel CVE data, the web rules and, on Ubuntu servers, the Ubuntu package CVE data used by the OS-package CVE check. roamswitch-updater receives Ed25519-signed data from lafine.net with an HTTPS GET (the signed manifest is checked daily, the CVE data itself about every 30 days, the web rules only when the manifest has a newer version), with no query string and no cookies, a fixed User-Agent: RoamSwitch-Updater as the only header that names the software, and nothing about this host is sent. It can be turned off with roamswitch server config set cve_kernel_map_updates_enabled false (or the setup wizard or the server TUI), and the audit still works from the built-in baseline when it is off, although the OS-package CVE check on an Ubuntu server then reports no local CVE data. All other traffic that leaves the host is limited to the following, which the operator has explicitly enabled: notifications (Telegram / LINE / webhook), npm signature verification (npm_audit_signatures_enabled, only when the command is run) and event forwarding (forward_enabled, off by default; §16). Event forwarding goes only to destinations you name yourself (a SIEM, syslog, a webhook); TLS syslog is sent through the system openssl and the structured webhook through a curl subprocess. In addition, Sensor pairing (sensor_pairing_enabled, off by default) and the LLM integration of the bundled separate tool accessscope (off by default; on the command line, only when --llm is given) connect externally only when the operator enables them. The list of exceptions is in the Zero Telemetry section of the Server Edition Security Whitepaper.System Requirements
- Supported Distributions: Ubuntu 22.04 / 24.04 LTS, Debian 12+, AlmaLinux / Rocky Linux / RHEL 9+, Fedora 39+, openSUSE Leap 15.5+, Raspberry Pi OS (64-bit)
- Architectures:
x86_64(amd64) oraarch64(arm64) - Kernel: Linux 5.10+ (
nftables,cgroups v2, eBPF BTF recommended) - Footprint: 20–30 MB resident memory, <0.1% CPU during idle/normal workloads
2. Installation Procedures
Using the official signed repositories ensures automatic dependency resolution and seamless updates via your standard system package manager.
2.1 APT (Ubuntu / Debian / Raspberry Pi OS)
# 1. Register the repository signing key
curl -fsSL https://lafine.net/apt/roamswitch-archive-keyring.asc \
| sudo gpg --dearmor -o /usr/share/keyrings/roamswitch-archive-keyring.gpg
# 2. Add the repository
echo "deb [arch=amd64,arm64 signed-by=/usr/share/keyrings/roamswitch-archive-keyring.gpg] https://lafine.net/apt stable main" \
| sudo tee /etc/apt/sources.list.d/roamswitch.list
# 3. Install
sudo apt update && sudo apt install roamswitch-server
2.2 DNF / RPM (Fedora / RHEL / AlmaLinux / Rocky Linux)
# 1. Import the GPG key
sudo rpm --import https://lafine.net/rpm/RPM-GPG-KEY-roamswitch
# 2. Add the repository config file
sudo curl -fsSL -o /etc/yum.repos.d/roamswitch.repo https://lafine.net/rpm/fedora/roamswitch.repo
# 3. Install
sudo dnf install roamswitch-server
2.3 openSUSE (zypper)
sudo rpm --import https://lafine.net/rpm/RPM-GPG-KEY-roamswitch
sudo zypper addrepo https://lafine.net/rpm/opensuse/roamswitch.repo
sudo zypper refresh && sudo zypper install roamswitch-server
roamswitch) and Server Edition (roamswitch-server) declare reciprocal package conflicts. Always select roamswitch-server on headless server environments.3. Initial Setup & Service Verification
3.1 Service Status
Following installation, roamswitch-server.service starts immediately and is enabled at system boot.
sudo systemctl status roamswitch-server.service
3.2 Interactive Setup Wizard
Run the interactive setup wizard to configure ports, maintenance source IPs, notification channels and the containment action (press Enter to accept each default).
sudo roamswitch server setup
- Ports: enter the SSH maintenance ports (default
22) and the public service ports (default80,443), each comma-separated. - Maintenance source IPs: SSH bastion / monitoring IPs or CIDRs (e.g.
203.0.113.50/32). The wizard requires an answer and does not accept a blank. To allow SSH from any source, typeANYWHERE(the admin SSH path then stays open to the whole internet during isolation as well). - Notification channels: enable Telegram bot, LINE Messaging API and/or a generic webhook (Slack / Discord / Teams).
- Response to a critical eBPF event: choose
isolate(isolate host),freeze(freeze the process) oralert_only(notify only). After saving, you are prompted to send a test notification and restart the service. - Automatic CVE data updates (default
y): choose whether the kernel CVE database used by the container-isolation audit, the web rules and, on Ubuntu servers, the Ubuntu package CVE data used by the OS-package CVE check are fetched anonymously from lafine.net (the signed manifest is checked daily, the CVE data itself about every 30 days and the web rules only when the manifest has a newer version; HTTPS GET only, no query string or cookies, a fixedUser-Agent: RoamSwitch-Updateras the only header that names the software, Ed25519 signature verified). Nothing about this host is sent. It is the only external communication that is on by default in the Server Edition, and answeringnturns it off (the audit itself still works from the built-in baseline, but the OS-package CVE check on an Ubuntu server then reports no local CVE data). If it is already disabled in an existingserver.conf, the default answer staysn. - File Scan Guard settings: choose whether to enable virus scanning for mail relays, shared folders and upload destinations (§13), and set the target directories. Optional, off by default.
- Resource Guard settings: enable the resource-exhaustion guard that detects sustained RSS growth (memory leaks/DoS) or crash loops in network-exposed services.
- Overview of always-on core protections: displays the core defenses that run at all times regardless of the wizard — inbound default-drop, FIM, eBPF integration, and so on (no input required).
- Investigation-agent hand-off (investigation) settings: choose whether to automatically delegate first-pass investigation — for critical eBPF events, or Critical-Path FIM / dependency-lockfile tampering detections, whenever they were judged notify-only — to an external AI agent CLI (Claude Code / agy / Codex CLI / OpenCode, or a custom command) (§10.1). Off by default.
-100 (e.g. -1001234567890). Confirm the correct value from the "chat":{"id":...} field in curl -s "https://api.telegram.org/bot<token>/getUpdates".3.3 34-Item Server Security Audit
sudo roamswitch status --server
Performs an instantaneous 34-check audit of firewall rules, Frag Gap mitigation, Yama LSM restrictions, permissions, and Docker port exposure, producing a 0–100 security score and grade (S to C).
Each check item is now also tagged, for compliance-reporting purposes, with a NIST CSF 2.0 function/category (always present when applicable) and, only where a confident mapping exists, a CIS Controls v8 control number.
4. Firewall & Port Rules
/etc/roamswitch/server.conf is forced to 0600 permissions (root read/write only). The format is flat key=value; section headers such as [network] are ignored.
# --- Firewall ---
ssh_ports=22 # SSH management port (kept even during emergency isolation)
allowed_ports=80,443 # Public service ports
whitelist_ips=203.0.113.10/32 # Maintenance source IP / CIDR (blank = allow all, formerly admin_source_ips)
protect_docker_ports=true # DOCKER-USER protection (blocks containers from bypassing the firewall)
preserve_ssh_on_isolation=true # Preserve the SSH management path even during emergency isolation (false = full cutoff)
# --- eBPF runtime guard ---
action_on_critical=isolate # isolate | freeze (SIGSTOP + network cutoff) | alert_only
ebpf_socket_path=/run/roamswitch/events.sock
# --- Egress / C2 blocking & DNS sinkhole ---
egress_ip_blocklist_enabled=true # Default true. The seed bundled with the package lists only the three attack servers JPCERT/CC published; nothing else is blocked until a feed is installed
metadata_watch_enabled=true # Default true. Notify when a non-root program or container connects to cloud metadata (log-only, never blocks; detects SSRF)
metadata_watch_allow_users= # Accounts that legitimately use the metadata service (names or numeric ids, comma-separated; root is always exempt)
dns_sinkhole_enabled=false # Default false (to avoid breaking internal DNS / split-horizon setups)
dns_sinkhole_provider=quad9 # quad9 | cloudflare | adguard | cleanbrowsing
# --- Container-isolation CVE data (enabled by default) ---
cve_kernel_map_updates_enabled=true # Default true. The only external communication on by default (signed kernel CVE data, web rules and, on Ubuntu servers, Ubuntu package CVE data; manifest daily, CVE data about every 30 days; anonymous, receive-only, signature-verified). Set false to stop it
# --- Notifications ---
telegram_enabled=false
telegram_bot_token=
telegram_chat_id=
line_enabled=false
webhook_enabled=false
webhook_url=
webhook_block_private_addresses=false # opt-in SSRF guard
webhook_allow_addresses=
# --- System ---
language=ja
fim_check_interval_secs=300 # Formerly named scan_interval
isolation_hard_cap_secs=3600 # Seconds before a high-confidence isolation moves to DEGRADED mode (0 = hold until released)
After editing the file directly, restart the service to apply the changes (roamswitch-server has no reload):
sudo roamswitch server restart
4.1 Per-Severity Policy (guard.yaml, optional)
If action_on_critical alone isn't granular enough, create /etc/roamswitch/guard.yaml (a template ships at /etc/roamswitch/guard.yaml.example). When absent, an equivalent policy is synthesized automatically from server.conf's action_on_critical, so existing deployments are unaffected.
policies:
on_warning:
action: log_and_notify
on_critical:
server_action: isolate_process
kill_process: true
fallback_to_host_isolation_after_retries: 3 # Auto-escalates to host isolation if the same process recurs
on_emergency:
action: isolate_host_all
kill_process: true
server_safeguards:
maintenance_ssh_ports: [22]
safety_timer_secs: 300 # Seconds until a low-confidence isolation is restored automatically (0 disables). Not applied to high-confidence isolations (see below)
Automatic release of an isolation (by confidence): releasing on a fixed timer alone lets a patient attacker simply wait it out, so the trigger of each isolation is classified by confidence. (1) Low confidence (eBPF Error / Warning and below, generic port anomalies): fully restored after a short timer (safety_timer_secs, 300 s by default). (2) High confidence (canary tampering, ransomware-style mass rewriting, eBPF Critical / Emergency / Alert, repeated escalation of the same rule): never restored silently. After the short timer the cause keeps being re-checked, and the isolation is released with a notification only when the cause is verified cleared. If that cannot be confirmed, nobody has acknowledged it and the hard cap is reached (isolation_hard_cap_secs in server.conf, 3600 s by default, 0 = no cap), it moves to DEGRADED mode. (3) DEGRADED mode keeps blocking new traffic other than the management paths (the maintenance SSH ports and the whitelisted IPs) and never opens on a timer. It is released only by a verified-cleared re-check, an explicit ack (an ack while DEGRADED means release) or sudo roamswitch emergency-restore. In every isolation, established connections other than the management paths (a C2 channel or reverse shell the attacker already has open) are cut. Each transition is recorded in the incident timeline and notified in 10 languages.
sudo roamswitch server ack
roamswitch server ack acknowledges a FULL isolation while keeping it strict (the operator has taken ownership, so no move to DEGRADED and no automatic release) and releases a DEGRADED one.
4.2 Egress / C2 Blocking and DNS Sinkhole
With egress_ip_blocklist_enabled=true (default), outbound connections to known-malicious IPs are blocked with nftables. The feed is /var/lib/roamswitch/threatfeed/malicious_ips.txt (managed by the updater) when it exists, otherwise the seed bundled with the package (/usr/share/roamswitch/threatfeed/); one is used instead of the other, they are not merged. The seed lists only the three attack-server addresses JPCERT/CC published in its 2026-10-15 report on attacks against web services and APIs (210.149.87.120, 118.26.36.16, 69.10.51.162). Addresses of rented servers can be reassigned to someone else, so if a legitimate destination stops working, check the matching line in that file.
Setting dns_sinkhole_enabled=true forces resolution through a filtering DNS resolver (Quad9 / Cloudflare / AdGuard / CleanBrowsing), blocking known-malicious domain resolution. It defaults to false since it can break internal DNS or split-horizon setups.
With metadata_watch_enabled on (the default), the nftables table inet roamswitch_metadata_watch writes a kernel-log line only when a non-root program or a container newly connects to a cloud metadata address (169.254.169.254, 169.254.170.2, 100.100.100.200, fd00:ec2::254), and the server daemon notifies once a day per account (per source address for a container). It only logs and notifies; it never blocks. When an attacker hands the app a URL on their own server that redirects (302) to an internal address, the access log shows only the external URL, so this outbound traffic is the only place the second hop is visible. An application that legitimately uses its cloud role is reported too, so list its account in metadata_watch_allow_users to exempt it (a container cannot be exempted).
4.3 Incoming-Port-Scan-Detection Guard
Detects source IPs that connected to many distinct ports in a short time, by correlating nftables log rules with journald (enabled by default). High-confidence scan sources can be auto-blocked for a set duration. When used alongside RoamSwitch Sensor (a different feature from §10.1 — full-port audit probes from a paired Sensor are excluded as a false-positive mitigation), the Sensor's own IP is automatically excluded.
# --- Incoming port-scan detection (enabled by default) ---
port_scan_detect_enabled=true # false to disable
port_scan_detect_min_distinct_ports=15 # Judged a scan when connections reach this many distinct ports
port_scan_detect_window_secs=300 # Aggregation window for detection (seconds)
port_scan_block_enabled=true # Auto-blocks the detected scan source IP
port_scan_block_duration_secs=600 # Duration of the automatic block (seconds)
You can review detection history with the roamswitch port-scan command.
4.4 DNS Tunnel Detection Guard
Inspects only this host's own outbound DNS traffic (UDP/TCP port 53) via a dedicated NFQUEUE consumer. A query counts as a suspicious shape when its label length exceeds 40 bytes AND its Shannon entropy exceeds 3.8, or when its qtype is TXT/NULL, and an alert fires once 10 or more such queries occur within the last 60 seconds (a 300-second cooldown prevents repeat-alert spam for the same condition). It targets the query patterns characteristic of DNS-tunneling/exfiltration tools such as iodine and dnscat2. The underlying nftables rule carries the same bypass flag as this daemon's other DNS-related guards, so DNS name resolution is never disrupted even if this guard is stopped or unresponsive. This is a notify-only feature — it never blocks traffic automatically. As with every other setting, a change to dns_tunnel_detect_enabled takes effect only after a service restart.
# --- DNS tunnel detection (enabled by default) ---
dns_tunnel_detect_enabled=true # false to disable
4.5 SSH brute-force guard
The guard reads sshd's failed logins the moment they are written and counts them per source address (on by default). A source that reaches the configured number of failures (by default 6 within 600 seconds) loses new connections only to the SSH ports, through the nftables table inet roamswitch_sshguard. The first ban lasts 1 hour, grows fourfold each time the same source comes back, and is capped at 7 days. At the moment of the ban it also cuts the connections that source already had open to SSH (by deleting their conntrack entries and adding a rule that drops the reply direction). Only a source with no authenticated session, no public-key login on record and no exemption is banned, and this is checked once more right before cutting, so an authenticated session and an exempt source are never cut. The guard sends nothing out; it only runs journalctl, nft, loginctl and similar commands locally.
The log is followed with journalctl -f. journald records which process wrote each line, so lines written by anything other than sshd are ignored. Where journald is not available, the guard follows /var/log/auth.log or /var/log/secure, but any local user who may use syslog can forge lines in those files and get a third party banned; where journald is available, leave ssh_guard_log_source at its default auto. The failures counted are a failed password, an unknown user, a disconnect before authentication that had named a user, "maximum authentication attempts exceeded", and PAM authentication failures. A failed public key (Failed publickey) is not counted, because a legitimate client holding several keys produces one every time. The source address is read from the end of the line, from <IP> port <N>. An attacker can put any text in the user name, but a forged source address written there does not get a third party banned. IPv6 sources are counted per /64 and a ban covers the /64 (only the single source address if the /64 also holds an address that must stay reachable, such as the gateway).
Never banned are: the gateway, DNS resolvers, this server itself, loopback, whitelist_ips, ssh_guard_allow, the source of an open SSH session that has authenticated, any address that logged in with a public key within the last 30 days (ssh_guard_trust_key_days; a successful password login does not count), and a paired Sensor. When one of these sources piles up failures, the guard only records it. Private addresses are not exempt, because a compromised machine inside the network can be the origin of a brute-force run as well. A source that merely has a connection open before authentication is not exempt either.
When fail2ban (with an enabled sshd jail) or CrowdSec (with a running firewall bouncer) already guards sshd, the guard yields the banning so that the two do not ban the same address twice, and only detects and notifies (ssh_guard_yield_to_fail2ban, true by default). With ssh_guard_block_enabled=false it also only detects and notifies. It notifies when it bans (or, in detect-only mode, when the threshold is reached) and when a login succeeds from a source that had piled up failures (Critical: a brute-force run may have succeeded; the session is not cut). It does not notify for every failure, and sources beyond 10 within an hour are folded into one notification.
List the bans with roamswitch server bans (--json is available; the list is readable by root only, so use sudo) and lift one with sudo roamswitch server unban <IP | IPv6 /64 | --all>; lifting a ban also clears that source's failure count. Bans are restored with the time they have left after the server restarts. roamswitch emergency-restore does not lift SSH bans (putting the network back is no reason to let in whoever was guessing passwords); the recovery menu (roamswitch recover) and uninstalling remove the whole table. The MCP tool get_ssh_guard_incidents is read-only, and there is no tool that lifts a ban. In the Server TUI, the "SSH brute-force" section of the Incidents screen lists the bans and lifts them (when started as root; §17).
roamswitch server bans
sudo roamswitch server unban 203.0.113.9
sudo roamswitch server unban --all
There are 10 settings, all starting with ssh_guard_ (§18.1). Change one with, for example, sudo roamswitch server config set ssh_guard_max_failures 10, and apply it with sudo roamswitch server restart. Set ssh_guard_enabled=false to stop the guard altogether, or ssh_guard_block_enabled=false to make it detect and notify only. The "SSH Brute-force Protection" item of the 34-item diagnostic also passes while this guard is running and banning.
Limits: attempts that only fail public-key authentication are not counted. A brute-force run spread over many source addresses is counted per source, so it is not stopped until one source reaches the threshold. The fundamental remedy is to disable password authentication and allow public keys only.
5. SSH Lockout Prevention Failsafe
Accidental lockout via firewall misconfiguration is an intolerable operational disaster. RoamSwitch Server Edition incorporates multi-tiered safeguards:
- ESTABLISHED / RELATED Unconditional Pass: Active sessions are never dropped during rule reloads.
- SSH kept alive during Air-Gap isolation: even when an anomaly triggers an emergency isolation,
preserve_ssh_on_isolation = truekeeps new and established traffic on the management paths only — the maintenance SSH ports and the admin source IPs (whitelist_ips) — so you can investigate and recover without going on site. Every other established connection (C2, data exfiltration or a reverse shell the attacker already has open) is cut. If no admin source is specified (whitelist_ipsand the allowed addresses in guard.yaml are both empty), the maintenance SSH ports keep accepting new connections from any source during isolation, and the server logs a warning at startup. Withpreserve_ssh_on_isolation=false, isolation cuts SSH as well. Server Edition 1.10.4 and earlier had a defect. The isolation ruleset dropped the SSH replies, so the operator's SSH did not work during isolation. It was fixed in 1.10.5. - Atomic nftables Loading with Rollback: Rule syntax errors instantly trigger a zero-downtime rollback to the preceding verified ruleset.
6. Container Protection (Docker / Podman)
Docker's default behavior creates custom iptables rules that bypass standard host firewalls, exposing container ports (e.g. -p 8080:8080) directly to the Internet.
With protect_docker_ports=true (the default), RoamSwitch adapts to Docker’s firewall backend so that traffic to containers is also unreachable from outside unless it targets a port listed in ssh_ports or allowed_ports. With the iptables / iptables-nft backend it inserts an inspection rule at the top of the DOCKER-USER chain (for IPv4, and also through ip6tables when Docker IPv6 is enabled). With Docker’s native nftables backend (Docker 29 and later, where DOCKER-USER does not exist) it creates its own inet roamswitch_docker_guard table on the forward hook ahead of Docker’s chains (one inet table covers both IPv4 and IPv6). The decision uses the original pre-DNAT destination port (ctorigdstport) and applies only to traffic arriving on the external interface, so container egress and container-to-container traffic are untouched. It prevents accidental exposure of, for example, a development database container.
The guard is idempotent and self-heals every minute: if dockerd starts after RoamSwitch, if a Docker restart or reload rewrites the rules, or after a manual nft flush or the loss of the nftables table, it is re-applied, while a guard that is already current is left untouched.
A real-time guard watching docker events also detects containers started with --privileged or with /var/run/docker.sock mounted (container-escape risk) right after they start and notifies you. In addition, the container exec guard (container_exec_guard_enabled, on by default) inspects binaries executed inside containers with YARA (an evasion that deletes the file after running it is still inspected, because the content is read from the already-open file descriptor). The guard also marks containers that are already running at startup and follows new containers automatically. Both are notify-only and never block automatically. They also work with Docker 29 (since 1.11.1). When the event subscription keeps failing, the reconnect interval grows from 5 seconds up to 5 minutes.
7. Critical Path File Integrity Monitoring (FIM)
More than 150 critical files — critical binaries such as /bin/login, /usr/bin/sudo and /usr/sbin/sshd, authentication configuration such as /etc/shadow and /etc/sudoers, and systemd units — are monitored by SHA-256.
The scope is not limited to individual files. In critical directories (/etc/pam.d, /etc/sudoers.d, /etc/ssh/sshd_config.d, /etc/cron.*, /etc/systemd/system, /etc/ld.so.conf.d, /etc/profile.d and /root/.ssh (except known_hosts)) the appearance of a new file is detected as well, and the authorized_keys of every account that can log in, /etc/ld.so.preload (it normally does not exist; its appearance is a sign of a local rootkit) and RoamSwitch’s own binaries are covered too. Temporary units that snapd generates (snap-*.mount and so on) are excluded. New-file detection only works against a baseline that includes this scope, so run sudo roamswitch fim update once after upgrading. Files that fim verify cannot read when run without root are reported as “skipped, unreadable”, not as tampering.
7.1 Manual Hash Verification
sudo roamswitch fim verify
7.2 Event-Driven Detection (Instant)
Monitored files are individually watched via fanotify (FAN_CLASS_NOTIF, write-completion events), so tampering is re-hashed and detected the instant it happens rather than waiting for the next periodic scan. The periodic scan (default fim_check_interval_secs=300 seconds) remains as a backstop for any missed events. Edits that put a new file under the same name (install, sed -i and the like) are detected too, and the notification names the process that made the change and its SSH login (§7.5).
7.3 Automatic Baseline Sync on OS Updates
On Debian / Ubuntu, package installation auto-configures the DPkg::Post-Invoke hook at /etc/apt/apt.conf.d/99roamswitch-fim. On Fedora / RHEL / Rocky / AlmaLinux / openSUSE, a systemd.path unit (roamswitch-fim-update.path) watching /var/lib/rpm plays the same role.
This means that when an administrator runs sudo apt upgrade (or dnf upgrade / zypper update) and binaries are legitimately updated, RoamSwitch detects it automatically and quietly re-syncs the hash baseline (preventing false-positive alerts). To sync manually:
sudo roamswitch fim update
When the daemon starts for the first time and builds the baseline, and a package manager (apt/dpkg, dnf/rpm, zypper or pacman) is busy, it waits up to 120 seconds for it to finish before building. Even if you install it in the same apt install as Apache or similar, the temporary files dpkg is working on (such as *.dpkg-new) no longer end up in the baseline and look like tampering in the next verification (1.11.18 and later). If it is still busy after 120 seconds, a warning is logged and the baseline is built anyway.
7.4 Honeytokens (Credential Decoys)
On startup, taking advantage of running as root, the daemon idempotently deploys three decoy credential files under /root: .aws/credentials, .ssh/id_rsa, and .docker/config.json (collectively, honeytokens). It never overwrites a real file that already exists at any of those paths. A background watcher process uses inotify to detect any read access to these files and treats it as post-compromise credential reconnaissance (MITRE ATT&CK T1552: Unsecured Credentials), immediately sending a Critical-severity notification and recording it to the incident timeline. It is on by default and can be changed with the keys honeytokens_enabled (false stops planting and watching; decoys already placed stay) and honeytokens_home (where the decoys are planted; default /root). The packages up to 1.10.4 did not include roamswitch-honeytokens, which this feature needs, so it did not run on installs from the packages. It has been bundled since 1.10.5, and the problem that the systemd sandbox blocked writes to /root on the Server Edition was fixed in 1.10.10. Since 1.11.1, every docker command that RoamSwitch itself runs is started with an empty private directory (/run/roamswitch/docker-config as root) as DOCKER_CONFIG, so it does not trigger the decoy .docker/config.json (before that, the daemon's monitoring and roamswitch status --server could read the decoy and raise a false detection). A read by any other program is still detected as before.
If a real access to any honeytoken is detected, anticipating that the attacker will keep digging, RoamSwitch deploys up to two additional, thematically related decoys per trigger (for example, reaching .aws/credentials also plants .aws/config; reaching .ssh/id_rsa plants .ssh/config; reaching .docker/config.json plants .kube/config), up to six total across all triggers. If you notice these unfamiliar files accumulating under /root during an incident investigation, that is expected behavior.
7.5 Intrusion path (who made the change, and through which SSH login)
The notifications about a changed file (Critical Path FIM and lockfile FIM) now carry the process that made the change (account, executable, command) and the SSH login that process belongs to (the source IP and port, the login time, the authentication method and the public-key fingerprint). This is on by default (fim_intrusion_context_enabled). The moment an event is read, the daemon takes a snapshot of the writing process's /proc/<pid> (executable, user, loginuid, sessionid, cgroup, parent chain), because a process such as tee or sed is gone a millisecond later. A process that had already exited is filled in from the exec recorder (when it is enabled). The link to a login uses the SSH guard's session table first, and otherwise the Accepted lines of logind and journald. It works with the SSH guard disabled. Nothing is sent anywhere.
Only values reported by the kernel are trusted: the executable comes from /proc/<pid>/exe (not argv[0]), the login from loginuid (set once at login and inherited across sudo and su) and the cgroup's session-N.scope, and the rest from the parent chain. Values a process can change itself, such as comm, the command line and the environment, are shown but never used to decide anything; a process that merely calls itself sshd is not taken for an SSH login. A notification keeps three answers apart: found (source, time, method, key); "No SSH login was found" (the check showed a systemd service, a container, a local login session, or a process that belongs to no login session); and "The login path could not be determined" (the process was already gone and is not in the exec recorder, logind did not answer, the change was found by the periodic scan so the process is unknown, and so on). Something that could not be checked is never reported as "not a login". The notification goes out even if the lookup fails.
/etc/ssh/sshd_config: modified
Process: root (logged in as admin) / /usr/bin/tee / command: tee /etc/ssh/sshd_config (pid 4121)
SSH login: the session admin opened from 203.0.113.25:51544 at 2026-10-17 03:12:08 (authentication: publickey, key SHA256:AbCdEf0123456789)
/etc/sudoers: modified
The login path could not be determined (the change was found by the periodic scan, so the process that made it is not known)
Edits that put a new file under the same name are now detected too: install, sed -i, and cp followed by mv in the same directory. A per-file mark sits on the inode, so this kind of edit used to raise no event. The daemon now also marks the directories that hold watched files, and when a file with another name is closed in the same directory it looks the name up again a few milliseconds later. Limit: a file written in a different directory and moved in with mv is found only by the periodic scan (fim_check_interval_secs). Then the writing process is not known, and the notification says so.
Besides the notification, the finding is kept in the incident timeline (type fim) and in /var/lib/roamswitch/fim_intrusions.json (root only, the latest 100). The MCP tool verify_fim returns it in an added field, recentIntrusionContext (only when run as root). The verdict on whether a file was tampered with does not change. Limits: with the exec recorder disabled, and a writing process that lives only for an instant, the executable is not known, and the parent process's executable and command line are shown instead. The notifications of the persistence watch (persistence_watch) carry no process information, so they get no intrusion path. The path shows the process that made the change and its login, no further: the way in itself (the vulnerability that was exploited, or how a key was stolen) is not shown. Set fim_intrusion_context_enabled=false to turn it off.
8. eBPF Runtime Guard & Falco / Tetragon Integration
Hooks into kernel-space eBPF events to detect Frag Gap kernel privilege escalation, container escapes, and unauthorized reverse shells. The default sensor is Falco (JSONL over a UNIX domain socket). Selecting sensor.type: tetragon in guard.yaml lets you use Cilium Tetragon as the sensor instead — it simply subscribes to Tetragon's native gRPC API over the same UNIX socket, adding no new listening port.
8.1 Anti-Bloat Architecture
To avoid the “syslog bloat” and disk pressure that plague conventional security tools, RoamSwitch connects to Falco directly over the UNIX domain socket /run/roamswitch/events.sock. The socket is created root-only (0600), so only Falco running as root can write to it (no extra permission setup is needed).
- Bundled
/etc/falco/config.d/99-roamswitch-optimized.yamlstreams events directly into the daemon with zero disk I/O. Falco's output is passed to the socket withsocatinside that configuration, so the Server Edition deb / rpm packages installsocatas a recommended package (Recommends); it is not needed if you do not use Falco. /etc/logrotate.d/roamswitch-falcoensures clean daily rotation for any fallback file outputs.
8.2 Autonomous Containment
Upon detecting a critical attack, RoamSwitch immediately issues SIGSTOP to freeze the offending process PID and applies nftables Air-Gap isolation.
8.3 Automatic Forensic Evidence Capture
An evidence bundle is created automatically whenever the eBPF Runtime Guard takes a per-process containment action (a SIGSTOP freeze plus a network cut). Under the default policy in §4.1, Critical-severity events follow this path; Emergency-severity events, which isolate the entire host, do not currently produce a bundle. A bundle contains a best-effort, bounded ~30-second packet capture on the default routing interface (silently skipped, with the omission recorded, if tshark is not installed), and, when the offending PID is known, a /proc/<pid> snapshot: status, cmdline, maps, environ (values whose name contains KEY/TOKEN/SECRET/PASSWORD are replaced with [REDACTED]), a list of open file descriptor link targets, and the ancestor process chain up to PID 1. A manifest.json recording the SHA-256 hash of every captured file is bundled alongside them for later tamper verification.
Bundles are stored under /var/lib/roamswitch/incident-evidence/<timestamp>-<id>/. They are never pruned automatically. To reclaim disk space, explicitly remove bundles older than the retention period (30 days by default) with:
sudo roamswitch-incident-capture prune [--keep-days N]
You can also capture a manual snapshot at any time, without waiting for a containment action, with:
sudo roamswitch-incident-capture snapshot [--pid PID] [--reason TEXT] [--json]
8.4 Measuring how fast containment is, and the heartbeat of the alert path
How accurate and how fast eBPF containment is depends on the rules of the sensor (Falco or Tetragon). In fact, the standard rules of Falco 0.45.0 (29 of them) include none that detects a reverse shell on the host: the reverse-shell rules are for containers only, and some of them are dropped by RoamSwitch's priority: warning condition. RoamSwitch does not assume the rules are good; it makes up for that in three ways: it measures how long containment takes, it checks regularly that the alert path is alive, and it detects reverse shells by itself.
Measuring containment latency: for each eBPF incident the daemon records the time from Falco's detection to its reading the alert (detect_to_receive_ms), from reading it to the response being finished (receive_to_action_ms, measured after the isolation call returns; for the host Air-Gap, until the ruleset is loaded), and their sum (detect_to_action_ms). The values appear as added fields in ebpf_incidents.json and in the MCP tool get_ebpf_incidents. The notification for an isolated process carries one line, "from detection to response: N ms". Measured on an Ubuntu 24.04 VM (Falco 0.45.0, modern eBPF): 1 to 2 ms from detection to receipt, 41 to 62 ms from receipt to process isolation plus SIGKILL (42 to 64 ms in total), and 109 ms in total for the host Air-Gap (inside the 150 ms target). The figures belong to that VM. Tetragon stamps the time on receipt, so it has no detect_to_receive_ms.
Heartbeat (ebpf_heartbeat_interval_secs; 600 seconds by default, 0 turns it off, otherwise 60 to 86400): the package ships a Falco rule of RoamSwitch's own, 96-roamswitch-heartbeat.yaml. At each interval the daemon opens a throwaway path that only root can use (a random name under /run/roamswitch/heartbeat/) and measures how long the alert of that rule takes to come back through Falco's output. One round trip shows at once that the rule is loaded, that Falco is running and that the output path is alive. Only when two checks in a row do not come back (the second one a minute after the first) does it warn, "the rules are not loaded or the output is not arriving", at most once in 24 hours. A heartbeat line is dropped without reaching containment, notification, the allowlist or the incident log. Nothing is sent anywhere. Measured round trips were 2 to 6 ms; with the rule removed the warning came after about 164 seconds, and after restoring it the state was back to normal in about 75 seconds.
The state is kept in /var/lib/roamswitch/ebpf_heartbeat.json and is read by the heartbeat field of the MCP tool get_ebpf_incidents and by the diagnostic K-04 (the eBPF runtime agent; the number of diagnostic items stays at 34). Even when Falco is running, K-04 fails if the heartbeat does not come back. Limits: it checks one round trip of one rule (the open-family system calls) and cannot tell that another rule is broken on its own. Something that breaks between two checks (10 minutes by default) is not seen until the next one. It is not done for Tetragon. On a host where Falco is not running, or where the rule file is missing, it checks nothing and warns about nothing (the existing "guard stopped" notification covers that). Falco writes one warning line to syslog per check (144 lines a day at the default).
8.5 Reverse-shell detection (RoamSwitch's own)
Without relying on Falco's rules, RoamSwitch reads /proc itself every 2 seconds and looks for a shell or interpreter (sh, bash, python, perl, ruby, php, node and the like) whose standard input and output are one TCP connection this host opened to an outside address. bash -i >& /dev/tcp/…, python, perl, nc -e and socat exec: are covered. Only what is seen twice in a row is reported, once per process. By default only public outside addresses count; private addresses (RFC 1918, CGNAT, link-local) count only with reverse_shell_watch_include_private=true, because ssh ProxyCommand, containers and local development make them common and legitimate. Loopback is never watched. Connections the host accepted (by inetd or socat TCP-LISTEN) are not covered either. Measured on an Ubuntu 24.04 VM, bash /dev/tcp, python, perl, nc -e and socat exec: were each detected about 2.4 seconds after the connection. ssh, ssh with a ProxyCommand, git over ssh and an nc client are not detected.
A detection enters the same path as a Falco alert (allowlist, notification cool-down, the per-severity actions of guard.yaml, the incident record) as a Critical eBPF event named RoamSwitch Reverse Shell. By default that isolates the process and kills it. A reverse shell owned by root cannot be isolated per process, so, as for any Critical event, it goes on to the Air-Gap of the whole host. There are two ways out if you worry about a legitimate program being flagged by mistake: reverse_shell_watch_notify_only=true (a Warning with a notification only; nothing is isolated or killed; false by default) and an allowlist entry, sudo roamswitch server allowlist add "RoamSwitch Reverse Shell" <process name>. The three setting keys are reverse_shell_watch_enabled (true by default), reverse_shell_watch_include_private (false) and reverse_shell_watch_notify_only (false).
Limits: it narrows by name (process name and executable name), so a home-made binary under a name other than a shell or interpreter that ties its standard input and output to a socket is not found. Connections through loopback (such as the turn-back of ssh -R) and connections shorter than 2 seconds are not seen, and neither are processes that cannot be seen from the host's process space. This detection exists only in the Server Edition; the Mac and the Linux client do not have it.
9. Emergency Alerts (Telegram / LINE / Webhook)
Dispatches immediate alerts to external notification channels when port anomalies, FIM tampering, or eBPF containment events occur:
[notifications]
language = ja
# Telegram: bot token and chat ID
telegram_bot_token = "123456789:ABCdefGHIjklMNOpqrSTUvwxYZ"
telegram_chat_id = "-1001234567890"
# LINE: Messaging API channel access token and user ID
line_channel_access_token = "YOUR_LINE_ACCESS_TOKEN"
line_user_id = "U1234567890abcdef1234567890abcdef"
# Generic webhook (Slack, Discord, Teams, your own monitoring platform)
webhook_url = "https://hooks.slack.com/services/T00/B00/XXXXX"
Even without any of these external channels configured, every alert sent is also kept locally for the past 7 days. Check it with roamswitch notifications.
Refusing internal webhook destinations (opt-in): with webhook_block_private_addresses=true (default false = unchanged), a notification webhook is refused when its destination is, or (for a hostname) any one of its resolved addresses is, loopback, private (10/8, 172.16/12, 192.168/16), link-local (169.254/16, including the cloud metadata endpoint 169.254.169.254), ULA (fc00::/7) or CGNAT (100.64/10). The reason goes to the log and to the roamswitch notify-test error. Alternate spellings such as 2130706433 or 0x7f.1 and IPv4-mapped IPv6 are classified too, and the verified addresses are pinned into curl, so DNS rebinding cannot swap the target. Legitimate internal destinations such as an in-house SIEM are permitted with webhook_allow_addresses=10.20.0.0/16,fd00::/8 (comma-separated CIDRs). Event-forwarding webhooks have the same pair, forward_webhook_block_private_addresses and forward_webhook_allow_addresses. Even with the option false, a destination that is a cloud metadata address logs a warning (the send is not stopped). If an HTTP proxy environment variable is set, resolution and connection happen at the proxy and are outside this check.
10. AI Agent / MCP Integration (Model Context Protocol)
RoamSwitch Server Edition includes roamswitch-mcp, allowing AI agents (Claude, Gemini, Cursor) to monitor infrastructure health and posture.
Configuration Example (`claude_desktop_config.json`)
{
"mcpServers": {
"roamswitch": {
"command": "/usr/bin/roamswitch-mcp",
"args": []
}
}
}
10.1 Investigation-Agent Hand-off (investigation — delegating first-pass automated investigation to an AI agent)
While §10's roamswitch-mcp serves queries from a human's AI client, investigation hand-off is a separate mechanism where the daemon itself autonomously launches an external AI agent CLI, triggered by a detection event. It targets critical eBPF events that were judged notify-only (did not trigger automatic isolation or process freezing), plus Critical-Path FIM and dependency-lockfile tampering detections. When enabled, the daemon first generates a local heuristic first-pass triage report (likelihood of false positive, reasoning, suggested next steps), then launches the configured AI agent CLI as a subprocess with that report attached, delegating further investigation and summarization. Off by default.
# Append to /etc/roamswitch/guard.yaml (a separate file from server.conf)
investigation:
enabled: true
command: "claude" # "claude" | "agy" | "codex" | "opencode" | any custom command
args: ["-p"] # The prompt is automatically appended as the final argument
timeout_secs: 180
The same settings can also be configured from the interactive wizard of sudo roamswitch server setup (§3.2, final step). Choose from presets for Claude Code (claude -p), agy (agy -p --dangerously-skip-permissions), Codex CLI (codex exec), OpenCode (opencode run), or a custom command. On a re-run, the matching preset is auto-selected based on the saved configuration.
run_as user is not saved by the wizard (CLI and TUI), and the daemon does not start it. A tool launched in a mode that skips permission prompts, such as --dangerously-skip-permissions, raises the risk that malicious input makes the agent do something unintended. The wizard shows this warning when agy is chosen. Where untrusted log input paths exist, a tool setup that does not skip permission prompts is recommended.The investigation hand-off is never invoked while the network is cut off (e.g. during an active Air-Gap, or any state equivalent to isolate_host_all).
11. CLI Command Cheat Sheet
| Command | Privileges | Description |
|---|---|---|
sudo roamswitch status --server |
root | Show the 34-item server security score and status (--json prints the same report as JSON) |
sudo roamswitch ports |
root | Audit open listening ports and bound processes |
sudo roamswitch fim verify |
root | Verify SHA-256 integrity of critical path system files |
roamswitch notifications |
User | Show the notification history of the past week (every alert sent); --json and --export FILE are supported |
sudo roamswitch server setup |
root | Launch interactive initial configuration wizard |
sudo roamswitch fim update |
root | Refresh FIM SHA-256 baseline database |
sudo roamswitch server config set <key> <value> |
root | Change and persist a single setting (see §18 for the key list) |
sudo roamswitch emergency-restore |
Root | Disarm all eBPF / firewall isolation and restore the initial baseline |
roamswitch port-scan |
User | Shows the incoming-port-scan-detection guard's (§4.3) detection history (source IP, whether it was auto-blocked, and block duration) |
roamswitch sensor [key|list|pair|unpair|request-audit|results] |
User / root | Mutual trust with a RoamSwitch Sensor through pairing codes. pair --addr <IP> --code <code> --fingerprint <SHA-256> pairs (the fingerprint is the value shown on the Sensor’s screen), repin updates the pin after the Sensor regenerated its certificate, and request-audit requests an active vulnerability audit (fetch the result with results). Requires sensor_pairing_enabled: true (off by default). See the Sensor Operations Manual for details |
roamswitch server tui |
User / root | Start the TUI that covers every function (read-only for ordinary users, full control as root; §17) |
sudo roamswitch server ack |
root | Acknowledge the current emergency isolation (keeps a FULL isolation strict, releases a DEGRADED one; §4.1) |
roamswitch server ebpf |
User | Show the eBPF runtime guard’s current containment state and detected incident history |
roamswitch server bans [--json] |
User / root | Shows the SSH brute-force guard (§4.5): its state and the sources banned right now with the time left. --json is available. The list of bans is readable by root only |
sudo roamswitch server unban <IP | IPv6 /64 | --all> |
root | Lifts an SSH-guard ban and clears that source's failure count (give an IPv6 source as its /64; --all lifts every ban) |
sudo roamswitch server allowlist [list|add <rule> <process> [--exe <path>]|remove <rule> <process>] |
root | Manage the permanent false-positive allowlist. With --exe the entry is bound to the real executable path so a renamed process cannot use it (a name-only entry can be spoofed, and a warning is printed) |
roamswitch frozen [list|resume <PID>|resume all] |
User / root | List the processes the guards hold in SIGSTOP and release one that was a false positive (SIGCONT); resume all releases every one |
sudo roamswitch events search|tree|export|verify|status |
root | Search the exec recorder (--since 2h --pid --exe --user --parent --text --json), print a process tree, export, verify the hash chain and show status (§15) |
sudo roamswitch forward status|test|queue |
root | Show event-forwarding status, send a test event and list the retry queue (§16) |
roamswitch timeline / notifications [--json] [--export FILE] |
User | Show the unified incident timeline (experimental) or the notification history; --json prints a JSON array and --export FILE writes JSON Lines |
roamswitch audit-agent-setup [--json] |
User | Read-only check of the instruction files, skills and OpenClaw settings given to AI agents for risky content (home folder only, no network; --json prints JSON). Also available from "Tools" in the Server TUI |
12. Troubleshooting & FAQ
Q1. Will RoamSwitch unexpectedly disconnect my SSH session?
No. In normal operation RoamSwitch unconditionally allows established/related connection states and ssh_ports (22 by default). Even when a serious attack triggers an Air-Gap emergency isolation, preserve_ssh_on_isolation=true (the default) keeps management sessions on the maintenance SSH ports and admin IPs (every other established connection is cut). Server Edition 1.10.4 and earlier had a defect. The isolation ruleset dropped the SSH replies, so the operator's SSH did not work during isolation. It was fixed in 1.10.5.
Q2. How do I regain access if accidentally locked out?
Log in through your cloud provider's web console (VNC / serial), then run sudo roamswitch emergency-restore to disarm the isolation, or sudo systemctl stop roamswitch-server to stop the service.
Q3. How do I expose ports for newly added web services (e.g. Nginx)?
Run sudo roamswitch server config set allowed_ports 80,443,3000 (or edit /etc/roamswitch/server.conf directly), then run sudo roamswitch server restart.
13. File Scan Guard (protecting mail relays, shared folders, and upload endpoints)
An optional feature relevant only if this host has a directory that stores files for others — a mail relay, a Samba/NFS share, a file upload endpoint. The embedded YARA engine always scans clamav_scan_dirs with no external dependency; setting clamav_enabled=true adds ClamAV as a second opinion. Confirmed threats are automatically quarantined to /var/lib/roamswitch/quarantine and the operator is notified.
# --- File Scan Guard (ClamAV, opt-in) ---
# The embedded YARA engine always scans clamav_scan_dirs unconditionally.
clamav_enabled=false # true enables additional inspection via clamdscan
clamav_scan_dirs= # Absolute paths to scan (comma-separated)
clamav_scan_interval_secs=86400 # Scan interval (seconds, minimum 3600)
clamav_freshclam_interval_secs=7200 # Signature DB auto-update interval (seconds, minimum 1800)
Also configurable via the interactive sudo roamswitch server setup wizard; check its configuration and quarantine status via the get_file_scan_guard_status MCP tool.
14. Log Audit (Log Pattern Anomaly Detection & Notification)
A periodic journalctl scan turns log messages into templates to detect new patterns (log shapes never seen before) and frequency anomalies (a statistical spike, Z-score > 3.0). This is a lightweight mechanism that needs no extra daemon or external dependency, so it's enabled by default. Detections are sent through the same notification channels as File Scan Guard and FIM (Telegram / LINE / Webhook). Any API keys or tokens that might appear in a log line are automatically masked before being included in the notification.
# --- Log Audit (enabled by default) ---
log_audit_enabled=true # false to disable
log_audit_interval_secs=1800 # Scan interval in seconds (minimum 300)
This can also be configured from the interactive sudo roamswitch server setup wizard, and the latest detection results can be checked via the roamswitch audit-logs CLI command or the audit_security_logs MCP tool.
AccessScope, an access-log diagnosis tool (a separate tool): the Server Edition deb / rpm include the accessscope command of a separate tool that diagnoses web server and SSH access logs (the window app is not included). It is read-only, and its analysis uses neither an LLM nor the network. Analysis by an LLM is optional and off by default (when enabled, it can run as root too, but the LLM then runs with root's rights, so an unprivileged user is recommended). accessscope scan --auto diagnoses this host's logs, and accessscope discover lists where the logs are. See the AccessScope page for details. The same Server Edition deb / rpm also include the personalsoc and pathscope commands of two other separate tools, PersonalSOC (checks this host's logs and defenses) and PathScope (analyzes privilege-escalation paths); the window apps are not included. The commands themselves do no network communication. PersonalSOC's LLM analysis runs only when you pass --llm.
15. Exec Recorder & Correlation Rules (Local, Notify-Only)
Process executions (exec) are recorded through the netlink proc connector: executable, arguments, parent lineage, UID, cwd, container ID and SHA-256. It is on by default, and the records are stored in the root-only directory /var/lib/roamswitch/exec_log on this host as segmented, hash-chained JSON Lines (0600, 200 MB / 14 days by default) and are never sent anywhere. The log is tamper-evident (events verify), not tamper-proof. If the proc connector cannot be opened it degrades to a low-rate /proc scan. Recording runs on its own threads, separate from the fanotify permission-event loop, and keeps its own CPU use within a budget (3% by default).
Search and export locally (the log is root-only, so sudo is normally required).
sudo roamswitch events status
sudo roamswitch events search --since 2h --exe curl
sudo roamswitch events tree 4242
sudo roamswitch events export --since 1d > exec-events.jsonl
sudo roamswitch events verify
Correlation rules EXR-001 to EXR-007 (notify-only, each with a MITRE ATT&CK technique ID): EXR-001 a shell spawned by a web service / EXR-002 execution from /tmp, a deleted file or memfd / EXR-003 execution right after a download / EXR-004 an interpreter one-liner using base64 or eval / EXR-005 obtaining uid 0 without setuid / EXR-006 execution of a binary just placed by cron or systemd / EXR-007 execution after ld.so.preload changed. Each rule has a deliberately quiet default and never isolates by itself. Silence individual rules with exec_rules_disabled (for example EXR-003,EXR-004) and adjust the users EXR-001 watches with exec_rule_service_users.
Only if you explicitly set exec_rules_isolation_optin (default false) to true are high-severity rules treated as a high-confidence isolation trigger (§4.1). By default the rules only notify. The records can also be read through the read-only MCP tools search_exec_events and get_process_tree.
16. Event Forwarding (SIEM / syslog / Webhook, Opt-In)
Detection events can be forwarded to destinations you specify at your own organization. It is off by default: nothing is sent until you set forward_enabled=true and configure at least one destination, and there is no vendor destination built in. What is forwarded are the incident timeline, notification history, eBPF guard, isolation state transitions, FIM, canary and exec-rule alerts; the raw exec log itself is not forwarded. There are three kinds of destination.
- syslog (
forward_syslog_url:udp://host:514/tcp://host:port/tls://host:port): RFC 5424 or CEF (forward_syslog_format). TCP / TLS framing isoctet(RFC 6587, the default) orlf. You can name a CA for TLS verification withforward_tls_ca_file, and the connection is delegated to the systemopenssl s_client. - JSON Lines file (
forward_file_path): one event per line, rotated byforward_file_max_mb(50 by default) andforward_file_keep(5 by default). - Structured webhook (
forward_webhook_url): a JSON POST signed with HMAC-SHA256 using a shared secret (forward_webhook_secret, orforward_webhook_secret_filepointing at a0600file), carried in theX-RoamSwitch-Signatureheader. Delivery is done by acurlsubprocess, and the secret is never put on the command line.
Events that could not be delivered (TCP / TLS / webhook) are put in an on-disk retry queue (0600, capped by forward_queue_max_mb, 20 by default, and forward_queue_max_age_hours, 72 by default) and retried with exponential backoff, also after a restart. forward_min_severity (default low) sets the lowest severity that is sent. Delivery runs on a dedicated thread so no hot path is blocked, and when the queue overflows the oldest items are dropped first.
Check status, test connectivity and inspect the retry queue with the following commands (restart the service after changing the settings).
# /etc/roamswitch/server.conf
forward_enabled=true
forward_syslog_url=tls://siem.example.org:6514
forward_syslog_format=rfc5424
forward_tls_ca_file=/etc/ssl/certs/corp-ca.pem
forward_min_severity=medium
sudo roamswitch server restart
sudo roamswitch forward status
sudo roamswitch forward test
sudo roamswitch forward queue
17. Server TUI (a Terminal UI for Every Function)
roamswitch-server-tui (also started by roamswitch server tui) is a TUI that reaches every Server Edition function — spread across the CLI verbs and the two configuration files (server.conf and guard.yaml) — with keyboard operation alone over SSH (it works at 80x24, in 10 languages). The development tests fail whenever a CLI verb or configuration key gains no counterpart in the TUI.
- Permission mode: started as an ordinary user it is read-only (changes and privileged actions are disabled, and the
sudo roamswitch …command you would need is shown instead). Started as root it can act; high-impact operations show what they will do first, and the most dangerous ones require typingyes. It never starts the daemon and opens no network socket of its own. - Screens (switch with the digit keys):
1Dashboard (health, isolation state) /2Protection (eBPF isolation, canary, FIM, Docker, egress and DNS, exec recorder, rules and event search, feeds, XDP gate) /3Incidents (all incident types including the SSH brute-force ban list and unban, timeline, notification history, frozen processes, quarantine vault, allowlist) /4Notifications and forwarding /5Sensor /6Config /7Service /8Tools /9Log audit /0About and help.?shows the keys of the current screen,Lcycles the 10 languages,rreloads andqquits (asking first when configuration changes are unsaved). - Config editor: generated from a schema, it edits all 146 keys of
server.confandguard.yaml(§18) with validation of type, range and allowed values. You review a diff (with secrets masked) before saving, and saving takes a timestamped backup and writes atomically. Keys that need a restart, and keys where a wrong value can cut you off or weaken protection, are marked. The investigation-agent setup (§10.1) can be written too, after choosing a preset and previewing it.
Start it with:
sudo roamswitch-server-tui # full control
roamswitch-server-tui # read-only
roamswitch server tui # same as roamswitch-server-tui
18. Configuration Key Reference (server.conf / guard.yaml)
roamswitch-server does not support reloading its configuration: every key needs a service restart to take effect (sudo roamswitch server restart). Change server.conf keys with roamswitch server config set <key> <value> or the editor in §17; change guard.yaml with the editor or by editing it by hand.
18.1 server.conf (109 keys)
| Key | Type / values | Default | Restart |
|---|---|---|---|
language | system | ja | en | zh-hans | zh-hant | ko | de | fr | es | it | pt | ja | Yes |
ssh_ports | port list | 22 | Yes |
allowed_ports | port list | 80,443 | Yes |
whitelist_ips | list | — | Yes |
preserve_ssh_on_isolation | bool | true | Yes |
telegram_enabled | bool | false | Yes |
telegram_bot_token | string | — | Yes |
telegram_chat_id | string | — | Yes |
line_enabled | bool | false | Yes |
line_channel_access_token | string | — | Yes |
line_user_id | string | — | Yes |
webhook_enabled | bool | false | Yes |
webhook_url | URL | — | Yes |
webhook_block_private_addresses | bool | false | Yes |
webhook_allow_addresses | CIDR list | — | Yes |
action_on_critical | isolate | freeze | alert_only | isolate | Yes |
ebpf_socket_path | abs path | /run/roamswitch/events.sock | Yes |
ebpf_heartbeat_interval_secs | 0 (off) | int 60–86400 | 600 | Yes |
isolation_hard_cap_secs | int ≥ 0 | 3600 | Yes |
fim_check_interval_secs | int ≥ 10 | 300 | Yes |
fim_intrusion_context_enabled | bool | true | Yes |
protect_docker_ports | bool | true | Yes |
dns_sinkhole_enabled | bool | false | Yes |
dns_sinkhole_provider | quad9 | cloudflare | adguard | cleanbrowsing | quad9 | Yes |
egress_ip_blocklist_enabled | bool | true | Yes |
metadata_watch_enabled | bool | true | Yes |
metadata_watch_allow_users | list | — | Yes |
cve_kernel_map_updates_enabled | bool | true | Yes |
clamav_enabled | bool | false | Yes |
clamav_scan_dirs | abs path list | — | Yes |
clamav_scan_interval_secs | int ≥ 3600 | 86400 | Yes |
clamav_freshclam_interval_secs | int ≥ 1800 | 7200 | Yes |
lockfile_watch_dirs | abs path list | — | Yes |
npm_audit_signatures_enabled | bool | false | Yes |
log_audit_enabled | bool | true | Yes |
log_audit_interval_secs | int ≥ 300 | 1800 | Yes |
resource_guard_enabled | bool | true | Yes |
resource_guard_rss_sample_interval_secs | int ≥ 10 | 60 | Yes |
resource_guard_rss_window_secs | int ≥ 300 | 1800 | Yes |
resource_guard_rss_min_samples | int ≥ 3 | 10 | Yes |
resource_guard_rss_min_growth_pct | int ≥ 10 | 50 | Yes |
resource_guard_rss_recovery_drop_pct | int ≥ 1 | 10 | Yes |
resource_guard_rss_min_kb | int ≥ 0 | 20000 | Yes |
resource_guard_crash_loop_window_secs | int ≥ 60 | 300 | Yes |
resource_guard_crash_loop_min_restarts | int ≥ 2 | 3 | Yes |
resource_guard_correlation_window_secs | int ≥ 60 | 600 | Yes |
resource_guard_zombie_min_count | int ≥ 0 | 20 | Yes |
resource_guard_load_min_normalized | float ≥ 0.0 | 0.5 | Yes |
resource_guard_load_enabled | bool | true | Yes |
resource_guard_cpu_enabled | bool | true | Yes |
resource_guard_cpu_sample_interval_secs | int ≥ 0 | 60 | Yes |
resource_guard_cpu_window_secs | int ≥ 0 | 600 | Yes |
resource_guard_cpu_min_samples | int ≥ 0 | 8 | Yes |
resource_guard_cpu_pct_threshold | float 0.0–1000.0 | 80.0 | Yes |
container_exec_guard_enabled | bool | true | Yes |
port_scan_detect_enabled | bool | true | Yes |
port_scan_detect_min_distinct_ports | int ≥ 0 | 15 | Yes |
port_scan_detect_window_secs | int ≥ 0 | 300 | Yes |
port_scan_block_enabled | bool | true | Yes |
port_scan_block_duration_secs | int ≥ 0 | 600 | Yes |
ssh_guard_enabled | bool | true | Yes |
ssh_guard_block_enabled | bool | true | Yes |
ssh_guard_max_failures | int 2–1000 | 6 | Yes |
ssh_guard_window_secs | int 10–86400 | 600 | Yes |
ssh_guard_ban_secs | int 60–2592000 | 3600 | Yes |
ssh_guard_ban_max_secs | int 60–31536000 | 604800 | Yes |
ssh_guard_allow | IP/CIDR list | — | Yes |
ssh_guard_log_source | auto | journal | abs path | auto | Yes |
ssh_guard_yield_to_fail2ban | bool | true | Yes |
ssh_guard_trust_key_days | int 0–3650 | 30 | Yes |
dns_tunnel_detect_enabled | bool | true | Yes |
reverse_shell_watch_enabled | bool | true | Yes |
reverse_shell_watch_include_private | bool | false | Yes |
reverse_shell_watch_notify_only | bool | false | Yes |
sensor_pairing_enabled | bool | false | Yes |
sensor_colocated | bool | false | Yes |
forward_enabled | bool | false | Yes |
forward_syslog_url | URL | — | Yes |
forward_syslog_format | rfc5424 | cef | rfc5424 | Yes |
forward_syslog_framing | octet | lf | octet | Yes |
forward_tls_ca_file | abs path | — | Yes |
forward_file_path | abs path | — | Yes |
forward_file_max_mb | int 1–10000 | 50 | Yes |
forward_file_keep | int 1–100 | 5 | Yes |
forward_webhook_url | URL | — | Yes |
forward_webhook_secret | string | — | Yes |
forward_webhook_secret_file | abs path | — | Yes |
forward_webhook_block_private_addresses | bool | false | Yes |
forward_webhook_allow_addresses | CIDR list | — | Yes |
forward_min_severity | info | low | medium | high | critical | low | Yes |
forward_queue_max_mb | int 1–10000 | 20 | Yes |
forward_queue_max_age_hours | int 1–8760 | 72 | Yes |
exec_recorder_enabled | bool | true | Yes |
exec_recorder_retention_mb | int 1–100000 | 200 | Yes |
exec_recorder_retention_days | int 1–3650 | 14 | Yes |
exec_recorder_hash_chain | bool | true | Yes |
exec_recorder_hash_exe | bool | true | Yes |
exec_recorder_cpu_budget_pct | int 1–50 | 3 | Yes |
exec_rules_enabled | bool | true | Yes |
exec_rules_disabled | string | — | Yes |
exec_rule_service_users | string | — | Yes |
exec_rule_window_secs | int 5–3600 | 120 | Yes |
exec_rules_isolation_optin | bool | false | Yes |
honeytokens_enabled | bool | true | Yes |
honeytokens_home | abs path | /root | Yes |
harden_userns_enabled | bool | true | Yes |
dns_tunnel_entropy_allowlist | list | — | Yes |
fleet_forwarding_allowed_hosts | list | — | Yes |
fleet_forwarding_allow_plaintext | bool | false | Yes |
18.2 guard.yaml (37 keys)
| Key | Type / values | Default | Restart |
|---|---|---|---|
sensor.type | falco | tetragon | falco | Yes |
sensor.transport | string | unix_socket | Yes |
sensor.socket_path | abs path | /run/roamswitch/events.sock | Yes |
sensor.reconnect_interval_ms | int 100–3600000 | 1000 | Yes |
policies.on_warning.action | log_and_notify | isolate_process | isolate_host_all | log_and_notify | Yes |
policies.on_warning.server_action | log_and_notify | isolate_process | isolate_host_all | — | Yes |
policies.on_warning.kill_process | bool | false | Yes |
policies.on_warning.freeze_process | bool | false | Yes |
policies.on_warning.fallback_to_host_isolation_after_retries | int 0–1000 | — | Yes |
policies.on_error.action | log_and_notify | isolate_process | isolate_host_all | isolate_process | Yes |
policies.on_error.server_action | log_and_notify | isolate_process | isolate_host_all | — | Yes |
policies.on_error.kill_process | bool | false | Yes |
policies.on_error.freeze_process | bool | false | Yes |
policies.on_error.fallback_to_host_isolation_after_retries | int 0–1000 | — | Yes |
policies.on_critical.action | log_and_notify | isolate_process | isolate_host_all | isolate_process | Yes |
policies.on_critical.server_action | log_and_notify | isolate_process | isolate_host_all | — | Yes |
policies.on_critical.kill_process | bool | true | Yes |
policies.on_critical.freeze_process | bool | false | Yes |
policies.on_critical.fallback_to_host_isolation_after_retries | int 0–1000 | 3 | Yes |
policies.on_emergency.action | log_and_notify | isolate_process | isolate_host_all | isolate_host_all | Yes |
policies.on_emergency.server_action | log_and_notify | isolate_process | isolate_host_all | — | Yes |
policies.on_emergency.kill_process | bool | true | Yes |
policies.on_emergency.freeze_process | bool | false | Yes |
policies.on_emergency.fallback_to_host_isolation_after_retries | int 0–1000 | — | Yes |
server_safeguards.allow_established_connections | bool | true | Yes |
server_safeguards.maintenance_ssh_ports | port list | 22 | Yes |
server_safeguards.whitelist_cidrs | list | — | Yes |
server_safeguards.whitelist_dns_servers | list | — | Yes |
server_safeguards.safety_timer_secs | int ≥ 0 | 300 | Yes |
adaptive_security.enabled | bool | true | Yes |
adaptive_security.untrusted_network_actions.elevate_falco_profile | string | — | Yes |
adaptive_security.untrusted_network_actions.auto_enable_userns_restriction | bool | false | Yes |
investigation.enabled | bool | false | Yes |
investigation.command | abs path | — | Yes |
investigation.args | list | — | Yes |
investigation.run_as | string | — | Yes |
investigation.timeout_secs | int 1–86400 | 180 | Yes |
19. Web shell and injected-code detection (Web Rules)
This detects PHP, JavaScript, JSP and ASP web shells and injected code running on your web server, PHP hidden in images and .htaccess abuse (from Linux 1.11.23). No setup is needed: it finds the document roots in the Apache, nginx and Caddy configuration and checks a file right after it is written and closed there (it uses notify-only fanotify, so it can never stall another process). A periodic scan every 6 hours covers dropped events and files that were there before the start. ClamAV is not required.
Behaviour is set in /etc/roamswitch/web_rules.conf (root-owned, mode 0644; a file others can write is ignored). Apply a change with sudo roamswitch server restart. A template is printed by roamswitch web-rules example conf.
| Key | Meaning (default) |
|---|---|
quarantine | Automatic quarantine level. off never quarantines, critical only critical, high critical and high (default: critical). A high finding at the critical level only notifies and fires the hook; the file is left alone. |
account_action | Whether to revoke the login shell of the owner of a detected file. off / revoke_shell (default: off). Only critical or high findings that were quarantined count. Root, UIDs below account_min_uid, logged-in users and the daemon's own user are never touched. The previous shell is recorded and sudo roamswitch web-rules unlock-account <user> restores it. |
account_min_uid | account_min_uid: the lowest UID the account response may act on (default 1000; it cannot go below 100). |
scan_dirs, auto_docroots, watch, scan_interval_secs | scan_dirs (comma-separated absolute paths), auto_docroots (default true; false stops the automatic discovery from the web server configuration), watch (default true; false turns off the write watcher and keeps only the periodic scan) and scan_interval_secs (default 21600, minimum 3600). |
# /etc/roamswitch/web_rules.conf
quarantine=critical
account_action=off
account_min_uid=1000
#scan_dirs=/srv/sites/a,/srv/sites/b
auto_docroots=true
watch=true
scan_interval_secs=21600
A detection sends a notification (Telegram, LINE, webhook) that names the rule and whether the file was quarantined or left in place. To look without moving anything, use sudo roamswitch web-scan <folder> (look-only; --quarantine to quarantine) or the Tools screen of the Server TUI. Your own rules go in /etc/roamswitch/rules.d/*.json. To call your own script after a detection, place it at /etc/roamswitch/hooks/post-detection (the hit arrives as JSON on standard input; only a root-owned file that others cannot write, in a safe directory, runs; there is no shell and it is stopped after 30 seconds). Rule updates ride on the daily receive-only update. With cve_kernel_map_updates_enabled off there is no network contact at all and the built-in rules are used.