0004-vault-atomic-update-user.patch makes the Vault IAM backend's UpdateUserAccount -- what admin update-user, and so every key rotation, runs -- one KV v2 write with cas set to the version it read, retried from a fresh read on a check-and-set conflict (up to 5 attempts). Upstream deleted the entry and created it again: in between, another instance on the same KV path whose IAM cache missed answered InvalidAccessKeyId, a failure between the two calls lost the account, and two updates at once lost one of them. Touches only auth/iam_vault.go and a new auth/iam_vault_update_test.go. tests/vault-update/proof.sh proves it on leil-rig against an OpenBao dev server, with two gateways on one KV path and gateway A's OpenBao calls held 3 s by a proxy so upstream's gap can be observed. On leil-test-1: patched 20 passed, 0 failed; 0001-0003 control 13 passed, 7 failed (entry missing 3.00 s, 28 InvalidAccessKeyId on the second gateway, concurrent rotation lost, account gone after a refused write). go test on the node: 35 of 35 packages ok. docs/vault-atomic-update.md has the problem, the change, the proof and its result; README and the multi-version pipeline doc point at it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
s3-gateway
Downstream build pipeline that produces de-branded versitygw releases — the upstream Versity S3 Gateway with Versity trademarks and logos removed from the admin web UI.
Same pattern as scoutfs-build: pull an upstream
release tag, apply our patch series scoped to that tag, build
with upstream's GoReleaser, and publish to Gitea Releases.
Artifact names are intentionally unchanged — the binary stays
versitygw and the deb/rpm packages stay versitygw, so builds are
drop-in compatible with existing configs, systemd units, and the
versitygw CLI. Only the embedded web UI is rebranded to "S3 Gateway".
The v1.11.0 series also carries two behavioural patches:
- Owner-scoped bucket access: a bucket's ACL and policy may name only the owner's own keys, public access is read-only, and deleting a bucket or changing its versioning, object lock or ownership controls is left to the gateway's root and admins. See docs/owner-scoped-access.md.
- Atomic Vault IAM update: with the Vault IAM backend,
update-user(a key rotation) is one check-and-set write of the account's KV entry instead of a delete and a create, so the account never goes missing and a failed update leaves it as it was. See docs/vault-atomic-update.md.
upstream tag (vX.Y.Z) this repo
github.com/versity/versitygw patches/<tag>/*.patch
│ │
▼ ▼
ci/clone-source.sh ───► ci/apply-patches.sh ───► ci/build.sh ───► ci/publish-release.sh
(shallow clone tag) (git am --3way) (goreleaser) (Gitea release assets)
ci/support-matrix.yaml records which upstream versions have a curated
patch set and maps each to its patch directory.
Layout
s3-gateway/
├── ci/
│ ├── support-matrix.yaml # upstream tag -> patch directory (registry)
│ ├── clone-source.sh # shallow-clone upstream versitygw at a tag
│ ├── apply-patches.sh # git am --3way patches/<tag>/*.patch
│ ├── build.sh # goreleaser release (binaries + deb/rpm)
│ ├── publish-release.sh # upload dist/* to a Gitea release
│ └── run.sh # local end-to-end: clone + patch + build one tag
├── patches/
│ ├── v1.11.0/ # the supported series
│ │ ├── 0001-webui-debrand.patch
│ │ ├── 0002-webui-dark-theme.patch
│ │ ├── 0003-owner-scoped-access.patch
│ │ └── 0004-vault-atomic-update-user.patch
│ └── archive/ # superseded; kept, not built
│ ├── v1.10.0/
│ ├── v1.8.0/
│ ├── v1.7.0/
│ ├── v1.6.0/
│ └── v1.5.0/
├── tests/
│ ├── owner-scope/ # leil-rig proof test for 0003 (proof.sh, s3req.py)
│ └── vault-update/ # leil-rig proof test for 0004 (proof.sh, watch.py, vaultproxy.py)
├── .gitea/workflows/build.yml
└── docs/
├── multi-version-pipeline.md
├── owner-scoped-access.md # what 0003 does, and its proof test
└── vault-atomic-update.md # what 0004 does, and its proof test
Build locally
Prerequisites: git, go, and goreleaser.
# Build one upstream tag end-to-end → artifacts in src/dist/
VGW_TAG=v1.11.0 bash ci/run.sh
# Fast offline build against a local upstream clone:
VGW_REPO_URL="/path/to/versitygw" VGW_TAG=v1.11.0 bash ci/run.sh
Or run the steps individually:
VGW_TAG=v1.11.0 bash ci/clone-source.sh # → src/
VGW_TAG=v1.11.0 bash ci/apply-patches.sh # git am the series onto src/
VGW_TAG=v1.11.0 bash ci/build.sh # goreleaser → src/dist/
src/ and src/dist/ are gitignored.
Patches
Patch sets live in patches/<dir>/ as git format-patch files and are
applied with git am --3way, in filename order. The directory for a
tag comes from the patches: field in ci/support-matrix.yaml
(defaults to the tag name).
That matrix is an allowlist: ci/apply-patches.sh refuses any tag
it does not list, so a stray tag push or a mistyped dispatch can't
publish a build of a superseded release.
Currently maintained:
| Upstream tag | Patch set | Contents |
|---|---|---|
v1.11.0 |
v1.11.0/ |
0001-webui-debrand.patch, 0002-webui-dark-theme.patch, 0003-owner-scoped-access.patch, 0004-vault-atomic-update-user.patch |
v1.11.0 is the only supported version. It supersedes v1.10.0
with 42 further upstream fixes, so the older series — v1.10.0,
v1.8.0, v1.7.0, v1.6.0 and v1.5.0 — are no longer built.
Their patches are kept under
patches/archive/ for reference and for a
deliberate rebuild; see that directory's README.
Note that protection against CVE-2026-111943 (the Go crypto/x509
eMudhra distrust, announced in upstream's v1.10.0 release notes)
comes from the Go toolchain these binaries are built with, not
from the upstream tag. Build with Go 1.26.10 / 1.27.3 or later.
What the de-brand patch does
- Sidebar/login logos → a plain "S3 Gateway" wordmark.
- Page titles: "VersityGW Dashboard" → "Dashboard" (Buckets, Users, Explorer and the v1.8.0 IAM pages too).
- Browser tab titles → "S3 Gateway – …".
- Removes the sidebar Resources section (external
github.com/versitylinks). - Replaces the Versity favicon with a neutral SVG; deletes the Versity logo PNGs.
- Removes the visible "© Versity Software Inc." footer from the login page.
Deliberately kept (license compliance): the per-file Apache-2.0
license headers — which carry the upstream copyright notice as an HTML
comment at the top of each file — and the NOTICE file. Upstream is
Apache-2.0; the de-brand strips the visible Versity trademarks (logos,
wordmarks, and the login footer) while preserving the copyright notice
in the source headers.
What the dark-theme patch does
0002-webui-dark-theme.patch re-skins the admin UI to match the
kanrisha web app — a near-black palette with indigo/blue accents —
without touching the page markup. It works entirely through the two
shared asset files plus bundled fonts:
tailwind-config.js— remaps the named color tokens (surface,charcoal,primary,accent) onto the dark palette and switches the sans/mono fonts to Inter / JetBrains Mono.theme.css— sets the dark body, restyles the bespoke components (sidebar active state, dropdowns, toggles, checkboxes, file rows, login) in indigo, and adds an override layer that flips the built-in Tailwind white/gray/status utilities the markup uses.fonts.css+assets/fonts/*.woff2— self-hosts Inter and JetBrains Mono (variable, latin + latin-ext) so the UI stays fully offline, matching the existing self-hosted Roboto.
The only markup change is the login page's light background gradient,
which becomes a solid dark surface. It is part of the supported
v1.11.0 series, and of every archived set except v1.5.0.
What the owner-scoped access patch does
0003-owner-scoped-access.patch changes authorization in auth/ (plus
one call in s3api/controllers/bucket-put.go). A bucket's owner is the
account its ACL names; the owner's keys are every account with that
account's UserID.
- Only the owner's keys.
PutBucketPolicy,PutBucketAcl(body and grant headers) andCreateBucket's grant headers — so alsoversitygw admin create-bucket --grant-*— refuse any account whoseUserIDis not the owner's. Root is refused too. - Public means read.
all-usersgetsREADonly; a policy may allow"*"onlys3:GetObjectands3:ListBucket.public-read-writeand every other public grant are refused. - A policy adds to the ACL. An
Allowfrom either is enough and a policyDenywins — except that the owner's key is never denied. - Root/admin only:
DeleteBucket,PutBucketVersioning,PutObjectLockConfiguration,PutBucketOwnershipControls(andDeleteBucketOwnershipControls). - An ACL holding the
all-usersREADgrant can be written back unchanged (upstream answers500).
It and 0004 are the series' only patches outside webui/, and both
have unit tests: after a rebase run go test ./auth/ ./s3api/... (on
Linux — on Windows upstream's standalone-IAM, posix and CLI tests fail
with or without the series) and
the leil-rig proof test in
docs/owner-scoped-access.md.
What the atomic Vault update patch does
0004-vault-atomic-update-user.patch changes one call in
auth/iam_vault.go, the Vault IAM backend's UpdateUserAccount —
what versitygw admin update-user, and so every key rotation, runs.
Upstream deletes the account's KV entry and creates it again; in
between the account does not exist, so another gateway instance on the
same KV path whose IAM cache misses answers InvalidAccessKeyId, and a
failure between the two calls loses the account for good.
- One write. The entry is read with its KV version, changed, and
written back with
casset to that version. - Conflicts retry. If another writer got there first, the update starts again from a fresh read, up to 5 times.
- Unchanged: "user not found" for a missing account (also one
deleted mid-update, which is never re-created), the re-login on a
403, and every other Vault call. - KV v2 now keeps the account's earlier versions, old secrets included,
up to the mount's
max_versions.
Its unit tests run with the ones above (go test ./auth/); its
leil-rig proof test is in
docs/vault-atomic-update.md.
Adding / refreshing a version
When upstream cuts a new release you want a build of, you rebase the patch series onto it and add a patch directory. See docs/multi-version-pipeline.md for the full recipe. In short:
# Rebase the series onto the new tag, re-export into patches/vX.Y.Z/
git clone https://github.com/versity/versitygw && cd versitygw
git -c core.autocrlf=false checkout -b debrand vX.Y.Z
git am --3way /path/to/s3-gateway/patches/<prev>/*.patch # resolve any conflicts
git format-patch --binary vX.Y.Z..HEAD -o /path/to/s3-gateway/patches/vX.Y.Z/
Then add the tag to ci/support-matrix.yaml, commit, and build it (see
below).
CI
.gitea/workflows/build.yml builds one version per run and
publishes it to a Gitea release in this repo. It triggers two ways — no
scheduled/cron builds:
- By new tag — push a
vX.Y.Ztag to this repo; CI builds upstream versitygwvX.Y.Zwithpatches/vX.Y.Z/. - One-shot — run the build workflow manually (workflow_dispatch) and enter the upstream tag to build.
Either way the tag must appear in ci/support-matrix.yaml; if it does
not, the patch step fails the run rather than publishing an
un-de-branded or superseded build.
Configuration:
- Runner: any Linux runner (
runs-on: ubuntu-latest). The Go toolchain comes from thegolangcontainer, so the runner only needs Docker. - Secret:
TOKEN_GITEA— a Gitea PAT with write:repository (clones this repo + creates releases / uploads assets). - Variable (optional):
VGW_REPO_URLto override the upstream URL.
License
Apache-2.0, matching versitygw upstream. Upstream copyright and
NOTICE are retained in every build.