By the time ZimaOS onboarding finishes, a ZimaBoard 2 is already a member of a ZeroTier network. Nothing in the setup flow asks about it. That network is set to public, the box ships with no firewall, and SSH, Samba and a web terminal all listen on every interface the board has, including the virtual one. None of that makes ZimaOS unsafe on its own. It does mean the defaults assume a trusted network and a Remote ID nobody else will ever see, and you should decide for yourself whether those assumptions hold.
So, is ZimaOS safe? This guide answers that with evidence from a real board: every port ZimaOS opens, how ZimaOS remote access over ZeroTier actually works, the published ZimaOS vulnerabilities and which release fixed them, and the gaps (no firewall, no 2FA, password SSH, NTLMv1 on Samba). Then it closes them in seven steps. If the board is still in its box, start with our ZimaBoard 2 setup guide in this series, which covers first boot, the update and enabling SSH.
Is ZimaOS safe? The short answer
ZimaOS is reasonably safe on a home LAN once it is updated, and noticeably less safe than it looks if you leave the defaults alone. Our board shipped on ZimaOS 1.5.0, a release covered by a critical login bypass that IceWhale fixed in 1.5.3, so the first update is a security fix, not a nice-to-have. After updating, the unauthenticated API endpoints named in past advisories all refused us. The weak points that remain are configuration, not bugs, and every one of them has a fix below.
| What we checked | ZimaOS default | Risk | Fix |
|---|---|---|---|
| Remote Access (ZeroTier) | On, public network, board is its own controller | Anyone holding the Remote ID can join | Step 2 or step 3 |
| Firewall | None: INPUT policy ACCEPT, no nftables | Every service reachable on every interface | Step 7 |
| SSH | Root login off, per-source penalties on, passwords allowed | Password guessing, slowed but not stopped | Step 4 |
| Web terminal (ttyd, port 7681) | Runs with SSH, plain HTTP | Password crosses the network unencrypted | Step 4 and step 7 |
| Web UI | HTTP only; HTTPS optional, no redirect | Session traffic readable on the wire | Step 5 |
| Samba | SMB2 minimum, NTLMv1 permitted, force user root | Weak auth accepted, files written as root | Step 6 |
| Two-factor login | Not supported | One password guards the dashboard | Keep the UI off the internet |
| Known CVEs | Fixed in current releases | Old images are exposed | Step 1 |
What a fresh ZimaOS install exposes
Start with what is listening. On the board (over SSH, as the admin user you created during setup) list every TCP socket that is not bound to loopback:
sudo ss -tlnp | grep -v 127.0.0.1
Everything here binds to 0.0.0.0 or *, which means the LAN, Docker’s bridge, the libvirt bridge and the ZeroTier interface alike:
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 0 0.0.0.0:3702 0.0.0.0:* users:(("wsdd2",pid=1826,fd=9))
LISTEN 0 0 0.0.0.0:3702 0.0.0.0:* users:(("wsdd2",pid=1826,fd=8))
LISTEN 0 0 0.0.0.0:3702 0.0.0.0:* users:(("wsdd2",pid=1826,fd=7))
LISTEN 0 0 0.0.0.0:7681 0.0.0.0:* users:(("ttyd",pid=1212,fd=13))
LISTEN 0 0 0.0.0.0:139 0.0.0.0:* users:(("smbd",pid=1731,fd=29))
LISTEN 0 0 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1207,fd=6))
LISTEN 0 0 0.0.0.0:445 0.0.0.0:* users:(("smbd",pid=1731,fd=28))
LISTEN 0 0 0.0.0.0:37163 0.0.0.0:* users:(("znet",pid=2671,fd=26))
LISTEN 0 0 0.0.0.0:5355 0.0.0.0:* users:(("wsdd2",pid=1826,fd=17))
LISTEN 0 0 0.0.0.0:5355 0.0.0.0:* users:(("wsdd2",pid=1826,fd=16))
LISTEN 0 0 0.0.0.0:5355 0.0.0.0:* users:(("wsdd2",pid=1826,fd=15))
LISTEN 0 0 0.0.0.0:38597 0.0.0.0:* users:(("znet",pid=2671,fd=30))
LISTEN 0 0 0.0.0.0:1910 0.0.0.0:* users:(("ssdpd",pid=1614,fd=3))
LISTEN 0 0 0.0.0.0:9993 0.0.0.0:* users:(("zerotier-one",pid=13236,fd=7))
LISTEN 0 0 *:3702 *:* users:(("wsdd2",pid=1826,fd=10))
LISTEN 0 0 *:139 *:* users:(("smbd",pid=1731,fd=27))
LISTEN 0 0 *:45189 *:* users:(("znet",pid=2671,fd=29))
LISTEN 0 0 *:80 *:* users:(("zimaos-gateway",pid=1189,fd=11))
LISTEN 0 0 *:22 *:* users:(("sshd",pid=1207,fd=7))
LISTEN 0 0 *:443 *:* users:(("zimaos-gateway",pid=1189,fd=21))
LISTEN 0 0 *:445 *:* users:(("smbd",pid=1731,fd=26))
LISTEN 0 0 *:37573 *:* users:(("znet",pid=2671,fd=31))
LISTEN 0 0 *:5355 *:* users:(("wsdd2",pid=1826,fd=18))
LISTEN 0 0 *:9527 *:* users:(("zimaos",pid=1617,fd=24))
LISTEN 0 0 *:9993 *:* users:(("zerotier-one",pid=13236,fd=8))
Port 443 only appears because we had already switched on HTTPS (step 5). SSH on 22 and the web terminal on 7681 appear once SSH Access is enabled in Developer mode. Samba, the discovery daemons, the ZimaOS backend on 9527 and both remote access services (zerotier-one and IceWhale’s own znet) are there from the first boot. Nmbd also binds NetBIOS name service to the ZeroTier address specifically, so the board announces itself on the virtual network too.
Nothing filters any of it. Check the firewall on both address families:
sudo iptables -S INPUT; sudo ip6tables -S INPUT
The only rule is the jump libvirt adds for its own bridge. The policy is ACCEPT:
-P INPUT ACCEPT
-A INPUT -j LIBVIRT_INP
-P INPUT ACCEPT
-A INPUT -j LIBVIRT_INP
ZimaOS does not ship nft, ufw, firewalld or fail2ban. The iptables it does ship is the nf_tables backend (iptables v1.8.11 (nf_tables)), plus iptables-save and iptables-restore, which is enough to build a firewall in step 7. The root filesystem is a read-only squashfs image, so you cannot install a firewall package, but /etc is a writable overlay whose upper layer lives in /mnt/overlay/upper_etc.
How ZimaOS remote access works: a public ZeroTier network the board controls
The Remote Access feature in Settings, Network is ZeroTier, the same peer-to-peer VPN many homelabs already run. Each ZimaOS device runs its own ZeroTier network controller and hosts a private-to-it network called IceWhale-RemoteAccess. Your board is not sharing a network with other IceWhale customers. IceWhale’s remote access documentation says the same thing: ZimaOS runs its own network controller, so no outside party holds administrative rights over it.
What the documentation doesn’t spell out is how that network admits members. Ask the local ZeroTier service:
sudo zerotier-cli info
sudo zerotier-cli listnetworks
We replaced the real node address and network ID in this output with made-up values of the same shape. Notice that the first 10 hex digits of the network ID match the node address, which is how ZeroTier names a network hosted by that node’s own controller. The type column reads PUBLIC:
200 info 8d4f1c27a0 1.14.0 TUNNELED
200 listnetworks <nwid> <name> <mac> <status> <type> <dev> <ZT assigned ips>
200 listnetworks 8d4f1c27a0e1b5c3 IceWhale-RemoteAccess 6a:1f:03:aa:9e:5b OK PUBLIC zt3j2g3ido fd8d:4f1c:27a0:e1b5:c399:938d:4f1c:27a0/88,10.147.13.1/24
The controller API on port 9993 confirms it. It needs the local auth token, so run these on the board:
TOKEN=$(sudo cat /var/lib/zerotier-one/authtoken.secret)
NWID=$(sudo zerotier-cli listnetworks | awk 'NR==2{print $3}')
curl -s -H "X-ZT1-Auth: ${TOKEN}" "http://127.0.0.1:9993/controller/network/${NWID}" | python3 -c "import json,sys;d=json.load(sys.stdin);print(json.dumps({k:d[k] for k in ['name','private','ipAssignmentPools','routes']},indent=2))"
"private": false is the line that matters. The pool hands out 10.147.13.1 to 10.147.13.254, and the only route is that /24:
{
"name": "IceWhale-RemoteAccess",
"private": false,
"ipAssignmentPools": [
{
"ipRangeEnd": "10.147.13.254",
"ipRangeStart": "10.147.13.1"
}
],
"routes": [
{
"target": "10.147.13.0/24",
"via": null
}
]
}
ZeroTier’s protocol documentation is blunt about what public means: access control is disabled, new members are automatically marked as authorized by the host controller, and it is not possible to de-authorize a member from a public network. The network’s flow rules on our board were the default accept rules, so nothing narrows what a member can reach. Put plainly, the 16-character Remote ID is the credential. Anyone who has it and runs a ZeroTier client can join, get an address in 10.147.13.0/24 and talk to the board’s SSH, Samba, web terminal, web UI and any Docker app ports it publishes at 10.147.13.1. The route covers only that /24, so a joined stranger reaches the board itself, not the rest of your LAN.
We did not join the network from a second machine to prove it, so treat this as the documented behavior of a public ZeroTier network rather than a demonstrated break-in. IceWhale’s own wording lines up: its docs tell you to treat the Remote ID like a password, and the Settings page warns that the ID is critical to the security of the device:

Two other things happen while Remote Access is on. ZeroTier on our board ran in TUNNELED mode, relaying over TCP 443 because it couldn’t punch a direct UDP path. And znet, the IceWhale service that owns a second tunnel interface (tun0 at 10.126.126.1), kept connections open to public DNS-over-HTTPS resolvers on TCP 443 and to six relay servers on TCP 11010. That is how Zima Client finds the board from outside, and it is also outbound traffic you should know about if you watch your firewall logs.
1. Update ZimaOS before anything else
Our ZimaBoard 2 arrived on ZimaOS 1.5.0 in September 2026. That image is affected by the two critical advisories in the table below, one of which lets anyone log in to the web UI with a system username such as www-data and any password. Update from Settings, General, Update (the setup guide in this series walks through it, including the blank-screen gotcha after the reboot), and do it before the board goes anywhere near a port forward or a tunnel.
These are the advisories published on the ZimaOS GitHub security page, with the affected and patched versions each advisory lists:
| Advisory | Issue | Severity | Affected | Patched |
|---|---|---|---|---|
| GHSA-gq27-h664-6xp4 | Authenticated arbitrary file read via /v2_1/files/file/download path traversal | High | 1.6.1 | 1.7.1 |
| CVE-2026-21891 | Login bypass with system usernames (root, www-data, nobody and others) and any password | Critical | 1.5.0 and older | 1.5.3 |
| CVE-2026-28798 | Unauthenticated access to internal services through /v1/sys/proxy when a Cloudflare Tunnel exposes the UI | Critical | 1.5.0 and older | 1.5.3 |
| CVE-2025-64427 | SSRF through /v1/sys/proxy for authenticated users | High | 1.5.0 and older | 1.5.3 |
| CVE-2026-28442 | Deleting system files through the files API | High | 1.5.2-beta3 | 1.5.3 |
| CVE-2026-28286 | Creating files in system directories through the files API | High | 1.5.2-beta3 | 1.5.3 |
| GHSA-57r9-43pc-gcp2 | Regression: /v1/users/name discloses usernames without auth | Moderate | 1.5.0 | 1.5.1 |
The list goes further back: five 2024 advisories fixed in 1.2.5 (file read, directory listing, an info leak and two username disclosures), and two 2025 localhost file API bugs (CVE-2025-58431 and CVE-2025-58432) whose advisories list 1.4.1 and older as affected without naming a patched release. After updating, confirm the unauthenticated endpoints behave. Set the board’s address once, from any machine on the LAN:
export ZIMA_IP="192.168.1.177"
export ZIMA_USER="cfgadmin"
export LAN_CIDR="192.168.1.0/24"
Swap in your board’s IP, the admin username you picked during onboarding and your LAN subnet. If the board still takes its address from DHCP, pin it first with our ZimaOS static IP guide, because the commands below and your bookmarks are written against that address. The variables only live in the current shell, so re-export them if you open a new terminal. Now probe the endpoints with no session token:
for p in /v1/sys/debug /v1/sys/version /v1/users/name /v1/users/status; do printf '%s ' "$p"; curl -s -o /dev/null -w '%{http_code}\n' "http://${ZIMA_IP}${p}"; done
On ZimaOS v1.7.1 the username endpoint from the 1.5.0 regression now returns 401, along with the debug and version endpoints:
/v1/sys/debug 401
/v1/sys/version 401
/v1/users/name 401
/v1/users/status 200
The one 200 is /v1/users/status, which the login page needs before anyone signs in. It gives away only that the device is initialized and its GPU count: {"success":200,"message":"ok","data":{"gpuCount":0,"gpus":0,"initialized":true,"key":""}}. A 200 on /v1/users/name means you are on an unpatched image.
2. Turn off ZimaOS remote access if you don’t use Zima Client
If you never installed Zima Client on a phone or laptop, the ZeroTier network is doing nothing for you except existing. Open Settings, Network and switch Remote access off:

The Remote ID row disappears, but in our capture the Connection list above it still showed the Virtual Network (Remote) entry with 10.147.13.1. Don’t trust the UI for this. Check the services instead:
systemctl is-active zerotier-one znet
ZeroTier stops. znet keeps running:
inactive
active
On our board, ip -br link | grep -E 'zt|tun' printed nothing afterwards, so both the ZeroTier interface and tun0 were gone, and the outbound TCP sessions to the relays and DNS-over-HTTPS resolvers closed. znet itself stayed active with a handful of UDP sockets open. Turning the toggle back on brought back the same network ID, so switching off is not a way to rotate a leaked ID. That is what step 3 is for.
If you want remote access without a public network, IceWhale’s App Store lists Tailscale, WireGuard Easy, Firefly and NetBird. Tailscale is the least effort, and every device has to sign in to your own tailnet before it can connect. We cover it on a normal Linux host in the Tailscale install guide, and the WireGuard walkthrough explains the key exchange if you prefer running your own tunnel.
3. If you keep Remote Access, guard and audit the Remote ID
Keep Remote Access on only if you use Zima Client, and then treat the Remote ID the way you treat an SSH private key. Do not paste it into forum posts, support chats or screenshots (ours is redacted above for exactly this reason). Because a public ZeroTier network cannot de-authorize a member, the only way to shut out someone who has the ID is to replace it. The three-dot menu next to Copy ID has a Reset Network ID option. According to IceWhale’s docs the old ID stops working immediately, and existing connections and shares become invalid, so every Zima Client device has to reconnect afterwards.
You can also see who has joined. The controller keeps a member list, and on a network nobody else should be on it holds exactly one entry, the board itself. Reuse the TOKEN and NWID variables from earlier:
curl -s -H "X-ZT1-Auth: ${TOKEN}" "http://127.0.0.1:9993/controller/network/${NWID}/member"; echo
Our list had one member, the board’s own node address (sanitized here), followed by the member revision:
{"8d4f1c27a0":2}
Each phone or laptop running Zima Client adds one node address. Any address you can’t match to a device you own means the ID has leaked: reset it.
4. Lock down SSH and the web terminal
SSH Access and the web-based terminal live in Settings, General, Developer mode, alongside HTTPS and the Samba switches used in the next two steps:

The shipped sshd configuration is better than most appliances. Dump the effective settings:
sudo sshd -T | grep -Ei "^(permitrootlogin|passwordauthentication|maxauthtries|persourcepenalties|persourcemaxstartups|pubkeyauthentication|listenaddress) "
Root login is off, and OpenSSH’s per-source penalties throttle an address that keeps failing (20 seconds for each connection that ends in failed logins, accumulating up to 10 minutes). Password login is still on:
listenaddress [::]:22
listenaddress 0.0.0.0:22
maxauthtries 4
permitrootlogin no
pubkeyauthentication yes
passwordauthentication yes
persourcemaxstartups 3
persourcepenalties crash:90 authfail:20 noauth:10 grace-exceeded:45 refuseconnection:10 max:600 min:5 max-sources4:65536 max-sources6:65536 overflow:permissive overflow6:permissive
The simplest fix is to leave SSH Access off unless you are actively using it. The web terminal is the bigger problem. ttyd listens on port 7681 over plain HTTP, and although it runs a login prompt (/usr/libexec/ttyd-login checks the user, then hands over to login), your password travels unencrypted. It is tied to SSH in its unit file:
systemctl cat ttyd | grep -E "Requires|PartOf|ExecStart"
The unit requires sshd.service and is declared part of it, so stopping SSH stops the terminal with it:
Requires=sshd.service
PartOf=sshd.service
ExecStart=/usr/bin/ttyd -W /usr/libexec/ttyd-login
If you do keep SSH on, switch it to keys. The admin account’s home directory on ZimaOS is /DATA, not /home, and /DATA is owned by root with mode 755, so ssh-copy-id cannot create .ssh there on its own. Create it first, on the board and logged in as your admin user:
sudo install -d -m 700 -o "$(id -un)" -g "$(id -gn)" /DATA/.ssh
A root-owned, non-writable home is fine for sshd’s StrictModes check, which is on. Now copy your key from your workstation and prove it works:
ssh-copy-id "${ZIMA_USER}@${ZIMA_IP}"
ssh -o PasswordAuthentication=no "${ZIMA_USER}@${ZIMA_IP}" 'echo key login works'
Do not skip that second command. Only when it prints key login works should you turn passwords off. ZimaOS’s sshd_config has no Include line and no sshd_config.d directory, so edit the explicit PasswordAuthentication yes line in place, validate, and restart. The stock file already sets KbdInteractiveAuthentication no, so PAM’s keyboard-interactive route is closed and this one line is enough. Keep your current session open while you do it:
sudo sed -i 's/^PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sshd -t && sudo systemctl restart sshd
sudo sshd -T | grep -E '^(passwordauthentication|kbdinteractiveauthentication) '
Both lines should now read no. We confirmed the stock values and the effective result with sshd -T overrides on our board, but did not switch the review unit itself to key-only login, so run the key test before you rely on it. The edit lands in the /etc overlay, so check it again after each ZimaOS update and after toggling SSH Access in the UI. This does nothing for ttyd, which authenticates through login and PAM, not sshd, and still accepts passwords. That is one more reason to firewall port 7681 in step 7.
5. Turn on HTTPS for the web UI (and know what it doesn’t do)
The HTTPS toggle sits in the same Developer mode screen. Flip it and ZimaOS asks you to save and reload the WebUI:

After Save, the gateway config on the board has TLS enabled on 443:
grep -A3 '\[ssl\]' /etc/casaos/gateway.ini
The certificate path points at a bundle ZimaOS generated itself:
[ssl]
enabled = true
port = 443
cert_path = /var/lib/casaos/ssl/zimaos.local.fullchain.pem
ZimaOS itself has no openssl binary, so inspect the certificate from your workstation (the -ext option needs OpenSSL 1.1.1 or newer; on macOS use Homebrew’s openssl rather than the bundled LibreSSL):
echo | openssl s_client -connect "${ZIMA_IP}:443" -servername zimaos.local 2>/dev/null | openssl x509 -noout -subject -issuer -enddate -ext subjectAltName
It is signed by a ZimaOS CA and valid only for zimaos.local, so browsers will warn when you connect by IP:
subject=C=CN, O=IceWhaleTech, OU=ZimaOS, CN=ZimaOS Local
issuer=C=CN, ST=Shanghai, L=Shanghai, O=IceWhaleTech, OU=ZimaOS, CN=ZimaOS-CA
notAfter=Mar 25 09:09:42 2029 GMT
X509v3 Subject Alternative Name:
DNS:zimaos.local
The catch: HTTP is not redirected. Port 80 keeps serving the full UI after HTTPS is on:
curl -sI "http://${ZIMA_IP}/" | head -1
A 200, not a 301:
HTTP/1.1 200 OK
Bookmark https://zimaos.local/ (or the HTTPS IP address) and use only that. Anyone who types the bare IP still gets plaintext, and HTTPS does nothing to stop someone on the network from reaching the login page. Never port-forward the UI to the internet. With no 2FA, one password is all that stands in front of it.
6. Tighten Samba where ZimaOS allows it
Samba is on from the first boot. The Supports Samba1 switch is off by default, which is the right call, and it should stay off. Look at the global settings ZimaOS generates:
sudo testparm -s 2>/dev/null | grep -Ei "min protocol|ntlm auth|map to guest|force user|create mask|\["
Two lines stand out. ntlmv1-permitted accepts the old NTLMv1 challenge, which is crackable offline, and force user = root means every file written over SMB is owned by root on disk:
[global]
map to guest = Bad User
ntlm auth = ntlmv1-permitted
server min protocol = SMB2
force user = root
Guest access is limited in practice: an anonymous session listed only IPC$, no shares. Resist the urge to edit smb.conf, though. ZimaOS regenerates it (the file’s timestamp changed when we signed in), so hand edits get overwritten. That is the opposite of a hand-built Samba server, where the config file is yours. What you can control here is exposure. Turn the Samba service switch off on boards that don’t serve files over SMB, give share users long unique passwords, and restrict ports 139 and 445 to your LAN with the firewall in the next step.
7. Add a LAN-only firewall to ZimaOS
ZimaOS has no firewall UI, but it ships iptables and systemd, and /etc is writable. The ruleset below adds one chain in front of INPUT. Traffic from loopback, from established connections and from your LAN (matched by both the physical interface and the subnet) passes untouched. SSH, the web terminal and Samba arriving on any other interface, the ZeroTier and znet tunnels included, get dropped. Matching the interface matters: ZeroTier emulates Ethernet, so a remote member can reach the board over a link-local IPv6 address, and a rule keyed on the address alone would let it straight in. The web UI on 80 and 443 and the ZimaOS backend on 9527 are left alone, because Zima Client needs them. That means anyone on the ZeroTier network can still reach the password-only login page, so if you don’t use Zima Client, switch Remote Access off as in step 2 rather than relying on this unit. Create the IPv4 rules file:
sudo vim /etc/zimaos-lan-only.rules
Paste the ruleset exactly as written. The eth0 and 192.168.1.0/24 values get replaced with yours in a moment:
*filter
:ZIMA-LAN-ONLY - [0:0]
-A ZIMA-LAN-ONLY -i lo -j RETURN
-A ZIMA-LAN-ONLY -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
-A ZIMA-LAN-ONLY -i eth0 -s 192.168.1.0/24 -j RETURN
-A ZIMA-LAN-ONLY -p tcp -m multiport --dports 22,139,445,7681 -j DROP
-A ZIMA-LAN-ONLY -p udp -m multiport --dports 137,138 -j DROP
COMMIT
SSH, Samba and ttyd all listen on IPv6 as well, and the ZeroTier interface carries an IPv6 address, so the IPv6 file matters just as much. Open it:
sudo vim /etc/zimaos-lan-only6.rules
This version accepts any IPv6 source that arrives on the LAN interface (link-local, ULA or global), so Windows clients that reach Samba over IPv6 keep working:
*filter
:ZIMA-LAN-ONLY - [0:0]
-A ZIMA-LAN-ONLY -i lo -j RETURN
-A ZIMA-LAN-ONLY -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
-A ZIMA-LAN-ONLY -i eth0 -j RETURN
-A ZIMA-LAN-ONLY -p tcp -m multiport --dports 22,139,445,7681 -j DROP
COMMIT
Swap in your LAN interface (the one holding your LAN address in ip -br addr; eth0 on ours) and subnet, then dry-run both files. If you cable both 2.5GbE ports into the LAN, duplicate the RETURN line for the second interface. Export LAN_CIDR on the board too, and LAN_IF alongside it. --test parses the ruleset against the running kernel without touching the live tables, and prints nothing when the rules are valid:
export LAN_IF="eth0"
sudo sed -i "s|192.168.1.0/24|${LAN_CIDR}|; s|-i eth0|-i ${LAN_IF}|" /etc/zimaos-lan-only.rules
sudo sed -i "s|-i eth0|-i ${LAN_IF}|" /etc/zimaos-lan-only6.rules
sudo iptables-restore --test --noflush /etc/zimaos-lan-only.rules && echo v4 ok
sudo ip6tables-restore --test --noflush /etc/zimaos-lan-only6.rules && echo v6 ok
Rules loaded by hand vanish at reboot, so a oneshot unit loads them at boot. Create it:
sudo vim /etc/systemd/system/zimaos-lan-only.service
The ExecStartPre deletes keep a restart from stacking duplicate jumps in INPUT:
[Unit]
Description=Restrict SSH, web terminal and Samba to the LAN
Before=network-pre.target
Wants=network-pre.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=-/usr/sbin/iptables -D INPUT -j ZIMA-LAN-ONLY
ExecStartPre=-/usr/sbin/ip6tables -D INPUT -j ZIMA-LAN-ONLY
ExecStart=/usr/sbin/iptables-restore --noflush /etc/zimaos-lan-only.rules
ExecStart=/usr/sbin/iptables -I INPUT 1 -j ZIMA-LAN-ONLY
ExecStart=/usr/sbin/ip6tables-restore --noflush /etc/zimaos-lan-only6.rules
ExecStart=/usr/sbin/ip6tables -I INPUT 1 -j ZIMA-LAN-ONLY
ExecStop=-/usr/sbin/iptables -D INPUT -j ZIMA-LAN-ONLY
ExecStop=-/usr/sbin/ip6tables -D INPUT -j ZIMA-LAN-ONLY
[Install]
WantedBy=multi-user.target
Validate the unit, then enable it. Run this from a LAN machine so your own SSH session matches the RETURN rule:
sudo systemd-analyze verify /etc/systemd/system/zimaos-lan-only.service
sudo systemctl daemon-reload
sudo systemctl enable --now zimaos-lan-only.service
sudo iptables -L ZIMA-LAN-ONLY -n -v --line-numbers
systemd-analyze verify also prints warnings about some stock ZimaOS units (legacy /var/run PID paths in smb.service, nmb.service and a few IceWhale services). Those aren’t yours and are harmless. Both rule files passed --test and the unit passed verify on our board. We did not leave the firewall running on the review unit, so reboot yours once and rerun the last command to confirm the chain comes back. The packet counters on the DROP lines are where blocked off-LAN SSH or SMB attempts should show up.
Keep your current SSH session open while you do this and log in from a second terminal before closing it. The ESTABLISHED rule keeps the first session alive, so if LAN_IF was wrong (the NIC sits in a bridge, or the cable is in the other 2.5GbE port) you can still back out with sudo systemctl disable --now zimaos-lan-only.service. Without that session, the fix needs a keyboard and a Mini DP screen on the board.
Two limits to be clear about. Ports published by Docker apps are DNATed through the FORWARD chain, so this INPUT chain doesn’t cover app ports; filter those in DOCKER-USER if you need to. If you rely on Zima Client for file access, test it after enabling the unit, since the rules deliberately block SMB over the virtual network. And if you took the Tailscale route from step 2 and want SSH or SMB over your tailnet, add -A ZIMA-LAN-ONLY -i tailscale0 -j RETURN below the eth0 line in both files and re-run the --test commands.
Does ZimaOS support 2FA?
No. ZimaOS has no two-factor authentication for the web UI or for local accounts. An IceWhale staff member confirmed it on the IceWhale community forum in December 2025 (“ZimaOS does not currently support 2FA”), and the thread is still collecting requests for it. Until it ships, the failure mode is simple: whoever gets the admin password gets the whole box, including Docker and root-owned SMB shares. Use a long unique password, keep the dashboard reachable only from your LAN or through a VPN you control, and put 2FA on that VPN instead (Tailscale’s login provider or hardware security keys both do the job).
ZimaOS security audit checklist
Run through this after setup and again after every ZimaOS update, because updates and UI toggles can undo edits in /etc:
- Settings, General, Update shows no pending release, and
curlto/v1/users/namewithout a token returns 401. - Remote access is off, or you use Zima Client and the controller’s member list holds only devices you own.
- The Remote ID has never appeared in a screenshot, ticket or chat. If it has, reset it.
- SSH Access is off when you aren’t using it. When it is on,
sshd -Tshowspasswordauthentication no. - You open the dashboard only over HTTPS, and it is not port-forwarded or tunnelled to the internet.
- Supports Samba1 is off, and Samba is off entirely on boards that don’t share files.
systemctl is-active zimaos-lan-onlyprintsactiveafter a reboot, andiptables -S INPUTandip6tables -S INPUTboth include the ZIMA-LAN-ONLY jump.- From a device that reaches the board over Zima Client’s ZeroTier network instead of the LAN, SSH and port 445 both time out.
A board that passes all eight is in far better shape than it was when it left the factory. The remaining risk is the one ZimaOS can’t fix for you yet: a single password on the dashboard.