Files
s3-gateway/README.md
T
57_WolveandClaude Opus 5.5 a52f92c100 patches: add v1.11.0 atomic Vault IAM update patch and its leil-rig proof
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>
2026-11-11 02:04:31 -06:00

264 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.