Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions changes/turnkey.changelog
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
turnkey-core-19.0 (1) turnkey; urgency=low

* An image is exported with root locked, or the build fails. The last
step of root.patched, mk/turnkey/seal-root, accepts a root password
field of '*', '!' or '!*' and nothing else, without printing it, then
writes the build date (UTC) to /etc/keel/build-date. A build time
ROOT_PASS is ignored, with a line saying so and not its value, by
conf/turnkey.d/rootpass and conf/samba-rootpass: it gave every copy
of the image the same password, and five older WordPress images
shipped U6aMy0wojraho, the crypt() of the empty string. inithooks'
first boot offers to keep a root password set when the container was
created (pct create --password) only when the image shipped none and
the password changed on or after the build date. tests/seal-root.bats
(100 percent of its lines); the ROOT_PASS tests of rootpass.bats,
samba-rootpass.bats and before-firstboot.bats now expect it ignored.

* A login gets a UTF-8 locale. 'conf/turnkey.d/locale' wrote LC_ALL=C
and LC_CTYPE=C into /etc/default/locale, which pam_env gives every
login, so a shell ran in an ASCII locale and dialog, confconsole after
Expand Down
14 changes: 6 additions & 8 deletions conf/samba-rootpass
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@ NO_PASSWORD='*'

passwd --unlock root # useful when testing in chroot

# A build time ROOT_PASS is not used, as in conf/turnkey.d/rootpass: the
# image ships both accounts without a password, and the first boot sets
# them (overlays/samba-fileserver, 35samba-*).
if [ -n "$ROOT_PASS" ]; then
echo "root:$ROOT_PASS" | chpasswd
else
usermod -p "$NO_PASSWORD" root
echo "samba-rootpass: ROOT_PASS is ignored; the image ships root locked" >&2
fi

if [ -n "$ROOT_PASS" ]; then
(echo "$ROOT_PASS" ; echo "$ROOT_PASS" ) | smbpasswd -a -s root
else
smbpasswd -a -n root
fi
usermod -p "$NO_PASSWORD" root
smbpasswd -a -n root
14 changes: 9 additions & 5 deletions conf/turnkey.d/rootpass
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@
# passwordless account"), exits 3 and leaves the rest of that hook undone.
NO_PASSWORD='*'

# A build time ROOT_PASS is not used: it was a password every copy of the
# image shared, and the first boot could not tell it from one set when the
# container was created. The image ships root locked, which
# mk/turnkey/seal-root checks before export; the first boot sets it.
if [ -n "$ROOT_PASS" ]; then
echo "rootpass: ROOT_PASS is ignored; the image ships root locked" >&2
fi

# chroot only environments don't need a root password
if [ ! "$CHROOT_ONLY" ]; then
if [ -n "$ROOT_PASS" ]; then
echo "root:$ROOT_PASS" | chpasswd
else
usermod -p "$NO_PASSWORD" root
fi
usermod -p "$NO_PASSWORD" root
else
passwd --lock root
fi
3 changes: 3 additions & 0 deletions mk/turnkey.mk
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ define _root.patched/post
fab-chroot $O/root.patched "dpkg -i *.deb && rm *.deb && rm -f /var/log/dpkg.log"

fab-chroot $O/root.patched "which postsuper >/dev/null && postsuper -d ALL || true"

# last: root locked or the build fails, and the build date stamped
$(FAB_PATH)/common/mk/turnkey/seal-root $O/root.patched
endef
root.patched/post += $(_root.patched/post)

Expand Down
44 changes: 44 additions & 0 deletions mk/turnkey/seal-root
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/bash
# seal-root ROOTFS
#
# The last step of root.patched (mk/turnkey.mk): fails the build unless
# root's password field in ROOTFS/etc/shadow is '*', '!' or '!*' (what
# passwd --lock makes of '*'), then writes the build date, YYYY-MM-DD in
# UTC, to ROOTFS/etc/keel/build-date.
#
# inithooks' first boot offers to keep a root password set when the
# container was created (pct create --password) only when the password
# changed on or after that date: the image must ship none of its own. A
# build with ROOT_PASS shipped one every copy shared, and five older
# WordPress images shipped U6aMy0wojraho, the crypt() of the empty string.
#
# The field is compared and never printed.

set -euo pipefail

if [[ $# -ne 1 ]]; then
echo "usage: seal-root ROOTFS" >&2
exit 2
fi
rootfs=$1
shadow=$rootfs/etc/shadow

if [[ ! -r "$shadow" ]]; then
echo "seal-root: cannot read $shadow" >&2
exit 1
fi

if ! entry=$(grep -m 1 '^root:' "$shadow"); then
echo "seal-root: $shadow has no root entry" >&2
exit 1
fi
IFS=: read -r _ field _ <<< "$entry"

if [[ "$field" != '*' ]] && [[ "$field" != '!' ]] && [[ "$field" != '!*' ]]; then
echo "seal-root: root has a password in $shadow; an image ships" \
"root locked ('*'), and the first boot sets its password" >&2
exit 1
fi

mkdir -p "$rootfs/etc/keel"
date -u +%F > "$rootfs/etc/keel/build-date"
4 changes: 2 additions & 2 deletions tests/before-firstboot.bats
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,11 @@ build_image() {
[ "$status" -eq 0 ]
}

@test "a build given a root password is held shut the same way" {
@test "a build given a root password is held shut, and the password is not set" {
export ROOT_PASS=s3cret
build_image
run ! unit_would_start "$DROPIN"
run authenticates "s3cret"
run refuses "s3cret"
[ "$status" -eq 0 ]
run refuses ""
[ "$status" -eq 0 ]
Expand Down
1 change: 1 addition & 0 deletions tests/coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ targets=(
"conf/samba-rootpass:tests/samba-rootpass.bats"
"conf/desktop:tests/desktop.bats"
"conf/turnkey.d/rootpass:tests/rootpass.bats"
"mk/turnkey/seal-root:tests/seal-root.bats"
"conf/turnkey.d/webmin-enable:tests/webmin-enable.bats"
"conf/turnkey.d/webmin-pam:tests/webmin-pam.bats"
"conf/turnkey.d/webmin-net:tests/webmin-net.bats"
Expand Down
28 changes: 14 additions & 14 deletions tests/rootpass.bats
Original file line number Diff line number Diff line change
Expand Up @@ -62,27 +62,27 @@ setup() {
[ "$status" -eq 0 ]
}

@test "a build time ROOT_PASS is the password, and nothing else is" {
# A build time ROOT_PASS was a password every copy of the image shared, and
# the first boot cannot tell it from one set when the container was created
# (mk/turnkey/seal-root). The image ships root locked whatever it says.

@test "a build time ROOT_PASS is ignored and root stays locked" {
export ROOT_PASS=s3cret
run "$SCRIPT"
run --separate-stderr "$SCRIPT"
[ "$status" -eq 0 ]
run authenticates "s3cret"
[ "$(field_of root)" = '*' ]
run refuses "s3cret"
[ "$status" -eq 0 ]
run refuses ""
run nothing_authenticates
[ "$status" -eq 0 ]
}

@test "a ROOT_PASS holding a glob character is set as written" {
mkdir -p "$IMAGE/build"
touch "$IMAGE/build/root:pass"
cd "$IMAGE/build"
export ROOT_PASS='p*ss'
run "$SCRIPT"
[ "$status" -eq 0 ]
run authenticates 'p*ss'
[ "$status" -eq 0 ]
run refuses 'pass'
@test "an ignored ROOT_PASS is said, without its value" {
export ROOT_PASS='n0t-in-the-log'
run --separate-stderr "$SCRIPT"
[ "$status" -eq 0 ]
[[ "$stderr" == *"ROOT_PASS is ignored"* ]]
[[ "$output$stderr" != *"n0t-in-the-log"* ]]
}

@test "a chroot only build locks the account instead" {
Expand Down
29 changes: 9 additions & 20 deletions tests/samba-rootpass.bats
Original file line number Diff line number Diff line change
Expand Up @@ -40,29 +40,18 @@ setup() {
[ "$(tail -1 "$STUB_LOG")" = "smbpasswd -a -n root" ]
}

@test "a build time ROOT_PASS is the password for both, and nothing else is" {
export ROOT_PASS=s3cret
run "$SCRIPT"
[ "$status" -eq 0 ]
run authenticates s3cret
[ "$status" -eq 0 ]
run refuses ""
[ "$status" -eq 0 ]
[ "$(tail -1 "$STUB_LOG")" = "smbpasswd -a -s root" ]
[ "$(cat "$STUB_LOG.stdin")" = "$(printf 's3cret\ns3cret')" ]
}
# A build time ROOT_PASS was shared by every copy of the image; the image
# ships both accounts without one whatever it says (mk/turnkey/seal-root).

@test "a ROOT_PASS holding a glob character is set as written" {
mkdir -p "$IMAGE/build"
touch "$IMAGE/build/root:pass"
cd "$IMAGE/build"
export ROOT_PASS='p*ss'
run "$SCRIPT"
[ "$status" -eq 0 ]
run authenticates 'p*ss'
@test "a build time ROOT_PASS is ignored for both accounts" {
export ROOT_PASS=s3cret
run --separate-stderr "$SCRIPT"
[ "$status" -eq 0 ]
run refuses 'pass'
[ "$(field_of root)" = '*' ]
run refuses s3cret
[ "$status" -eq 0 ]
[ "$(tail -1 "$STUB_LOG")" = "smbpasswd -a -n root" ]
[[ "$stderr" == *"ROOT_PASS is ignored"* ]]
}

@test "the build password is not written to the build log" {
Expand Down
117 changes: 117 additions & 0 deletions tests/seal-root.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/usr/bin/env bats
# Tests for mk/turnkey/seal-root, the last step of root.patched: an image is
# exported with root locked, or the build fails, and it carries the date it
# was built on.
#
# Why: inithooks' first boot offers to keep a root password set when the
# container was created (pct create --password). It can only tell such a
# password from one the image shipped if the image shipped none, and five
# older WordPress images shipped U6aMy0wojraho, the crypt() of the empty
# string; a build with ROOT_PASS set shipped a password every copy shares.

bats_require_minimum_version 1.5.0

setup() {
TESTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")" && pwd)"
SCRIPT="$TESTS_DIR/../mk/turnkey/seal-root"
ROOTFS=$BATS_TEST_TMPDIR/rootfs
mkdir -p "$ROOTFS/etc"
}

# shadow_root FIELD
# A shadow file whose root entry has password field FIELD.
shadow_root() {
printf 'daemon:*:20718:0:99999:7:::\nroot:%s:20718:0:99999:7:::\n' \
"$1" > "$ROOTFS/etc/shadow"
}

@test "a root field of '*' passes and the build date is stamped" {
shadow_root '*'

run "$SCRIPT" "$ROOTFS"

[ "$status" -eq 0 ]
[ "$(cat "$ROOTFS/etc/keel/build-date")" = "$(date -u +%F)" ]
}

@test "a root field of '!' passes" {
shadow_root '!'

run "$SCRIPT" "$ROOTFS"

[ "$status" -eq 0 ]
}

@test "a root field of '!*', what passwd --lock makes of '*', passes" {
shadow_root '!*'

run "$SCRIPT" "$ROOTFS"

[ "$status" -eq 0 ]
}

@test "a real password hash fails the build and is not printed" {
shadow_root '$y$j9T$abcdefghijklmnop$qrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123'

run --separate-stderr "$SCRIPT" "$ROOTFS"

[ "$status" -eq 1 ]
[[ "${stderr:-}" == *"root has a password"* ]]
[[ "$output${stderr:-}" != *'$y$'* ]]
[ ! -e "$ROOTFS/etc/keel/build-date" ]
}

@test "the hash of the empty password fails the build" {
shadow_root 'U6aMy0wojraho'

run --separate-stderr "$SCRIPT" "$ROOTFS"

[ "$status" -eq 1 ]
[[ "$output${stderr:-}" != *U6aMy0wojraho* ]]
}

@test "an empty field, which lets anyone in, fails the build" {
shadow_root ''

run "$SCRIPT" "$ROOTFS"

[ "$status" -eq 1 ]
}

@test "a locked real hash fails too: unlocking it gives the password back" {
shadow_root '!$y$j9T$abcdefghijklmnop$qrstuvwxyz'

run "$SCRIPT" "$ROOTFS"

[ "$status" -eq 1 ]
}

@test "a shadow file with no root entry fails the build" {
printf 'daemon:*:20718:0:99999:7:::\n' > "$ROOTFS/etc/shadow"

run --separate-stderr "$SCRIPT" "$ROOTFS"

[ "$status" -eq 1 ]
[[ "${stderr:-}" == *"no root entry"* ]]
}

@test "a root file system without a shadow file fails the build" {
run --separate-stderr "$SCRIPT" "$ROOTFS"

[ "$status" -eq 1 ]
[[ "${stderr:-}" == *"cannot read"* ]]
}

@test "the appliance build seals root.patched as the last step of its post hook" {
local mk=$TESTS_DIR/../mk/turnkey.mk
local hook
hook=$(sed -n '/^define _root.patched\/post/,/^endef/p' "$mk")
last=$(grep -v '^[[:space:]]*\(#.*\)\?$' <<< "$hook" | tail -2 | head -1)
[[ "$last" == *'common/mk/turnkey/seal-root $O/root.patched' ]]
}

@test "no root file system given is a usage error" {
run "$SCRIPT"

[ "$status" -eq 2 ]
}
Loading