Skip to content
Open
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
26 changes: 15 additions & 11 deletions COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ acceptance test of a recipe, docs/org-plan.md section 1).
| `overlay/usr/lib/inithooks/firstboot.d/40wordpress` | `tests/hook.bats` (29 tests) | 97.73 percent (43/44) under kcov | the hook itself, run for real |
| `overlay/usr/local/bin/keel-wp` | `tests/wrappers.bats` (25 tests) | 100 percent (8/8) under kcov | both cache branches, the quoting, the exit code it hands back, `DEBUG`, and the `turnkey-wp` link run for real |
| `overlay/usr/local/sbin/keel-wordpress-update` | `tests/wrappers.bats` (the same 25) | 100 percent (21/21) under kcov | both guards refused and satisfied, each wp-cli call made to fail, the whole ownership boundary, and the two names it must not take from the environment |
| `tests/lib/boot-test-lib.sh` | `tests/boot-test.bats` (73 tests) | 98.95 percent (282/285) under kcov | parsing, addresses, deadlines, the container marks, every verdict, and the image carrying none of the build time archive files |
| `conf.d/zzz-keel-archive` | `tests/keel-archive.bats` (13 tests) | 100 percent (26/26) under kcov | every way it enables and every way it refuses, including a staging keyring left in the image |
| `conf.d/zz-project-packages` | `tests/project-packages.bats` (14 tests) | 100 percent (31/31) under kcov | shared with keel-nodebb, where the pattern is maintained |
| `tests/lib/boot-test-lib.sh` | `tests/boot-test.bats` (79 tests) | 99.32 percent (292/294) under kcov | parsing, addresses, deadlines, the container marks, every verdict, and the image carrying none of the build time archive files |
| `conf.d/zzz-keel-archive` | `tests/keel-archive.bats` (17 tests) | 100 percent (37/37) under kcov | common's source and 990 pin verified and left alone, both written where common did not ship them, a 1001 pin refused, testing never enabled, and every way it refuses, including a staging keyring left in the image |
| `conf.d/zz-project-packages` | `tests/project-packages.bats` (16 tests) | 100 percent (35/35) under kcov | shared with keel-nodebb, where the pattern is maintained |
| `bin/keel-archive-check` | `tests/archive-check.bats` (27 tests) | 100 percent (54/54) under kcov | the build time check of tracker#7: the copy is the live archive, the entry names the keyring through signed-by, nothing says trusted=yes, and the copied InRelease verifies against the staging key |
| `overlay/usr/lib/inithooks/bin/wordpress.py` | `tests/dialog.bats` (3 tests) | not measured (kcov measures the shell) | dialog wrapper, run as the hook runs it inside a pseudo terminal: the answers reach the hook and the boxes are drawn on the terminal |
| `overlay/usr/lib/inithooks/lib/*.php` | the boot test | integration only | two PHP files `wp eval-file` runs; `conf.d/main` has PHP lint them |
| `conf.d/main` | the build | integration only | build time script, 0004 pragmatic limits |
| `tests/boot-test.sh` | itself | integration only | the thin main of the acceptance test: keel and LXC as root |

Total over the eight measured shell files: **99.12 percent (564/569)**, 221
Total over the eight measured shell files: **99.33 percent (589/593)**, 239
bats tests, none failing. `tests/coverage.sh` fails below `COVERAGE_THRESHOLD`, which the workflow
sets to **97**, the lowest measured file. It is only ever raised (decision
0006).
Expand All @@ -30,11 +30,11 @@ sets to **97**, the lowest measured file. It is only ever raised (decision
kcov line coverage (threshold 97 percent):
100.00 21/21 keel-wordpress-update
100.00 8/8 keel-wp
100.00 31/31 zz-project-packages
100.00 26/26 zzz-keel-archive
100.00 35/35 zz-project-packages
100.00 37/37 zzz-keel-archive
99.00 99/100 wordpress.sh
97.73 43/44 40wordpress
98.95 282/285 boot-test-lib.sh
99.32 292/294 boot-test-lib.sh
100.00 54/54 keel-archive-check

### The two operator commands, and the link beside each
Expand Down Expand Up @@ -143,7 +143,7 @@ boot-test: /wp-admin/ answered 200 with the dashboard for the logged in admin
boot-test: a wrong password was refused
boot-test: https://archive.keellinux.org trixie is enabled and verified with /usr/share/keyrings/keel-archive-keyring.gpg
boot-test: apt-get update read https://archive.keellinux.org trixie and verified its signature
boot-test: apt takes inithooks from https://archive.keellinux.org at priority 1001, candidate 2.3.6+keel5
boot-test: apt takes inithooks from https://archive.keellinux.org at priority 990, candidate 2.3.6+keel5
boot-test: keel-transition is not in the image, which is what makes the next step a proof
boot-test: the keel-transition archive is 13836 bytes
boot-test: apt fetched keel-transition from https://archive.keellinux.org, against the digest of the signed index
Expand All @@ -168,7 +168,7 @@ the image deliberately stale so the archive is always ahead. Both would be
lies told to make a test pass.

The second is also dangerous here, and measuring it is what settled the
argument. `/etc/apt/preferences.d/keel` pins our origin at **1001**, the
argument. `/etc/apt/preferences.d/keel` pinned our origin at **1001**, the
priority that downgrades as well as upgrades. With the image one release ahead
of the archive:

Expand All @@ -181,8 +181,12 @@ The following packages will be DOWNGRADED:

and `keel-archive-keyring 0.1.0` ships only the **revoked** signing subkey
`694DE5E8`, so the appliance would have lost the ability to verify the archive
at all. The rule that follows is the one the policy check now guards: **an
image must not carry a project package the signed archive has not got.**
at all. The rule that followed was: **an image must not carry a project
package the signed archive has not got.** The pin is now 990 (tracker#23,
Keel-Linux/common#30), below 1000, so apt keeps a newer installed version
instead of downgrading it, and the policy check accepts exactly that case: the
candidate is the installed version alone, newer than the archive's, from no
other source.

So the test asserts the path instead of the increment, in four steps that are
all true today: `apt-get update` verifies the archive's signature;
Expand Down
9 changes: 7 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,11 @@ The appliance ships ``/etc/apt/sources.list.d/keel.sources`` **enabled** for
the signed ``trixie`` distribution of ``https://archive.keellinux.org``, with
``Signed-By`` naming ``/usr/share/keyrings/keel-archive-keyring.gpg`` from the
``keel-archive-keyring`` package, and ``/etc/apt/preferences.d/keel`` pins that
origin at 1001. So on a booted appliance::
origin at 990. Both come from common (``overlays/turnkey.d/keel-apt``,
Keel-Linux/common#30); on a bootstrap from before that, ``conf.d/zzz-keel-archive``
writes the same two files. 990 makes our build of a package the candidate over
any other archive and never replaces a newer installed version; the 1001 this
recipe used to ship downgraded (tracker#23). So on a booted appliance::

apt-get update # reads our archive and verifies its signature
apt-get upgrade # takes newer Debian and newer project packages
Expand All @@ -149,7 +153,8 @@ The boot test proves that path rather than proving that a newer version
happens to exist on the day it runs. It asserts that ``apt-get update``
verifies our archive's signature; that ``apt-cache policy`` shows a project
package the image carries with our archive as the source of its candidate at
that pin priority; that ``keel-transition``, which the image does not carry,
that pin priority, or the installed version kept because it is newer than
our archive's, which is what 990 promises; that ``keel-transition``, which the image does not carry,
comes down from our archive against the digest the signed index holds; and
that a project package the image does carry is downloaded again and put
through dpkg. apt refuses an archive it cannot verify before it asks for a
Expand Down
22 changes: 22 additions & 0 deletions changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
turnkey-wordpress-19.0 (6) turnkey; urgency=medium

* The overlay no longer ships /etc/apt/sources.list.d/keel.sources or
/etc/apt/preferences.d/keel (the Keel origin at 1001). At 1001 apt
downgraded every package newer than the archive's (tracker#23; COVERAGE.md
measured keel-archive-keyring 0.1.1 going back to 0.1.0), and both files,
at the paths common uses, would override the source and the 990 pin
Keel-Linux/common#30 ships. conf.d/zzz-keel-archive now verifies
common's source and pin and changes nothing, writes the same two files
(stable enabled, testing disabled, pin 990) on a bootstrap from before
common shipped them, refuses any other pin priority, and no longer
enables every Enabled: no line, which would have turned on the testing
track. The build time archive still wins over TurnKey's 999 pin during
the build: conf.d/main pins it by its Label, l=Keel Linux staging, at
1001, and conf.d/zz-project-packages removes that pin with the build time
source and fails if any apt file of the image still names that archive.
The boot test expects the archive at 990, reads only the stable stanza
of keel.sources, and accepts an installed project package newer than the
archive's that apt keeps, which is the no downgrade case.

-- Marcos Mendez <mendez.foto@gmail.com> Fri, 02 Oct 2026 17:00:00 +0000

turnkey-wordpress-19.0 (5) turnkey; urgency=medium

* bin/wordpress.py draws its password boxes on the terminal.
Expand Down
10 changes: 10 additions & 0 deletions conf.d/main
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,16 @@ apache2ctl configtest
# do with it. The conf script runs with stdin closed, dpkg reads end of file at
# the prompt and leaves confconsole "install ok unpacked", which fails this
# script. Keep the overlay's file without asking.
#
# The build time archive has to win over every other source here: a
# bootstrap from before Keel-Linux/common#30 pins TurnKey's archive at 999.
# It used to win through the overlay's /etc/apt/preferences.d/keel at 1001,
# which then shipped in the image and downgraded every package newer than the
# archive's (tracker#23). This pin names the staging distribution by its
# Label, exists only during the build and goes with the build time source
# (conf.d/zz-project-packages).
printf 'Package: *\nPin: release l=Keel Linux staging\nPin-Priority: 1001\n' \
> /etc/apt/preferences.d/keel-staging
export DEBIAN_FRONTEND=noninteractive
apt-get install -y --only-upgrade \
-o Dpkg::Options::=--force-confdef \
Expand Down
22 changes: 14 additions & 8 deletions conf.d/zz-project-packages
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ KEEL_APT_ROOT="${KEEL_APT_ROOT:-/srv/keel-apt}"
KEEL_APT_REPO="$KEEL_APT_ROOT/repo"
KEEL_STAGING_LIST="${KEEL_STAGING_LIST:-/etc/apt/sources.list.d/keel-staging.list}"
KEEL_STAGING_KEYRING="${KEEL_STAGING_KEYRING:-/etc/apt/keyrings/keel-staging-keyring.asc}"
KEEL_SOURCES="${KEEL_SOURCES:-/etc/apt/sources.list.d/keel.sources}"
KEEL_STAGING_PIN="${KEEL_STAGING_PIN:-/etc/apt/preferences.d/keel-staging}"
KEEL_APT_ETC="${KEEL_APT_ETC:-/etc/apt}"
KEEL_APT_LISTS="${KEEL_APT_LISTS:-/var/lib/apt/lists}"
KEEL_PROJECT_PACKAGES="${KEEL_PROJECT_PACKAGES:-inithooks confconsole keel}"

Expand Down Expand Up @@ -74,14 +75,19 @@ for package in $KEEL_PROJECT_PACKAGES; do
done

# the build time package source is not for appliances: remove the source entry,
# the copy of the archive it names and the keyring the build verified that
# archive with, and keep the documented, disabled entry for the future signed
# repository (overlay). The staging key signs whatever the build host produced,
# so an image that kept it would carry trust in a nightly (tracker#7).
# common/removelists-final/turnkey removes the same three paths at the end of
# the build, whatever a recipe does.
# the pin conf.d/main gave it for the build, the copy of the archive it names
# and the keyring the build verified that archive with. The staging key signs
# whatever the build host produced, so an image that kept it would carry trust
# in a nightly (tracker#7). common/removelists-final/turnkey removes the same
# paths at the end of the build, whatever a recipe does. The appliance's own
# Keel source and its 990 pin are common's, or conf.d/zzz-keel-archive's on a
# bootstrap from before common shipped them, so nothing the image keeps may
# still name the build time archive.
rm -f "$KEEL_STAGING_LIST"
rm -f "$KEEL_STAGING_PIN"
rm -f "$KEEL_STAGING_KEYRING"
rm -rf "$KEEL_APT_ROOT"
rm -rf "${KEEL_APT_LISTS:?}"/*
grep -q '^Enabled: no' "$KEEL_SOURCES"
apt_files=("$KEEL_APT_ETC/sources.list" "$KEEL_APT_ETC/sources.list.d" "$KEEL_APT_ETC/preferences" "$KEEL_APT_ETC/preferences.d")
leftover=$(grep -rlsE 'trixie-staging|/srv/keel-apt|l=Keel Linux staging' "${apt_files[@]}" || true)
[ -z "$leftover" ] || fatal "these apt files still name the build time archive: $(tr '\n' ' ' <<< "$leftover")"
86 changes: 64 additions & 22 deletions conf.d/zzz-keel-archive
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
#!/bin/bash -e
# Turn on the project's signed APT archive, which is the appliance's update
# path, and prove that the key it will be verified with is in the image.
# The project's signed APT archive, which is the appliance's update path:
# its source enabled, its pin at 990, and proof that the key it will be
# verified with is in the image.
#
# Keel-Linux/common#30 ships the source and the pin to every image
# (overlays/turnkey.d/keel-apt); this script then checks them and changes
# nothing. On a bootstrap from before that change it writes the same two
# files. The recipe's overlay used to ship them, the pin at 1001, which made
# apt downgrade every package newer than the archive's (tracker#23); at 990
# the archive's build is still the candidate over any other archive, and a
# newer installed version is never replaced.
#
# Runs last, after conf.d/zz-project-packages has removed the build time
# file: source and emptied /var/lib/apt/lists. The order matters: with this
# source enabled during the build, apt would resolve the project packages over
# the network instead of from the copy of the archive inside the build tree,
# which is the one thing zz-project-packages exists to prove, and the layer
# would need a name to be reachable to build at all.
# file: source and its pin and emptied /var/lib/apt/lists. The order matters:
# with this source enabled during the build, apt would resolve the project
# packages over the network instead of from the copy of the archive inside
# the build tree, which is the one thing zz-project-packages exists to prove.
#
# It does not run "apt-get update". A conf script has no guarantee of a
# network, an index fetched here would be stale in the image anyway, and
Expand All @@ -19,9 +27,11 @@
# scratch tree.

KEEL_SOURCES="${KEEL_SOURCES:-/etc/apt/sources.list.d/keel.sources}"
KEEL_PREFS="${KEEL_PREFS:-/etc/apt/preferences.d/keel}"
KEEL_KEYRING="${KEEL_KEYRING:-/usr/share/keyrings/keel-archive-keyring.gpg}"
KEEL_ARCHIVE_URI="${KEEL_ARCHIVE_URI:-https://archive.keellinux.org}"
KEEL_ARCHIVE_SUITE="${KEEL_ARCHIVE_SUITE:-trixie}"
KEEL_PIN_PRIORITY=990
KEEL_STAGING_LIST="${KEEL_STAGING_LIST:-/etc/apt/sources.list.d/keel-staging.list}"
KEEL_STAGING_KEYRING="${KEEL_STAGING_KEYRING:-/etc/apt/keyrings/keel-staging-keyring.asc}"

Expand All @@ -30,12 +40,17 @@ fatal() {
exit 1
}

# the stanza of the stable track: the first that names KEEL_ARCHIVE_SUITE,
# or the first of the file when none does, so a wrong suite is reported
stable_stanza() {
awk -v suite="$KEEL_ARCHIVE_SUITE" 'BEGIN { RS = "" } { if (first == "") first = $0 } $0 ~ "(^|\n)Suites:[ \t]*" suite "[ \t]*(\n|$)" { print; found = 1; exit } END { if (!found) print first }' "$KEEL_SOURCES"
}

# field NAME: the value of a deb822 field of the stable stanza
field() {
# field NAME FILE: the value of a deb822 field, first occurrence
awk -v name="$1:" '$1 == name { $1 = ""; sub(/^ /, ""); print; exit }' "$2"
awk -v name="$1:" '$1 == name { $1 = ""; sub(/^ /, ""); print; exit }' <<< "$stanza"
}

[ -f "$KEEL_SOURCES" ] || fatal "$KEEL_SOURCES is not in the image"
[ -s "$KEEL_KEYRING" ] || fatal "$KEEL_KEYRING is missing or empty: the plan asks for keel-archive-keyring"

# The build time source must be gone before this one is enabled, and so must
Expand All @@ -47,30 +62,57 @@ field() {
[ ! -e "$KEEL_STAGING_KEYRING" ] \
|| fatal "$KEEL_STAGING_KEYRING is still in the image: the staging key must not reach an appliance"

# A keyring with no key in it verifies nothing and apt says so only at run
# time, so the key is counted here. gpg is in the image (core), and reading a
# keyring needs no network and no agent.
keys=$(gpg --show-keys --with-colons "$KEEL_KEYRING" 2>/dev/null | grep -c '^pub:' || true)
[ "$keys" -ge 1 ] || fatal "$KEEL_KEYRING carries no public key"

# Written only when common did not ship them: the same text as common's.
if [ ! -e "$KEEL_SOURCES" ]; then
mkdir -p "$(dirname "$KEEL_SOURCES")"
printf '%s\n' \
"# Keel Linux packages, from the Keel repository (handbook decision 0039)." \
"# trixie is the stable track; trixie-testing is off unless the operator" \
"# turns it on. Written by keel-wordpress's conf.d/zzz-keel-archive where" \
"# common did not ship it (Keel-Linux/common#30)." \
"Types: deb" "URIs: $KEEL_ARCHIVE_URI" "Suites: $KEEL_ARCHIVE_SUITE" \
"Components: main" "Enabled: yes" "Signed-By: $KEEL_KEYRING" "" \
"Types: deb" "URIs: $KEEL_ARCHIVE_URI" "Suites: $KEEL_ARCHIVE_SUITE-testing" \
"Components: main" "Enabled: no" "Signed-By: $KEEL_KEYRING" > "$KEEL_SOURCES"
fi
if [ ! -e "$KEEL_PREFS" ]; then
mkdir -p "$(dirname "$KEEL_PREFS")"
printf '%s\n' \
"# Keel Linux: prefer the project's packages over Debian's, never by" \
"# going backwards (tracker#23)." \
"Package: *" "Pin: release o=Keel Linux" "Pin-Priority: $KEEL_PIN_PRIORITY" > "$KEEL_PREFS"
fi

# Never the unsigned staging distribution: it exists only inside a build.
suite=$(field Suites "$KEEL_SOURCES")
stanza=$(stable_stanza)
suite=$(field Suites)
[ "$suite" = "$KEEL_ARCHIVE_SUITE" ] \
|| fatal "$KEEL_SOURCES names the suite '$suite', not '$KEEL_ARCHIVE_SUITE'"
case "$suite" in
*staging*) fatal "$KEEL_SOURCES names an unsigned staging distribution" ;;
esac

uri=$(field URIs "$KEEL_SOURCES")
uri=$(field URIs)
[ "$uri" = "$KEEL_ARCHIVE_URI" ] \
|| fatal "$KEEL_SOURCES names '$uri', not '$KEEL_ARCHIVE_URI'"

signed_by=$(field Signed-By "$KEEL_SOURCES")
signed_by=$(field Signed-By)
[ "$signed_by" = "$KEEL_KEYRING" ] \
|| fatal "$KEEL_SOURCES is signed by '$signed_by', not '$KEEL_KEYRING'"

# A keyring with no key in it verifies nothing and apt says so only at run
# time, so the key is counted here. gpg is in the image (core), and reading a
# keyring needs no network and no agent.
keys=$(gpg --show-keys --with-colons "$KEEL_KEYRING" 2>/dev/null | grep -c '^pub:' || true)
[ "$keys" -ge 1 ] || fatal "$KEEL_KEYRING carries no public key"
[ "$(field Enabled)" != no ] || fatal "$KEEL_SOURCES: $KEEL_ARCHIVE_SUITE is not enabled"

sed -i 's/^Enabled: no$/Enabled: yes/' "$KEEL_SOURCES"
grep -qx 'Enabled: yes' "$KEEL_SOURCES" \
|| fatal "$KEEL_SOURCES is still not enabled"
# every priority the pin file gives, which must be the one 990
priorities=$(awk '$1 == "Pin-Priority:" { print $2 }' "$KEEL_PREFS" | sort -u)
[ "$priorities" = "$KEEL_PIN_PRIORITY" ] \
|| fatal "$KEEL_PREFS pins at '$(tr '\n' ' ' <<< "$priorities")', not $KEEL_PIN_PRIORITY (tracker#23)"
grep -qx 'Pin: release o=Keel Linux' "$KEEL_PREFS" \
|| fatal "$KEEL_PREFS does not pin o=Keel Linux at $KEEL_PIN_PRIORITY"

echo "[zzz-keel-archive] $uri $suite enabled, verified with $KEEL_KEYRING ($keys key)"
echo "[zzz-keel-archive] $uri $suite enabled, verified with $KEEL_KEYRING ($keys key), pinned at $KEEL_PIN_PRIORITY"
5 changes: 0 additions & 5 deletions overlay/etc/apt/preferences.d/keel

This file was deleted.

Loading
Loading