Two faults, both mine, both reported from the live server.
The page had a route and no navigation entry. A page reachable only by typing
its URL does not exist as far as the operator is concerned, and "Betrieb →
Hostnamen und Zertifikate" was exactly as findable as I had made it: not at all.
It sits under Betrieb rather than System because what is set there decides
whether an address ANSWERS — that is operations, not configuration.
And calling files.… without a path redirected to app.… The host-bound group only
claimed /bootstrap.tar.gz and /{file}; a bare / matches neither, so the request
fell through to the host-agnostic routes and landed on the portal. Two holes,
because / and a multi-segment path miss the placeholder for different reasons,
and both are closed now.
404 rather than a redirect, and that is the point rather than a detail. The
redirect told anybody who tried the name where the portal lives and that both
sit on the same machine — the one thing every other hostname in routes/web.php
is careful not to say. An address with nothing to offer has nothing to tell
either.
2037 tests pass, assets build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The page was a wall. Six paragraphs of procedure stacked above a form, so the
thing the page actually asks for — four fields — sat underneath an essay about
what would happen afterwards. It answered everything and showed nothing.
It is now built the way the settings page is built, because that page was
rebuilt for the same reason and there is no case for a second idiom: an eyebrow,
a title, a sticky rail on the left and panels of rows on the right.
The rail carries the six steps as two-word labels rather than paragraphs. That
is what the question at this moment actually is — where am I, how much is left —
and it fits in 232 pixels. The numbers carry the state; a tick beside them would
be a second sign for one statement, and a number can be counted.
What a step MEANS now appears in the row where it is due, not six times in
advance. The two provider steps get a panel of their own above the form, because
they have to be done before you save: the one-time code starts expiring the
moment you do. Everything after the form waits until there is something to say.
After saving the command becomes the page. Full width, its own framed block with
the warning in the header strip rather than floating above, and the two
remaining steps below it as ordinary rows.
Two token bugs went with it. `bg-canvas` does not exist — it was a class that
compiled to nothing, which is part of why the block looked wrong. And
`text-accent` on white is 2.9:1; the config says in as many words to use
accent-text for anything read, so the current step number does.
Also fixed, and it would have broken every takeover in production: PublicSiteGate
is appended to the whole web group, so while the site is hidden a server in a
rescue system fetching the archive would have received the 503 placeholder and
piped it into tar. The operator would have seen an unpack error on a machine
only reachable through the provider's console, with nothing pointing at a switch
in the admin area — and they are by definition neither on a management network
nor signed in, since leaving that state is the whole point. Exempted by ROUTE
NAME, not by hostname: the docblock rightly warns that exempting by host means
trusting a header the caller picks, but that warning is about the entire portal.
Behind these two routes are documents that are public anyway and an archive that
404s without a valid one-time code.
2026 tests pass, assets build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A domain that exists only to serve one installer is not worth a certificate.
One that also carries the AGB, the AV and the TOM is — those addresses go into
contracts and onto invoices and have to still resolve in three years, which an
address that moves with the next rebuild of the portal cannot promise. That
reason is what changed the answer.
files.… over cdn./storage./archiv.: a CDN is an edge network and this is not
one, so the name would be a lie the day a real CDN goes in front of it.
"storage" reads like object storage or customer data, and a customer seeing it
will wonder whether their files live there. "archiv" says superseded, which the
terms currently in force are not — and R13 keeps paths and names English
anyway.
Two halves on that host, with opposite rules, and that is the whole point of
giving it its own name:
Public — storage/app/files/public/, served to anyone, indexable, because
somebody looking for the terms should find them. Versioned filenames:
agb-2026-01.pdf, never agb.pdf, so a contract signed in January cannot come to
point at conditions written in July. The rule is written where somebody will
look for it rather than enforced, because a upload that rejects a filename
helps nobody.
Private — the installer, and it is not a file in that directory at all: it is
built from deploy/bootstrap on demand. The gate is the one-time enrolment code
that is ALREADY in the pasted line, resolved without being consumed, because
the code is still needed for every progress report and for the registration at
the end. No second secret: a dedicated download token would never expire, would
sit in shell histories forever, and would travel in the same line as the
WireGuard private key — protecting the least sensitive thing with the exposure
of the most sensitive one. 404 rather than 403 on a bad code, and noindex on
the response.
Path traversal is answered before it starts: basename() only, no directory tree
under public/ by design, and dotfiles refused. The test walks ../../.env three
different ways.
Empty FILES_HOST keeps the archive on the portal host exactly as before, so
nothing breaks in the window between setting the variable and the DNS record
existing.
The hostname had to move into phpunit.xml rather than a config()->set(): routes
are bound at boot, so a test that sets it afterwards is setting it too late.
2025 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>