Commit Graph

12665 Commits

Author SHA1 Message Date
Feng Ruohang 062a91beed fix: pin the CREDITS module closure to the shipped linux target
The go list -deps closure is GOOS-specific, so the CREDITS generated on
darwin (carrying go-m1cpu) could never match a Linux regeneration
(ethtool, numcpus), and the check-gen guard failed on its first CI run.
Generation now pins linux/amd64 with the release build tags, so the
output is identical on every host.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 19:48:33 +08:00
Feng Ruohang 16b78eb4e5 build: point the release surfaces at RELEASE.2026-08-06
Bundle mcli RELEASE.2026-08-06T00-00-00Z in the container image (pin
and both architecture digests; interop with the new server was
verified end to end during release acceptance) and align the CI
fixture default in install-mcli.sh.

Point the Helm chart defaults at the first real silo image tag:
docker.io/pgsty/silo is a fresh repository, so the inherited
RELEASE.2026-08-04 default could never pull. Chart version moves to
7.0.1 with appVersion RELEASE.2026-08-06T00-00-00Z; the packaging
tripwire in verify-helm-migration.sh follows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 19:10:56 +08:00
Feng Ruohang 9462cce16e fix: rewrite the healthcheck output selection as a switch
gocritic's ifElseChain check fails make lint on the --json/--quiet
branch introduced by b6d47b739; the quality job runs make lint on
every push to main. Behavior is unchanged (verified by re-running the
json/quiet success and failure cases byte-for-byte).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 19:09:26 +08:00
Feng Ruohang b6d47b739c fix: harden healthcheck and distroless lanes per adversarial review
Findings from an adversarial review (Codex, gpt-5.6-sol at max effort)
of 2ff594f4b and 4c34d2309, each independently verified before fixing:

- SBOM generation: buildx attaches a provenance attestation, so every
  per-arch digest names an OCI index; Syft's platform default on an
  amd64 runner cannot resolve an arm64-only index and the step dies.
  Pass --platform explicitly on all four Syft calls (the two classic
  lanes had the same latent defect - the renamed workflow has not run
  yet, which is why it never fired).
- Release ordering: the HEALTHCHECK survival check now runs against
  the pushed architecture image before the versioned and rolling
  multi-arch manifests are created, so a broken health config blocks
  their promotion; the comment now states honestly that the
  arch-suffixed tags are already public at that point.
- Gate assertions: tar's member-argument mode exits non-zero on any
  missing name, which under pipefail masked a found forbidden file
  when exactly one of them existed; -tv prints symlinks as
  'name -> target', defeating $-anchored greps; and the licenses
  check proved only one-of-three. Export the rootfs once and assert
  every required and forbidden entry individually (busybox/sh and
  usr/bin/mc[li] now covered), and match the image healthcheck as an
  exact array instead of a substring.
- Probe target vs CLI-configured servers: a probe process cannot see
  PID 1's argv, so --url gains EnvVar MINIO_HEALTHCHECK_URL as the
  documented way to point the baked-in HEALTHCHECK at a server whose
  address/TLS comes from command-line arguments (verified end to end:
  server on --address :9010, env var alone turns the container
  healthy). Baseline regenerated for the new env token.
- IPv6 zone identifiers: serialize probe URLs via url.URL.String()
  so [fe80::1%eth0]:9000 becomes a valid %25-escaped URL (tests added).
- Boolean flags: read --json/--quiet via Bool() so --json=false is
  false, instead of IsSet() which treats any occurrence as true.
- Docker's HEALTHCHECK timeout raised to 10s: an outer deadline equal
  to the probe's own 5s always SIGKILLed the probe before it could
  print its diagnostic line.
- test-release path filter now also triggers on cmd/healthcheck-main.go
  and cmd/main.go, so subcommand regressions run the image gate.

Not adopted: require_text's comment-insensitivity in verify-rebrand.sh
(snapshot-tripwire by design, consistent with its other assertions -
the semantic check lives in the CI gate now), and full
staging-then-promote tag publishing (a workflow-wide redesign shared
with the classic lanes, tracked as follow-up).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 17:27:22 +08:00
Feng Ruohang 4c34d23099 build(docker): add the distroless image variant as a pilot
Publish pgsty/silo:<RELEASE>-distroless (and a rolling :distroless tag)
alongside the classic image: gcr.io/distroless/static-debian12 plus
exactly one program, the silo binary. No shell, no mc, no curl, no
entrypoint script - the binary is the ENTRYPOINT and the baked-in
exec-form HEALTHCHECK runs 'silo healthcheck ready'. The classic image
and its mc-based health checks are deliberately unchanged.
Design: silo.pgsty.com/compatibility/feature/healthcheck/

/data is created in the image layer, world-writable, because Docker
seeds fresh volumes from the layer mountpoint and no entrypoint exists
to repair ownership at runtime (issue #55); the parent directory is
copied from a throwaway busybox stage since COPY of a directory copies
contents rather than the entry itself, which would silently leave
/data at root:0755 and break every non-root run. The MINIO_USERNAME
drop-user path is not supported in this variant; use --user.

test-release.yml now builds the real Dockerfile.distroless on every
gate run (it has no download stages, so it stays offline) and asserts:
the HEALTHCHECK survives into the image config, /data ships 0777, no
shell and no /usr/bin/minio are present, Docker's health state turns
healthy from the baked probe alone, the probe binary execs without a
shell, and SIGTERM still stops the server gracefully - as root and as
--user 1001:1001. docker-release.yml gains the distroless build lanes,
multi-arch manifests, SBOM and provenance attestations, and a
release-blocking check that the pushed manifest still carries the
HEALTHCHECK (a Docker extension absent from the OCI image spec).

Verified locally on linux/arm64: full gate assertions plus bare
'docker run ... healthcheck' exit-code semantics and --version
passthrough.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 16:40:04 +08:00
Feng Ruohang 2ff594f4bb feat: add the native silo healthcheck subcommand
Add 'silo healthcheck [live|ready|cluster|cluster-read]', a thin
anonymous HTTP client for the server's own /minio/health/* endpoints,
so containers without a shell, curl, or mc can still run health
checks. Design: silo.pgsty.com/compatibility/feature/healthcheck/

The check vocabulary maps 1:1 onto the health API paths; the probe
target is derived from the server's own --address/MINIO_ADDRESS
contract with HTTPS auto-detected from the certs directory, and can
be overridden with --url. Exit codes are 0/1 only (Docker reserves 2);
diagnostics (x-minio-server-status, quorum headers) go into a single
output line for docker inspect. The request is strictly anonymous (a
credentialed request would be rejected by the reserved-path guard),
the transport bypasses HTTP_PROXY, and certificate verification is
skipped to match kubelet HTTPS probe behavior. Cluster checks default
to a 15s deadline so the server's 10s cluster_deadline can elapse.

Compatibility notes: the preserved /minio/health/* path literals and
the MINIO_ADDRESS env var are upstream wire/config surface, reused on
purpose; the rebrand-guard baseline is regenerated for the new route
literals (tests included) with zero new exported symbols. The docker
entrypoint argv translation learns the new command name.

Verified: unit tests, entrypoint tests, go vet, plus an end-to-end
run against a live server covering all four checks, --maintenance
(412), --json, usage errors, unreachable and timeout paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 16:32:16 +08:00
Feng Ruohang 219670d317 build: ship LICENSE and NOTICE in every package and image
The RPM, DEB and APK payloads carried no license material at all, and
the OCI image copied LICENSE and CREDITS but not NOTICE. For an AGPL
distribution that is not defensible packaging. Every delivery channel
now carries the same license set:

- nfpm packages install LICENSE and NOTICE under /usr/share/doc/silo/.
  The entries are declared once per packager because nfpm only honors
  type: license on rpm and silently drops such entries from deb and apk
  -- the rpm keeps its %license flag, deb and apk carry plain files at
  the same path. CREDITS stays out of the OS packages deliberately: at
  1.8MB it would dominate an otherwise ~40MB payload.
- The OCI image gains /licenses/NOTICE next to the existing LICENSE and
  CREDITS, in the Dockerfile and in both context-staging paths (the
  make docker target and docker-release.yml).
- The GoReleaser archives list their files explicitly; the default
  globs would keep missing NOTICE.

NOTICE's first line now reads 2015-2025 to match the banner constants
fixed in b57275be3 -- it had kept upstream's stale 2015-2023 claim --
and verify-rebrand.sh pins both NOTICE copyright lines.

The payload locks move in the same commit: sign-release-rpms.sh expects
the six-file listing, and the test-release validation asserts presence,
count, the rpm %license flag, and sha256 equality of the packaged
LICENSE and NOTICE against the repository files across all three
formats and both architectures.

Also dropped the unit's WorkingDirectory=/usr/local, a leftover from
upstream's /usr/local/bin install layout. Debian policy 9.1.2 keeps
packages out of /usr/local entirely; the binary already installs to
/usr/bin and the service does not depend on its working directory.

Verified locally with nfpm v2.47.0 (the CI-pinned version): built all
six packages via package-release.sh, then ran the test-release
validation step verbatim (extracted from the workflow YAML) and the
signing script's payload comparison against them in an Ubuntu 24.04
container -- both pass. Built the arm64 image from the real
Dockerfile.goreleaser and confirmed /licenses holds CREDITS, LICENSE
and NOTICE. make rebrand-guard passes with the new NOTICE pins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Feng Ruohang <rh@vonng.com>
2026-08-06 15:03:35 +08:00
Feng Ruohang 6bd9cf77ef build: regenerate CREDITS from the linked module set and guard it in CI
The shipped CREDITS predated the fork's dependency work: 38 of the
modules in the current go.mod, among them lestrrat-go/jwx, go-tpm,
go-spiffe and the charmbracelet family, had no entry at all, while
entries lingered for modules no longer in the build. An attribution
file that silently drifts from go.mod is worse than none, so it is now
generated, not curated.

buildscripts/gen-credits.sh (make credits) rebuilds the file from the
licenses of every module go list -deps reports for the main package --
the set actually linked into the silo binary, so test-only and tool
dependencies stay out. Entries keep the established name / URL /
license-text layout. New over the old file:

- Modules replaced in go.mod are annotated with the repository that
  actually serves them, so the pgsty/silo-console, pgsty/mc and
  pgsty/silo-pkg forks are named next to their upstream import paths.
- Bundled NOTICE files are reproduced after the license text, which
  Apache License 2.0 section 4(d) requires when redistributing; 23
  modules carry one.
- minio/colorjson, minio/csvparser and minio/filepath publish no
  license file at all; they repackage Go standard library code and
  their sources carry the Go Authors' BSD-style header, so the Go
  project license is reproduced for them with a note saying why.
- The Go license text itself comes from the pinned golang.org/x/sys
  module rather than GOROOT, because Homebrew's Go omits
  GOROOT/LICENSE and the module copy is version-locked.

check-gen now runs the generator and fails on a CREDITS diff, the same
treatment go.mod and go.sum already get, so dependency changes cannot
leave stale attributions behind. Output is deterministic: two runs are
byte-identical, and LC_ALL=C sorting plus version-pinned inputs keep it
that way across machines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Feng Ruohang <rh@vonng.com>
2026-08-06 15:03:09 +08:00
Feng Ruohang a6d6d9b028 build: update the embedded Console to SILO Console v2.1.1
Pins pgsty/silo-console at 72fc0a5e, the v2.1.1 tag, replacing the
v2.1.0 commit from earlier today. The pseudo-version form stays because
the compatibility module path github.com/minio/console carries no /v2
suffix, so a v2.x semver tag is not directly selectable.

v2.1.1 is 5 commits over v2.1.0. The one substantive change for the
server is a fix that keeps unresolved i18n placeholders out of the
metrics dashboard legends; the rest is the v2.1.1 changelog, dropping
the container-image advertising from the Console docs, and regenerated
embedded web assets.

Verified: go mod tidy -compat=1.26 is a clean no-op afterwards, which is
what make check-gen asserts in CI, and go build ./... and go vet ./...
pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Feng Ruohang <rh@vonng.com>
2026-08-06 14:16:35 +08:00
Feng Ruohang 05be686b84 docs: update README and svg log 2026-08-06 14:08:01 +08:00
Feng Ruohang b57275be34 docs: adopt the no-CLA plus DCO policy and fix copyright terms
Brings the server in line with the contribution and copyright policy
already adopted in pgsty/mc.

Copyright terms were derived from the clock: startupBanner overwrote the
ldflags-injected CopyrightYear with time.Now().Year() and printed it as
the end of MinIO, Inc.'s term, so every January would have extended the
upstream copyright claim past the 2025 end of upstream development, and
release builds silently discarded the injected year. Both banners now
credit MinIO, Inc. for 2015-2025 and PGSTY from 2025 through the
release-stamped year, falling back to the current year in source builds.
NOTICE names PGSTY as the holder of the fork's modifications, matching
the banners and the packaging vendor field.

Contributions are accepted inbound=outbound under AGPL-3.0-or-later with
no CLA - the core is Copyright MinIO, Inc., so the combined work can
never be relicensed and a CLA would buy nothing. What a fork carrying a
downstream delta does need is provenance, so DCO 1.1 sign-off is now
mandatory and enforced by a workflow that rejects unsigned non-bot
commits. CONTRIBUTING documents sign-off, repair, cherry-pick provenance,
dual copyright headers, trailer preservation across squash merges, and
that assistive-tooling trailers carry no authorship or copyright claim.

verify-rebrand.sh pins the copyright split and the policy files so
neither can regress.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Feng Ruohang <rh@vonng.com>
2026-08-06 12:43:58 +08:00
Feng Ruohang 6740e6978f ci: move the workflow actions onto the Node 24 runtime
actions/checkout@v4 and actions/setup-go@v5 declare runs.using: node20, so
every Go CI job printed the runner's Node 20 deprecation warning. The
release, test-release and docker-release workflows carried six more node20
actions that never appeared in that warning only because those workflows
run on a tag or a dispatch rather than on every push: upload-artifact v4,
goreleaser-action v6, build-push v6 and the docker qemu/buildx/login v3
line. All of them move to the current majors, which declare node24. The
SHA-pinned rest already qualified -- attest and sbom-action are node24,
cosign-installer is composite.

Checked against what those majors removed. build-push v7 dropped
DOCKER_BUILD_NO_SUMMARY and DOCKER_BUILD_EXPORT_RETENTION_DAYS and
setup-buildx v4 dropped its deprecated inputs, none of which are used here.
checkout v7 refuses to check out fork PRs under pull_request_target and
workflow_run, and no workflow here triggers on either.

setup-go v6 exports GOTOOLCHAIN=local, the one change with teeth. go.mod
declares `go 1.26.5` and carries no toolchain directive, every job resolves
that same version through go-version-file, and nothing in the module graph
asks for more, so there is nothing left for the go command to fetch:
`go build ./...` passes under GOTOOLCHAIN=local. The tools installed from
outside the module are in range too -- nfpm v2.47.0 requires 1.26.4 and
govulncheck v1.6.0 requires 1.25.0 -- and golangci-lint arrives as a
prebuilt binary from upstream's install script, not through the toolchain.

The runner floor that comes with node24 (v2.327.1) is met by GitHub-hosted
runners, and every job here is ubuntu-latest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 10:46:29 +08:00
Feng Ruohang 62717d7bf6 build: update the embedded Console to SILO Console v2.1.0
Pins pgsty/silo-console at 343d9442, the v2.1.0 tag, replacing the v2.0.0 commit
from 2026-08-04. The pseudo-version is still the form used here because the
compatibility module path github.com/minio/console carries no /v2 suffix, so a
v2.x semver tag is not directly selectable; the comment above the directive now
names the release the commit actually corresponds to.

v2.1.0 is 23 commits over v2.0.0. The substantive ones for the server are a
hand-rolled EN/ZH i18n layer with a language toggle covering login, navigation,
dashboard, buckets and object browser, identity and configuration, event
destinations, observability and admin tools; a dashboard migration to MinIO
Metrics V3; and removal of the remaining SUBNET references from health
reporting, which matches this repository's own decision to close the upstream
telemetry surfaces. The rest is UI polish and regenerated embedded web assets.

Verified: go mod tidy -compat=1.26 is a clean no-op afterwards, which is what
make check-gen asserts in CI, and go build ./... and go vet ./... pass.

Not included here: the GitHub Action version bumps and the .gitignore entry
sitting in the working tree are someone else's change and stay uncommitted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 10:04:23 +08:00
Feng Ruohang f1c77d5a2b ci: let the READMEs name the old repository without pointing at it
The previous commit added a rename note to both READMEs and broke the gate I
added one commit earlier. That gate rejected the string pgsty/minio anywhere
outside three guard files, which was the wrong invariant: the point is that the
old name must never be a live target, not that it must never be spoken. A README
explaining where the MinIO-named artifacts went has to name them, and doing so
is the opposite of stranding a reader on a dead repository.

Split into two rules that say what is actually meant:

- No live URL may resolve to the old repository anywhere, READMEs included -
  github.com/pgsty/minio and hub.docker.com/r/pgsty/minio are rejected outright.
  This is the rule that protects users.
- The bare name is allowed only in five files: the pinned pre-rebrand image
  digest in the upgrade test, the two guards that refuse a legacy image, and the
  two READMEs.

Both halves are negative-tested: adding a github.com/pgsty/minio link to
README.md fails rule one, and adding a bare pgsty/minio to docs/docker/README.md
fails rule two.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 09:47:48 +08:00
Feng Ruohang c47733abc4 docs: point readers at the archived minio branch
Someone arriving at main after the rename has one question the README did not
answer: where did the MinIO-named builds go. Both READMEs now say it directly —
the archived minio branch and the releases up to RELEASE.2026-08-04T00-00-00Z
hold the artifacts maintained under the original identity, and those assets and
the docker.io/pgsty/minio image stay published and unmodified.

The note also records the rename itself with its date, and repeats the one fact
that determines whether an operator has any work to do: only product and
artifact names changed. MINIO_* variables, minio_* metrics, x-minio-* headers,
/minio/* routes and the on-disk format are the same.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 09:46:31 +08:00
Feng Ruohang c46b16ec62 chore: cut over to pgsty/silo and main
The transitional references land in one commit, because they are only correct
together: the repository is pgsty/silo, its default branch is main, and nothing
in the tree should point a user at the old names.

Changed:

- Workflow branch filters. go.yml and vulncheck.yml gated on `branches: master`
  for both push and pull_request, so renaming the default branch would have
  taken automatic CI offline with no error and no signal - the workflows would
  simply never trigger again. They now name main.
- Release target. goreleaser's `release.github.name` becomes silo, which is
  what actually decides where a tagged build publishes. sign-release-rpms.sh's
  GH_REPO default follows.
- The OCI `image.source` label, the Helm chart `sources` entry, the security
  advisory link in the issue-template config, and the go.mod comment citing the
  LDAP TLS fix.
- 115 occurrences across README, README_ZH, SECURITY, CONTRIBUTING and 30 docs
  pages, including 72 links that also carried the master branch in their path.
  Those matter most: GitHub redirects clone, fetch, push and web URLs after a
  rename, but raw.githubusercontent.com does not, and neither follows a branch
  rename - every one of those links would 404 twice over.
- Three error strings in cmd/erasure-sets.go, cmd/storage-errors.go and
  internal/config/errors.go that print an issue URL to operators. These are Go
  string literals inside rebrand-guard's brand allowlist, so the baseline is
  regenerated. The regeneration removes exactly those three entries and adds
  none; all twelve other protected sets, including the 9014 exported symbols,
  are byte-identical.
- The transitional-naming disclaimers in README, README_ZH, SECURITY and
  CONTRIBUTING are dropped, since they no longer describe anything.

Deliberately unchanged, all three because they exist to reject or freeze the old
name rather than to point at it:

- buildscripts/minio-upgrade.sh pins pgsty/minio@sha256:b6bfe72... - the frozen
  pre-rebrand image is the control group for the MinIO-to-Silo upgrade test.
- helm-migration-guard rejects any rendered container still pulling pgsty/minio.
- verify-rebrand.sh rejects the same in the delivery surfaces.

Also unchanged: docs/config/README.md links to pgsty/mc/blob/master, and that
repository's default branch really is still master. It moves when mc does.

verify-rebrand.sh gains three assertions so this cannot silently regress: no
source reference may name pgsty/minio outside the three allowlisted guards, no
link may target pgsty/silo's master branch, and go.yml and vulncheck.yml must
filter on main. Both new rejections were negative-tested - reintroducing a
master branch filter and adding a pgsty/minio URL each fail the gate with the
specific message.

This commit assumes the rename actually happens. Until the GitHub branch and
repository renames are executed, the links it introduces do not resolve.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 09:28:06 +08:00
Feng Ruohang fd2ca1c6d2 docs: rebrand the repository documentation, templates and dashboards
README, README_ZH, SECURITY, COMPLIANCE, CONTRIBUTING, NOTICE,
code_of_conduct, the vulnerability and PR-etiquette documents, the GitHub issue
and pull request templates, and the docs/ tree all present Silo as the product.
The Grafana dashboards under docs/metrics/prometheus/grafana/ have their panel
titles and descriptions rebranded while every minio_* query, label and
expression is left alone, so existing alerts and recording rules keep matching.

The distinction the review demanded is applied per hit rather than by
search-and-replace:

- Product and command text becomes Silo and silo: install and run instructions,
  systemd examples, compose services, download links, badges.
- Protocol and interface text keeps MinIO: MINIO_* variables, minio_* metrics,
  x-minio-* headers, /minio/* routes, .minio.sys, arn:minio, and API field and
  error names.
- Attribution keeps MinIO and gains the fork's own: the AGPL obligations,
  original copyright, CREDITS and NOTICE stay, with the modification notice
  added alongside rather than replacing them.
- Historical and third-party references are left as facts, not rewritten for
  brand tidiness.

README and README_ZH each carry an explicit non-affiliation notice, document
the side-by-side package migration including the
/etc/systemd/system/silo.service.d/10-legacy-user.conf drop-in for keeping a
legacy UID/GID, and state that recursive chown is never performed. The trademark
attribution uses the policy's approved "based on MinIO technology" wording, not
the shortened form the policy rejects.

github.com/pgsty/minio links are left in place and labelled transitional. The
repository has not been renamed, and rewriting them now would produce documented
URLs that 404 until the cutover; they change in the cutover commit together with
the goreleaser release target, the OCI source label and the raw-content branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 08:49:30 +08:00
Feng Ruohang 6613c2a3cb test: pin the external fixtures and run the suites against the silo binary
The test and verification scripts invoked ./minio and pulled their tooling from
upstream infrastructure with no integrity check. Every `curl | tar` of a client
or an old server binary was an unverified execution path in a script that
regularly runs as a privileged user, and several fetched a floating "latest".

Two installers replace all of it:

- install-mcli.sh resolves a pinned pgsty/mc release, downloads the archive and
  its checksum manifest, requires exactly one valid manifest entry for the
  asset, verifies it, and installs. MCLI_BIN with a mandatory MCLI_SHA256 lets
  an offline or air-gapped run supply its own binary, still checksum-checked.
- install-verified-fixture.sh takes source, expected SHA-256 and target, and
  refuses anything that does not match. Sources may be a URL or a local file.

Every script that previously downloaded mc now calls install-mcli.sh. The three
places that genuinely need an upstream artifact - the old MinIO server binary
for the LDAP IAM upgrade-import test, the 2021 mc for the three-site
replication test, and the functional-tests.sh fixture - go through
install-verified-fixture.sh with the digest recorded inline. Those dl.min.io
URLs remain on purpose: they are historical upstream artifacts needed to prove
upgrade compatibility, and they are now pinned and verified rather than
trusted.

The scripts otherwise switch to ./silo, silo.service, the silo container and
compose service names, and SILO_CONFIG_DIR. run-multi-site-minio-idp.sh is
renamed to run-multi-site-silo-idp.sh with the Makefile target following.
buildscripts/minio-upgrade.sh keeps its name and its `minio server` argv - it
exists to test the MinIO-to-Silo upgrade, so the old side must stay old - but
it is now pinned to an image digest rather than a tag, and its `docker system
prune` and `docker volume prune` calls are removed. Those ran unfiltered
against the developer's whole Docker installation; the resiliency tests had the
same problem and lose their prune and `docker ps -q` sweeps too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 08:48:58 +08:00
Feng Ruohang bd8df51665 ci: gate the rebrand on compatibility, packaging and provenance evidence
A rebrand of this size cannot be reviewed by reading the diff. The risk is not
what was renamed but what was renamed by accident: an environment variable, a
metric, a header, a route, a storage marker or an exported symbol that some
deployment depends on. So the invariants get a machine check.

rebrand-guard extracts the identifiers a product rename must not touch - the
go.mod module path, github.com/minio/* imports, MINIO_* variables, minio_*
metric literals, x-minio-* headers, route string literals, the hardcoded grid
routes in internal/grid/manager.go, the reserved bucket root, .minio.sys
markers, arn:minio and minio:s3 policy values, every exported symbol in cmd/
and internal/, and every branded string literal - and compares them to a
checked-in baseline. Any removal or addition fails the build, in both
directions, so a future change cannot quietly drop one either.

The recorded delta against the pre-rebrand tree is: module path, 137 imports,
19 metrics, 15 storage markers, 58 policy values and all 9014 exported symbols
byte-identical; environment 437 -> 436, headers 85 -> 84, routes 331 -> 330.
Each of those three is accounted for. MINIO_CONFIG_DIR and MINIO_VERSION were a
shell local and a compose variable in test scripts, never server settings.
x-minio-common was a docker-compose extension key, never an HTTP header.
/api/health/upload was the SUBNET upload path in the deleted callhome.go.

verify-rebrand.sh covers what the identifier guard cannot see: that the retired
upstream delivery paths stay deleted, that the packaging and container files
name silo, that silo.service keeps its Conflicts and both EnvironmentFile
lines, that silo.env contains no active assignment that would shadow the legacy
file, that no active delivery surface still publishes pgsty/minio, and that no
non-test Go source contains a dl.min.io, subnet.min.io, api.min.io,
slack.min.io or play.min.io URL.

Both, plus the entrypoint argv test, run in go.yml, release.yml and
test-release.yml. test-release.yml also gets the path filters right: it was
watching minio.service, which no longer exists, so the packaging gate would
have gone silently dormant. It now watches the unit, defaults, sysusers,
lifecycle scripts, helm chart and both guards, and asserts the four-file
payload, the absence of any cross-name Provides/Obsoletes/Conflicts, and that a
container started with the legacy `minio server` argv really runs silo as PID 1.

release.yml and docker-release.yml additionally verify that the checked-out
revision matches GITHUB_SHA before building. GitHub's OIDC certificate records
GITHUB_SHA, not the ref passed to actions/checkout, so a manual dispatch from
the wrong ref would have produced provenance describing different source from
the bytes being published. They now emit SBOMs and attestations for archives,
packages and images, and docker-release verifies the release workflow's
attestation on every artifact it consumes. Images publish to pgsty/silo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 08:48:39 +08:00
Feng Ruohang e071bb77e4 build(helm): replace the minio chart with a silo chart that preserves identity
helm/minio becomes helm/silo: chart name silo, version 6.0.0 -> 7.0.0, the
MinIO wordmark icon replaced with the project's own, image.repository and
mcImage.repository pointing at pgsty/silo, and the container command changed to
silo. User-visible titles, comments and documentation links are rebranded. The
MINIO_* environment variables and every existing values key are kept - the
first Silo chart is a rename, not a values-schema migration.

The hard problem is that a chart rename normally rewrites Kubernetes resource
identity, and a StatefulSet's selector and volumeClaimTemplate are immutable.
An existing release upgraded carelessly would either fail or orphan its PVCs.
Two things address that:

- Templates no longer derive the container name from .Chart.Name. It comes from
  a helper, so nameOverride can pin it, which means an existing release can be
  upgraded with nameOverride=minio, fullnameOverride=<existing-fullname> and
  serviceAccount.name=minio-sa and render byte-stable identity while switching
  chart and image.

- helm-migration-guard and verify-helm-migration.sh make that a gate rather
  than a documented hope. The script lints the chart, renders it in distributed
  and standalone modes plus the optional templates, then renders the legacy
  chart from a pinned commit and the new chart with those three overrides and
  compares resource identity. The guard additionally rejects any rendered
  container still pulling pgsty/minio or invoking /usr/bin/minio. It runs
  through a pinned alpine/helm image when helm is not installed locally, so the
  gate does not depend on the developer's machine. Currently green over 7
  compared resources.

Rollback is asymmetric and the README says so: the old chart with the new image
survives via the entrypoint argv shim, but the new chart with an old MinIO
image does not, because `silo server` is not a command that binary knows. Only
`helm rollback` is supported, never an image-only downgrade.

Not addressed here: the default image tag is pgsty/silo:RELEASE.2026-08-04T00-00-00Z,
which does not exist yet. The chart must not be published until the first Silo
image is pushed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 08:48:17 +08:00
Feng Ruohang 30749911bd build(docker): ship the silo binary and translate the legacy argv command
Dockerfile.goreleaser now copies silo to /usr/bin/silo, defaults to
CMD ["silo"], and labels the image as Silo. MINIO_UPDATE_MINISIGN_PUBKEY is
removed from the image environment: with the updater permanently disabled it
was dead weight, and leaving upstream's verification key in a Silo image
implied a trust relationship that does not exist. The MINIO_* runtime
environment variables, ports, volume and health endpoints are unchanged.

The image keeps shipping mcli with an /usr/bin/mc symlink. That is the client,
not a MinIO-branded alias for the server binary, and the Helm post-install job
and existing container scripts call it by name.

docker-entrypoint.sh translates a legacy first argument: `minio server /data`
becomes `silo server /data`, so an existing `command: minio server ...` in
compose or a Pod spec keeps working across the image swap. The translation is
argv-level only - no file named minio is installed, and an explicitly
overridden `entrypoint: /usr/bin/minio` still fails, which is the honest
outcome since that path genuinely no longer exists.

The entrypoint also fixes an unrelated startup hazard it was already carrying:
when the image runs under an arbitrary UID, HOME points at an unreadable /root
and the server probes its default config directory during initialization. It
now falls back to /tmp when HOME is unset, /root, missing or unwritable.

docker-entrypoint_test.sh pins all of it - empty argv, legacy minio, native
silo, bare flags, and an explicit shell command that must stay explicit. It is
wired into make rebrand-guard and into the go.yml, release.yml and
test-release.yml gates, so removing the shim breaks CI rather than breaking
users. Passing locally.

download-static-curl.sh gains checksum verification for the curl it fetches
into the build stage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 08:47:59 +08:00
Feng Ruohang 15ab10833b build: rename the delivery artifacts to silo and complete the package payload
Everything a user installs is renamed, and the package finally installs enough
to be startable on a clean host.

Artifact names
- goreleaser.yml: build id, binary, archive and checksum manifest become silo_*.
  release.github.name stays "minio" with a comment - the GitHub repository has
  not been renamed yet, and pointing at pgsty/silo before the rename would 404.
  Also adds per-archive SPDX SBOMs and a keyless cosign signature over the
  checksum manifest, so the signed manifest covers archives and SBOMs together.
- nfpm.yml: package name silo, and the binary moves from /usr/local/bin/minio
  to /usr/bin/silo. /usr/local is not on the default PATH of a systemd unit and
  is not FHS-correct for a distribution package.
- package-release.sh, sign-release-rpms.sh and verify-build-provenance.sh follow
  the new names; the RPM signing script asserts NAME=silo and the new four-file
  payload. nfpm is now invoked from the repository root so relative script paths
  in the config resolve regardless of the caller's directory.

Package relationships are deliberately empty
No Provides, Obsoletes, Replaces or package-level Conflicts. Obsoletes: minio
cannot distinguish a pgsty package from upstream's own identically named one,
so an unattended dnf upgrade could silently swap a different vendor's product
for this one. With no relationships, both packages coexist, their file sets do
not overlap, and migration and rollback are single explicit commands. The
mutual exclusion lives in the unit instead: silo.service carries
Conflicts=minio.service plus After=minio.service.

Payload, from two files to four
- /usr/bin/silo
- /usr/lib/systemd/system/silo.service
- /etc/default/silo, installed config|noreplace
- /usr/lib/sysusers.d/silo.conf

The old package shipped a unit referencing an account nothing created, so a
clean install could not start. postinstall.sh now creates the silo system
account through systemd-sysusers, useradd or BusyBox adduser in that order and
runs daemon-reload. It never stops a service, never chowns data and never
touches /etc/default/minio. preremove.sh disables silo.service only on a real
removal - Debian "remove", RPM 0, Alpine's dotted version - so upgrades leave
the running service alone. lifecycle_test.sh exercises both against a stubbed
PATH, so a green run cannot create an account or touch the host.

silo.service reads /etc/default/minio then /etc/default/silo, in that order, so
an existing node's MINIO_* values keep working and the new file overrides them.
The packaged silo.env therefore ships comments only: any active assignment
would shadow the legacy file with an empty value.

Makefile: build/install/install-race produce ./silo, and the docker target now
assembles a context from a locally built linux binary plus Dockerfile.goreleaser
instead of the deleted Dockerfile. The hotfix, hotfix-push, docker-hotfix and
docker-hotfix-push targets are gone - they downloaded upstream's pkger, signed
with upstream's minisign key and scp'd to dl-N.minio.io. verifiers now depends
on a new rebrand-guard target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 08:47:44 +08:00
Feng Ruohang 77bdc4c0cd feat(server): present Silo identity and close the inherited upstream services
Two coupled changes that must land together, because the same files carry both:
the server now identifies itself as Silo, and every path that would have called
home to a MinIO-operated service is closed.

Product identity
- build-constants.go: store name, UA name and startup banner become Silo. The
  Go identifiers (MinioStoreName, MinioBannerName, ...) keep their names on
  purpose - renaming exported symbols would churn the compatibility surface for
  a cosmetic gain, and the rebrand guard freezes that surface.
- main.go, server-startup-msg.go, ftp-server.go and the user-visible log, help
  and error strings across cmd/ and internal/ switch to Silo. Original MinIO
  copyright, LICENSE, NOTICE and CREDITS are untouched; --version now prints
  the upstream copyright, the pgsty modification notice, and the trademark
  policy's approved "based on MinIO technology" attribution.
- api-headers.go: the HTTP Server header becomes "Silo". This is the one
  externally observable identity change, so TestCommonHeadersUseSiloProductName
  pins it - probes that sniff for "MinIO" must move to capability detection.
- Prometheus metric HELP strings keep their MinIO wording. They are part of the
  metrics contract the guard protects, not product copy.

Configuration directory
- config-dir.go: new installs use ~/.silo. If only ~/.minio exists it is still
  read, with a one-time notice and no files moved. If both exist ~/.silo wins
  and an ambiguity warning is emitted; an explicit --config-dir always wins.
  Covered by TestSelectDefaultConfigDir. The internal .minio.sys layout is
  never renamed - this rule applies to the user config directory only.

Upstream service lockdown
- globalInplaceUpdateDisabled is now true at initialization rather than being
  set from MINIO_UPDATE. common-main.go still parses MINIO_UPDATE so upgrading
  nodes do not fail on an unknown key, but warns that the value is ignored;
  there is no way to re-enable the updater. TestInplaceUpdateCannotBeEnabled
  guards that. Without this, an admin with mc could have overwritten
  /usr/bin/silo with an upstream MinIO binary.
- verifyBinary and commitBinary refuse early; the ServerUpdate v1/v2 admin
  routes and the peer-rest update endpoints stay registered and keep returning
  the existing programmatic error, so clients see a stable failure rather than
  a 404.
- MinioReleaseBaseURL and defaultMinisignPubkey are emptied: no dl.min.io
  download root, and upstream's minisign key is no longer a trust root for
  anything this fork ships.
- cmd/callhome.go is deleted and internal/config/subnet/ is reduced to parsing
  its old keys and reporting that the integration is disabled. config-current.go
  warns instead of failing when callhome or SUBNET settings are present, so an
  upgraded node with those keys still starts.
- internal/config/errors.go replaces the MinIO Slack and support entry points
  with Silo documentation and issue links. Error codes and programmatic fields
  are unchanged.

Verified: the compatibility baseline is unchanged except for the deliberate
removal of the /api/health/upload SUBNET route; go build, go vet and the full
cmd/ and internal/ unit suites pass; a locally built binary starts, serves
S3/Admin/metrics on the unchanged /minio/* routes, answers with Server: Silo,
and falls back to a pre-existing ~/.minio with the expected notice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 08:46:58 +08:00
Feng Ruohang 15def34dce chore: drop the unpublished upstream delivery and branding residue
The tree still carried a complete second publishing pipeline inherited from
upstream that pgsty has never operated, plus MinIO-branded artwork the fork has
no right to redistribute. None of it is a pgsty release artifact, so the
history-is-immutable rule does not protect it: nothing here was ever published
under this repository's name, and git history plus the pre-cutover tag keep the
provenance.

Removed, by category:

- charts.min.io machinery: CNAME (charts.min.io), _config.yml, index.yaml,
  helm-reindex.sh and 90 helm-releases/minio-*.tgz. GitHub Pages is not enabled
  on this repository, so this was dormant - but enabling Pages for any reason
  would have stood up a convincing impostor of the official MinIO chart
  repository from our own domain.

- Dockerfiles that consume upstream binaries: Dockerfile, Dockerfile.cicd,
  Dockerfile.hotfix, Dockerfile.release, Dockerfile.release.old_cpu,
  Dockerfile.scratch and docker-buildx.sh. These fetch or expect a MinIO-built
  binary; a Silo image must only ever contain an artifact this repository built
  from this commit. Dockerfile.goreleaser, the one the release workflow
  actually uses, stays.

- buildscripts/upgrade-tests/ (compose.yml, minio.env, nginx.conf), which
  pinned minio/minio images through a MINIO_VERSION compose variable. The
  upgrade test is rebuilt on image digests in a later commit.

- docs/hotfixes.md, describing an upstream hotfix distribution channel that
  does not exist for this fork.

- MinIO trademark artwork: .github/logo.svg (a recoloured MinIO wordmark), the
  docs/screenshots/ set, the Grafana dashboard screenshots and
  docs/federation/lookup/bucket-lookup.png. The dashboards themselves are kept
  and rebranded separately; only the images carrying MinIO chrome go.

Deliberately not touched: published releases, tags, image digests and package
signatures remain byte-identical. This commit only removes paths that were
never part of a pgsty release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 08:46:31 +08:00
Feng Ruohang d88f46ccee ci: build the release runtime image and assert graceful shutdown
The release image (Dockerfile.goreleaser) was only ever built by
docker-release.yml, which is workflow_dispatch only - so the runtime layer and
entrypoint were never exercised by CI until an actual publish, where a broken
COPY path or a signal-handling regression would surface at the worst possible
moment. MC already covers this in its test-release; the server did not.

Add an offline, deterministic smoke: assemble a minimal image from the
linux/amd64 binary goreleaser already built and the real entrypoint, then start
the server and `docker stop` it on both the default and the MINIO_USERNAME
drop-privilege paths, asserting PID 1 is minio, the exit is 0, the shutdown is
sub-timeout, and the "Exiting on signal" log is present. The mcli-download build
stage is skipped deliberately - it hits the GitHub API and would make this gate
flaky and non-reproducible; its simple, checksum-guarded logic still runs at
publish time.

This is the regression guard for the exec-into-chroot entrypoint fix. Also add
dockerscripts/docker-entrypoint.sh to the pull_request paths so an entrypoint
change actually triggers this pipeline. Verified locally end to end: the fixed
entrypoint passes both paths; reverting the exec makes the drop-privilege path
time out to exit 137 and the step fails.

Co-authored-by: Claude <noreply@anthropic.com>
RELEASE.2026-08-04T00-00-00Z
2026-08-05 06:29:22 +08:00
Feng Ruohang 021110b451 ci: make the lint, getdeps, and release gates fail honestly
Three gate-correctness fixes:

- lint: `command typos && typos ./ || echo skipping` ran typos twice (POSIX
  `command` executes it) and, via `&& ... || echo`, turned a real typo finding
  (nonzero exit) into the "not installed" message with exit 0 - so spelling
  issues could never fail the gate. Use `command -v` to test presence and run
  typos once, letting its findings surface.

- getdeps: `curl -sSfL ... | sh` took the pipeline's exit from sh, and make's
  /bin/sh has no pipefail, so a failed or partial download of the golangci-lint
  installer was reported as success and a truncated script could run. Download
  to a temp file under `set -e` (with a trap to clean it up) and execute that,
  so a curl failure aborts the target. Verified: a 404 now exits nonzero
  instead of silently continuing.

- release: workflow_dispatch checked out github.ref (the branch it ran from)
  while naming artifacts after the input tag, so a release could be built from
  one ref and published under another. Pin checkout to the requested tag.

Not addressed here: the server's release container image is still only built
by the publish-time docker-release workflow, never in CI (MC covers this in
its test-release). Flagged for a separate decision.

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-05 01:06:12 +08:00
Feng Ruohang aa51393694 build: install the systemd unit under /usr/lib, drop the template residue
nFPM placed minio.service at /lib/systemd/system. On merged-usr EL/Fedora the
file lands correctly through the symlink, but the RPM database records /lib/...,
so `rpm -qf` disowns it and - more importantly - systemd's
%transfiletriggerin watches /usr/lib/systemd/system, so the path recorded as
/lib/... never fires the automatic `daemon-reload` on install or removal. Move
the unit to the canonical /usr/lib/systemd/system, which both restores that
trigger and makes the package own the path it ships.

The destination is asserted verbatim in several places, all updated in lockstep
so the packaging gate still passes: the six name/payload/sha checks in
test-release.yml (rpm/deb/apk) and expected_payload in sign-release-rpms.sh.
Verified by building a real deb with the new config: the unit is at
./usr/lib/systemd/system/minio.service.

Also drop the `# Built for ${project.name}...` line from minio.service. nFPM
expands variables in content src paths, not file bodies, so that pkger-era
placeholder was being written verbatim into every installed unit.

Not addressed here, deliberately: the package still does not create the
minio-user account the unit references. That matches upstream MinIO's own
packages (both rely on the documented manual useradd) and is not a fork
regression, so it stays a documentation step rather than a scriptlet.

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-05 01:03:43 +08:00
Feng Ruohang e064b5555f build: remove the untracked shadow .goreleaser.yml
A gitignored .goreleaser.yml sat in the repo root, so a bare `goreleaser`
invocation - the default discovery path - picked it up instead of the
.github/goreleaser.yml that CI passes with --config. The two had diverged
completely: the shadow published the GitHub release directly (draft: false),
pushed pgsty/minio:latest from a dockers section, and stamped a different
vendor and artifact naming. Since RPM signing runs on the maintainer's own
machine, that shadow config was one stray `goreleaser release` away from
publishing a mislabelled release and overwriting the Docker latest tag.

Delete the file and drop its .gitignore entry so any future .goreleaser.yml
shows up as untracked and is caught by the release workflow's clean-checkout
check, rather than silently steering local builds. CI is unaffected: it names
its config explicitly. (A copy of the removed file is preserved out of tree
for reference.)

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-05 01:00:52 +08:00
Feng Ruohang 2ca4971d91 build: stop stamping the build machine's GOPATH/GOROOT into the binary
gen-ldflags injected -X cmd.GOPATH / cmd.GOROOT from the builder's environment,
baking absolute paths like /Users/<user>/go into every released binary. That
defeats -trimpath and makes the build unreproducible: a third party rebuilding
the same tag gets different bytes and cannot verify checksums.txt.

The values only seed logger.Init's source-path trim list, and under -trimpath
the binary's paths are already relative, so there is no build-machine prefix
left to trim - the trim list also still gets runtime.GOROOT() and
build.Default.GOPATH at run time. Dropping the two stamps changes no observable
logging behaviour; cmd.GOPATH/GOROOT keep the empty defaults a plain go build
leaves.

Verified: gen-ldflags output no longer contains cmd.GOPATH/GOROOT; a
-trimpath release build has zero occurrences of the builder path (was 1);
Version, ReleaseTag and CommitID stamps are intact.

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-05 00:59:06 +08:00
Feng Ruohang 4c185d5a66 ci: stop interpolating the dispatch tag into the release script
The compute step spliced ${{ github.event.inputs.tag }} straight into the
run: body, so a dispatch tag containing shell metacharacters was parsed as
script - arbitrary code in a job holding a contents:write token. Pass the
input through the environment instead (INPUT_TAG), the same shape
docker-release.yml already uses, so the value reaches bash as data.

Second vector: the old format check compared the sed output against the input
to decide validity, and sed anchors ^...$ per line. A tag carrying a newline
passed the check on its first line and the remaining lines flowed into
$GITHUB_ENV, setting arbitrary variables (PATH, LD_PRELOAD, ...) for every
later step. Replace it with a bash =~ whitelist anchored to the whole string,
which rejects any multi-line value up front.

Verified: a legal RELEASE.* tag is accepted and yields the right PKG_VERSION;
a `"; touch ...; #` injection and a newline-carrying PATH/LD_PRELOAD payload
are both rejected; an empty input falls back to the git ref name.

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-05 00:57:10 +08:00
Feng Ruohang ca674a6967 build: cross-compile only the targets we publish
The crosscompile gate built 15 OS/arch combinations, ten of which we never
ship (ppc64le, mips64, s390x, mips, riscv64, 386, arm, freebsd, netbsd,
openbsd). On a cold CI cache each target costs ~80s, so the full set ran ~21
min and tripped the job's 20-min timeout, cancelling the only CI run for the
release HEAD. Trim the list to the exact goos/goarch matrix the release
actually produces (see .github/goreleaser.yml): linux, darwin, windows on
amd64 and arm64.

This also closes a coverage gap: windows/arm64 is published but was not being
compile-checked. The trimmed set builds all six in well under the timeout.

Note: netbsd is no longer compile-checked here, so the go-systemd v22.6.0 pin
(kept because v22.7.0 does not build on netbsd) loses its CI guard. The pin is
retained deliberately - it is harmless on the platforms we ship and upgrading
go-systemd is a separate decision - but a netbsd regression in a dependency
would now surface only if that build is exercised out of band.

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-05 00:55:31 +08:00
Feng Ruohang 3b8a55deef fix: exec into the dropped-privilege process so signals reach MinIO
The two chroot branches that drop privileges when MINIO_USERNAME/GROUPNAME
(and optionally MINIO_UID/GID) are set ran chroot as a child of the entry
shell, leaving the shell as PID 1. A SIGTERM from `docker stop` or an
orchestrator then went to the shell, which does not forward it, so MinIO was
never asked to shut down and was killed after the stop timeout (exit 137) with
no "Exiting on signal" log - risking in-flight requests and data at the flush
boundary. The default branch already exec's; these two now do too, so MinIO
runs as PID 1 and receives the signal directly.

Verified in a faithful reproduction of the release runtime layer: all three
paths (default, USERNAME only, USERNAME+UID/GID) now stop in ~0.2s with exit 0
and log the graceful shutdown, where the two drop-privilege paths previously
timed out to exit 137.

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-05 00:53:25 +08:00
Feng Ruohang 11d79fddc3 ci: gate build, vet, tests, lint, generation, race, and crosscompile
Make the repository's actual quality contract visible in CI. Check formatting, build and vet the tree, run cmd and internal tests, enforce pinned lint and generated-file cleanliness, exercise S3 Select under the race detector, and cross-compile every supported target.

Keep each concern in a separate job so failures identify the missing guarantee instead of hiding behind a single aggregate test result.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 23:02:16 +08:00
Feng Ruohang 475236c79c test(s3select): stop racing minio-go's parser for the response body
SelectResults spawns a parser goroutine that drains and closes the
response body when the stream ends; deferring res.Close() had the test
drain and close the same bytes.Reader concurrently, which the race
detector catches reliably. Give the test body a close signal and wait
for the parser to finish instead of competing with it.

The double-close lives in minio-go's client parser, which no server
code path uses; it remains worth an upstream report.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 23:02:16 +08:00
Feng Ruohang 1814ae52f4 build: regenerate and verify all generated outputs
Expand the generated-file gate beyond *_gen.go and go.sum to cover generated tests, msgp output, stringer files, go.mod, and untracked generated artifacts.

Regenerate the 19 stale stringer outputs with the go.mod-pinned x/tools version. Runtime String output is unchanged, and a repeated generation pass now leaves the tree byte-identical.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 23:02:16 +08:00
Feng Ruohang 632ade111b build: pin golangci-lint installation
Installing golangci-lint from the moving master branch made local and CI results depend on the day they ran. Pin v2.11.3 and install it into a versioned path so version changes cannot accidentally reuse a stale binary.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 23:02:16 +08:00
Feng Ruohang 32863c8523 ci: publish container images from a published release, on demand
The release workflow built and pushed the container images in the same
run that created the draft, so pgsty/minio:latest moved to a build
nobody had signed or published yet. Abandoning that draft left latest
pointing at it with no way to notice, and the images were the one
artifact of a release that escaped the draft gate entirely.

Image publishing moves to its own dispatch-triggered workflow that
takes a tag and refuses to touch anything that is not a published,
non-prerelease release, and that is not the latest one - since it moves
the latest image tag, running it for an older release would silently
roll users back. It builds from the archives attached to that release,
checked against the published checksums, rather than rebuilding from
source: the image then contains the same binary the tarball does, by
construction rather than by assumption.

GoReleaser loses its dockers and docker_manifests sections along with
the QEMU, buildx and registry-login steps that only existed to serve
them, and the release job drops packages: write, which was granted for
a GHCR push that never happened.

Releasing now has one more manual step. That is the point: tagging no
longer moves docker latest, so the tag can be cut, inspected, signed
and published before anything reaches users who pull by tag.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 23:02:16 +08:00
Feng Ruohang cf7df097b2 ci: verify release provenance, names, checksums, and payloads
Refuse dirty release checkouts and verify that every GoReleaser binary records the tagged revision with vcs.modified=false before packaging or publication.

Exercise the complete nFPM output in the release test pipeline: assert the six public names, validate every checksum and package identity field, and prove that RPM, DEB, and APK payloads contain the exact source binary and systemd unit. Validate release scripts and make their identity expectations the single source of truth.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 23:02:16 +08:00
Feng Ruohang 10c7670b80 build: package releases with nFPM under the PGSTY identity
Replace minio/pkger's hard-coded upstream identity with an in-tree nFPM configuration. Packages now name PGSTY as vendor and maintainer, use the SILO homepage and SPDX license, and preserve the established package names, versions, payload paths, modes, and checksum format.

Resolve both the release binary and systemd unit independently of the caller's working directory. The release and test workflows share the same package script, while the signing script consumes the same final metadata contract.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 23:02:16 +08:00
Feng Ruohang 9c799f42d5 build: stage draft releases for local RPM signing
GitHub Actions cannot hold the Pigsty RPM signing key, so the release workflow must stop before publication. Create releases as append-only drafts, refuse to overwrite existing assets, and make publication an explicit action after local signing and review.

Add a maintainer-side signing command that downloads the two RPMs, verifies checksums and package identity, signs them with the Pigsty key, regenerates checksums, and only replaces draft assets when --upload is explicitly requested. Published releases are never modified.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 23:02:16 +08:00
Feng Ruohang 8eae745ab2 docs(security): record the 2026-08-04 hardening advisories
Consolidate the release-cycle security ledger after the implementation commits are stable. Document internode containment, policy-condition source hardening, the opt-in trusted-proxy boundary, and the bucket/object authorization tightening with fork-local identifiers where no CVE exists.

Use silo-pkg v3.11.0 as the maintained dependency reference, explain compatibility and migration behavior, and link each entry to the rewritten commit that actually carries the fix.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 23:02:16 +08:00
Feng Ruohang b42ee4e8ac chore: ignore maintainer-only workspace files
Keep unpublished security working notes, local agent instructions, and maintainer scratch material out of the public source tree. Already tracked security documentation remains tracked; the ignore rules only prevent accidental additions.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 23:00:31 +08:00
Feng Ruohang 5f4513fd40 docs: migrate Silo resources to unified portal
Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 23:00:31 +08:00
Feng Ruohang dfe6698627 helm: harden defaults for RELEASE.2026-08-04
Point both the server and bundled client jobs at the release tag being cut, keep chart appVersion aligned, and retain the maintained pgsty/minio image selected by the public chart baseline.

Stop creating the console/console123 administrator by default. Leave an explicit change-me example while rendering no user-creation job unless an operator opts in. Chart packaging and repository indexing remain separate release steps.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 23:00:31 +08:00
Feng Ruohang 2602177ef6 fix: guard the ReadParts trace path against an empty part list
ReadParts built its disk-health trace path from partMetaPaths[0] with
no length check, so a caller passing no paths at all indexed an empty
slice. xlStorage.ReadParts handles an empty list perfectly well - it
returns an empty result - so the panic came entirely from the metrics
bookkeeping wrapped around it.

The reachable caller is ReadPartsHandler, which decodes its path list
from a msgpack request body that an authenticated peer controls, and
neither the handler, the storage-REST client, nor the path guard
rejects an empty one: guardPaths ranges over the slice, so an empty
slice passes vacuously.

net/http recovers a panicking handler, so this is not a crash - which
is what makes it worth fixing rather than merely tidy. ReadPartsHandler
calls keepHTTPResponseAlive before it calls ReadParts, and that helper
spawns a goroutine whose only exit is receiving from the channel done()
writes. Panicking in between skips both done(err) and done(nil), so the
process survives and the keep-alive goroutine and its ten-second ticker
stay parked forever - one per request, driven by a request body the
caller chooses. Repeating one malformed frame exhausts the node.

The fix follows DeleteVersions in the same decorator, which already
guards the identical "merely for tracing" lookup; ReadParts was the one
method missing the pattern. It also covers the second entry point, the
per-disk errgroup in readParts, where a panic has no recover at all and
would take the process down. That path is screened at the S3 boundary
today, so this is defence in depth there.

No error is returned for the empty case. The storage layer's answer to
an empty list is an empty result, and turning that into an error would
be a behaviour change on a path that is merely degenerate.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 23:00:31 +08:00
Feng Ruohang 9dd1dc172d fix: restore the merrs tag on dangling-object deletion audit records
joinErrs ranged over its own empty accumulator string instead of the
errs slice, so the loop body never executed and the function
unconditionally returned "". Its only caller feeds the merrs tag of the
DeleteDanglingObject audit event, so every dangling deletion was
recorded without the per-drive metadata errors: the record showed what
was deleted but not which drives errored or why quorum was lost.

Range over errs instead. The existing separator logic is already right
once the loop runs, since a leading nil error appends "<nil>" and every
later element gets its comma. Upstream's open minio/minio#21580 fixes
the same bug with a strings.Builder rewrite, not taken here: the
function runs once per dangling deletion over a drive-count-sized
slice, and the one-word change is the entire defect.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 23:00:31 +08:00
Feng Ruohang 0c14d81510 fix(notify): quote libpq connection parameters
The discrete PostgreSQL connection path concatenated raw values into a libpq keyword/value string. Whitespace, quotes, or backslashes could split a value into additional parameters or make an otherwise valid configuration fail to parse; the path also used the unsupported keyword username instead of user.

Render every generated value as a single-quoted libpq parameter, escape quotes and backslashes, and use the correct user key. Keep the existing connection_string form untouched. The earlier attempt to register migrated PostgreSQL and MySQL fields is deliberately absent because those key names collide with the legacy connection-string tokenizer.

Focused tests cover ordinary values, whitespace, quotes, backslashes, and parameter-shaped input.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 23:00:31 +08:00
Feng Ruohang 162ded3438 fix: register NATS/AMQP notify config keys read by parsers
GetNotifyNATS reads user_credentials, nkey_seed and tls_handshake_first
and GetNotifyAMQP reads immediate, but none of them were registered in
DefaultNATSKVS/DefaultAMQPKVS or the help schema, so CheckValidKeys
rejected any enable=on target carrying them. Worse, the legacy config
migration wrote exactly these keys - including the env var name
MINIO_NOTIFY_NATS_USER_CREDENTIALS used as a config key, because the
NATSUserCredentials constant doubled as both - so a migrated NATS config
failed validation on every load, and the FetchEnabledTargets fail-fast
then silently disabled all bucket notification targets.

- Register user_credentials/nkey_seed/tls_handshake_first (NATS) and
  immediate (AMQP) in the default KVS and help schema; split
  NATSUserCredentials into a real config key plus EnvNATSUserCredentials
  (all env var names byte-stable)
- Fix legacy migration: SetNotifyNATS writes the proper key;
  SetNotifyAMQP no longer writes cfg.Immediate under the internal key
  and now carries both immediate and internal
- Tolerate the legacy MINIO_NOTIFY_NATS_USER_CREDENTIALS key written by
  pre-fix migrations (NATS-scoped, load path only) with fallback read;
  env > user_credentials > legacy key
- Print key names only, never values, in the invalid-keys error of both
  CheckValidKeys forms; rejected values can carry credentials
- Add an AST-based audit test asserting parser reads, migration writes
  and help entries stay within the registered key set for all ten notify
  subsystems, with floor assertions so collector drift fails loudly
- Document (unchanged) FetchEnabledTargets fail-fast and pin it with a
  characterization test

Known same-class gap left in place and pinned by the audit's allowlist:
SetNotifyPostgres/SetNotifyMySQL write five unregistered DSN-era keys;
tracked for a follow-up issue.

Closes #39

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 23:00:31 +08:00
Feng Ruohang fe6dc47804 feat: add a trusted-proxy boundary for the client source address
The address MinIO attributes a request to is read from X-Forwarded-For,
X-Real-IP or RFC 7239 Forwarded, and never from the connection unless all
three are absent. It becomes aws:SourceIp and the audit remotehost field,
so any client that can reach the API port chooses the value an IpAddress
condition is evaluated against and the address every logged action is
attributed to.

MINIO_API_TRUSTED_PROXIES now selects who may make that claim:

  unset     the historical behaviour, unchanged
  none      no forwarded header is believed; the TCP peer wins
  <CIDRs>   believed only from listed peers, chains read right-to-left

Reading right-to-left is what makes an appending proxy safe: each hop
appends the peer it actually saw, so an entry a client injected can only
sit to the left of one a proxy wrote. The stock nginx recipe
$proxy_add_x_forwarded_for appends, which leaves the client's entry
left-most - exactly where the untrusted path reads - so a deployment with
no direct route to the API port was forgeable too.

_MINIO_API_XFF_HEADER is deliberately untouched, in semantics and in read
timing. Widening it to mean "trust nothing" was implemented and reverted:
it is the only part of this change that could alter a deployed
configuration, and the new variable expresses the same guarantee at no
compatibility cost. Upstream's TestXFFDisabled is retained verbatim.

Notes on the allow-list mode, all covered by tests:

  - it must name proxies, not the subnet they sit in; listed entries are
    skipped while walking, so a range covering clients lets them forge
  - a cluster must list its own nodes, because MinIO forwards between
    them and a client can force a hop via the ListObjectsV2 token
  - loopback is trusted as a peer, not as a chain entry, so FTP and SFTP
    keep attributing their sessions
  - the node-to-node forwarder drops X-Real-IP and Forwarded from a peer
    not entitled to have set them
  - the walk scans the header in place and stops after 100 hops, so a
    long chain costs neither allocation nor unbounded work

No behaviour change for any deployment that does not set the new
variable: the untrusted path is a verbatim copy of the previous function
body, differentially verified against it over ~5.1M header combinations.
The LDAP STS allow-list now shares the list parser as pure code motion,
verified identical across every combination of 37 allow-list values and
21 peer addresses.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 23:00:31 +08:00
Feng Ruohang 744a9dcd71 fix: bind s3:versionid conditions to the effective object version
A bucket policy that allows s3:DeleteObject only when s3:versionid is null
-- Condition {"Null": {"s3:versionid": "true"}}, the idiom for "let clients
delete current objects but not roll back versions" -- denied every delete,
including the version-less ones it was meant to permit (upstream issue
minio/minio#21735).

getConditionValues wrote "versionid": {""} unconditionally. The condition
engine decides Null by slice length (nullfunc.evaluate), so a present-but-
empty value reads as "key present": Null:true never matched and Null:false
always did. Absent and empty were indistinguishable.

Writing the key only when the request names a version fixes the reported
case but, alone, opens a worse one. DeleteObjects carries each object's
version in the XML body, which getConditionValues -- reading only r.Form --
never sees. A body version would then vanish from the map, read as null,
and a policy meant to protect old versions would authorize deleting a
specific one. So authorization also rebinds versionid to the effective,
server-resolved reqInfo.VersionID for DeleteObjectAction: the per-entry
body value that checkRequestAuthTypeWithVID already sets in the
DeleteObjects loop, deleting the key when that value is empty. A
query-level ?versionId on a DeleteObjects POST no longer leaks into any
entry's decision.

Finally, trim the version the condition builder reads. newContext and
getOpts both TrimSpace it before the object layer acts, so an untrimmed
value here let a padded ?versionId=V%20 present a different s3:versionid
than the version actually operated on, sidestepping a Deny keyed on
StringEquals s3:versionid. DeleteObjectAction was already immune via the
trimmed reqInfo value; this covers GetObject, tagging, retention, and the
copy-source read.

Tests: an end-to-end DeleteObjects against a Null:{s3:versionid:true}
policy over versioned objects (with a decoy query versionId proving the
per-entry body value wins), and a unit test asserting key presence,
trimming, and the copy-source fallback.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 23:00:31 +08:00