Repository navigation
Serve a first boot page that loads nothing from a third party - #14
Conversation
The page the appliance serves before anybody has logged in loaded three scripts from two other hosts. Closes #13. Two came from 29tagid, appended at first boot: https://ajax.turnkeylinux.org/initfence/$build/$version/$app.js https://ajax.turnkeylinux.org/initfence/$build/$version/$app.direct The third was in the packaged page itself, a jQuery from ajax.googleapis.com that nothing on the page uses: the inline script below it is plain DOM. It was there for the remote script, and the issue did not name it, which is why the test parses the page for every host it would fetch from rather than grepping for a hostname. What the tag was for, established before removing it. Introduced in 2015 as "include script support for AJAX updates": the .js let TurnKey change the fence page after an image shipped, and the .direct is a beacon whose only job is to be logged, telling them the page was hit directly, broken down by build, version and appliance. The same pair was once applied to the web control panel index too, and that half was deleted in 2016. Both endpoints answer 404 today, measured, so what shipped was a beacon to a service that no longer exists. What depended on it: only itself. tagid_version and tagid_build existed solely to fill in those URLs, and tagid_build was the only reader of /etc/apt/apt.conf.d/01turnkey in this repository, which common is removing (Keel-Linux/common#6). bin/secalerts.sh reads that file too but independently, and its fallback to turnkey-version already handles the file being absent; measured, it does not break. tagid_app_name stays: the @APP_NAME@ substitution is the hook's other job and is not the tag. tagid_strip_remote_scripts is new, and is why this is not a pure deletion. The served page lives in /var, which survives, so an index tagged by an older image is still on disk and 29tagid is the only thing that ever looks at it again. Measured on the wordpress-demo container: the served page carries all three scripts. The hook now removes any script element with a remote src from the page it renders. It writes through a temporary file and moves it into place, so a failure cannot leave the fence with a truncated index. 27 bats, both files at 100 percent under kcov 43 (29tagid 17 of 17, lib/tagid.sh 2 of 2), total 99.53. Every assertion mutation checked.
2.3.6+keel6, and the measured numbers in COVERAGE.md: 124 bats over six files, the two touched files at 100 percent, total 99.53, the shell gate unchanged at 98 because lib/init-fence.sh is still the lowest at 98.44.
|
Review of Checked first, because the branch rests on them
MEDIUM —
|
…arser Review found the strip and the test's detector blind in the same place: both read double quoted attributes of <script>, and the detector also of <link>, <img> and <iframe>, so an unquoted script src, or any remote stylesheet, image or frame, got past the defence and the suite stayed green. Reproduced on the branch head. The pages are now judged by: - tests/remote_loads.py, HTMLParser based, which reports every URL on another host a page fetches on load: every link element, src, srcset, poster, data and background attribute, SVG href, base, meta refresh, and url() and @import in style attributes and elements, in any quoting, with tabs, backslashes and entities normalised the way a browser does. It has its own tests, 48 cases, hostile and benign, the packaged page among them. - remote_tokens in the bats file, which knows no HTML beyond where a tag starts and ends and reports any URL with a host in a tag that is not a link. It catches what HTMLParser does not ('<!--><img src=...>' hides the image from the parser and not from a browser), which a test records. tests/test-tagid.bats serves each hostile page the review measured, and two parser differentials, as a page inherited in /var, and requires the served page to be the packaged one, named. Also: an inherited stylesheet and a file the package does not ship, no temporary file inside the served directory or left beside it, every step of the rebuild failing on its own, the hook running under turnkey-init. Against the branch as reviewed, 8 of the 27 fail. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfWQScmDZ94KCS5DMYrfe6
…inherited ones A change of approach within this branch, so the three parts: 1. Why stripping does not work. tagid_strip_remote_scripts removed <script> elements whose src was double or single quoted and remote. Review measured an unquoted src, a stylesheet, an image and a frame getting past it, and a script with a body leaving its body behind as page text. Each is a regular expression away from being handled, and the next form is another. 2. Whether it can be made to work. Only as far as a parser agrees with a browser, and none does on malformed markup: Python's HTMLParser, the best candidate here, does not see an image after '<!-->' or after '</script x>', and a browser loads both. A defence built on it would have the detector's blind spots, which is what review found the first time. 3. Why rebuilding is better. It needs no judgement of the page at all. Nothing in the tree writes a page into the /var copy but this hook, so replacing that copy with the packaged pages loses nothing, and a page the package did not ship is not served, however it is written. The packaged pages are what the tests judge, by two detectors. What 29tagid does now, on every run, under turnkey-init as well: - builds the pages in a directory beside HTDOCS (mktemp in its parent), never inside the directory the fence serves, which review noted the temporary index was; - names the appliance in the copy, then swaps it in with two renames and removes the old one; - on any failure, removes HTDOCS: the fence serves the packaged htdocs when the writable copy is missing, so a failure serves the unnamed page rather than an inherited one. It exits 1 with a <3> message; it used to exit 0 and keep the old page; - stops before touching anything when mktemp fails (an empty directory name would have sent the copy to /) or when the default file sets no HTDOCS (the reviewed hook then copied the pages into its working directory, which the new test caught by doing it). Under turnkey-init it used to exit at once. That exit was for the naming half; the pages are what an operator whose first boot did not complete gets served, so it runs. lib/tagid.sh is down to tagid_app_name. The changelog entry is 2.3.6+keel7: this lands after #18, which has keel6. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfWQScmDZ94KCS5DMYrfe6
…script master gained #16, which turned two bare negations in the old tests/test-tagid.bats into 'run !'. This branch rewrote that file and uses 'run !' throughout, so its version is kept whole. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfWQScmDZ94KCS5DMYrfe6
|
Review addressed in 7ab1826 and e314934, then master merged in (c3371c8). 29tagid no longer strips anything. It rebuilds the /var copy from the packaged pages next to the served directory, runs under turnkey-init as well, and falls back to the packaged pages if it fails. The commit message has the justification. Two detectors that share no parser now judge the page. The changelog is now keel7, so this lands after #18. |
…script master gained #19 (keel-host-keys, 2.3.6+keel6) and #18 (the fence, 2.3.6+keel7). This branch's entry becomes 2.3.6+keel8, above both, with the date of this merge. No code overlaps: #18 changes lib/init-fence.sh and the fence unit, and fence_htdocs, which this branch's failure path relies on to serve the packaged pages, is unchanged. Shell total 99.68, 29tagid 29/29; Python 196 passed, 3 skipped. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfWQScmDZ94KCS5DMYrfe6
Serve a first boot page that loads nothing from a third party
The page the appliance serves on first boot, before anybody has logged in,
loaded three scripts from two other hosts. It now loads none.
Closes #13.
What was there
Measured on the
wordpress-democontainer, 2026-09-28, on the page that isactually served (
/var/lib/inithooks/turnkey-init-fence/htdocs/index.html):Two are the ones the issue names, appended by
29tagidat first boot. Thethird is not in the issue: a jQuery from
ajax.googleapis.com, in thepackaged page itself, that nothing on the page uses: the inline script
below it is plain
document.getElementById. It was there to support theremote script. That is exactly why the test parses the page for every host
it would fetch from instead of grepping for a hostname: a hostname grep is
what let this one sit next to the two that were noticed.
What the tag was for, established before removing it
The issue asked for this, so it was settled first rather than after.
Introduced in 2015 (
52e7c8e, "include script support for AJAX updates")and extracted into
29tagida week later (dc9fd92):.jslet TurnKey change the fence page's content after an imagehad shipped, without shipping anything;
.directis a beacon. It has no content to return; requesting itis the point. It told TurnKey the fence page had been hit directly, broken
down by build, version and appliance: which is the same triple the URL
path announces.
The same pair was once applied to the web control panel's index as well
(
webcp/$build/$version/$app.js); that half was deleted in 2016(
50101fc).Both endpoints answer 404 today:
So what every Keel appliance shipped was a beacon to a service that no
longer exists, and a page that is unverifiable by construction:
keel verifycan attest to every byte in the layer and say nothing about ascript fetched at view time.
What depended on it
Only itself, which is what the issue asked to establish up front.
tagid_versionandtagid_buildexisted solely to fill in thoseURLs. Both are gone, with
tagid_is_taggedandtagid_render_scripts.tagid_buildwas the only reader of/etc/apt/apt.conf.d/01turnkeyin this repository: it parsed the build tags out of the apt User-Agent.
The appliance still resolves apt, security and its vendor through TurnKey common#6 removes that file, so the two changes fit together;
a test asserts the hook is fine without it.
bin/secalerts.shreads the same file, independently, for the versionit POSTs to
hub.turnkeylinux.org. It is guarded(
[ -e "$f" ] && ...) and falls back toturnkey-version. Measured thatit survives the file's removal and does not trip its own
set -e. It isuntouched here; that it talks to the Hub at all is a separate subject.
tagid_app_namestays. The@APP_NAME@substitution is the hook'sother job and is not the tag.
29tagidstill does it and still reloadsthe fence.
Why this is not a pure deletion
HTDOCSis/var/lib/inithooks/turnkey-init-fence/htdocs, and/varsurvives.
29tagidused to copy the packaged pages only when that directorydid not exist, so an index tagged by an older image stays tagged, and this
hook is the only thing that ever looks at it again. The measurement above is
that page.
The first version of this branch stripped remote
<script>elements fromit with a regular expression. Review measured an unquoted
src, astylesheet, an image and a frame getting past it. So
29tagidno longerjudges the page at all: on every run, under
turnkey-inittoo, it rebuildsthe
/varcopy from the packaged pages beside the served directory, namesthe appliance in it, and swaps it in. A page the package did not ship is
not served, however it is written. Nothing in the tree writes into that
copy but this hook. If the rebuild fails, the copy is removed and the fence
serves the packaged pages (
fence_htdocs), and the hook exits 1 with a<3>message. The commit message has the three part justification for thechange of approach.
How it is asserted
Not by reading the hook's source, and not by grepping for
ajax.turnkeylinux.org. Every page the fence serves is judged by twodetectors that share no parser:
tests/remote_loads.py(HTMLParser, everyattribute and style construct that fetches, 48 cases of its own) and a tag
level regular expression that knows no HTML and catches what HTMLParser
misses on malformed markup. Each hostile page the review measured, and two
parser differentials, are served as inherited pages and must be gone.
Tests
tests/test-tagid.bats: 27 tests; against the first version of the branch,8 fail. 125 bats in all,
29tagid29/29,lib/tagid.sh1/1, total 99.55;Python 196 passed, 3 skipped.
Changelog:
2.3.6+keel8, after #19 (keel6) and #18 (keel7).Noticed, not changed
The page still carries
<a href>links towww.turnkeylinux.org(theappliance product page and the contact page) and calls the appliance a
TurnKey one in its prose. Those are branding and vocabulary, not a script
and not a fetch (nothing is requested until an operator clicks), so they
are out of this issue's scope and left for the vocabulary work.
Test plan
tests/coverage.sh: six files, both touched ones at 100 percentpytest: 187 passed, 3 skipped, unchangedshellcheck -S warningon the hook and the library (the library'sSC2148 is pre-existing and matches
lib/ipconfig.shandlib/init-fence.sh, which are sourced the same way)ajax.turnkeylinux.orgendpoints probed, 404bin/secalerts.shchecked against a missing01turnkeyhost's
core.tar.zstis in a known-inconsistent state and a repair ispending, so the build lock was deliberately not taken. The measurement
above was taken read-only with
lxc-attach.Fits with Keel-Linux/common#6, which removes the
01turnkeythis hookstopped reading. Org-wide context: Keel-Linux/tracker#15.