[Module] ZFW v1.0.10 — host firewall as a ZimaOS dashboard tile

I tracked this down. That “Open” button is part of ZimaOS’s Network widget, not ZFW. Each click spawns a fresh ttyd web-terminal on a random port from the kernel’s ephemeral range (32768–60999), bound to all interfaces. It’s a host process, so ZFW’s default-deny drops it — which is the firewall doing its job.

No, ZFW won’t auto-allow these, by design. The port is unpredictable, so “allowing ttyd” would mean opening the entire ephemeral range LAN-wide — and the same mechanism also serves a root login shell, so silently punching a hole there is exactly what a firewall shouldn’t do.

If you need it remotely with ZFW on, the cleanest option is one rule allowing TCP 32768–60999 from your admin machine’s IP only (ZFW supports port ranges + source-IP scoping). That keeps default-deny for the rest of the LAN. Otherwise, use those tools from the host locally.

The real fix belongs upstream: ZimaOS should route this terminal through its gateway on port 80 (already open), like every other backend, instead of binding a random public port.

Hi,

I have a question about the workflow.

When I create or modify a firewall rule, I click Apply (or Safe-Apply), which I would expect to apply the new rules immediately.

However, I often find that I have to disable the firewall completely and then enable it again before the new rule actually starts working.

Is this expected behavior, or should clicking Apply always be enough to activate new or modified rules without having to disable and re-enable the firewall?

Thanks!

Clicking Apply / Safe-Apply is enough — you shouldn’t need to disable and re-enable. A new or changed rule takes effect on all new connections immediately (I verified this on a live host: a fresh connection to a port is blocked the instant you Apply).

What you ran into is normal stateful-firewall behaviour: ZFW, like any conntrack-based firewall, lets already-open connections keep flowing. A new block rule only stops new connections — it can’t tear down a session that was already established. So if you had that service open in a browser tab or an app when you applied the rule, that one existing connection kept working until it closed. Disabling/re-enabling dropped the connection, so it reconnected and hit the block — which is why the toggle seemed to be required.
Two tips:

  1. Click Save rules first, then Safe-Apply on the Firewall tab — rule edits aren’t compiled until they’re saved.
  2. New connections are blocked instantly already. To make a block also drop an already-open connection without the toggle, ZFW 1.0.21 will flush the connection-tracking entries for a port the moment you block it — so the disable/enable step goes away entirely.

I reinstalled ZFW v1.0.21 on ZimaOS v1.7.0 update.
Everything worked well locally. My websites and CMS.
Today, I connected to my CMS outdoors and they could not be reached. I disabled ZWF and now everything works externally.
Is this malfunction caused by ZimaOS v1.7.0 update?

I don’t think it’s a general issue. It should work.

First, make sure you’ve allowed the required port and confirmed the rule. If it still doesn’t work, try disabling ZFW and then enabling it again. That usually reloads the firewall rules, and external access should start working again.

I’ve been using ZFW since v1.0.10. Everything has always worked well.
By switching to ZimaOS v1.7.0 and reinstalling ZFW v1.0.21, everything works fine locally. My 443 CMS on a domain name seemed to work normally without any apparent problems.
All port rules for my apps are “allowed”. I checked them one by one. Everything works and opens normally.
It was on this day, when I connected outside my home, that I noticed that I could not reach my CMS. my wife also told me that she had not been able to synchronize external calendars on these websites.
I was able to join my ZimaOS with the ZeroTier application without any problem. I was also able to deactivate ZFW remotely and join my CMS.
There is an unresolved topic regarding network-dependent applications since the ZimaOS 1.7 update. Is this the cause?

I’m not sure if it’s related to the unresolved network issue in ZimaOS v1.7.0, so I can’t say for certain.

However, since everything started working again as soon as you disabled ZFW, I’d first try re-enabling ZFW and checking that all the required ports are still set to Allowed. Sometimes simply restarting the firewall causes it to reload the rules correctly.

If the problem comes back after enabling ZFW again, then it could be a bug introduced with the ZimaOS v1.7.0/ZFW v1.0.21 combination. That would definitely be worth reporting to the IceWhale team.

Short answer: this is not the ZimaOS 1.7 network-apps issue. It is a bug in ZFW, and I have a fix.

What is happening

Your CMS is a Docker container, and ZFW filters container ports in Docker’s
DOCKER-USER chain. That works — over IPv4. From your LAN you reach the
CMS over IPv4, so everything looks correct, and it is.

From outside, your domain name is very likely resolving to an AAAA record,
so the client connects over IPv6 — and that takes a completely different
path through the kernel. On a stock ZimaOS, Docker’s IPv6 support is off, so
containers have no IPv6 address of their own. An IPv6 connection to a published
port is instead handled by Docker’s docker-proxy helper on the host, which
means it is filtered by the host’s IPv6 input chain (ZFW-IN6) and never
reaches DOCKER-USER.

ZFW was only putting the host half of your rules into that IPv6 chain. No
Docker container port ever got an allow rule there, so with the default policy
on Deny, every IPv6 connection to your CMS was dropped — while the rule table
showed a green “Allow”, and while IPv4 kept working perfectly from the LAN.

There is a second half to this, and it explains why it only started now. Before
ZFW 1.0.17, the IPv6 chain was being written into the wrong iptables backend
(legacy while ZimaOS 1.6.2+ actually runs nf_tables), so it was inert — it
existed but no packet ever traversed it. You came from 1.0.10. Upgrading to
1.0.21 is the moment IPv6 filtering became real on your machine for the first
time. Nothing about your setup changed; ZFW’s IPv6 filtering simply started
working, and it was working wrongly.

ZeroTier kept working because ZFW always bypasses the ZeroTier and Tailscale
interfaces, and disabling ZFW removed the chain entirely — both consistent with
this.

Please confirm it — one command

Try to reach the CMS from outside, then on the ZimaOS box run:

sudo journalctl -k --since "-5 min" | grep -E "ZFW-IN6-DROP|ZFW-DOCK-DROP"
  • Lines with ZFW-IN6-DROP and an SRC= address containing colons
    → it is the IPv6 problem described above.
  • Lines with ZFW-DOCK-DROP and a normal public IPv4 SRC=
    → it is the second cause below, which is worth checking either way.

Please also check the Source column of your rules

Open the Rules tab and look at the Source column, not just the Action
column. If your rules were re-created on the fresh install (the “Recommended
defaults” set), every one of them has your LAN range as its source, e.g.
192.168.1.0/24 — including the rules for your Docker apps. Those rules read
“Allow”, and they allow only your LAN. Anyone coming from the internet has
a different source address, matches no rule, and is blocked. That alone would
produce exactly the symptom you describe, on IPv4, with no IPv6 involved.

Two rules with an IPv4 source can never apply to IPv6 either — that is a
limitation of ip6tables, not something I can work around: there is no way to
write “192.168.1.0/24” as an IPv6 match.

Fix right now, without waiting for a release

For each service that must be reachable from the internet, add one rule:

  • Action: Allow
  • Source: Any
  • Ports: the published port (443 for your CMS)
  • Zone: Docker

Keep your existing LAN rule next to it; leave the default policy on Deny. Then
add a second rule with Zone Host and Source Any for the same port — that
is the one that opens the IPv6 input chain on 1.0.21. Use Safe-Apply so the
120-second timer can rescue you, and test from a phone on mobile data (not from
your LAN, and not over ZeroTier — neither of those goes through the path that is
broken).

For services that should stay private, leave them scoped to your LAN. In that
case the IPv6 drop is doing what you want.

What changes in ZFW 1.0.22

  1. Rules now cover Docker-published ports on IPv6 as well, so the second rule
    above stops being necessary.
  2. A rule that cannot apply to IPv6 — because its source is an IPv4 address or
    range, or a country — is now marked IPv4 only in the rule table, with a
    tooltip explaining what to change. This used to be completely silent, which
    is the real reason this cost you a day.
  3. If the default policy is Deny, your host has a public IPv6 address, and not
    one rule reaches the IPv6 chain, the Rules tab now says so in a banner
    instead of showing a table full of green “Allow” rows.

One thing 1.0.22 deliberately does not do: it does not widen a LAN-scoped
rule to the internet on your behalf. That has to stay your decision — it just
stops being invisible.

The symptom description (“works locally, dead from outside, fine over ZeroTier”) is what pinned it down :slight_smile:

@Lintux

I think I understood the principle for opening port 443.
I would update ZWF to v1.0.22.
I will open port 443 for everyone in the Docker zone. Should I link each container individually or not?
I will test tonight with a cell phone and let you know tomorrow.
Thank you again for your responsiveness.

Yes to the update — and it’s directly about your case.

v1.0.22 came out of exactly this scenario: a service published on 443 in a container, reachable from the LAN, dead from the internet, while every rule in the table read “Allow”. So update first, then judge what’s still missing.

The cause is worth knowing, because it explains why your rules looked right. ZimaOS runs Docker without ip6tables (there’s no “ipv6” key in /etc/docker/daemon.json), so containers hold no IPv6 address and Docker publishes no IPv6 DNAT rule. Every published port is served by the userland docker-proxy listening on [::], which means an inbound IPv6 connection terminates on the host and is filtered in ZFW-IN6 — it never reaches FORWARD/DOCKER-USER. Before v1.0.22 a Docker-zone rule wrote no allow into ZFW-IN6, so IPv6 clients hit the catch-all drop while IPv4 stayed open through DOCKER-USER. That’s precisely why testing from inside the LAN couldn’t see it.

On linking each container: no, don’t — not for this.

Container binding does something different from what the name suggests. When a rule is bound to a container, the daemon looks that container up in the live inventory at recompile time and substitutes its currently published ports into the rule, replacing whatever port list you typed. It exists so a rule can follow an app across restarts and port remaps.

So if you bind a container in order to “open 443”, you don’t get 443 — you get every port that container publishes. That’s wider than you asked for, and it changes silently whenever the container’s port mapping changes.

For your goal, one plain port rule is both simpler and tighter:

  • one rule, ports ), action = allow.Matching in DOCKER-USER is by destination port (–ctorigdstport
    443), with no contthat single rulecovers whichever container publishes 443, and keeps working if you
    replace or rename
  • In practice only one container can publish 0.0.0.0:443 anyway.
    If several serviceon 443, only theproxy needs the rule at all — the backends behind it don’t need
    published ports.

Use container bindinely move and youwant the rule to track them. Not when you want one specific port
open.

One thing that wiln’t use “all ports” on a Docker-zone rule.

An explicit port (443) is mirrored into the IPv6 chain. ports =
all on a Docker-zo “every containerport” is not “every host port”, and mirroring it would switch off
the IPv6 default-dmaOS UI itself. Ifyou do it anyway, the Rules tab badges that rule “IPv4 only” with
the reason docker-s so. Give it theexplicit port.

Zone: prefer auto over docker. auto resolves per port against the
live published-por-IN,Docker-published ports to DOCKER-USER. If you ever move the
service to networklows it; a dockerrule would quietly stop covering it.

“For everyone” — be deliberate about the source. Source any means
anyone who can reale internet once443 is forwarded on the router. If the service is only meant for
LAN and VPN clientranges instead; ifit really is public, consider the per-rule rate limit.

Then Apply and Commit. Apply is a Safe-Apply with a 120-second
dead-man switch: it window, ZFWremoves the firewall entirely rather than restoring your previous
rules — you won’t left unprotectedwithout noticing.

Finally, verify from outside, over both families — this failure mode is invisible

curl -4 -sS -o /detps:///
curl -6 -sS -o /dev/null -w ‘%{http_code}\n’ https:///

If v4 answers and v6 hangs, that’s the exact gap v1.0.22 closes — and the Rules tab ell you per rulewhether it reaches the IPv6 chains and why not

To put it simply, I’m not a specialist, I open port 443 for everyone in auto mode.
Yes, only the Proxy (NPM) manages connections on the 443.
The Proxy (NPM) is set for 192.168.1.0/24 (CIDR) so I put it for everyone (ANY) ?
I installed the new version of ZFW on my test server to see what it looks like.
I’ll test this evening on my server in production and say tomorrow.

Short answer: only switch to Any if 443 has to be reachable from outside your LAN. Two separate things are going on here.
▎
▎ 1. Your “443 for everyone, zone Auto” rule already covers NPM.
▎ Rules are evaluated top to bottom, first match wins — and an Allow rule never blocks anything. Traffic that doesn’t match a rule just falls through to the next one. So the NPM rule scoped to 192.168.1.0/24 is not restricting anything as long as the “443 from Any” rule exists. Leave it or delete it; for port 443 it changes nothing either way.
▎
▎ 2. The scope only matters if a rule is the only one covering that port — and then it matters twice.
▎ - 192.168.1.0/24 matches only clients whose address is inside that range. A visitor from the internet arrives with a public source IP and is dropped.
▎ - More important since 1.0.22: an IPv4 CIDR is never applied to the IPv6 chain. ZFW mirrors a rule to IPv6 only when the source is Any or an IPv6 range. If your ZimaOS has a public IPv6 address and the default policy is Deny, a rule scoped to 192.168.1.0/24 means every client that resolves your domain to an AAAA record gets dropped — while the same site works fine from the LAN over IPv4.
▎
▎ That’s exactly what the new “IPv4 only” badge next to a rule tells you in 1.0.22. Hover it and it explains why. And if no rule at all reaches IPv6, you get an orange banner above the rule table saying so. IPv6 drops are logged with the prefix ZFW-IN6-DROP and show up in the Events tab.
▎
▎ Rule of thumb for NPM:
▎ - NPM reachable from the internet → source Any. That is the only setting that also covers IPv6.
▎ - NPM only used inside your house → keep 192.168.1.0/24, that’s the safer choice. Just don’t expect it to answer over IPv6.

I have just installed ZFW version 1.0.22 on my server in production.
I just opened port 82 for the ZimaOS WebUI for my local network.
I opened port 443 for everyone in auto (NPM).
I took the 2mn test and my websites are well distributed on the internet (test on my phone in external 5G).

Tested last night and today remotely and works wonderfully well.
I think I understood the configuration of ZFW well overall.
Single command line installation is no longer available for this latest version. It was practical for those just starting out rather than downloading the file.
I found the complete uninstall file on this forum, too bad it is not present on the public GitHub.
I will now test the geographic permissions as well as the unique IPs to ban.
However, I cannot open the system WebUI by the CPU-RAM shortcut. The port cannot be fixed as it changes with each opening attempt.
Many thanks again for this ZFW application as well as Cron and integrated into ZimaOS.

I updated ZimaOS to 1.7.1-beta1 to fix the port fixing bug.

ZFW no longer works on my two Zima servers. The page is empty and indicates a version error and HTTP 401.

If your ZFW (or any other third-party module) went blank after updating to 1.7.1-beta1, this is why.

ZimaOS 1.7.1-beta1 changed the iss claim of the access token issued by the user-service:

ZimaOS access token refresh token
1.7.0-beta1 casaos refresh
1.7.1-beta1 zimaos refresh

Measured with live logins on a ZimaCube before and after the update, not guessed.

Why that takes a module down. The ZimaOS gateway proxies module routes without
authenticating them — a curl from any LAN device reaches a module’s backend directly.
So every module has to verify the session token itself. And a correct verification has to
pin the issuer, because the refresh token is signed with the same key and is valid for
seven days; without an issuer check it would be accepted as a fully-privileged session.

That pin is what broke. My firewall module ZFW pinned casaos, so after the update it
refused every genuine login and the whole UI was dead — empty tabs, and a 401 in the
browser console that no normal user ever opens.

The part I would like IceWhale to think about: this rename only broke the
implementations that got it right. A module that checks nothing but the signature and
exp sailed through the update — and it also happily accepts a seven-day refresh token as
an admin session. Doing the secure thing was the thing that got punished.

And it was shipped without a word. The 1.7.1-beta1 release notes list 13 items — file
service security, memory usage, empty folders, backups, USB tasks, RAID display, app data
on uninstall, Docker networking, port config, YAML saving, App Store windows, app
migration. None of them mention authentication, tokens, sessions, or the user-service.
There is no changelog in the image either.

@IceWhale — I am not asking you to freeze internal identifiers. This is a beta, and
renaming one is legitimate. I am asking for two things, both cheap:

  1. One line in the release notes when something authentication-relevant changes.
    That turns a day of debugging into a five-minute fix, for every module developer at once.
  2. A short, stable statement of what third-party modules may rely on for session
    validation: the JWKS endpoint, the accepted issuer values, the algorithm. Right now
    every module developer has to reverse-engineer it from a live login — and then has no
    way of learning that it changed, except by having their users report a broken UI.

Fixed in ZFW v1.0.23, which accepts both issuer values so it works across the rename
and on older ZimaOS alike: Release v1.0.23 — ZimaOS 1.7.1-beta1 renamed the session-token issuer · chicohaager/zfw · GitHub
Cheers

Many thanks for your responsiveness. ZWF updated with just the proposed line of code. My configuration reappeared identically. Works perfectly.

Hi,

i’m using Zima for a while now.
As Morgyann, since en update of Zima, external access was unable until I refreshed rules.

As you mentionned earlier

All was working fine until last week end.
I saw that you published an update of ZFW, but I cannot access servers from a VPN connection.

My box provider (Free) enables a VPN link with Wireguard.
I can access my different devices through it (Freebox dashboard…), but ZFW blocks anything through it.

I tried to create new rules with correct IP adress / Ports. But no way.

When I’m connected through Wireguard, the IP is on the range 192.168.27.0/24

I’m under ZimaOS 1.7.0 / ZFW 1.0.23

Locally, I have no problem to enter my different servers dashboard.

Zima App client, doesn’t work so fine externally too…

Any clue about how to repair my parameters?

Thanks so much.

Yoh from France.

Hi Yoh,

Thanks — your report turned up an actual bug, and there are two separate things going on. One is a ZFW bug that is now fixed; the other is a rule that needs adding.

  1. The Zima App client — that one was my fault, and it’s fixed

ZimaOS ships IceWhale’s own remote-access mesh, Zima Net (znet), which owns the network interface tun0. ZFW auto-allowed every other mesh — Tailscale, ZeroTier, WireGuard — but not that one, so it was dropping ZimaOS’ own remote access. Measured on a ZimaCube:

ZFW-IN-DROP IN=tun0 SRC=10.126.126.243 DST=10.126.126.1 PROTO=TCP DPT=9527

125 dropped SYNs in a single connection attempt, while the client sat on “connecting” forever.

Your 1.7.0 is affected. I checked that specifically rather than assuming: I downloaded the official ZimaOS 1.7.0 image, verified it against IceWhale’s published checksum, and unpacked it — usr/bin/znet, its service unit, and a systemd preset reading enable znet.service are all in there. Zima Net ships and is on by default on stock 1.7.0.

Fix: upgrade ZFW to v1.0.24, released today:

On Zima, as root — pick the arch that matches your box

cd /tmp
curl -LO https://github.com/chicohaager/zfw/releases/download/v1.0.24/zfw-1.0.24-amd64.tar.gz
tar xzf zfw-1.0.24-amd64.tar.gz && cd zfw-1.0.24-amd64 && sh install.sh

It updates in place, keeps your rules, and after it you click Apply → Confirm once. (The Docker Hub installer image is still on 1.0.23 for the moment; use the tarball.)

  1. The Freebox VPN — that one is a rule, and it’s working as configured

This is a different mechanism and the upgrade will not fix it by itself.

When ZFW builds its starter rules it detects your LAN (say 192.168.1.0/24) and writes that subnet as the source into every one of them — Web UI, SSH, Samba, and one per published container port. The policy is default-deny. Your Freebox VPN clients arrive with a source in 192.168.27.0/24, a different subnet, so no rule matches and they are dropped. Local access works, VPN access doesn’t — exactly what you describe.

And note: ZFW does auto-allow WireGuard, but only when WireGuard terminates on the ZimaOS host. Yours terminates on the Freebox, so the Zima only sees normal packets on its LAN port with a 192.168.27.x source. That auto-allow cannot help here.

Step 1 — look before you write rules. Open ZFW → Events, then try the failing connection from the VPN. Every drop is listed with Source IP, destination port, protocol and zone. That answers three things at once:

  • Which source does the Zima actually see? If the Freebox source-NATs the VPN traffic, you’ll see the box’s LAN address there instead of 192.168.27.x — and a rule written for 192.168.27.0/24 could never match. This may well be why your new rules had no effect.
  • Which zone? host = a port ZimaOS itself serves (80/443 UI, 22, Samba). docker = a published container port. Different chains; a rule in the wrong zone does nothing.
  • IPv4 or IPv6?

If nothing shows up in Events while it fails, ZFW isn’t what’s blocking you.

Step 2 — the rule. Rules → add:

  • Direction: Inbound
  • Source: IP range (CIDR) → 192.168.27.0/24, or whatever Events showed
  • Ports: the ones you need · Protocol: TCP or Both
  • Zone: the usual trap. “Automatic” decides per port from the container ports published at that moment — if the container was stopped when you saved, it filed the port under host, where it does nothing for container traffic. When in doubt, make two rules: one Host service, one Docker container.

Then Apply → Confirm within 120 s → Commit (boot persistence).
On IPv6: a rule with an IPv4 source is never mirrored into the IPv6 chain — an IPv4 CIDR simply cannot express an IPv6 rule. If Events shows drops in the IPv6 zone, add a second rule with the IPv6 prefix as its source.

Why it broke after the ZimaOS update: Docker flushes its filter chain on every restart, and a system update restarts Docker. With rules committed, ZFW re-applies automatically — but against the container/port inventory from before the update. If the update changed which ports are published, re-applying (which recompiles against the live inventory) is the right fix, not a workaround.

If you paste the Events lines you see while the VPN connection fails, I can tell you exactly which of the two you’re hitting.

Hi Lintux,

thank you very much for your help.

For the fisrst point,
Glad that my feedback has permitted to solve a bug…

I’ll test Zima App later…

For the 2nd,

I think I’m good now.

I guess my rules wasn’t well created.

Wrote new ones. Now, it seems to be good.

But my link speed is horribly slow.
I’ll have a look from another access point.

Don’t know if it is due to VPN passthrough or mobile link.

Thanks again.
Yoh