# 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//*.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//*.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//` 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//*.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.