Security hardening #55

Merged
mi merged 8 commits from security-hardening into main 2026-09-11 14:11:23 +02:00
Owner
No description provided.
mi added 3 commits 2026-09-11 13:25:44 +02:00
an artifact is served on an origin of its own
werkator build succeeded after 01:59
cd95682f2a
A build writes the HTML it stores as an artifact, and Werkator served that
HTML on its own origin with scripting enabled. The control token lives in
localStorage of exactly that origin, so a page a build produced could read
it — or skip the reading and call the JSON API directly, since it already
was the site. The operator is invited into it: the repository line links
"artifact", and a Gitea status carries the artifact page as its target_url.

Every artifact response now carries "sandbox allow-scripts", which puts the
document on an opaque origin, plus "nosniff". Interactive reports keep
working; they no longer share an origin with the instance that serves them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
the Docker socket is opt-in, and pinned
werkator build succeeded after 01:30
4ae2f59daa
Every Docker build got the host's daemon socket mounted, whether it had any
use for one or not. The daemon acts outside the boundaries this runner sets:
a container started through the socket can bind-mount paths the build itself
cannot reach, so the mount is a way around the sandbox for every build that
does not need it.

docker.socket now says whether a build gets it, defaults to false, and is
stripped from the branch layer like docker.enabled and docker.network — a
branch must not be able to hand itself the way out. Builds that genuinely
need a daemon (Testcontainers) set it in the repository's configuration and
are then as privileged as the daemon is, which is the honest statement.

Locating the socket is unchanged: whether the daemon is rootless still
decides the ownership repair, and that has nothing to do with whether this
build may talk to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The runner mounted the entire repository directory read-write, and only the
git metadata was protected on top of it. That left the committed
.werkator.yml at the repo root writable — the layer the pinned settings are
read from. A build could therefore set requirePullRequest, werkdock.enabled
or werkdock.binary for every build that came after it, which is precisely
what stripPinned refuses to let a branch do through its own committed file.

The checkout is mounted read-only now. Writable are the worktree this branch
builds in and that worktree's git admin directory, both mounted after and
nested inside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mi added 1 commit 2026-09-11 13:27:43 +02:00
Files.copy follows symlinks, and the collection runs in the server process on
the host — outside whatever sandbox the build ran in. A build therefore did
not need to be able to read the machine config, the control token or a
neighbouring repository's checkout: pointing a link at one of them was enough
to have Werkator read it and store the content as a plain file, which for a
public repository is a public download.

A link whose target stays inside the collected directory is copied as before
— that is a report referring to itself. A link that leaves it, or one that
dangles, raises ArtifactEscapeException: the half-filled incoming directory
is discarded, so nothing of that build is stored at all.

Since the collection runs after the terminal status is recorded, the executor
takes the status back on that exception and records FAILED, with a line in
the live log. A build whose result cannot be collected did not succeed, and
saying so late beats saying nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mi added 1 commit 2026-09-11 13:28:44 +02:00
release notes name PR#55
werkator build succeeded after 01:22
fae0ef7a7a
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mi added 1 commit 2026-09-11 13:42:59 +02:00
stripPinned takes keys out of blocks, and a branch that writes no block gave
it nothing to take: `docker: null` passed the strip untouched, deepMerge
replaced the host's whole docker section with it, and the binding fell back
to the data-class defaults — docker disabled. A branch could therefore run
natively on a host that requires containers, by writing a value nobody reads
as an attack. `builds.default: null` and `builds.default: false` did the same.

Two answers, because they cover different things. checkLayerShape refuses a
branch layer that writes a non-map where a block belongs, naming the key —
ignoring it would leave a line that does something other than what it says.
enforcePinned writes the pinned values back from the host's layers after the
merge, so the pinning holds by what the result says rather than by what was
subtracted, including shapes nobody has thought of yet.

builds.<name>: null stays a branch's way of saying it runs no such build. The
host's builds.default is the fallback the pinned values come from when the
branch empties it — that is the path the inheritance would otherwise have
carried the sandbox policy along, and it runs where the branch can reach it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mi added 1 commit 2026-09-11 13:45:27 +02:00
a build's log file is a name, not a path
werkator build succeeded after 01:22
912cec039c
stdoutLog and stderrLog were passed to stagingDir.resolve, and resolve hands
an absolute argument straight back. They are ordinary branch settings, so a
branch could name any path: the server process then opened it on the host —
outside the sandbox the build itself runs in — truncated it and wrote the
build's output into it. Anything the service user may write was reachable
that way, including other repositories' state and git hooks. A relative ../
did the same.

Both are checked for being a plain file name before anything is opened, and
a build that asks for something else does not start.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mi added 1 commit 2026-09-11 13:47:52 +02:00
PR-doc for the security hardening
werkator build succeeded after 01:22
01d86ff712
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mi merged commit 01f59d1a06 into main 2026-09-11 14:11:23 +02:00
mi deleted branch security-hardening 2026-09-11 14:11:23 +02:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mi/werkator#55