ยง1Overview & Core Tenets
RoamSwitch Server Edition is a headless, resident autonomous network security and integrity protection suite purpose-built for Linux server infrastructures across cloud providers (AWS, GCP, Azure, bare-metal VPS) and on-premises data centers.
Unlike client workstation editions focused on mobility and Wi-Fi perimeter adaptation, the Server Edition is founded on five uncompromising tenets: Inbound Default Drop, Critical Path File Integrity Monitoring (FIM), Preemptive Kernel LPE & Privilege Escalation Hardening, eBPF Runtime Threat Telemetry, and Autonomous Air-Gap Host Isolation with Pre-severance Emergency Alerts.
The Zero Telemetry principle โ no external-communication library in the daemon itself โ is enforced at the code level (by default the only thing that leaves the host is a receive of signed data (the kernel CVE data, the web rules and, on Ubuntu servers, the Ubuntu package CVE data used by the OS-package CVE check; the manifest is checked daily, the CVE data itself about every 30 days), which can also be turned off, and every other exception is something the operator explicitly opts into; see the core design principles in ยง1), and secrets (bot tokens and configuration) are protected with 0600 permissions.
1.1 Comparison: Client Edition vs. Server Edition
| Dimension | Client Edition (Desktop/Mobile) | Server Edition (Headless/Infrastructure) |
|---|---|---|
| Deployment | Laptops, mobile developer workstations | Cloud VMs, bare-metal nodes, container hosts |
| Network Behavior | Dynamic profile switching based on Wi-Fi SSID / Gateway MAC | Static Inbound Default Drop. Only explicitly allowed ports are exposed |
| User Interface | GTK3 Desktop GUI + Tray indicator | Pure Headless CUI. CLI commands + interactive wizard + a terminal UI (roamswitch-server-tui) |
| Incident Notifications | Desktop notifications (libnotify) | Direct push to Telegram Bot, LINE Messaging API, and Webhooks (Slack/Discord) |
| Primary Defenses | Public Wi-Fi ARP spoofing, BadUSB, ransomware document encryption | External reconnaissance drop, FIM tampering detection, container escapes, kernel LPE (CVE-2026-53362) |
1.2 Core Architectural Principles
- Zero Telemetry: by default, nothing about the user or the environment is sent outside. The daemon itself has no HTTP/TLS communication library; only for notifications and forwarding the operator has configured does it run the system
curl(Telegram / LINE / webhook; secrets such as tokens and URLs are passed in a config on standard input, never on the command line) andopenssl(TLS syslog forwarding) as subprocesses. The only external communication enabled by default, apart from package-manager updates, is the following. (1) Whilecve_kernel_map_updates_enabled(default true) is on,roamswitch-updaterreceives signed data anonymously from lafine.net. This covers the kernel CVE data for container-isolation auditing, the web application detection rules (web rules) and, on Ubuntu servers, the Ubuntu package CVE data used by the OS-package CVE check. The signed manifest and its signature are checked every day (two GETs); the kernel CVE data and the Ubuntu package CVE data are fetched only when about 30 days have passed since the last refresh (a server that has no Ubuntu data yet fetches it at the next daily check), and the web rules only when the manifest has a newer version. It is a plain HTTPS GET with no query string and no cookies, whose only header that names the software is a fixedUser-Agent: RoamSwitch-Updater; only data whose Ed25519 signature verifies is accepted, and nothing about this host is sent. The operator can turn it off withroamswitch server config set cve_kernel_map_updates_enabled false, the setup wizard or the server TUI; an explicit false in an existingserver.confis kept as it is, and the audit still runs from the embedded baseline when it is off. The OS-package CVE check on an Ubuntu server, however, then reports that no local CVE data is installed and returns no result. Every other exception is something the operator explicitly opts into. (2) Event forwarding (forward_enabled, default false) sends only to the syslog / JSON Lines file / signed webhook destinations the operator configured. (3) Only ifnpm_audit_signatures_enabled(default false) is enabled and the operator runsroamswitch audit-npm-signatures <DIR> --confirmdoesnpmitself contact registry.npmjs.org. (4) Only ifsensor_pairing_enabled(default false) is enabled does it connect, and only to the Sensor address the operator enters (the Sensor is a separate product and is not covered by Zero Telemetry). (5) Fleet (Business tier) is device-side code only: a device that is not enrolled sends nothing and does not resolve any name; the management service is not deployed or released, and by design there is no remote-command path. (6) The LLM integration of the bundled separate toolaccessscope(access-log security diagnosis) is off by default. Only when it is enabled (on the command line, when--llmis given) may a diagnosis summary, evidence log lines and environment information (for the log-finding feature) be sent to an external service through the LLM command the operator chose. The daemon (roamswitch-server) takes no part in this communication, AccessScope can run its LLM analysis as root (the LLM then runs with root's rights, so an unprivileged user is recommended), and it is outside Zero Telemetry. The bundled separate toolpersonalsoc(checks logs and defenses) works the same way: only when--llmis specified, a summary of the diagnosis may be sent to an external service through the LLM command the operator chose (outside Zero Telemetry). Thepathscopecommand does not call an LLM. (7) Only ifclamav_enabled(default false) is turned on does the daemon runfreshclamto receive ClamAV signatures (once when no signature database exists yet, and, whileclamav_scan_dirsis set, everyclamav_freshclam_interval_secs, default 7200 seconds). - Inbound Default Drop: New inbound TCP connections to any port other than the explicitly declared service ports and the administrative SSH maintenance ports are discarded at the
nftableskernel level without a TCP RST. Loopback, established connections and ICMP/ICMPv6 are still accepted. - Fail-Closed Autonomous Containment: The default-drop and isolation rules live in the kernel, so they survive daemon restarts and process crashes (
Restart=always). The Docker published-port guard is re-checked every 60 seconds, and the egress blocklist and port-scan block tables every 300 seconds, and any rule that disappeared is re-installed. The DEGRADED isolation rules are checked on the 15-second evaluator tick and re-installed if missing. If the default-drop ruleset itself is flushed from outside, it is not re-applied automatically; the โInbound default denyโ check ofroamswitch server statusreports it. - Protection by 0600 permissions: the configuration file
/etc/roamswitch/server.confis forced to mode 0600 whenever it is created or updated. Secrets such as tokens and webhook URLs are also handed tocurlin a config on standard input and are never placed on the command line (which other users can see through/proc/<pid>/cmdline).
ยง2Threat Landscape & Adversary Model
Modern internet-facing servers confront relentless automated scanning, container breakout vectors, and zero-day kernel privilege escalations.
- Automated Reconnaissance & DB Exposure: Internet-wide scanners locating unintentionally exposed internal database ports (MySQL 3306, Redis 6379, Postgres 5432).
- Critical Path Tampering & Backdoor Insertion: Intruders establishing persistence by modifying core authentication binaries (
/bin/login,/usr/bin/sudo,/etc/pam.d/*,/etc/shadow). - Kernel Privilege Escalation (CVE-2026-53362 Frag Gap): Unprivileged attackers weaponizing user namespaces (
unshare -U -r) to trigger kernel memory corruption and escalate to root. - Container Socket Breakout: Misconfigured containers with mounted
/var/run/docker.sockor--privilegedflags executing root actions on the host OS. - Process Memory Snooping: Attackers attaching
ptraceor reading/proc/$pid/memacross processes to extract private keys or credentials.
ยง3System Architecture
RoamSwitch Server Edition operates as native Rust binaries designed for minimal memory and zero CPU overhead.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ User & administrator space โ
โ โ
โ roamswitch (CLI) roamswitch-mcp (AI Agent) โ
โ ยท server status (34-item) ยท security audit tool suite โ
โ ยท server config / setup ยท stdio JSON-RPC โ
โ ยท fim verify / update โ
โ โ โ โ
โโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโ
โ (CLI commands & config apply) โ (stdio)
โโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโ
โ System foundation (systemd + root privilege) โ
โ โ
โ roamswitch-server-daemon โ
โ โโโ CriticalPathFim (SHA-256 background sweep) โ
โ โโโ eBPF / Falco integration module โ
โ โโโ Notifier (instant Telegram / LINE / Webhook alerts) โ
โ โโโ Air-Gap Controller (deploys nftables emergency rules) โ
โ โ
โ Config: /etc/roamswitch/server.conf (permission 0600) โ
โ Integrity DB: /var/lib/roamswitch/fim.db โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ยง4Core Defense Mechanisms
4.1 Boundary Security (Inbound Default Drop)
nftables applies policy drop to the input chain of the inet roamswitch_server table, permitting established connections, loopback, ICMP/ICMPv6, and new inbound TCP connections only to the configured ssh_ports and allowed_ports.
Even if an Air-Gap (emergency isolation) is triggered, the admin paths โ the maintenance SSH ports (ssh_ports) and the registered maintenance source IPs (whitelist_ips), both new and established connections โ are kept in the ruleset, which prevents a situation where remote recovery becomes impossible (preserve_ssh_on_isolation, default true). Every other established connection (an attackerโs already-open C2 or reverse shell, for example) is cut by the default-drop chains. If no admin source is specified (whitelist_ips and 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. With preserve_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.
4.2 Critical Path File Integrity Monitoring (FIM)
Monitors the SHA-256 hash integrity of the files most critical to boot, authentication, networking and system administration. The scope is 23 fixed paths (/etc/shadow, /etc/passwd, /etc/sudoers, /etc/ssh/sshd_config, /bin/login, /usr/bin/sudo, /usr/sbin/sshd, /usr/lib/systemd/systemd and others); every file directly inside 12 directories (except known_hosts and known_hosts.old in /root/.ssh, which are rewritten on every SSH connection) (/etc/pam.d, /etc/sudoers.d, /etc/ssh/sshd_config.d, /etc/cron.d and /etc/cron.{hourly,daily,weekly,monthly}, /etc/systemd/system, /etc/ld.so.conf.d, /etc/profile.d, /root/.ssh), where the appearance of new files is also detected; RoamSwitchโs own binaries; each login-capable userโs authorized_keys; and /etc/ld.so.preload, which must not exist (its appearance is detected). After legitimate package updates the good baseline is refreshed automatically: on Debian-based systems by the /etc/apt/apt.conf.d/99roamswitch-fim hook (apt upgrade and so on), and on RHEL-based systems and openSUSE by a systemd.path unit watching /var/lib/rpm (roamswitch-fim-update.path); both run roamswitch fim update.
Identifying the 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.
The measurements were made on an Ubuntu 24.04 VM with the operator's source placed in a separate network namespace (netns). With sudo tee, with a process detached by nohup (its source IP was still identified after the operator logged out, with the note "That session has already ended"), with a sudo -i chain and with a very short-lived cat, the notification showed the IP, the user and the authentication method. A local login produced "No SSH login was found". A process that disguised its name with exec -a "sshd: โฆ" was not taken for SSH.
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.
4.3 Kernel & Container Hardening
Frag Gap (CVE-2026-53362) Mitigation: Enforces user.max_user_namespaces = 0 via sysctl, neutralizing LPE vectors. Yama LSM Protection enforces kernel.yama.ptrace_scope = 2, preventing unauthorized memory inspection. Container workloads are audited for docker.sock exposure. On the Server Edition this prohibition of unprivileged namespaces is on by default (harden_userns_enabled). It breaks rootless Podman and similar, so set it to false if you rely on them.
Dockerโs published ports (-p) are known to bypass the hostโs standard firewall, so with protect_docker_ports=true (the default) traffic arriving from the external interface whose pre-DNAT destination port is not among the allowed ports (ssh_ports, allowed_ports) is dropped (return traffic, loopback and container-to-container traffic are not affected). On the iptables / iptables-nft backend the rules go into the DOCKER-USER chain (IPv4, and also via ip6tables when Docker IPv6 is on); on Dockerโs native nftables backend (Docker 29 and later, a setup with no DOCKER-USER) they go into a dedicated inet roamswitch_docker_guard table (forward hook, priority -10), and that single ruleset covers both IPv4 and IPv6. The daemon re-checks periodically and reinstalls rules that disappear. In addition, an always-on guard detects the start of a --privileged container or a /var/run/docker.sock mount in real time through docker events and notifies you immediately as a container-escape risk (no configuration needed; notify-only, with no automatic blocking). For hosts that handle files handed to others, such as mail relays or shared folders, the optional File Scan Guard (built-in YARA plus optional ClamAV) provides periodic scanning and automatic quarantine.
Container Exec Guard (container_exec_guard_enabled, default true). It sets fanotify FAN_OPEN_EXEC (notification class) on the overlay2 root of each running Docker container and inspects the content of every executed file with the built-in YARA rules, notifying on a match. Because it subscribes to docker events and then lists running containers with docker ps, containers that were already running when the daemon started are covered too. The executed fileโs content is read from the file descriptor that fanotify hands over, so it can be inspected even if the file deletes itself right after running. It is notify-only and does not stop or deny execution (the Server Edition has no permission-class fanotify at all). Docker 29 dropped the deprecated Status from docker events messages, so up to 1.11.0 the subscription failed at once and the guard kept reconnecting every 5 seconds without ever getting a subscription. Since 1.11.1 it uses Action, which has the same value on older Docker too. When the subscription keeps failing, the reconnect interval doubles from 5 seconds up to 5 minutes, and the warning for the same cause is logged only the first time.
4.4 eBPF Runtime Threat Telemetry & Anti-Bloat Tuning
Utilizes Modern eBPF (CO-RE / BTF) probes to intercept privilege escalation and container breakout attempts in milliseconds. To eliminate the standard pain points of Falcoโlog bloat, disk exhaustion, and CPU overheadโRoamSwitch standardizes an optimized profile (/etc/falco/config.d/99-roamswitch-optimized.yaml):
- Severity Filtering (
priority: warning): Suppresses noisy operational telemetry (Info/Notice) by 95%, surfacing strictly actionable security incidents. - Kernel-Level Pre-Drop (
drop_failed_exit: true): Discards failed syscall exits directly in the kernel probe, cutting CPU context switches by over 50%. - Memory Footprint Optimization (
cpus_for_each_buffer: 2): Compresses ring buffers from 8MB down to 2MB, strictly limiting memory usage to 30โ50MB. - Direct UNIX Domain Socket IPC: Direct streaming to
/run/roamswitch/events.sockcompletely eliminates disk I/O thrashing. - Automated Log Cap (
/etc/logrotate.d/roamswitch-falco): Daily rotation withmaxsize 50Mand 7 compressed cycles keeps Falco log growth in check.
Accuracy and speed of eBPF containment (not left to Falco's rules)
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).
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.
4.5 Emergency Air-Gap Isolation with Pre-Severance Alerts
Immediately prior to cutting network interfaces upon critical tampering, the daemon synchronously dispatches emergency alerts to Telegram, LINE, and Webhooks. Alerts automatically append the server Hostname and Primary IP Address.
๐จ [RoamSwitch IMMINENT AIR-GAP] CRITICAL: IMMINENT AIR-GAP HOST ISOLATION (cve_2026_53362_lpe)
๐จ CRITICAL SECURITY INCIDENT DETECTED: cve_2026_53362_lpe
Host: production-node-01 (IP: 192.168.1.10)
Process: exploit_payload (PID: 4921)
ACTION: Initiating host Air-Gap network isolation (SSH preservation active).
WARNING: Outbound internet connectivity will be cut immediately after this message.
Timestamp: 2026-09-06 18:00:00
โ ๏ธ Network connectivity will be severed immediately.The release conditions depend on the confidence of the trigger. Low-confidence triggers (an eBPF Error-tier event and below, for example) are fully restored on a short timer (server_safeguards.safety_timer_secs in guard.yaml, default 300 seconds). High-confidence triggers (eBPF Critical/Emergency/Alert, escalation to host isolation through a rule that keeps re-firing, canary tampering, and so on) never restore silently. From the short timer onward the cause is re-checked (is the frozen PID still alive, did the rule fire again, and so on), and the isolation is released with a notification only when the cause is verified gone. If it cannot be verified and there is no ACK by the cap (isolation_hard_cap_secs, default 3600 seconds; 0 = no cap), it moves to DEGRADED. DEGRADED keeps the admin path (maintenance SSH ports and maintenance source IPs, new and established), allows new outbound traffic only to the maintenance source IPs, and never re-opens on a timer. Release is only by a re-check that verifies the cause is gone, by roamswitch server ack (an ACK during DEGRADED means release), or by roamswitch emergency-restore. An isolation that has been ACKed while FULL is treated as taken over by the operator: it neither restores on the timer, nor releases automatically even when the cause is verified gone, nor moves to DEGRADED, and stays until emergency-restore releases it. Every transition is written to the incident timeline and notified.
Process-execution recorder & event forwarding
A local-only recorder that logs exec through the netlink process connector runs enabled by default (shared with the Client Edition). It stores the executable path, the command-line arguments (length-capped, control characters escaped), the parent-process lineage, the uid, the container ID, the executableโs SHA-256 and similar fields in /var/lib/roamswitch/exec_log as JSON Lines with a hash chain (0600; 200 MB / 14 days by default). The hash chain makes tampering detectable but not preventable (an attacker who gains root can rewrite the whole chain). The notify-only correlation rules EXR-001 to EXR-010 (a shell spawned by a web service, execution from /tmp or memfd, attack-tool chains, and so on) never isolate on their own. Only if an operator explicitly enables exec_rules_isolation_optin (default false) is a high-severity rule treated as a high-confidence trigger for host isolation.
Event forwarding (forward_enabled, default false) sends only to the destinations the operator configured: syslog (RFC 5424 or CEF, over UDP / TCP / TLS), a JSON Lines file, or a webhook signed with HMAC-SHA256. TLS syslog goes through the system openssl s_client and the webhook through the system curl (secrets are passed in a config on standard input, never on the command line); the daemon has no TLS/HTTP crate. What is sent is limited to the fields host name, event ID, timestamp, source, severity, rule, action, PID, process name and summary โ for the detection events that appear in the incident timeline and notification history (eBPF, isolation-state transitions, FIM, canaries and so on) and for execution-rule notifications โ plus the execution logโs segment seal hashes. The raw execution log is not included.
DNS Tunneling / Exfiltration Detection
Enabling dns_tunnel_detect_enabled (default: true) inspects this host's own outbound DNS queries (UDP/TCP port 53 only) through a dedicated NFQUEUE consumer. A query is counted as โsuspicious-shapedโ โ the pattern characteristic of DNS-tunneling tools such as iodine or dnscat2 โ when its label is over 40 bytes long AND its Shannon entropy is over 3.8, or when its record qtype is TXT/NULL (TXT queries to clamav.net, used by ClamAV signature updates, are not counted); a notification is sent only once 10 or more such queries occur within a rolling 60-second window (repeat notifications for the same domain wait 300 seconds, then 600 and 1200 seconds while the burst continues, up to a maximum interval of 1800 seconds). Only DNS (UDP/TCP port 53) is inspected; no other traffic is examined. It is notify-only and never blocks automatically. The underlying nft rule carries the bypass flag, so DNS queries pass through unaffected even if the detector thread is stopped or unresponsive, so name resolution is never blocked by this feature failing. A change to this setting takes effect only after roamswitch server restart (no live reload).
Known limitation: long, hyphen-separated hostnames (for example, prod-us-east-1-web-server-node-0042) and legitimate activity that is especially realistic on a server โ such as a mail server verifying SPF/DKIM TXT records for many different sender domains in a short window โ were previously measured to trigger false positives here, because TXT/NULL bursts were counted with a single global counter that did not distinguish destination domains. Both were fixed on 2026-09-23. The fix is a configurable entropy allowlist plus keying the burst counter per apex domain (a simple approximation using the last two labels of the QNAME, not a strict Public Suffix List implementation). The allowlist ships with 16 major CDN/cloud/PaaS suffixes by default (cloudfront.net, akamaiedge.net, akamaized.net, edgekey.net, edgesuite.net, fastly.net, azureedge.net, googleusercontent.com, amazonaws.com, r2.dev, workers.dev, vercel.app, netlify.app, supabase.co, firebaseapp.com, web.app), and operators can add their own internal domains via ServerConfig.dns_tunnel_entropy_allowlist (a comma-separated key in server.conf, also editable from the TUI). The allowlist only suppresses the label-length-plus-entropy check; TXT/NULL qtype detection applies regardless of the allowlist, which prevents an attacker from disguising a tunnel under an allowlisted domain (the one exception is TXT to clamav.net, used by ClamAV signature updates, which is not counted because a third party cannot create subdomains under it). The burst counter is now keyed per apex domain, which eliminates the false positives caused by a single global counter that ignored the destination domain (the 60-second sliding window and 10-query threshold are unchanged by this fix; the re-notification interval was later changed to grow from 300 seconds up to 1800 seconds while a burst continues). Real malicious traffic from tools such as iodine or dnscat2 always rotates labels and qtypes under a single attacker-controlled domain, so narrowing to per-apex-domain counting does not reduce detection effectiveness.
SSH brute-force detection and blocking (ssh_guard)
A brute-force defense of RoamSwitch's own, active with ssh_guard_enabled (true by default). It follows sshd's log from journald with journalctl -f (only where journald is absent does it follow auth.log or secure), counts failures per source address, and for a source that reaches the threshold (by default 6 within 600 seconds) drops only new connections (ct state new) to the SSH ports in the inet roamswitch_sshguard table. 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. A ban starts at 1 hour, grows fourfold on each repeat and is capped at 7 days; the nftables timeout handles the expiry. It does not start the Air-Gap and sends nothing out (it only runs journalctl, nft, loginctl and similar commands locally). When fail2ban (with an enabled sshd jail) or CrowdSec (with a running firewall bouncer) already guards sshd, it yields the banning and only detects and notifies.
Log lines contain a string the attacker chooses (the user name). So the source address is read only from the end of the line, from <IP> port <N>, and a forged source address placed in the user name does not get a third party banned. From journald the guard reads only lines whose sender credentials (_UID, _COMM) are those of a root-owned sshd, and ignores lines written with logger -t sshd. Following a file (auth.log) is not the default, because the file has no field that vouches for the sender and any local user who may use syslog can forge lines. So that operators are not locked out, the gateway, DNS, the host itself, loopback, whitelist_ips, ssh_guard_allow, the source of an authenticated SSH session, any address that logged in with a public key within 30 days, and a paired Sensor are never banned. Private addresses are not exempt. A failed public key (Failed publickey) is not counted.
Measured (an Ubuntu 24.04 VM with a real journald and systemd): from the 6th failure to the ban took about 0.2 seconds (0.17 to 0.24 seconds). The operator's own source address was not banned, and an attempt with a forged source address in the user name did not get a third party banned. A real hydra run (4 parallel connections) was stopped after 6 failures. Restoring a ban with its remaining time after the server restarts, lifting a ban, and falling back to detect-and-notify while fail2ban guards sshd were checked as well. These figures come from this VM environment. Measured with the connection cut added: the failures that still got through after a ban numbered 16 to 24 before and 0 to 4 after. Deleting the conntrack entries alone did not cut the connections; they were cut only once a rule dropping the reply direction was added.
Web application rules (web shells and injected code)
The Server Edition had no write-triggered scan, only the periodic scan of clamav_scan_dirs. It now watches the document roots found in the web server configuration (Apache, nginx, Caddy) with notify-only fanotify (FAN_CLASS_NOTIF, which can never stall another process) and checks PHP, JavaScript, JSP and ASP files the moment they are written and closed, against the web application rules (web shells, injected obfuscated code, PHP hidden in images, .htaccess abuse). A periodic scan every 6 hours covers dropped events and files that were there before the start. The rules come in three layers (built in, installed by the daily updater after signature verification, and your own files in /etc/roamswitch/rules.d/*.json); the 18 rules contain no signature extracted from a real sample and describe publicly documented techniques only.
On the Server Edition, automatic quarantine is critical only by default. A high finding only notifies and fires the post-detection hook, and the file of a running site is left alone. Notifications (Telegram, LINE, webhook) are in 10 languages and name the rule and whether the file was quarantined or left. The rule fetch rides on the same single fetch as the daily updater and adds no new destination. With cve_kernel_map_updates_enabled off there is no network contact at all and the built-in rules are used. With a real fanotify watch, a web shell that was written was quarantined within 2 seconds and a harmless file was left alone.
After a detection, an executable placed at /etc/roamswitch/hooks/post-detection receives the hits as JSON on standard input (only a root-owned, non-writable file in a safe directory runs; there is no shell, the environment is minimal and it is stopped after 30 seconds). The optional account response (off by default) revokes the login shell of the owner of a quarantined file. It does nothing for root, accounts below account_min_uid (default 1000), anyone who is logged in, or the daemon's own user; the previous shell is recorded and roamswitch web-rules unlock-account restores it. Measured false positives: 0 in WordPress core (2,409 files), phpMyAdmin (3,356 files) and npm dependencies (14,743 files). Limits: only the first 256 KB (on write/execute) and 4 MB (scan) are read, split-up names such as 'ev'.'al' are not followed, and no per-malware signatures are shipped because there are no real samples.
Honeytokens (Credential Decoys)
On daemon startup, RoamSwitch idempotently deploys decoy credential files at known real paths that attackers and automated credential-stealing tools actually scan (/root/.aws/credentials, /root/.ssh/id_rsa, /root/.docker/config.json, /root/.pypirc and /root/.env.backup, under /root since the daemon runs as root), each containing a ROAMSWITCH_HONEYTOKEN marker (on by default; honeytokens_enabled turns it off and honeytokens_home changes where the decoys go). A real file that already exists at any of these paths is never overwritten (deployment always checks for existence first). Real access to a honeytoken can also trigger a capped cascading deployment of related additional decoys (an extra AWS profile, an SSH config, a kubeconfig, and similar). 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.5 as well. Since 1.11.1, every docker command that RoamSwitch itself runs is started with an empty private directory as DOCKER_CONFIG, so it does not trigger the decoy /root/.docker/config.json. A read by any other program is still detected as before.
A background watcher (roamswitch-honeytokens) uses notify-class fanotify (it blocks nothing) to monitor reads and opens of every decoy. The event carries the accessing PID, so the accessing executable is identified on the spot (where fanotify is unavailable it falls back to inotify plus fuser). Since no legitimate process has any reason to read these files, any access other than by RoamSwitch's own binaries, or by an OpenSSH client (ssh, scp, sftp, ssh-add, ssh-agent and similar, but not ssh-keygen) on the SSH decoys, is treated as a strong signal of an intruder's post-compromise reconnaissance (MITRE ATT&CK T1552, Unsecured Credentials) and a notification is sent. An access whose executable cannot be identified is not allowed through but treated as a detection. This is notify-only; it does not itself freeze or block the accessing process.
Known limitation: the body of the /root/.ssh/id_rsa decoy contains readable text and remains an invalid base64 body for a real OpenSSH private key. However, most secret scanners flag files purely by matching the -----BEGIN ... PRIVATE KEY----- header, so we judge the real-world impact on detection rate to be limited. Separately, on hosts where fanotify is unavailable and the watcher falls back to inotify, identifying the accessing process (via a fuser call) can fail to obtain a PID or process name for a very short-lived process that exits immediately after reading the file; in that case the process is simply recorded as unknown, which does not affect detection of the access itself (the event still fires and is still notified).
Forensic Evidence Capture Bundles
Evidence capture is launched in these cases. An eBPF runtime threat sensor event results in a successful per-process network isolation; that isolation fails and escalates to a host-wide Air-Gap; or a host-wide Air-Gap is executed. It is also launched on the Ransomware Canary path, when tampering is detected and the process is frozen and the host isolated. Where the cut is host-wide, the capture is launched before the cut and never delays it. Up to 1.10.4 it ran only after a successful per-process isolation; 1.10.5 widened that. If no target PID is known, the /proc snapshot is skipped. roamswitch-incident-capture is launched and saves an evidence bundle to <state directory>/incident-evidence/<timestamp>-<short id>/. What is captured: a bounded 30-second packet capture via tshark on the default-route interface (recorded as skipped if tshark is not installed), and a /proc/<pid> snapshot of the target process (status, cmdline, maps, an environ with values that look like secrets replaced by [REDACTED], a list of open file descriptor link targets (contents are not captured) and the process's ancestor chain). A single artifact's capture failing is not treated as fatal; it is recorded and the rest of the capture continues. The packages up to 1.10.4 did not include roamswitch-incident-capture, which this feature needs, so it did not run on installs from the packages. It has been bundled since 1.10.5.
The results are recorded in manifest.json, which also embeds the SHA-256 hash of every other file in the bundle (so later tampering can be detected). Bundles are never deleted automatically; old bundles are removed only when an operator explicitly runs sudo roamswitch-incident-capture prune [--keep-days N] (default retention: 30 days). A manual capture is also available at any time via sudo roamswitch-incident-capture snapshot [--pid PID] [--reason TEXT] [--json].
Captured evidence is not verified at capture time. To check for tampering afterward, run sudo roamswitch-incident-capture verify <bundle directory> [--json]. It re-hashes every file currently in the bundle and compares the result against the values recorded in manifest.json, reporting each discrepancy individually as modified, missing, or untracked (a file present that the manifest never recorded).
Known limitation: this verification compares the SHA-256 of manifest.json with records kept outside the bundle, in two places: a hash chain outside the bundle (evidence_anchor.log, each line committing to the previous one, set append-only where the filesystem allows) and the system log (tag roamswitch-evidence). Rewriting manifest.json after capture makes verify fail, and so does editing or removing a line of the chain, because the chain then breaks. It does not stop an attacker with root who can rewrite both the chain and wherever the system log is forwarded; forwarding the system log to another machine is recommended. Signing is not implemented. A bundle without a record (captured by an older version) is reported as โno recordโ, meaning there is no outside confirmation for it.
ยง534-Check Security Diagnostic Suite
The Server Edition executes 34 specialized security health audits:
| # | Category | Check Title | Standard & Logic |
|---|---|---|---|
| 1 | Auth & Access | SSH Root Login & Password Auth | Verifies PermitRootLogin no. Reports key-only access when password authentication is also disabled, and still passes when password authentication is enabled as long as direct root login is prohibited |
| 2 | Auth & Access | Empty Password Accounts | Ensures no user has blank password hash in /etc/shadow |
| 3 | Auth & Access | Sudoers Privilege Hygiene | Verifies that /etc/sudoers and /etc/sudoers.d contain no passwordless full-privilege NOPASSWD: ALL rule |
| 4 | Network | Inbound Default Deny | Audits nftables for default inbound drop policy |
| 5 | Kernel & Integrity | Unprivileged User Namespaces (Frag Gap) | Ensures user.max_user_namespaces = 0 (or kernel.unprivileged_userns_clone = 0) (CVE-2026-53362) |
| 6 | Kernel & Integrity | eBPF BTF Support | Validates kernel BTF type information via /sys/kernel/btf/vmlinux |
| 7 | Kernel & Integrity | eBPF LSM Active | Checks for bpf in kernel lsm= boot parameter |
| 8 | Network | XDP Boot-Time Gate | Fills the boot-time window before the Link Guard nftables table is installed with an XDP default-deny gate (experimental, opt-in and not loaded by default). Checks for the pinned link; not being loaded is not a failure |
| 9 | Kernel & Integrity | eBPF Runtime Security Agent | Checks that an agent such as Falco or Tetragon is running. While Falco runs, it also checks that the alert of RoamSwitch's own rule comes back (the heartbeat), and fails if it does not (naming any missing RoamSwitch Falco file) |
| 10 | Kernel & Integrity | Unified Incident Timeline | Checks that the unified timeline merging Link Guard, Canary, eBPF Guard and Resource Guard incidents chronologically can record (existing events, or a writable store), and shows the latest containment time |
| 11 | Container | Docker Socket Mount Audit | Detects container mounts of /var/run/docker.sock |
| 12 | Container | Docker Privileged Containers | Audits for high-risk --privileged container workloads |
| 13 | Container | Container Runtime Isolation (runc/gVisor/Kata) | Checks the default container runtime. A runc-based runtime (shares the host kernel) fails; gVisor (runsc) or Kata Containers (per-container VM isolation) passes. Not applicable if Docker/containerd is not detected |
| 14 | Container | Docker Firewall Bypass Protection (DOCKER-USER) | Verifies that RoamSwitch's own rules are actually present in the DOCKER-USER chain (always evaluated first) or in its dedicated Docker-guard nftables table, so published container ports cannot bypass the firewall's default deny through Docker's own rules. Evaluated only when protect_docker_ports is enabled in server.conf |
| 15 | Kernel & Integrity | ClamAV Live Detection Test (EICAR) | Two minutes after start and every 6 hours thereafter, the daemon scans a harmless EICAR test file from its own environment (for example inside a systemd sandbox) to confirm that ClamAV really detects it. Evaluated only when clamav_enabled is on in server.conf |
| 16 | Kernel & Integrity | fanotify Missed Events (Queue Overflow) | Checks whether the kernel has discarded FIM and container-execution watch events because the fanotify event queue overflowed. File writes and executions during an overflow escape real-time monitoring; if it happens often, raise fs.fanotify.max_queued_events |
| 17 | Kernel & Integrity | Known Kernel CVE Exposure | Matches the running kernel version against the local known-CVE map (the embedded baseline, or a signature-verified update). The check itself makes no network requests; map updates are fetched under cve_kernel_map_updates_enabled (default true, can be turned off). An undeterminable result is reported as inconclusive, not as a failure |
| 18 | Integrity | Critical Path File Integrity (FIM) | SHA-256 integrity verification of 23 fixed critical files, every file directly inside 12 directories, and more |
| 19 | Kernel & Integrity | Yama Process Protection | Verifies kernel.yama.ptrace_scope is 2 or 3 |
| 20 | Network | IP Forwarding Disabled | Verifies net.ipv4.ip_forward = 0 |
| 21 | Network | TCP SYN Cookie Protection | Verifies net.ipv4.tcp_syncookies = 1 |
| 22 | Network | Reverse Path Filtering (rp_filter) | Verifies reverse path filtering is on (net.ipv4.conf.all.rp_filter = 1 strict, or 2 loose for multihomed hosts) |
| 23 | Kernel & Integrity | Core Dump Restriction | Prevents process memory leaks via fs.suid_dumpable = 0 |
| 24 | Operations | Automatic Security Updates | Validates automatic patch application via unattended-upgrades (Debian/Ubuntu), dnf-automatic (RHEL/Fedora) and similar |
| 25 | Operations | NTP Time Synchronization | Checks synchronization via chrony or systemd-timesyncd |
| 26 | Operations | SSH Brute-force Protection | Passes while RoamSwitch's own SSH guard (ssh_guard) is running and banning, or verifies active monitoring via fail2ban or crowdsec |
| 27 | Kernel & Integrity | Unprivileged eBPF Disabled | Checks kernel.unprivileged_bpf_disabled is 1 or 2 |
| 28 | Network | ICMP Redirects Ignored | Prevents route poisoning via accept_redirects = 0 |
| 29 | Network | ICMP Echo Broadcasts Ignored | Mitigates Smurf amplifier DoS attacks |
| 30 | Kernel & Integrity | Shared Memory Hardening | Checks that /dev/shm is mounted with at least one of the nodev, nosuid or noexec options |
| 31 | Auth & Access | Legacy Insecure Protocols Absent | Verifies that no cleartext listening services (telnetd, rshd, tftpd, โฆ) and no cleartext clients (telnet, rsh, tftp, โฆ) are installed (services and clients are reported separately) |
| 32 | Kernel & Integrity | SUID Process Core Dumps Restricted | Prevents credential leaks on SUID binary crashes |
| 33 | Kernel & Integrity | Helper Programs (Decoys and Evidence Capture) Installed | The daemon starts the credential-decoy (honeytoken) program and the evidence-bundle program by name, and does nothing, silently, if one is missing. This checks that both are installed. |
| 34 | Network | Sensitive Files Exposed on the Web Server | Finds the document roots from the Apache / nginx / Caddy configuration and checks, with requests to the server itself, whether .git, .env, .DS_Store, phpinfo, SQL dumps, backups and private keys there can really be read through the web server. A file counts as exposed only when the response matches the start of the file on disk, so a site that answers 200 to every path, or a file already denied with 403, is not a false alarm. File contents are never reported, only paths. Not applicable without a web server; if it cannot be verified the result is "inconclusive" and the score is not reduced. |
Each of the 34 items is tagged, where applicable, with a NIST CSF 2.0 function/category identifier (e.g. PR.DS-01, DE.CM-09), and, only where a confident mapping exists, a CIS Controls v8 control number (e.g. 10.1, 4.1) as well. Ambiguous items are left unmapped rather than forced. This mapping is included in both the CLI (roamswitch server status) and MCP (get_security_report) output, letting operators and auditors who need compliance-framework coverage map RoamSwitch's checks onto existing frameworks mechanically.
ยง6Self-Check (Reproducible Verification in a Docker Environment)
An internal and external attack self-check is run against the roamswitch-server package installed from the official APT repository, with separate Docker containers acting as the external attacker. The measurement of 2026-09-07 on v1.3.2 was all 18 items PASS. On 2026-09-25 the suite was run again on a package built from the 1.10 source (arm64, Docker Desktop) and 19 checks passed. The re-run found six product defects, all fixed (included in 1.10.5): the package did not ship two helper binaries, the FIM reported the honeytoken decoys as new files, an operator acknowledgement (ack) was ignored, during isolation the replies of the preserved SSH channel were dropped and locked the operator out, port 22 stayed open even with preserve_ssh_on_isolation=false, and the systemd unit sandbox blocked the decoy and canary writes. You can reproduce this check on your own machine. See "Reproducing it yourself" below for the procedure and scripts.
| # | Scenario | Attack Technique | Result |
|---|---|---|---|
| PENT-S1 | Nmap SYN Stealth Scan | Probe across ports 22, 80, 443, 3306, 8080 | The nftables default drop discards traffic to unpermitted ports such as 3306 (MySQL) and 8080 without any response (Filtered). 100% blocked. |
| PENT-S2 | Unallowed Port Probing | curl direct connections to port 3306 and 8080 | With inbound default deny, only the permitted port 80 answers normally; all other ports are dropped and time out. |
| PENT-S3 | Egress / C2 blocklist | Outbound connection attempt to an address registered in the malicious-IP feed | Only the registered address is blocked by egress_ip_blocklist_enabled (default true, nftables); traffic to a non-blocklisted host is unaffected (not a blanket block) |
| PENT-S4 | FIM Tampering Attack | Inject backdoor payload into /bin/login |
SHA-256 Critical Path FIM detects the tampering immediately. After a legitimate restore, the baseline resynchronizes correctly. |
| PENT-S5 | guard.yaml severity-based containment |
Injected a Falco Critical event referencing a real process | With on_critical: isolate_process + kill_process, the offending process is network-isolated and then sent SIGKILL. |
| PENT-S6 | Protected-process safety rail | Injected the same kind of event referencing the real sshd PID |
The protected-process list (sshd, systemd, container runtimes and so on) keeps sshd running. Only a refusal is logged, as a false positive. |
| PENT-S7 | Safety timer | Left an Air-Gap incident unacknowledged (no operator ACK) | With safety_timer_secs, the host restores itself automatically when nobody responds. Once ACKed, isolation continues until emergency-restore. |
| PENT-S8 | Kernel LPE Frag Gap | Execute unshare -U -r as unprivileged user (checked on a real VM) |
Refused after start, although it is allowed without the product. user.max_user_namespaces and kernel.unprivileged_userns_clone were 0, Yama ptrace_scope was 2 and unprivileged eBPF was disabled (2026-09-25, Ubuntu 24.04 VM, checked on both the published v1.10.4 and the fixed build) |
Note: the PENT-S7 measurement dates from v1.3.2. In the current version an isolation from a high-confidence trigger is released once its cause is verified gone, and an isolation the operator acknowledged (ack) is held until the operator releases it (an ack that was ignored was found and fixed in the 2026-09-25 re-run). Low-confidence triggers recover with the same timer. See ยง4.
Kinds of checks — PENT-S8 confirms that a kernel feature (denying user namespaces) is in effect. PENT-S1 and S2 confirm the nftables rules RoamSwitch installs; the kernel is what actually drops the packets. The items that exercise RoamSwitch’s own logic are PENT-S3 (feed application), S4 (FIM), S5 and S6 (containment and protected processes) and S7 (safety timer).
SSH Lockout Prevention
preserve_ssh_on_isolation=true (default) is designed to keep the admin SSH channel alive during an Air-Gap (on_emergency). The 2026-09-25 re-run showed, however, that up to 1.10.4 the isolation ruleset dropped the SSH replies, so the operator was locked out during isolation; the earlier check had only succeeded after the isolation had been released. This was fixed in 1.10.5, and SSH passing during isolation is verified with the isolation cause still present.
Scope Limits (an Honest Disclosure)
Kernel hardening (disabling unprivileged user namespaces, raising Yama's ptrace_scope) cannot be verified end-to-end from inside a container. Those sysctls are not namespaced per-container, so a privileged (--privileged) container's writes to them land on the real machine running the test, not a sandbox. This verification therefore only confirms that the sysctl write degrades gracefully (no crash) against a read-only filesystem; confirming the value actually lands requires a real machine or VM, which is how the product is meant to run anyway. That confirmation was done once, on an Ubuntu 24.04 VM on 2026-09-25 (see the PENT-S8 row).
Reproduce It Yourself
Every result above can be reproduced 100% on your own machine, using nothing but the official distribution package, via the Docker test suite published in the official support repository (roamswitch-support) โ no source-code access, and no need to trust us, required.
# Fetch and reproduce the verification suite (automatically installs roamswitch-server from the official APT repository)
git clone https://github.com/lafine1211/roamswitch-support.git
cd roamswitch-support/test/docker/server
./run.sh
Builds three disposable containers (Target, Attacker, and a C2 decoy) on Docker's default bridge network and automatically verifies all 18 items, printing a report. The full run output on v1.3.2 is published at RESULTS-PENTEST-SERVER-2026-09-07.md (including, for transparency, the test-harness issues found and fixed while building it). The record of the 2026-09-25 re-run is in RESULTS-PENTEST-2026-09-25.md.
ยง7Operations, Deployment & Lifecycle
7.1 Headless CUI Tooling & Setup Wizard
Complete remote management via CLI:
roamswitch server status: Full 34-check diagnostic output (10 languages, following the invoking user's setting or the OS locale).roamswitch server setup: Interactive terminal wizard for ports and notifications.roamswitch server config show / set <key> <val>: Scriptable automation for Ansible/Terraform.roamswitch server test-notify [all|telegram|line|webhook]: End-to-end notification probe.roamswitch server config set language <ja|en|zh-Hans|zh-Hant|ko|de|fr|es|it|pt|system>: the language of the daemon's periodic diagnostic report (hourly) and of push notifications (Telegram / LINE / webhook, including the test message), one language per host.systemfollows the OS locale, and a locale that cannot be identified falls back toja. The display language ofroamswitch server statusdoes not follow this setting but the invoking user's setting or the OS locale.roamswitch server bans [--json]/sudo roamswitch server unban <IP|IPv6 /64|--all>: the state of the SSH brute-force guard, and listing and lifting its bans. The MCP toolget_ssh_guard_incidentsis read-only and cannot lift a ban.
Server TUI (roamswitch server tui)
A terminal UI that handles the whole of the Server Edition over SSH, in 80x24, with the keyboard alone (roamswitch server tui, or roamswitch-server-tui). It has 11 screens: dashboard, protection, incidents, notifications and forwarding, Sensor, configuration, service, tools, log audit, info and help, and Fleet. The configuration editor is generated from the key definitions (the schema) of server.conf and guard.yaml: it validates type, range and enumerated values, shows whether a restart is needed, shows a diff before saving, and saves with a timestamped backup and an atomic write (secret values such as tokens are masked and entered without echo). High-impact operations such as restoring an isolation, ACK, updating the FIM baseline and stopping the service show a confirmation screen that states the effect, and the highest-impact ones require typing yes (typed confirmation). Without root it is read-only and points to the sudo roamswitch ... command that is needed. There is no configuration reload; a restart of the service is needed for changes to take effect. The display supports 10 languages.
7.2 Packaging
Distributed via official Debian/Ubuntu .deb repositories and RHEL/Fedora and openSUSE .rpm repositories.
ยง8Conclusion
RoamSwitch Server Edition provides cloud and enterprise Linux infrastructure with โzero telemetry (the only external communication by default is a receive-only fetch of signed data that can be turned off)โ, โstrong perimeter protection through inbound default denyโ, โimmediate detection of critical-path tamperingโ and โpreventive blocking of kernel privilege escalationโ in a single integrated package.
A security suite for Linux servers that combines layered detection with autonomous containment (Air-Gap) across the main stages attackers target: initial reconnaissance, intrusion, privilege escalation and persistence.