The init fence closes a port it cannot redirect #18
No reviewers
Labels
No labels
accessibility
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Keel-Linux/inithooks!18
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/fence-no-silent-skip"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #17.
The problem
iptables_add_redirectskipped the REDIRECT rule for an address familywhose nat table cannot be used, warned, and carried on.
bin/turnkey-init-fencethen started the mini server and reported success, and the unit sat
active, while the ports the fence was asked to stand in front of reachedthe application over that family.
In an LXC container whose host has not loaded
ip6table_natthat is thenormal path, not an edge case, and the warning lands in the journal of a
first boot nobody is watching. A mitigation that declines and reports
success is worse than one that is not there, because the rest of the system
is written as though it worked.
Why the skip is not simply reverted
#3 put it there deliberately. Before it, the fence exited 3 on the first
family it could not install a rule for: the unit failed, the mini server
never started and
29tagidthen failed reloading it. Going back to thattrades one wrong answer for another, and a louder warning would still leave
the port open.
The container case #3 was serving is legitimate. What was wrong was the
conclusion: if the port cannot be redirected to the fence, the answer is
that the application behind it must not answer either, not that the fence
should shrug.
The change
fence_close_portrefuses the port in the filter table, which a containerdoes have, inserted at the head of the INPUT chain so that it precedes the
ACCEPT rules
webmin-fwinstalls for the same ports. Appending would haveput it after them and matched nothing. The rule is
! -i lo ... -j REJECT --reject-with tcp-reset: loopback is spared, as aREDIRECT in PREROUTING always spared it, and the reset is because an IPv6
client given the default ICMP error waits for its timeout instead of
failing (measured, below).
The head of the chain is the chain as it stands when the fence starts. A
restore of a saved firewall flushes the tables it names, and with them the
refusal and the REDIRECT rules alike. The unit is ordered after the
services that restore one at boot (
netfilter-persistent, Webmin'swebmin-iptablesandwebmin-ip6tables); a restore while the fence is upstill unfences every port, and the comment and changelog say so.
What the fence promises then holds either way:
HTTP_FENCE_PORTandHTTPS_FENCE_PORT, which keeps #3's outcome, including the29tagidreload;
<3>message, because then nothing at all is fencing it. That is the only case
left where the unit fails, and it is a case where failing is the truth.
Failing does not mean closed:
stop-postthen removes every rule thefence installed and the application answers on every port. It
does not cascade the way the pre-#3 failure did:
29tagidhas askedsystemctl is-activebefore reloading since 2.3.6+keel1, so a failed fenceis skipped rather than fatal to the hook.
fence_open_portundoes it, andiptables_delete_redirectcalls it for bothfamilies, so
stopand thestop-postcleanup after a failed run leave noport closed behind them. That keeps the existing policy: a failed fence
cleans up rather than locking the appliance out, and the port is open again
because the fence is gone, not because it pretended.
The message moved from
WARNING:to the<4>prefix the rest of the fileuses for a journal priority.
Tests
31 in the file, 116 in the suite: the refusal for one family and for both,
the insertion at the head of the chain, the removal loop, and the fatal path
twice, through the library and through the script. The second one matters on
its own: a library function that returns non-zero as an error only stops its
caller if the caller runs under errexit, and
bats runturns errexit off, soa test that only drives the library cannot see it (
docs/traps.md, "A batssuite cannot see a library that kills its caller").
One assertion in the pre-existing "both nat tables" test was a bare
!before a later command, so it asserted nothing; it is now a count that does.
Keel-Linux/inithooks#16 is rewriting the other negations in this file, so
expect a conflict in
tests/test-init-fence.batsbetween the two.tests/test-init-fence-netfilter.bats, 8 tests, asks the real netfilter:tests/netns-sandboxruns the library against realiptablesandip6tablesin a network namespace of its own, with a client namespace onthe other end of a veth pair, and connects over IPv6, IPv4 and loopback.
The only stand-in is the answer "no nat table" for one family. Against the
branch as first reviewed it fails 4, which are the review's findings plus
the silent IPv6 refusal.
kcov (first version):
lib/init-fence.sh78 of 79 lines, 98.73 percent,total 99.57; now 83 of 84, 98.81, total 99.58, 125 bats. Over
the 98 gate and decision 0003's 95 percent bar. The uncovered line is line
154, the first line of the multi-line
simplehttpd.pyinvocation that kcovattributes to a later line; it was uncovered before this branch and is not
part of it. Every line this branch adds is covered.
What this does not do
Nothing is built or published and no build lock was taken, so this has not
run on a booted container; the rules are measured in a network namespace,
with the nf_tables backend where the appliance uses iptables-legacy.
2.3.6+keel6is this one's; #14 has moved tokeel7and lands after it.Reviewed alongside Keel-Linux/common#12. Coverage reproduced exactly: 116
bats,
lib/init-fence.sh78/79 at 98.73, total 99.57, and the one uncoveredline is 154 and is uncovered on the parent commit too. The fatal path tested
twice, once through the library and once through the script, is the right
call and the only one that proves it —
docs/traps.md, "A bats suite cannotsee a library that kills its caller". I also confirmed
stop-postreachesiptables_redirect stopon a failed start, so nothing this branch insertssurvives a failed run.
HIGH —
lib/init-fence.sh:41and the comment at :29-31: position 1 in thelive chain is not where the appliance's own rules come from.
common'swebmin-fwdoes not append to a running chain; it writes/etc/iptables/rules.v4andrules.v6as whole tables,*filterthroughCOMMIT, meant to be applied with a restore. A restore flushes the chain,so it takes out this branch's REJECTs and the fence's own ACCEPTs together
and the insertion position buys nothing against it. On the appliance I could
inspect, nothing applies those files at boot — INPUT is empty with policy
ACCEPT, so the insertion is uncontested there — but Webmin's Firewall module
applies them on demand, and an administrator who touches it while the fence
is up silently unfences everything. The unit orders itself after
iptables.service,ip6tables.service,nftables.service,firewalld.serviceandipset.service, none of which is what applies thosetwo files. Either order against the thing that does, or narrow the comment
to say the position is about the live chain only.
MEDIUM —
lib/init-fence.sh:91: the fatal path ends unfenced, not closed.return 1reachesbin/turnkey-init-fenceunder-eu, the unit fails,ExecStopPostrunsstop-post,SERVICE_RESULTis not success andiptables_redirect stopopens every port again — including the ones thatwere successfully closed a moment earlier. So the outcome of "a port that can
be neither redirected nor refused is fatal" is: application answering on all
of them, no fence, and a unit that says so. That is the right trade against
the silent version and it is consistent with the cleanup policy, but the
changelog and the comment both read as though failing means closed. One
sentence in each saying which it is.
MEDIUM —
lib/init-fence.sh:41: the refusal has no interface match. AREDIRECT in PREROUTING never applied to locally originated traffic; a REJECT
in INPUT does. I grepped
firstboot.d,everyboot.dand every Keelappliance overlay and nothing talks to a fenced port over loopback today, so
this costs nothing now — but it is a real behaviour change for a first-boot
hook that one day does, and it will present as a hook that works everywhere
except where the fence had to fall back.
-i lo -j ACCEPTahead of it, or! -i loon the rule, is one line and keeps the two paths equivalent.LOW —
lib/init-fence.sh:39: the message precedes the rule.fence_close_portannounces the refusal and then tries to install it. On thefailing path the journal carries the claim and then its retraction, and on
the caller's
return 1the claim is the last positive thing said about thatport. Move the echo below the insert.
LOW —
lib/init-fence.sh:54-57:fence_open_portcannot tell "nothingleft to remove" from "cannot ask". Deliberate, and stopping does have to
succeed, so this is right — but the comment says "tolerates a family that
cannot be asked at all" without saying that the two are indistinguishable to
the caller. Half a sentence.
LOW — landing order. This branch and #16 collide on exactly one hunk of
tests/test-init-fence.bats: the bare!beforeprintf ... | grep -q WARNING. This branch's replacement is agrep -cthat is compared, which is what #16 is after and does not need
run !toget it, so this should land first and #16 drop that hunk — it still has four
other bare negations in the same file that this branch does not touch. Same
answer for
2.3.6+keel6: this one first, #14 bumps.The reasoning for not reverting #3 is correct, and closing what cannot be
redirected is the right shape. The findings are about what the insertion
position actually guarantees, what the fatal path leaves behind, and saying
both out loud where the next reader will look.
Warning
Review addressed in
36663c5and7cc4139, then master merged in (d24ca3e). The fence is now measured against real iptables in a network namespace. The unit starts after the services that restore a saved firewall at boot, and the comment says an on-demand restore unfences every port. The fatal path is documented as open. The refusal skips loopback and is now a tcp-reset, because the default ICMP error left IPv6 clients hanging until they timed out.