Hold an image shut until its first boot has set a password #12
No reviewers
Labels
No labels
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/common!12
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/webmin-auth-hardening"
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 #11.
The problem
A built appliance has accounts before its first boot has set a password on
any of them, and three things in the build made that state more open than it
has to be.
conf/turnkey.d/rootpass, for a build given noROOT_PASS, wrote apassword field that a submitted password can match, rather than one that
matches nothing until a password is set. The PAM stack the
webminpackageinstalls carries
nullok, which lets an account whose password is not setbe authenticated without one. And the web interface starts with the rest of
the boot, before the first boot scripts that configure the appliance have
run at all.
Established before changing anything
Why the placeholder existed, and why it cannot simply become
!./etc/default/inithooksshipsSUDOADMIN=false, sofirstboot.d/29sudoadminruns
turnkey-sudoadmin off --disable-setpasson every first boot, before30rootpass. That path callspasswd --unlock root. Measured againstshadow in a scratch root:
turnkey-sudoadminruns underbash -e, so exit 3 there ends the hookbefore
permitrootlogin_ssh,restart_sshdandset_status, and/etc/sudoadmin/stateis never written. Locking the account is thereforethe fix that breaks the first boot, which
docs/traps.mdputs at the top ofthe list.
*is what the account gets:crypt()returns it for no input,passwd --unlockaccepts it, andbin/setpass.pysets the real passwordwith
chpasswd, which replaces the field whatever it held.What the placeholder was for. Nothing in the first boot authenticates
against it.
sshdshipsPermitRootLogin yesbut OpenSSH defaultsPermitEmptyPasswords no, so it was never an SSH login. The first bootdialog runs on tty1 with
Conflicts=getty@tty1.service, so there is nologin prompt to answer during it either. What it bought was a console login
on an image whose first boot has not run.
Why not
allow=or a loopback bind inminiserv.conf. Both outlive thefirst boot: nothing removes them, so an
allow=written at build time locksout remote administration for good, and a loopback bind has to be rewritten
and the service restarted by something later. Holding the unit back needs no
undoing, so that is what is here instead.
The change
conf/turnkey.d/rootpasswrites*, with the reason!is not used in acomment. The same script expanded
ROOT_PASSagainst the build directorybefore handing it to
chpasswd, so a password holding a glob characterwas not the password that got set; it is quoted now.
conf/turnkey.d/webmin-pam, new, takesnullokout of the stack andfails rather than report success if the stack is absent or the option
survives.
conf/turnkey.d/webmin-enableholds the web interface back.webmin.servicegains a condition on the marker inithooks writes when its boot scripts are
done, and a path unit starts it when the marker appears. The conditions are
systemd triggering conditions, an OR, so an image with no inithooks at all
is not left without a web interface; the path unit triggers a one shot that
stays active, so it fires once and does not restart Webmin behind an
administrator who stopped it.
conf/samba-rootpass, which runs after rootpass on a Samba appliance andwrote the old field back, writes
*too, quotes the password and nolonger traces it into the build log.
What each part does, asked of the real
pam_unixon libpam 1.7.0-5 (theappliance's): the old field let any password in, without a prompt,
through any stack carrying
nullok, which includes Debian'scommon-auth.*refuses everything through every stack, sorootpassis the changethat holds the account shut. Removing
nullokfrom Webmin's stack alsocloses the old field there on 1.7 (not on libpam 1.5, where the empty
password then gets in);
common-authkeeps it, #13. The hold keeps theinterface down until the first boot's scripts have run, and the marker
means they ran, not that they succeeded.
Tests, and what is real in them
58 bats. The question "would this password get in" is put to Linux-PAM
itself:
tests/pam-authenticatecallspam_authenticatethrough libpamover the scratch image's own stack, in a private mount namespace with the
scratch shadow and passwd bind mounted over the real ones, so
pam_unixand its
unix_chkpwdhelper are the installed ones. (The first versionmodelled
pam_unixby hand, and review found the model wrong.) Whatpam_unixmakes of the crypt of the empty string differs between libpam1.5 and 1.7, so the five tests that depend on it run on 1.7 and skip by
name elsewhere; the CI runner has 1.5.3. Whether the interface would start
is answered by
systemd-analyze conditionover the packagedwebmin.serviceand the drop-in together, and both are handed tosystemd-analyze verify.The shadow tools and
smbpasswdare stubbed:usermod,chpasswdandpasswdchroot into the root they are given, so an ordinary user cannotaim them at a scratch tree. Each stub carries the behaviour it reproduces
and the command that measured it.
tests/before-firstboot.batsruns the three scripts over one image in theorder a build runs them and asks the whole question of the result, through
Webmin's stack and through
common-auth. With this branch'swebmin-pamand the previous
rootpassandwebmin-enable, it fails 4 of its 6tests; with only
rootpassreverted, 1.tests/rootpass.batsfails 5 of9 against the previous script,
tests/samba-rootpass.bats4 of 6.Coverage: 100 percent, 48 of 48 lines over the five measured files, kcov 43.
tests/coverage.shmeasures a list rather than one file.rootpassandwebmin-enablemoved from/bin/shto/bin/bashbecause kcov measuresbash and not dash and decision 0003 gives no exemption for a file a change
touches; rootpass uses no bash construct. The gate stays at 100.
What this does not do
Nothing is built, signed or published, and no build lock was taken, so no
appliance has been booted with this. The end to end verdict belongs to an
appliance boot test.
The marker the unit waits for is
/run/inithooks-complete, which today'sinithooks already writes, so this stands on its own and needs no ordering
against Keel-Linux/inithooks#17 or #18. The first boot that reaches it
records that in
/var/lib/webmin-after-firstboot/opened, and every bootafter starts Webmin without waiting. A first boot that never finishes keeps
Webmin shut until
systemctl start webmin-after-firstboot.serviceis runfrom a console.
common-authstill carriesnullok: #13.Reviewed as a security change. I re-measured every claim rather than reading
them; where I disagree it is because a measurement disagreed, and the
commands are given so you can settle it either way.
Reproduced as stated: the
passwd --unlocktable (!exits 3 and leavesthe field,
*and a hash exit 0 and leave the field,!<hash>exits 0 andstrips the
!), measured withunshare -r passwd --root; thatturnkey-sudoadminrunsuser_state root unlockunderbash -eahead ofrestart_sshdandset_status, so the exit 3 really does end that hook;that
bin/setpass.pypipes tochpasswdand nothing in inithooks orconfconsole reads the field;
crypt()returns*0for*on every input Itried; coverage 100.00 over the four files, 39/39, and
tests/rootpass.batsfailing 5 of 9 against the previous script.
HIGH —
tests/helpers.bash:83-104, and the prose derived from it.blank_equivalentandauthenticatesare a hand-written model ofpam_unix, and the model is missing two rules the module has. Inverify_pwd_hash()(libpam 1.7.0-5,modules/pam_unix/passverify.c:80-92)an empty submitted password is refused outright, before any comparison, when
the stack does not carry
nullok; and a stored field whose first characteris
*or!is refused outright whatever was submitted. The helper modelsneither, so it reports at least one acceptance the module does not, and the
suite is green against a
pam_unixthat does not exist. That isdocs/traps.md, "Asserting the configuration is not asserting thebehaviour", with the stub half of "the test stub has to be as impolite as
the program".
The instrument that would have caught it is sound; it was just fed wrong.
pam_read_passwords()(libpam/include/pam_inline.h) reads NUL-separatedstrings, so
delivers nothing and the helper tests the empty password whatever you piped
in — which is also why that sandbox looked as though it "refused every real
comparison". It does not; the user namespace is not the problem. Add the
terminator and a correct password returns 0:
with a scratch shadow bind-mounted over
/etc/shadowunderunshare -r -m.That gives a real table over both options and every field you care about.
Please correct the helper to carry both rules, re-run, and re-derive the
"each does part of it" sentence in the PR body and the wording in
changes/turnkey.changelogfrom the result — one half of it does notsurvive the corrected model.
HIGH —
conf/turnkey.d/webmin-enable:17-22: there is no way back, andnothing says so.
/run/inithooks-completeis written by inithooks'runat the end of the everyboot pass, on every boot, and
/runis a tmpfs. Sothis does not gate one boot; it gates every boot for the life of the
appliance, behind
inithooks.servicereaching that line. If it ever doesnot —
docs/traps.mdentry 1 is a first boot that blocks forever in onehook, hit three times here — the appliance has no web interface and no
console, on every boot, and
systemctl start webminexits 0 and startsnothing, because systemd counts an unmet condition as a skipped, successful
start. At minimum the changelog and the comment need the recovery line
(
touch /run/inithooks-complete, or startwebmin-after-firstboot.service). Better would be a second thing thedrop-in accepts that means "this is not the first boot", so a later boot
does not inherit the first boot's gate.
MEDIUM —
conf/turnkey.d/webmin-enable: the marker does not mean what thehold wants it to mean.
exec_scriptslogs a failing script and carries on,and the marker is touched afterwards regardless, so a boot in which
30rootpassexits non-zero still opens the interface. That is survivableonly because
rootpassnow writes a field nothing matches — which makesrootpass, not the hold, the change carrying the weight. The PR body readsthe other way round; worth correcting, because it changes what a future
reader thinks is safe to weaken.
MEDIUM —
conf/turnkey.d/webmin-pam:19: one stack out of the set./etc/pam.d/common-authon a built core carriespam_unix.so nullokaswell — it is Debian's default — and it is the stack nearly everything else
includes. "No appliance account is meant to be reachable that way and
nothing in an appliance asked for the option" applies to it at least as
strongly as to the one file this script edits. Either widen the script or
open the follow-up and name it here.
MEDIUM —
conf/samba-rootpass:7,10: both of this PR's defects, untouched.It carries the same placeholder and the same unquoted
$ROOT_PASSexpansionthis PR fixes two files away. "No Keel appliance builds it" is a good reason
not to ask an appliance boot about it; it is not a reason to leave the
second copy in the tree while the first is being corrected, and the PR body
points straight at it. It is two lines plus a look at
smbpasswd -n.LOW —
conf/turnkey.d/webmin-enable: the drop-in is never verifiedagainst the unit it modifies.
systemd-analyze verifyis handed the twonew units; the drop-in is only ever read by
systemd-analyze conditionoverits own Condition lines. A packaged
webmin.servicethat one day carries aplain
Condition*=of its own would AND with these two and never besatisfied. Verifying
webmin.servicewith the drop-in in place costs oneline.
LOW —
tests/before-firstboot.bats: the "four of five" is not reproducibleas written. Against the previous scripts with
webmin-pamabsentaltogether, all five die at status 127 on the missing script. Keeping
webmin-pamand reverting only the other two gives exactly the four youquote. Say which baseline the number is against.
LOW —
tests/coverage.sh: a three-way conflict, and not a mechanicalone. #5 and #8 both rewrite this file into a
targets=("file:suite")shapewith one kcov run per file; this branch rewrites it into one run over a
measuredlist. Whichever lands second has to be rewritten, not rebased.This is the security change — land it first and let the other two adopt
whichever shape wins.
The change does what it sets out to do, and the three parts are the right
three. The findings above are about what the tests prove, what happens on
the boot after the one this was written for, and one file left half-done.
None of them is a reason to keep the current state.
Warning
Review addressed in 7ba662a..7d4ef76, all new commits. The tests now call
pam_authenticatethrough the real libpam over the scratch stack. On libpam 1.7 the old field let any password in without a prompt through anynullokstack, so*is what holds the account shut. The PR body and changelog now say that, andcommon-authis #13. Webmin now opens for good once a first boot completes, with a recovery line, andsamba-rootpassis fixed. CI runs libpam 1.5.3, so the 5 version-specific tests skip there by name.