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>
264 lines
12 KiB
Markdown
264 lines
12 KiB
Markdown
# s3-gateway
|
||
|
||
Downstream build pipeline that produces **de-branded
|
||
[versitygw](https://github.com/versity/versitygw) releases** — the
|
||
upstream Versity S3 Gateway with Versity trademarks and logos removed
|
||
from the admin web UI.
|
||
|
||
Same pattern as [`scoutfs-build`](../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](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](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`](https://goreleaser.com).
|
||
|
||
```bash
|
||
# 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:
|
||
|
||
```bash
|
||
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/`](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/versity` links).
|
||
- 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) and `CreateBucket`'s grant headers — so also
|
||
`versitygw admin create-bucket --grant-*` — refuse any account whose
|
||
`UserID` is not the owner's. Root is refused too.
|
||
- **Public means read.** `all-users` gets `READ` only; a policy may
|
||
allow `"*"` only `s3:GetObject` and `s3:ListBucket`.
|
||
`public-read-write` and every other public grant are refused.
|
||
- **A policy adds to the ACL.** An `Allow` from either is enough and a
|
||
policy `Deny` wins — except that the owner's key is never denied.
|
||
- **Root/admin only:** `DeleteBucket`, `PutBucketVersioning`,
|
||
`PutObjectLockConfiguration`, `PutBucketOwnershipControls` (and
|
||
`DeleteBucketOwnershipControls`).
|
||
- An ACL holding the `all-users` `READ` grant can be written back
|
||
unchanged (upstream answers `500`).
|
||
|
||
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](docs/owner-scoped-access.md#proving-the-patch-is-in-on-leil-rig).
|
||
|
||
### 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 `cas` set 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](docs/vault-atomic-update.md#proving-the-patch-is-in-on-leil-rig).
|
||
|
||
## 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](docs/multi-version-pipeline.md) for the
|
||
full recipe. In short:
|
||
|
||
```bash
|
||
# 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.Z` tag to this repo; CI builds upstream
|
||
versitygw `vX.Y.Z` with `patches/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 the `golang` container, 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_URL` to override the upstream URL.
|
||
|
||
## License
|
||
|
||
Apache-2.0, matching versitygw upstream. Upstream copyright and
|
||
`NOTICE` are retained in every build.
|