Skip to content

feat(noauth): domains reachable without proxy authentication; list and setup fixes - #12

Merged
ks98 merged 24 commits into
mainfrom
feat/noauth-domains
Nov 3, 2026
Merged

ks98 merged 24 commits into
mainfrom
feat/noauth-domains

Conversation

@ks98

@ks98 ks98 commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

What

  • lmnsquid noauth <instance> list|add|remove|reload (ADR-016): per-instance list of domains reachable through the proxy without proxy authentication, from --school-subnets only. Use case: Microsoft 365, which Microsoft asks to exempt from proxy authentication. A recommended M365 list (endpoints.office.com, version 2026103000) is in docs/operations.md; the code ships no default content.
    • The blocklist wins over the noauth list. HTTPS on the noauth path is cut when the SNI is foreign or missing (no domain fronting). IP literals never match (dstdomain -n). Requests carrying a ticket behave exactly as before.
    • The noauth list rejects IP literals in every inet_aton notation and single-label names.
    • Known limits are documented in T16: exam accounts and the Internetsperre do not apply to listed domains.
  • Lists can no longer stop squid. A subdomain listed before its parent makes squid 6 abort with FATAL: Bungled. This affected the blocklist since 7.3.1, also via blocklist-refresh.sh. Writes now drop covered entries, and the image reads sanitized copies (lists-sync.sh), so a hand-edited file cannot stop an instance either. CRLF line endings are handled. Ignored entries are reported in the reload response and in lmnsquid logs.
  • Setup fixes found in a real installation:
    • docs/install.md: devices.csv role iponly. Any other role creates a computer account whose host/ SPN collides with HTTP/<proxy-fqdn>.
    • provision-keytab.sh now refuses before exporting when host/<fqdn> or host/<short> sits on another account.
    • discover-ad-facts.sh reads the realm on the DC, warns on a mismatching REALM, fills in the proxy FQDN, and offers a one-instance-for-all template.
  • Docs:
    • Corrected the HTTPS blocklist behaviour (ADR-002/014, T4/T8). A CONNECT denied by http_access is answered with the 403 page inside TLS using the proxy's self-signed certificate; the target is never contacted. Only SNI-based denials are terminated at the handshake.
    • The /etc/hosts note now covers cloud-init hosts.
  • Test plumbing: run.sh e2e uses the docker group without sudo, the DC smokes are wired into run.sh, and the E2E network moved to 10.228.0.0/24.

Release note

The template change lives in the data-plane image. Before tagging v7.3.8 DEFAULT_IMAGE must be pinned to the image build-image.yml builds from this merge (see RELEASE.md). Until then noauth reload answers 409.

How tested

  • Locally, run.sh quick and LMNSQUID_ALLOW_REAL=1 run.sh all pass (Kerberos E2E with Samba AD DC, DC smokes, docker integration). make deb in the pinned lmndev-runner builds 7.3.8.
  • Three independent cold verifications, including attack attempts without a ticket and an upgrade from 7.3.7.
  • Lab test against a real linuxmuster.net 7.3 server:
    • The provision-keytab refusal and the iponly path work; A and PTR records are kept.
    • The upgrade 7.3.7 → 7.3.8 on a systemd + Docker host keeps instances running and survives a reboot.
    • With the new image the noauth matrix holds.
  • Not tested: real Windows/Office clients, and the CI-built image (it exists only after the merge).

Kevin Stenzel added 24 commits October 30, 2026 15:01
run.sh e2e built the broken image and ran the docker-py integration
tests through sudo unconditionally, so on a host where the user may use
Docker but sudo needs a password (a developer box) cp-docker-it failed
before any test ran. It now uses sudo only when `docker info` fails, as
e2e_kerberos.sh and crabbox_bootstrap.sh already do.

The integration tests created their lists under the service default
/etc/linuxmuster-squid/blocklists, which only root may write; they now
use a directory under pytest's tmp_path like the secrets.
…tion

Some services break behind proxy authentication; Microsoft lists it among
the configurations known to cause Microsoft 365 connection issues. A
client-side bypass was not wanted, so an instance can now let listed
domains through the proxy without a ticket (ADR-016):

  lmnsquid noauth <name> list | add <domain> | remove <domain> | reload
  GET/POST/DELETE /v1/instances/{name}/noauth, POST .../noauth/reload

The list is <blocklists_dir>/<name>/noauth.domains, in the directory that
is already mounted at /etc/squid/lists, so the container spec (and its
fingerprint) does not change and an upgrade replaces no container for it.
Entries are normalized like the blocklist (.example.org = domain and
subdomains).

squid.conf (data-plane image):
- after the existing denies (Safe_ports, CONNECT only to 443, no IP
  literals): `deny noauth_domains blocked_domains` (the blocklist wins,
  403), then `allow noauth_domains school_net`, then the unchanged chain;
- the list ACL is `dstdomain -n`: without it an IP-literal URL whose
  address maps back to a listed name matched (measured: 200 without a
  ticket);
- a CONNECT to a listed domain is terminated at SslBump2 when its SNI is
  not on the list (ssl::server_name --client-requested) or when there is
  no SNI. ssl::server_name falls back to the CONNECT host without SNI, so
  a four-line external ACL helper (sni-sent.sh, %ssl::>sni) tells; squid
  sends it "<sni> <%DATA>", i.e. "- -" without SNI. No decryption: peek,
  splice and terminate only (ADR-002).
- entrypoint: a list directory without noauth.domains (an older control
  plane) gets an empty stand-in under /run and a notice.

Control plane: both list files are created before a container starts.
`noauth reload` first checks in the container that the rendered squid.conf
reads /etc/squid/lists/noauth.domains and answers 409 otherwise: the
pinned DEFAULT_IMAGE does not know the list yet, and a reload would report
success while changing nothing.

Tests: unit (normalization, unsafe names, API 401/403/404/409/422, CLI,
--help warnings, both files before create, noauth_active); Docker
integration with the real DockerService (new image reads and reloads the
list; the pinned image runs healthy next to it and is reported inactive);
Kerberos E2E with three new instances (squid-noauth, the pinned image, a
list directory without the file) covering listed/unlisted/blocked, foreign
SNI, blocked SNI, no SNI, IP literal, outside school_net and the ticket
matrix. Counter-runs without `-n` and without the SNI rule fail.

Docs: ADR-016, threat model T16 (exam mode, Internetsperre, no user in
the log), architecture, operations (commands, warnings, recommended
Microsoft 365 list from endpoints.office.com version 2026103000),
install.md optional step 8, README, test strategy, references.
debian/changelog: new 7.3.8 stanza.
… proxy SPN

Seen in a real installation: the proxy host was in devices.csv with a role
that gets a computer account, so sophomorix created PROXY$ with
host/proxy.<domain>. host/ is an alias of HTTP/ (sPNMappings host=...,http),
`samba-tool spn add HTTP/proxy.<domain> svc-squid` only logged
check_spn_alias_collision and added the SPN anyway, and the script, which
only looked for exactly HTTP/<fqdn> on another account and only before
adding it, exported a keytab. The second run took the idempotent branch and
did not check at all.

Now, on every run and before anything is changed or exported, it looks for
HTTP/<fqdn>, host/<fqdn> and host/<short-name> on any account other than the
service account (sAMAccountName compared case-insensitively, without the
trailing $) and stops with FATAL, exit 1, naming the account and, for a
computer account, the devices.csv role iponly. The check is fail-closed:
without ldbsearch (ldb-tools, a dependency of linuxmuster-base7 and
sophomorix4) or read access to sam.ldb nothing is exported. The FQDN must be
a host name (it goes into the LDAP filter).

provision_keytab_smoke.sh (E2E DC, Samba 4.15): a computer account with the
SPNs sophomorix sets (HOST/<name>, HOST/<name>.<domain>, RestrictedKrbHost/...)
-> abort, no keytab, SPN not added; the same with HTTP/ already on the
service account (samba-tool spn add accepted the colliding SPN there too)
-> abort; account removed -> keytab exported; sPNMappings host= contains
http; filter characters and a short name are refused. A-C unchanged.

docs/keytab-and-dns.md: the alias pitfall and what the script checks.
install.md step 1 recommended a static devices.csv entry for the proxy's
A/PTR records without naming a role. With the default role
(classroom-studentcomputer), server or any other role whose
COMPUTER_ACCOUNT is TRUE, sophomorix creates a computer account PROXY$ with
host/proxy and host/proxy.<domain>; host/ is an alias of HTTP/, so that
account answers for HTTP/proxy.<domain> and Kerberos SSO breaks (seen in a
real installation on 2026-10-30, where role iponly and
linuxmuster-import-devices fixed it: `samba-tool computer show PROXY` then
reports "Unable to find computer").

Evidence that iponly keeps the DNS records (from the sophomorix4 source,
upstream/sophomorix4 at eb0bbcc8):
- sophomorix.ini [computerrole.iponly] COMPUTER_ACCOUNT = FALSE;
- sophomorix-device puts every device with a fixed IP into dnsNode/dnsZone
  (A + reverse zone/PTR) regardless of the role and creates a computer
  account only when the role's COMPUTER_ACCOUNT is TRUE;
- sophomorix-test-2 asserts for switch01 (role switch, also
  COMPUTER_ACCOUNT = FALSE, comment "test for sophomorixRole iponly that
  there is no account") that no computer account exists while the A record
  and the reverse dnsNode do.
Not yet confirmed on the real system (`host <proxy-fqdn>` after the import).

install.md step 1 now asks for the role iponly, explains the conflict and
how to check it; keytab-and-dns.md names iponly under the DNS requirements.
The script put whatever REALM the admin passed into the printed create
commands; a short name (REALM=BS-WIZ for BS-WIZ.LLAN) went through
unchecked. It now reads the realm from the DC's own smb.conf with
`testparm -s --parameter-name=realm` ("the kerberos realm to use",
smb.conf(5)), the realm the DC's KDC serves and available without
arguments (samba-tool domain info needs the DC's IP). A given REALM is only
compared: a mismatch prints a WARNING and the DC's realm is used. Without a
readable realm the given REALM is used with a warning, or <REALM> is
printed.

The commands carry --visible-hostname proxy.<domain> (the realm in lower
case) instead of the placeholder proxy.<fqdn>; PROXY_FQDN overrides it, and
the header reminds that the name needs an A record and no computer
account. A third template (B) creates one proxy for everybody in
`internet`, as set up at a single-school site, and says who is in that
group per sophomorix.ini (teachers, students, parents, staff, extra
students/course members, school admins) and that other schools' users are
not.

discover_ad_facts_smoke.sh: realm and FQDN filled, no placeholder,
template B, no REALM -> DC realm without warning, a wrong REALM -> warning
and the DC realm, PROXY_FQDN honoured. install.md step 4 and the README
drop REALM= from the call.
Cold verification: squid 6.14 refuses a dstdomain / ssl::server_name list in
which a name covered by a listed parent domain comes first
(".a.example.org" before ".example.org", or "example.org" next to it):
"ERROR: '.a.example.org' is a subdomain of '.example.org'" and "FATAL:
Bungled". `squid -k reconfigure` then fails (reload 500, the running squid
keeps the old config) and the next start of the container fails. The other
order only warns; exact and case duplicates are accepted (measured for both
ACL types, with and without -n). `Blocklist.add` wrote sorted() output, so
"add graph.microsoft.com; add microsoft.com" produced exactly that; so did
blocklist-refresh.sh (`sed 's/^/./' | sort -u`) for any category list
holding a name and its subdomain. The blocklist had the bug before ADR-016.

At the root, for both lists:
- control plane: `uncovered()` drops every entry another dotted entry
  covers (compared lower-cased, without trailing dots) on every write;
  `add` of a name below a listed domain changes nothing, `add` of a parent
  removes the names below it, `remove` writes the cleaned list.
- blocklist-refresh.sh: the same rule (awk) before the atomic replace.
- data-plane image: squid reads /run/lmnsquid/lists/{blocked,noauth}.domains,
  built by the new lists-sync.sh from the mounted /etc/squid/lists at every
  start (entrypoint) and before every reload, with the same rule, so a
  hand-edited file cannot stop the proxy; ignored entries are named in the
  container log. A missing file is an empty list (this replaces the
  entrypoint's noauth stand-in; a noauth.domains created later is read by
  the next reload).
- reload: `sh -c` with a fixed script: lists-sync.sh where the image has it,
  then `squid -k reconfigure`; the old image keeps reading the mounted files.
  `noauth_active` now looks for the noauth ACL itself in squid.conf.

Tests: unit (`uncovered` cases, add/remove keep squid-loadable lists, reload
command and its error), blocklist smoke (refresh drops a covered subdomain),
E2E (squid-noauth mounts hand-edited lists with covered entries in both
lists: starts, logs them, reloads, comes back after a restart, blocked
subdomain 403; squid-pinned, the image without lists-sync.sh, gets its own
clean directory), cp-docker-it (such lists written after the start: reload
ok, restart, healthy). Counter-check: the same integration test with an
image whose lists-sync.sh only copies fails with "FATAL: Bungled".

Docs: operations (add semantics, hand edits, the M365 note now says why the
three covered names are left out instead of "squid only warns"),
architecture, ADR-014 amendment, threat model T8, test strategy;
debian/changelog bullet in 7.3.8.
Cold verification: `noauth add 172.28.0.20` and `noauth add com` were
accepted; with ".172.28.0.20" on the list `http://172.28.0.20/` got 200
without a ticket (squid matches an IP-literal URL against the list text;
`dstdomain -n` only stops the PTR route). That contradicted the changelog
and operations.md ("IP addresses never match"), and `com` would open a
whole TLD.

- normalize_noauth_domain(): normalize_domain plus at least two labels and a
  non-numeric last label (TLDs are never all-numeric). This refuses dotted,
  short (127.1), hex (0x7f000001) and decimal (2887516181) IPv4 forms and
  single labels; IPv6 never passed the name check. noauth_list() uses it;
  POST .../noauth takes a NoauthEntry, so the refusal is a 422 at the
  boundary. remove still takes any name, so a hand-written bad entry can be
  taken out.
- lists-sync.sh ignores such lines in a hand-written noauth.domains and
  names them in the container log ("ignored, not a domain name").
- The blocklist keeps accepting TLDs and addresses on purpose: blocking a
  TLD (`zip`) is a legitimate choice and a wrong entry there only blocks
  more (documented in operations.md).

Tests: unit (each IP form, IPv6, bracketed, mapped, `com`, `.com`,
`intranet` refused; `1und1.de`, `x.co.uk` accepted; blocklist still takes
`zip`/`10.0.0.1`; remove of hand-written `com` and an address; API 422; CLI
exit 1); E2E (hand-written `172.28.0.20` and `internal` in squid-noauth's
list: ignored and logged, `http://172.28.0.20/` 407, origin.example.internal
407). Docs: operations, ADR-016, threat model T16, test strategy; the
changelog bullet now says entries are refused.
…AULT_IMAGE

Cold verification: the 409 of `noauth reload` and operations.md told the
admin to `lmnsquid update <name>`, which pulls DEFAULT_IMAGE, still the
image without the list, so it cannot help until a release pins a newer
image.

- The 409 says what is true: the instance's data-plane image does not
  support the noauth list, so the list has no effect; a linuxmuster-squid
  release whose pinned image supports it is needed, and its package upgrade
  moves the instances onto that image. operations.md, install.md and
  ADR-016 say the same.
- RELEASE.md: a release whose image/ changed pins that image before it is
  tagged (merge -> build-image.yml on main -> PR raising DEFAULT_IMAGE to the
  reported digest -> CI -> version + tag), with 7.3.8 named as such a
  release. No digest is invented here.
- The E2E service squid-pinned and the integration test used DEFAULT_IMAGE
  as "the image without the list": after the re-pin they would have run an
  image that reads the list and failed (407 expected, 200 got). They are now
  squid-old / test_real_old_image_ignores_the_noauth_list with a constant,
  the digest 7.3.7 pins (d13625b5...), i.e. what instances run until the
  upgrade replaces their containers; deploy/e2e/lists-pinned is now
  lists-old. They need no change for the pin.
…per-case

Cold verification: REALM=example.internal (right realm, lower case) printed
a WARNING about a different realm. A given REALM is now upper-cased before
it is compared with the DC's realm and, without a readable DC realm, before
it goes into the commands: AD Kerberos realms are the DNS domain in upper
case. discover_ad_facts_smoke.sh: lower-case REALM -> no warning, realm
printed upper-case.
Cold verification: with a sam.ldb ldbsearch cannot read, the script ended
with exit 1 through set -e/pipefail and printed nothing, so the admin saw no
reason. Each conflict query now captures ldbsearch's exit status and output;
on failure it prints "FATAL: ldbsearch on <sam.ldb> failed, cannot check for
conflicting SPNs:" with ldbsearch's message, says nothing was changed and
exits 1 before any change or export (still fail-closed).
provision_keytab_smoke.sh case H: a non-ldb file as SAM_LDB -> FATAL with
the reason, no keytab. keytab-and-dns.md and the changelog bullet say so.
…n e2e

Cold verification: provision_keytab_smoke.sh and discover_ad_facts_smoke.sh,
which carry the tests of the two admin-script fixes, ran in no run.sh mode
and in no CI job. `run.sh e2e` (and so `all`) now runs them after the
Kerberos suite, one after the other (each starts and removes its own Samba
DC from the same compose file), with the usual dependency gate on docker.
No GitHub CI job: the heavy tier stays out of ci.yml, and a new job would be
a new required check of the main ruleset. test-strategy.md says so.
…the image

Cold verification 2: dotted forms with hex labels (0x7f.0x1, 1.2.3.0x4,
0x0a.0x00.0x00.0x01) passed `noauth add` and lists-sync.sh, because both only
required a non-numeric last label and "0x4" contains a letter. squid
normalises numeric hosts, so they did not open anything (measured 407), but
operations.md, ADR-016 and the changelog promise "IP addresses in any form".

normalize_noauth_domain() and lists-sync.sh now also refuse a name whose
labels are all inet_aton numbers (decimal, octal, 0x-hex). Names with such
labels next to a real one (0xcafe.com, 10.0.0.example, 1und1.de) still pass.

controlplane/tests/test_lists_sync.py runs image/lists-sync.sh as it is
(sh + awk; LISTS_SYNC_SRC/LISTS_SYNC_DST point it at a temp dir) and
normalize_noauth_domain on the same 24 cases (every IPv4 notation, IPv6,
single labels, valid names) and requires the same verdict, plus the
covered-entry rule against uncovered(). It is part of pytest, so CI runs it
in the existing fast job. Counter-check: with the Python half reverted,
exactly the five dotted hex cases fail.

Docs: operations.md, ADR-016 and test-strategy name the rule; the changelog
bullet mentions the notations.
Cold verification 2: a noauth.domains saved with Windows line endings was
ignored completely: the noauth branch of lists-sync.sh kept the \r in each
entry, so every line failed the name check ("ignored, not a domain name:
.office.com" with an invisible \r) and the effective list was empty. The
blocklist branch already stripped it. Both branches now drop \r before the
first field is taken.

Tests: test_lists_sync.py runs the script on CRLF files for both lists
(comments, trailing comment, covered entry) and fails against the previous
script; test_noauth.py checks that the control plane reads such a file and
writes it back with \n. operations.md: Windows line endings are fine.
…r and the log

Lab test: operations.md says entries ignored by lists-sync.sh show up in
`lmnsquid logs <name>` "at every start and reload", but on reload the
script ran through docker exec and its stderr was thrown away: the reload
answered {"reloaded": true} and the log stayed silent (only a restart
showed the lines).

Making the documented behaviour true instead of narrowing the docs: an
admin who reloads after a hand edit is exactly the one who needs to see
that an entry was ignored, and the answer is where the CLI shows it.
- The reload script (fixed text) keeps lists-sync.sh's messages, prints
  them and appends them to /run/lmnsquid/lists-sync.log; on failure it
  prints them and stops before squid -k reconfigure.
- The entrypoint mirrors that file into the container log (tail -F, like
  the access log), so `lmnsquid logs` shows them after a reload too.
- The reload answer carries them: {"name", "reloaded": true, "notices":
  [...]} for both `blocklist reload` and `noauth reload`; the old image has
  no lists-sync.sh and answers "notices": [].

Tests: unit (notices extracted from the exec output, other lines left out,
the script appends to the log), cp-docker-it (hand-edited lists: the
reload answer names the ignored entries and they appear in `svc.logs()`).
Docs: operations.md, architecture, changelog bullet.
Cold verification 2: the 409 of `noauth reload` only said a release with a
newer pinned image is needed. An instance that stays on its old image after
that release is installed (health-gated rollback in update-all, `lmnsquid
rollback`, its own --image) needs `lmnsquid update <name>`, which the text
did not say. It now names both: installing the release moves the instances
(update-all), and `lmnsquid update <name>` moves this one if it stayed on its
old image. Before the release that command pulls the same old image, so it
is not offered as the first step. operations.md says the same.
Cold verification 2: the docstring of api.reload_noauth said a container
started without noauth.domains never reads the list, and the comment in
DockerService.ensure_running said the entrypoint reads the file only when it
is there at start. Since lists-sync.sh, a missing file is an empty list and
the next reload reads one that appeared later; only an image without the
noauth ACL is refused (which is what noauth_active checks). Comments only.
Lab test (B3, also with 7.3.7): `curl --proxy … https://<blocked>/` gets
"HTTP/1.1 200 Connection established", then a self-signed certificate
(curl exit 60); with -k a 403 page, access log `NONE_NONE/403 … GET
https://<blocked>/`, and the blocked server is never contacted. The docs
said the handshake is cut and the proxy never decrypts. Kevin decided to
keep the behaviour and fix the docs.

What happens (squid 6.14, measured; squid.conf does not describe it, the
squid 6 source does: client_side_request.cc, "we will attempt to bump the
client to serve the error", and no bump while authenticating):
- allowed CONNECT: spliced, never decrypted;
- CONNECT denied by http_access with 403 (blocked host, not in the group,
  IP literal or port rules, outside the subnets): squid confirms it, does
  TLS with the client itself using the instance's throwaway bump.pem
  (generate-host-certificates=off) and serves its 403 inside; browsers
  show a certificate warning; a client that clicks through sends the
  request's path and headers to the proxy;
- blocked SNI behind an allowed CONNECT host, and a foreign or missing SNI
  on the noauth path: ssl_bump terminate, cut during the handshake;
- 407: never bumped.

Corrected: ADR-002 (clarification), ADR-014 limit, threat model T4 and T8,
architecture, operations, install.md steps 7 and 8, deployment-gpo
checklist, README, test strategy, the blocklist --help and the template
comments (comment-only change in the image). debian/changelog: one bullet
in 7.3.8 saying the description was wrong and the behaviour is unchanged.
Lab test (B4): after a reboot /etc/hosts again read `127.0.1.1 <fqdn>`
(cloud-init, manage_etc_hosts: true), so requests from the proxy host itself
were back outside --school-subnets (403). Per the cloud-init docs (module
"Update Etc Hosts", runs every boot) `true` re-renders /etc/hosts from the
template, `localhost` appends the 127.0.1.1 entry every boot, `false` leaves
it alone. install.md step 1 now says how to check which applies and the
durable fix: edit /etc/cloud/templates/hosts.debian.tmpl (name and its
`127.0.1.1 {{fqdn}} {{hostname}}` line checked with cloud-init 26.2 on Ubuntu
24.04) or set manage_etc_hosts to false where it is defined. references.md
records the sources, also for the squid error-delivery behaviour of the
previous commit.
…large lists

Cold verification 2, kept as documented limits:
- K2: two-label public suffixes (co.uk, com.de, github.io) pass the noauth
  name check and would open a whole registration space. No public-suffix
  list (YAGNI); operations.md, threat model T16 and `lmnsquid noauth --help`
  say to add the organisation's own domains only.
- K5: `noauth list` shows the file as it is, including hand-written lines
  the container ignores; what squid uses is what reload and the logs do
  not report as ignored.
- K6: lists-sync.sh and squid's parse take long for category lists of
  millions of entries (measured in the cold verification: 2 million ≈ 25 s
  + ≈ 55 s parse), which can exceed the 90 s health gate.
- K7: the M365 note no longer implies that id 2 is required.
The final `run.sh all` of this round failed twice in kerberos-e2e and both
DC smokes with "invalid pool request: Pool overlaps with other one on this
address space": another project on the build host created compose networks
every few minutes and Docker gave them its default /16 pools in turn
(172.28, 172.29, 172.30, 172.31.0.0/16), so the fixed 172.28.0.0/24 of the
E2E stack collided whenever one of them held 172.28. Waiting for one to go
away did not help, the next took its place.

The stack now uses 10.228.0.0/24, outside Docker's default pools
(172.17-31.0.0/16, 192.168.0.0/16) and not routed on the hosts in use (the
linuxmuster lab is 10.250.0.0/24). Same last octets as before; changed are
the compose network and addresses, the client's krb5.conf, the assertions,
the orchestrator, load_smoke.sh and the hand-written IP line in the noauth
test list. test-strategy.md names the network and why. Test data only; no
product code or image change. With it, `run.sh all`: 11 passed, 0 failed,
0 skipped.
CI run 41471268667 (fast/pytest): test_cli_noauth_help_warns failed with
"AssertionError: school-subnets". On GitHub Actions Rich styles the help
(GITHUB_ACTIONS / FORCE_COLOR: ANSI codes inside words such as
--school-subnets) and wraps it to the runner's width. Reproduced locally
with GITHUB_ACTIONS=true or FORCE_COLOR=1; it passed with COLUMNS alone.

The test now invokes the help with the colour variables removed,
NO_COLOR=1, TERM=dumb and COLUMNS=200, and still strips escape codes and
collapses whitespace before matching. Passes with GITHUB_ACTIONS=true,
FORCE_COLOR=1, COLUMNS=80 and COLUMNS=40; the whole suite passes under
GITHUB_ACTIONS=true FORCE_COLOR=1 COLUMNS=80. It is the only test that
matches help text.
CI run 41471268667 (upgrade-smoke, "Assert that nothing was lost"): the
mode/owner diff of /etc/linuxmuster-squid failed on
"+644 113:119 /etc/linuxmuster-squid/blocklists/all-teachers/noauth.domains",
the file update-all/reconcile now create next to blocked.domains (ADR-016).

The assertion stays strict and allows exactly that: for every
blocked.domains of the previous release that has no noauth.domains beside
it, the expected list gains `<same mode> <same owner> <dir>/noauth.domains`,
the file must exist and be empty; then the full diff runs as before. A file
the old release already had is not expected again. The checks are spelled
out (`|| { …; exit 1; }`, `if …; then exit 1`) because `set -e` ignores a
failure inside `&&` lists and after `!`.

Tested locally on simulated trees with the block extracted from ci.yml:
the CI case passes; an extra file, a wrong mode, a non-empty or a missing
noauth.domains and a changed mode elsewhere fail; an existing noauth.domains
from the old release passes. Changed identically in release.yml (the blocks
are byte-identical); no job added, renamed or removed.
CI run 41472957495 failed in the new check: "missing:
/etc/linuxmuster-squid/noauth.domains". The smoke also gives the admin a
top-level /etc/linuxmuster-squid/blocked.domains, and the check took every
blocked.domains as a place where the upgrade adds noauth.domains; the
control plane only creates it in blocklists/<name>/. The match is now
anchored to /etc/linuxmuster-squid/blocklists/<name>/blocked.domains. The
loop variable was also called `new`, which the step uses for the installed
version (the summary at its end); it is `noauth_file` now.

Retested locally with the CI layout (top-level blocked.domains, a version
in $new): the CI case passes and $new survives; an extra file, a
noauth.domains at the top level, a wrong mode, a non-empty or a missing
noauth.domains and another changed mode fail; an existing noauth.domains of
the old release passes. Identical in release.yml.
…T4 evidence

Cold verification 3:
- N1: operations.md gave `NONE_NONE/200 0 CONNECT <host>:443` for a blocked
  HTTPS host; measured is `TCP_DENIED/200 0 CONNECT <host>:443`, followed by
  `NONE_NONE/403 … GET https://<host>/…` when the client accepts the
  certificate. `NONE_NONE/200 0 CONNECT` followed by `TCP_TUNNEL/200` is an
  allowed, spliced connection; the doc now says so. No other doc has the
  pattern (grep).
- N4: threat model T4 (and T8, test-strategy) said the E2E checks the
  throwaway certificate. It asserts "not 200" behind the tunnel (and
  `200 403` in the noauth case); the certificate was only checked by hand in
  the lab. Worded accordingly; no new test.
- N2/N3 as one known-limit line in operations.md: control characters of a
  hand-written entry appear unescaped in `docker logs`, and lists-sync.log on
  the /run tmpfs grows with each reporting reload until a restart.
@ks98
ks98 merged commit 62fec62 into main Nov 3, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant