Skip to content

Fix CIDR whitelist entries being discarded on hot reload - #4

Merged
Robbie1977 merged 1 commit into
mainfrom
fix/cidr-whitelist-on-reload
Aug 1, 2026
Merged

Fix CIDR whitelist entries being discarded on hot reload#4
Robbie1977 merged 1 commit into
mainfrom
fix/cidr-whitelist-on-reload

Conversation

@Robbie1977

Copy link
Copy Markdown
Contributor

The bug

A CIDR range added to /logs/whitelist.txt works until the next hot reload, then silently stops:

Ignoring invalid whitelisted IP entry in /logs/whitelist.txt: 10.42.0.0/16
Ignoring invalid whitelisted IP entry in /logs/whitelist.txt: 129.215.0.0/16

Root cause

The list compiler had two copies. docker-entrypoint.sh compiles the lists once at container start; health-monitor.sh recompiles them on every change to blocked.txt/whitelist.txt, then reloads nginx.

When the entrypoint learned to route CIDR ranges into a geo map (273c827, 1ae044e), the monitor's private copy of the compiler did not. So:

  • a range accepted at boot is discarded by the first hot reload, and
  • because the monitor never writes whitelisted-cidrs.map at all, a range added after boot never takes effect without restarting the container.

That is why both 129.215.0.0/16 (a VPN pool) and 10.42.0.0/16 (the Rancher/Canal pod network the VFBquery warmup tool runs from) were being rejected.

The fix

Both scripts now source a shared ip-maps.sh, so they cannot drift again. Drift was the bug, so a third copy of the function would have been the wrong shape of fix.

The test suite includes static wiring assertions that grep both callers and fail if either stops sourcing the library, reintroduces a private copy of the compiler, or stops passing the CIDR map. Unit tests alone would not have caught the original bug — each copy of the compiler was individually correct.

Second defect, same asymmetry

The auto-block exemption used an exact string match (is_ip_listed), so an address inside a whitelisted CIDR could trip a probe pattern and get auto-blocked despite the operator having whitelisted its whole network — after which the two map files disagree about the same address. It is now range-aware (is_ip_whitelisted).

IPv6 ranges are not matched arithmetically (no bitwise ops in BusyBox awk, and :: compression makes textual prefix matching unsafe). Instead, if any IPv6 range is whitelisted at all, no IPv6 address is auto-blocked — erring towards leaving a scanner unblocked rather than locking out a trusted host, which is the right way round here: the probe filter still returns 403 either way, auto-blocking only saves the work of matching it.

BusyBox finding

Range membership is computed by division rather than 2^(32-bits). BusyBox can be built without libm, and there ^ aborts with Math support is not compiled in — which made every in-range address read as out-of-range. This was caught only by running the suite under the runtime image's own awk; it would have shipped silently and failed only in production.

Tests

test/ip-maps-test.sh, 46 checks: line classification, whitelist compilation (dedupe, sort, CRLF, uppercase hex, comments, junk, and an explicit assertion that 129.215.0.0/16 lands in the geo map and is not reported invalid), blocklist compilation, 15 IPv4 range-membership cases including /0, /32 and both boundaries, auto-block exemption, and the wiring assertions.

Run at image build time, so a regression fails docker build rather than surfacing hours later as a cache bypass that quietly doesn't happen. Gating on the build rather than a new workflow job was deliberate — CI already runs docker build.

Verified passing under BusyBox ash with a libm-less BusyBox awk, dash, bash in POSIX mode, and BusyBox ash with system awk. Also verified end-to-end against the real health-monitor.sh: appending the two ranges to a live whitelist now produces a whitelisted-cidrs.map containing both, with no "Ignoring invalid" warning.

README

Documents that the whitelist accepts ranges and why that matters (a VPN or pod network reassigned per session can be whitelisted once), that the blocklist deliberately does not, that rejected lines are reported on the container log, and — for the first time — the X-Force-Refresh header, which previously existed only as a code comment. Includes the x-cache-status: BYPASS vs HIT check for confirming a bypass actually happened.

A CIDR range added to /logs/whitelist.txt worked until the next reload,
then silently stopped:

    Ignoring invalid whitelisted IP entry in /logs/whitelist.txt: 10.42.0.0/16
    Ignoring invalid whitelisted IP entry in /logs/whitelist.txt: 129.215.0.0/16

The list compiler had two copies. docker-entrypoint.sh learned to route
ranges into a `geo` map (273c827, 1ae044e), but health-monitor.sh kept a
private pre-CIDR copy and recompiled the whitelist with it on every file
change. So ranges were dropped on reload, and because the monitor never
wrote whitelisted-cidrs.map at all, a range added after boot never took
effect without a container restart.

Both scripts now source ip-maps.sh, so they cannot drift again. The test
suite includes static wiring assertions that fail the build if they do --
unit tests alone would not have caught the original bug, since each copy
of the compiler was individually correct.

Also fixes a second defect exposed by the same asymmetry: the auto-block
exemption used an exact string match, so an address inside a whitelisted
CIDR could trip a probe pattern and be auto-blocked despite its whole
network being whitelisted, leaving the two map files disagreeing about
the same address. It is now range-aware.

Range membership is computed by division rather than 2^(32-bits): BusyBox
can be built without libm, and there `^` aborts with "Math support is not
compiled in", which made every in-range address read as out-of-range --
silently, and only on the alpine runtime image.

46 checks, run at image build time so a regression fails `docker build`
rather than surfacing later as a cache bypass that quietly doesn't happen.
Verified under BusyBox ash with a libm-less awk, dash, and bash in POSIX
mode.

README documents range support, the fact that rejected lines are logged,
and X-Force-Refresh, which until now existed only as a code comment.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y8ZAhRM7gh8arDx9SuSWcT
@Robbie1977
Robbie1977 merged commit 1411291 into main Aug 1, 2026
1 check failed
@Robbie1977
Robbie1977 deleted the fix/cidr-whitelist-on-reload branch August 1, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants