Skip to content

fix(server): builtin-mode fixes for Stronghold (task storage, docker config dir) - #1

Merged
trublast merged 7 commits into
trublast:mainfrom
vmrm:stronghold/task-storage
Sep 7, 2026
Merged

trublast merged 7 commits into
trublast:mainfrom
vmrm:stronghold/task-storage

Conversation

@vmrm

@vmrm vmrm commented Sep 7, 2026

Copy link
Copy Markdown

Summary

Two fixes for trdl compiled into Stronghold as a builtin secrets engine. Both are latent upstream (an external plugin never hits them) and both are opened against werf/trdl as well: werf#426 and werf#427. This branch is trublast/main (b7e692d) plus those two commits, so Stronghold's replace can point here instead of at a third fork.

Key changes

  • fix(server): read task storage from the tasks manager, not the request (fix(server): read task storage from the tasks manager, not the request werf/trdl#426) — the release and publish tasks read req.Storage after the request handler returned; the Vault router resets it to nil in the deferred block of routeCommon, so the first storage read in the task panics and takes the Vault process down. The task now uses the storage the tasks manager hands it (four sites). New suite server/task_storage_test.go runs both tasks with req.Storage = nil.
  • fix(server): keep BuildKit token seeds off an unwritable docker config dir (fix(server): keep BuildKit token seeds off an unwritable docker config dir werf/trdl#427) — the BuildKit auth provider does MkdirAll(config.Dir()) for its token seeds, which is /.docker in a pod with readOnlyRootFilesystem and no HOME; every pull failed with mkdir /.docker: read-only file system. The seeds move to os.TempDir()/trdl-docker-config when the default dir cannot be created; config.json is still read from the default location.

Why

Observed on a Stronghold 1.19 stand (release-1.19, three replicas): trdl/<engine>/release crashed the active replica with pgp.GetTrustedPGPPublicKeys({...}, {0x0, 0x0}) at path_release.go:133; with the first fix the task reached the build and failed on mkdir /.docker; with both, git-signatures v0.0.1 was released end to end through the kubernetes buildkitd driver (task SUCCEEDED in 3m00s, zero replica restarts, eight 0.0.1 targets in the TUF repository, sha512 of the linux-amd64 binary matching). Stronghold currently carries replace github.com/werf/trdl/server => github.com/vmrm/trdl/server v0.0.0-20260904213209-b50a4645f684, i.e. this exact branch head.

Review focus / risks

🤖 Generated with Claude Code

vmrm and others added 7 commits September 4, 2026 21:40
In builtin mode the Vault router resets req.Storage to nil once the request
handler returns (the deferred reset in routeCommon, vault/router.go), and the
release and publish tasks run later, on the tasks manager worker. Reading
req.Storage from the task then dereferences nil: the first storage read in
the task, GetTrustedPGPPublicKeys, panics and takes the whole Vault process
down with it. The tasks manager already hands the task its own storage, the
mount's storage view kept from the first request; use it for every storage
read inside the task.

An external plugin never saw this, because there req.Storage is a gRPC
storage client living in the plugin process, which the core's reset does not
reach. The unit tests mocked RunTask and never executed the task at all, so
the new suite runs both tasks after setting req.Storage to nil, the way the
router does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Vasily Marmer <vasily.marmer@flant.com>
(cherry picked from commit 1fb5e0c)
…g dir

The BuildKit auth provider persists registry token seeds under the docker
config dir and creates that directory on the first token request, so a
process without a writable home fails every pull with "mkdir /.docker:
read-only file system", anonymous pulls included. A builtin backend runs
exactly like that: Stronghold has readOnlyRootFilesystem, no HOME and only
/tmp writable. Read the config file from the default location as before,
then point config.Dir() at a directory under os.TempDir() when the default
one cannot be created, so only the seeds move and credentials in the
default config.json keep working.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Vasily Marmer <vasily.marmer@flant.com>
(cherry picked from commit 48215e2)
The comment next to the config dir redirect read as if a registry token
were written to disk. The file holds a locally generated random seed for
the client-side token authority key; it is neither a token nor a
credential, and it is never sent anywhere. Say so where the redirect is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Vasily Marmer <vasily.marmer@flant.com>
(cherry picked from commit 212a4b2)
…r config dir out of $HOME

Review found that the release scenario proved only that req.Storage is not
dereferenced: a task reading a fresh empty storage would pass too. The
storage handed to the task now counts List and Get calls and the test
requires at least one. The release scenario also reached
config.LoadDefaultConfigFile, creating the developer's ~/.docker as a side
effect; the docker config dir is pinned to a temp dir for the test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Vasily Marmer <vasily.marmer@flant.com>
(cherry picked from commit 296f7cd)
…truction

Review of the first cut found two defects. config.SetDir was never
restored, so the second build in the same process read config.json from
the seed directory and lost the credentials kept at the default location;
and the fallback was a guessable path in the shared temp dir that nothing
created or validated. The redirect now holds only while
NewDockerAuthProvider captures config.Dir(), under a mutex so concurrent
builds cannot observe each other's redirect, and the seed directory is
created once per process with os.MkdirTemp and a private mode. The tests
drive the provider through GetTokenAuthority, the call buildkitd makes on a
bearer challenge, instead of asserting on the global variable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Vasily Marmer <vasily.marmer@flant.com>
(cherry picked from commit e93a9e4)
The seed only ever needs to outlive one process, so prefer the
memory-backed /dev/shm for its directory and fall back to os.TempDir()
where there is none, so nothing lands on a node disk.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Vasily Marmer <vasily.marmer@flant.com>
(cherry picked from commit a6576be)
Several trdl mounts, in one namespace or across namespaces, share one
Stronghold process, and a per-process seed directory is shared between
them. Create the directory per build instead and remove it once the build
is over, so mounts share nothing through it and nothing outlives the
build. The cost is a fresh seed per build: on a shared buildkitd the
daemon cannot link a new build to the tokens a previous one fetched; with
a builder pod per build there is nothing to link.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Vasily Marmer <vasily.marmer@flant.com>
(cherry picked from commit 4a3a4fc)
@trublast
trublast merged commit 6185007 into trublast:main Sep 7, 2026
@vmrm
vmrm deleted the stronghold/task-storage branch September 7, 2026 19:35
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.

2 participants