Skip to content

This is the multi-page printable view of this section. .

Return to the regular view of this page.

Compatibility

Where Silo matches MinIO and where it deliberately differs — server, client, and console.

Current Console: v2.4.1, with mcli 20260916 and pkg v3.14.1. See the component matrix and password-policy migration.

Silo is a community fork of MinIO. This section records what Silo keeps from MinIO, where it deliberately differs, and what either means when moving between them.

Contents, in reading order: the migration guide (scope and Docker), native package migration (RPM/DEB), the server compatibility audit, the mcli client notes, and the console notes. The Feature Notes subsection covers the opposite direction: design records for capabilities Silo adds beyond upstream.

1 - Component Versions

Published SILO releases, source fixes verified on September 16, and coordinated upgrade requirements.

Verified on 2026-09-16. SILO releases its four components independently. A merged dependency update does not change an existing binary or image.

Published components

Component Latest published version What it contains
Server 20260903 pkg v3.13.2; upstream SDK 0e78d3f18efe; mcli 20260903; embedded Console source 464a59d73ada with v2.3.0 version identity
Standalone
Console
v2.4.1 pkg v3.14.1; mcli 20260916; upstream SDK 32e1f32cb176; restricted shared downloads, streaming ZIPs and signed artifacts
mcli 20260916 pkg v3.14.1; upstream SDK 32e1f32cb176; package version 20260916000000.0.0
Shared pkg v3.14.1 Own module path github.com/pgsty/silo-pkg/v3; CopyObject embedded-error handling; JWX v3.3.0 field-name escaping; upstream SDK 32e1f32cb176

Release notes: Server 20260903, Console v2.4.1, mcli 20260916, pkg v3.14.1. For embedded deployments, select Console and MC explicitly in the Server build. Package-repository mirrors may lag GitHub; the download page links directly to the published artifacts.

September 16 dependency graph

Console v2.4.1 selects the released pkg and MC sources below. These identities are verified through the public Go proxy and checksum database.

  • Console: v0.0.0-20260916075814-1360e26d976d1360e26d976d, tag v2.4.1.
  • pkg: v3.14.1fa657ef431ae22e720df37e5144cf00f67102945.
  • MC: v0.0.0-20260916070421-e952aa78f10ae952aa78f10a, tag RELEASE.2026-09-16T00-00-00Z.
  • Upstream minio-go: v7.3.1-0.20260915093545-32e1f32cb176.
  • JWX / strfmt / React Router: v3.3.0 / v0.27.2 / v7.18.4.

Console’s embedded frontend is rebuilt from this graph. Its Go dependency uses the canonical pseudo-version because the historical module path has no /v2 suffix. Server embedders must copy both Console and MC replacements; see the Console integration notes.

Review the password-permission migration before upgrading: admin:ChangeMyPassword and admin:CreateUser now express separate operations. The Server changelog and source links below describe the storage, IAM and HTTP changes needed for coordinated Server upgrades.

Storage, IAM and HTTP repairs

The following changes have merged into Server main. They remain absent from the published Server 20260903; use the linked PRs and source records to identify a build containing them.

Area Merged PRs Operator-visible behavior
Multi-pool storage #188
#189
Ordinary single-object version DELETE reconciles copies across pools; reconciliation preserves tag state. The opt-in GET-frequency pool-tiering feature was removed.
Conditional multipart completion #190 Preconditions use the logical current object across all pools, preventing an older pool copy from accepting a stale ETag or rejecting the current one.
Multipart discovery and cancellation #198 Default legacy listing and cancellation retain released behavior. Opt-in strict mode discovers durable uploads across pools/sets, continues after marker uploads disappear, and requires majority cancellation acknowledgements (#213). Strict mode requires a coordinated writer upgrade and legacy drain; see the upgrade contract.
Ordinary conditional PUT #207 Public write conditions use the logical current object across all pools, including draining pools. Readability and destination-version changes are detailed below.
IAM revocations #191
#192
Peer deletion notifications reload committed state. Durable deletion versions and retained revocation boundaries prevent stale site replay from restoring revoked identities or their older grants.
Replicated tags and delete markers #193
#196
SSE-KMS copies preserve tag revision times; tag deletion advances its revision and resists delayed events. Delete-marker purges retain their identity and retry state through MRF recovery.
Replica metadata #194 Restoring replication metadata no longer reintroduces the transport-only aws-chunked encoding into stored object metadata.
Request-header timeout #196 --read-header-timeout / MINIO_READ_HEADER_TIMEOUT reaches the HTTP server and imposes an absolute HTTP/1 header-reading deadline, even while bytes keep arriving. HTTP/1 bodies retain the existing rolling idle timeout.

Upgrade and compatibility requirements:

  • IAM requires a coordinated upgrade of all participating servers. Mixed old/new nodes sharing an IAM backend and rolling downgrade are unsupported. Back up complete IAM storage and required encryption material; a live admin export omits deletion history. Older credentials for a recreated parent may need reissuance. Pre-upgrade deletions whose history is already lost cannot be reconstructed automatically. Follow the IAM upgrade and rollback guide.
  • Unavailable pools can now fail writes and deletes more consistently. Conditional multipart completion fails if any pool’s metadata is unreadable, even when GET/HEAD can use another pool. Ordinary version DELETE also fails on unreadable pools or cleanup errors; insufficient read quorum returns 503 SlowDownRead. Retry after recovery. Successful deletion does not promise immediate removal from every drive while outbound delete replication is pending.
  • Access-frequency pool tiering was never in Server 20260903. Only builds containing the experimental feature need its configuration/XML migration. Ordinary lifecycle expiration, remote-tier transitions, rebalance and decommission remain available.
  • Purge audit status is normalized from COMPLETE to COMPLETED. Malformed historical tag revisions can fail and retry; this repair does not reconstruct their history. A shorter header timeout also constrains TLS handshake reads; this is not a new total-duration limit for HTTP/1 uploads or downloads.
  • Objects with recorded tag revisions take one extra metadata COPY per object during explicit resync or heal; when the destination is KMS-encrypted by bucket default, that copy rewrites the object data. Replication rules with tag filters still evaluate target eligibility against the post-deletion (empty) tagging state, and arbitrary site clock skew remains outside the repaired ordering guarantees. Both endpoints of a replication pair must run the repaired build for tombstones to be honored; an old peer still drops empty-value revisions. See Replicated Tag Ordering.

The R4–R8 integration record contains source hashes, local tests and remaining acceptance limits. PR #196’s 11 checks passed before merge. Those results establish source acceptance, not a new release or production cluster rollout.

Console shared downloads

Console #56 and Server #209 resolve the anonymous proxy boundary reported in Console #52. The proxy only accepts object-content GETs at the configured S3 origin and rejects redirects, system paths and query-selected non-download operations. No sharing-disable environment variable was added. Normal public, presigned and versioned downloads remain supported; see the behavior and design tradeoffs.

The fix is released in Console v2.4.1. The exact release source passed the complete CI matrix, vulnerability checks and release workflow. Real API and browser sharing tests passed in standalone and embedded deployments; the downloaded standalone binary also passed the sharing regression against a SILO fixture.

Ordinary conditional PUT

The separate cross-pool conditional PUT defect in #199 was reproduced on published Server 20260903. PR #207 was merged as 9b4ae82a29cc after its head 4620be394b52 passed all eight CI checks on 2026-09-16. The repair is on main and remains unreleased. Its behavior is:

  • Ordinary multi-pool If-Match / If-None-Match conditions use the logical current object across all pools. Unreadable metadata can prevent acceptance even when GET still works from another pool; read-quorum failures return 503. Restore readability or heal before retrying.
  • When a destination versionId is supplied, the public condition still compares the current object. The requested destination version is preserved; internal replication keeps its addressed-version checks. Unconditional PUT and single-pool conditions retain their existing behavior.
  • A successful conditional overwrite does not retire stale copies in other pools, and upgrading cannot recover historical accepted overwrites. Existing modification-time/pool ordering remains in use; this adds no global clock ordering guarantee.

The multipart-completion fix in #190 neither introduced nor repaired this PUT defect. Final packaged-candidate and rollout acceptance remain tracked in #203.

Work still pending

  • Upgrade and historical-state readiness: #200 tracks the IAM upgrade/restore rehearsal; #201 tracks historical replica inventory and repair validation. Source repairs do not automatically repair old state.
  • Release delivery: #202 collects release notes and component identities; #203 separately validates final artifacts and multi-process behavior. No new Server release is established by these tracking issues.
  • Multipart listing: #79 remains open for capacity/release acceptance and the known delayed-creation-write boundary. PR #198 repairs durable discovery, global pagination and static cancellation confirmation; it does not add a creation fence or certify large-scale scanning. The temporary 10,000-upload trial missed the provisional five-second page target; see the design record.

Dependency and release order

  1. Verify the upstream SDK commit and required fixes. Keep the upstream github.com/minio/minio-go/v7 path; the retired silo-go fork is not part of the maintained graph.
  2. Validate and publish pkg under github.com/pgsty/silo-pkg/v3, including its migration notes. Resolve the tag through the Go proxy and checksum database.
  3. Update MC to that pkg and SDK, validate it, then publish the calendar-tagged mcli release. Go consumers select its canonical pseudo-version.
  4. Update Console’s direct pkg requirement and explicit MC replacement, validate its embedded frontend and integration, and publish Console when a release is intended. An accepted immutable source commit can also be selected explicitly.
  5. Update Server’s direct pkg/SDK requirements and both PGSTY replacements, client archive hashes, image and Helm client pins. Validate the complete graph before a separate Server release, image publication and cluster rollout.

Go does not inherit a dependency module’s replace directives. Server must explicitly select both maintained Console and MC even when Console already selects MC. Console and MC retain their historical MinIO module paths; pkg uses its own path directly. Legacy transitive minio/pkg/v3 from colorjson/dperf is separate from the maintained policy implementation.

The main stack uses Go 1.27.1; pkg retains a Go 1.26 library floor and was also race-tested with Go 1.26.8. The Go x/* dependencies were refreshed. The effective go-systemd version stays v22.6.0 because v22.7.0 fails to compile on NetBSD; Console’s tablewriter v0.0.5 replacement preserves its MC API. These are documented compatibility pins, not missed automatic upgrades.

Other dependencies change for concrete CVE/bug fixes, not just newer major versions. The September 13 Go scans found no reachable or imported vulnerable package, but retained module-only GO-2026-5932 in unused OpenPGP code. A clean reachability result is not a claim that every selected module is advisory-free.

The supported integration target is the coordinated PGSTY stack. Compatibility with unmodified upstream MinIO/MC and other S3 implementations is best effort.

2 - Password Permissions

Version boundaries and migration for the ChangeMyPassword and CreateUser policy split.

Applies to Console v2.4.1 with pkg v3.14.1 and the matching SILO Server. The Console button, session capability and Server authorization must use the same action mapping. Follow the coordinated upgrade steps below and the component matrix.

Breaking change: the password-permission split changes the meaning of existing IAM policies. The same stored policy can authorize a request after this update that it denied before, or deny a request it previously authorized. This is a deliberate authorization change from adopting minio/pkg #262, independent of the minio-go SDK update. It must be called out as a breaking change in the release that first includes it; it is not a transparent dependency refresh.

SILO separates a user’s own password change from creating users or resetting another user’s password. The same add-user administration endpoint and mcli commands continue to work; the authenticated caller and target access key determine which permission is checked.

Request Permission Evaluation
Change the caller’s own password admin:ChangeMyPassword Allowed for an internal user with an attached policy unless explicitly denied.
Create another user or reset another user’s password admin:CreateUser Requires an explicit Allow; an explicit Deny wins.

The Console’s Change Password button uses admin:ChangeMyPassword. admin:CreateUser continues to control user administration. The password change still requires the current password. STS and service-account credentials cannot change their parent user’s password; root credentials and external identity provider passwords remain outside this endpoint.

What changes and why

Previously, both operations checked admin:CreateUser. Changing one’s own password used an implicit grant unless that action was explicitly denied; managing other users required an explicit Allow. The split retains these two evaluation rules but checks admin:ChangeMyPassword for the caller’s password. It lets an operator independently control password changes and user administration. This is a policy-design choice, not a required mitigation for the SDK signing or region-compatibility fixes.

The following cases assume an internal user with an attached policy, matching statement conditions, and no other applicable grants or denies:

Existing policy Own password before Own password after Create/reset another user, before and after
S3 read grant only Allowed Allowed Denied
Deny admin:CreateUser Denied Allowed Denied
Deny admin:ChangeMyPassword Allowed Denied Denied
Allow admin:CreateUser Allowed Allowed Allowed
Allow admin:CreateUser plus Deny admin:ChangeMyPassword Allowed Denied Allowed
Deny both actions, or Deny admin:* Denied Denied Denied

Wildcard denies that match admin:CreateUser but do not match admin:ChangeMyPassword, such as admin:Create*, have the same password compatibility change as the explicit CreateUser deny. An Allow never overrides a matching Deny. Granting only admin:ChangeMyPassword does not grant user administration.

The policy JSON format, stored documents and endpoint are retained, but that does not preserve their authorization semantics. This update does not rewrite saved policies or provide a switch that restores the old action mapping.

Preserve the behavior of existing policies

A saved Deny admin:CreateUser still prevents user creation and password resets for other users. It no longer prevents the caller from changing their own password. If an existing policy used that deny to lock the caller’s password, add admin:ChangeMyPassword to the same Deny statement before upgrading. For example, change that statement’s action list to:

{
  "Effect": "Deny",
  "Action": ["admin:CreateUser", "admin:ChangeMyPassword"]
}

This is a statement fragment, not a replacement for the entire policy. Preserve its other actions, resource scope and conditions, and all other statements. Check policies attached through groups as well as directly to users. The preceding SILO package version already recognizes both action names, so this dual deny can be prepared before the Server upgrade. Saved policy files are not migrated automatically; the operator must apply this change where the old password restriction is intended.

To adopt the new split and lock only the caller’s password while allowing separately granted user administration, deny only admin:ChangeMyPassword. That finer distinction is enforced only by Servers containing this change.

Built-in read-only policies

The built-in readonly policy now grants its original S3 read operations without its previous CreateUser deny. This has two compatibility effects:

  • A user of the old readonly policy could not change their own password; with the split they can, unless another applicable statement denies admin:ChangeMyPassword.
  • The old built-in readonly deny overrode a separate CreateUser Allow. The new built-in definition allows that independently granted user administration. This can broaden effective permissions for users with both policies attached.

The added consolereadonly policy also grants ListBucket for Console browsing and follows the new split. Neither read-only policy grants user administration or S3 writes on its own. Their S3 permissions do not implicitly lock passwords.

Saved policies and user overrides of canned policies are preserved on upgrade. A saved copy of the old read-only policy retains its CreateUser deny and still blocks a separate CreateUser Allow, even though it no longer blocks self-service password changes. Where no saved override exists, Server uses the updated built-in definition. Review the effective policy documents rather than assuming every policy named readonly has the same contents. To retain both old restrictions, attach a policy denying both actions or retain the saved readonly override and add ChangeMyPassword to its existing deny.

Console and package callers

silo-pkg’s Policy.IsAllowedActions now reports admin:ChangeMyPassword as implicit unless denied, and reports admin:CreateUser only when explicitly allowed. Its Go signature and the Go compatibility floor are unchanged, but its returned capabilities change. Console and other consumers must stop using the CreateUser capability as a proxy for permission to change one’s own password.

Coordinated upgrade and rollback

Upgrade SILO Server, silo-pkg and Console together, including the Console embedded in Server. Update mcli’s shared package and SDK pins as part of the same maintained stack. Mixed versions disagree about the self-service action and may show a button the Server refuses, hide an allowed operation, or fail to enforce a new password-specific deny on an old Server.

Before upgrading, export the affected user/group policy documents, review the cases above, and apply both denies wherever the old combined restriction must survive. Keep both denies throughout a rolling upgrade and any rollback window. Verify self-service password changes and other-user creation/password resets with the affected accounts. Complete the Server rollout and update Console before relying on the new independent permissions.

Rolling back the binaries does not convert policies. An old Server ignores Deny admin:ChangeMyPassword for this endpoint, so that deny alone cannot lock the password after rollback. Restore or retain the CreateUser deny when the password must remain locked; on the old Server it will also deny management of other users. The old Server cannot represent the new combination of allowing user administration while denying only self-service password changes.

Upstream MinIO compatibility remains best effort; the supported integration target is pgsty/silo.

3 - Access-Tiering Removal

Why the opt-in GET-frequency pool-tiering feature was removed, how to migrate builds that contained it, and what the upgrade acceptance did and did not prove.

Release boundary: the opt-in GET-frequency pool-tiering feature (community PR #60) existed only in main/snapshot builds. The published Server 20260903 predates it, and upgrading from the published version needs no access-tier configuration cleanup. The removal merged as PR #188; only deployments that ran a build containing the feature need this page.

Why it was removed

Access tiering was a default-off, opt-in scheduler that moved objects between local server pools according to GET frequency. As shipped it dragged a disproportionate maintenance surface across configuration, lifecycle parsing, the usage cache, statistics, and the core multi-pool write path — for a benefit that was never measured or claimed (no throughput, latency, or lock-traffic numbers existed). The removal decision kept everything that was independently correct:

  • ordinary lifecycle expiration and transitions to remote tiers;
  • rebalance and decommission;
  • the general multi-pool write, metadata, healing and conditional-deletion fixes from PR #178, including shared remote-tier reference protection.

The keep/remove split was validated by isolation controls: reverting the PR #178-era fixes wholesale failed 10 of 13 grouped checks, so they were deliberately preserved while the tiering feature itself went.

Before upgrading a build with access tiering

  1. Save a copy of the server ILM configuration and each affected bucket’s lifecycle XML. Use an API client that preserves the nonstandard XML; do not rely on a client model that silently omits unknown elements.
  2. On the old server, set ilm access_tiering=off and remove or disable any access-tier environment overrides. Let in-progress moves finish before replacing nodes. This reduces movement intermediate states; the removal itself changes no storage RPC protocol.
  3. Remove top-level AccessTierQuota and rule-level AccessTransition elements. Delete rules whose only action was AccessTransition. For a mixed rule, retain its filter, status, ID, and ordinary expiration/transition actions. An access-only rule loads harmlessly after upgrade, but becomes an actionless rule and fails validation on the next lifecycle edit. If no rules remain, delete the lifecycle configuration through the S3 API.
  4. Use a coordinated maintenance window: stop the deployment, install the same new binary on every node, then restart all nodes. The bootstrap check compares binary checksums and server environment settings — in a four-node test the first new node could not finish starting among three old nodes, and removing an old environment override on only some nodes can block startup even with matching binaries. Do not assume that an unchanged RPC protocol permits replacing one node at a time. The check runs only during startup; it is not a safety guarantee for nodes already running different binaries.
  5. After restarting, verify object reads, bucket listing, ILM worker settings, and a lifecycle edit. Check storage access from every request-serving node to each pool’s drives: successful reads or bucket listing establish less than complete drive reachability, and admin disk summaries only aggregate server-local state. Startup connection times vary, so a fixed sleep is insufficient.

What happens to stored state

State Behavior after removal
Ten old ILM keys access_tiering, access_pools, access_max_size, access_promote_watermark, access_bin_width, access_bins, access_flush, access_min_residency, access_workers, access_max_tracked are accepted but ignored. Existing transition/expiration worker settings are preserved.
Admin configuration Deprecated keys may still appear in mcli admin config get ilm; setting them may succeed but has no effect, even with access_tiering=on. Remove obsolete environment settings from deployment manifests.
Lifecycle XML AccessTierQuota and AccessTransition are ignored when read and omitted when re-encoded. The same parser handles new PUT requests, so these extensions are also silently discarded there; access-only rules still fail action validation.
Data-usage cache Both v8 and v9 caches are read, preserving ordinary counts, sizes, histograms, and remote-tier statistics. The retired hot-tier byte count is discarded; subsequent writes use v8. No feature-driven full statistics rebuild is required.
Objects already moved Remain in their current pools with the same versions and timestamps. There is no bulk move-back or object metadata rewrite.
Internal leftovers x-minio-internal-ilm-atier and .minio.sys/config/ilm/access/ counter objects may remain unused. They need no cleanup service or object scan.

Interrupted rebalance/decommission can leave the same version in more than one pool independently of access tiering; removing the scheduler does not remove such existing copies.

What the upgrade acceptance did and did not prove

The coordinated-upgrade procedure was validated in a local four-node, dual-pool experiment (single Docker Linux host, tmpfs drives): three full upgrade acceptances passed after a readiness-check correction, binding the production code at 41aa84609. The readiness method is reusable and is the part worth copying: from each request-serving node, probe a unique, never-written object key with GetObjectTagging — a path that waits for all drives — and match the responses against real storage trace entries (storage.ReadVersion returns the expected missing-object/version response), requiring a real response on every node-to-drive path for three consecutive rounds: 32 responses per round in this four-node, eight-drive topology. drive not found, timeouts, and missing trace entries fail this check; an unreachable drive is not a healthy drive reporting an absent object. Plain per-node 404s or admin disk summaries do not establish this.

Two earlier upgrade attempts in the same series failed (a DELETE returned 204, then a node answered HEAD/GET with 503); those instances remain open and unattributed — they were neither explained away nor converted into passing results by the later successes. Distributed production upgrades (cross-host, formal tags, packages, images) were not proven by these experiments. Treat the acceptance as: the procedure is validated in the tested topology, not that any distributed rollout is guaranteed.

Version deletion scope

The removal rode together with a repair of ordinary single-object DELETE ?versionId=..., which now reconciles the addressed UUID, null version, or delete marker across pools (including unqualified DELETE of a directory marker). A successful request applies the deletion to every resolved pool copy under existing per-pool quorum rules; pending outbound delete replication retains VersionPurgePending until the replication worker completes the purge — success does not guarantee immediate physical removal from every drive. If a pool is unreadable these requests can return 503 SlowDownRead (or retain other error codes) even when another pool has a readable copy; retry after recovery. Batch DeleteObjects already fans out across pools. Incoming replicated deletes, lifecycle expiration, free-version cleanup, and movement-internal calls keep their existing contracts — the ordinary DELETE repair is not a guarantee for every source of deletion.

Historical record

The feature’s introduction, subsequent fixes, rollback scope, review history, and the unresolved validation findings are preserved in the archived decision record at pinned commit 40220bd836cb and in the repository’s docs/bucket/lifecycle/access-tiering-removal.md; this page carries the operator-facing contract so neither is required reading. Component status lives in the version matrix.

4 - Migrate from MinIO to Silo

What changes, what stays, and how to switch a container deployment. Package installations are covered in Native Package Migration.

Migrating from MinIO to Silo is an in-place binary replacement, not a data migration. Nothing is exported or re-imported. In a container deployment the only required change is the image name. For RPM/DEB installations, see Native Package Migration.

What changes

In order of importance:

  1. Container image: minio/minio, quay.io/minio/minio, and pgsty/minio are all replaced by docker.io/pgsty/silo.
  2. Package, systemd service, and server executable: miniosilo.
  3. Upstream services: the in-place updater and MinIO-operated callhome/SUBNET are disabled; upgrades go through packages, images, or your orchestrator.
  4. Default OS service account: silo — fresh installations only; migrations keep running as the existing data owner.
  5. Default local configuration directory: ~/.minio~/.silo (fresh processes only; see server compatibility for the certificate fallback order — an existing ~/.minio/certs keeps being honored).
  6. Branding: banners, Console appearance, log wording, and product links say Silo.

What stays

  • Object data and the .minio.sys metadata directory — the on-disk format is unchanged and remains interoperable with MinIO in both directions.
  • Buckets, versions, users, access keys, policies, lifecycle rules, replication state, encryption metadata.
  • S3 API, SigV4 signing, SDKs, mc/mcli, presigned URL behavior.
  • Endpoint hostname, API port 9000, Console port, volume mounts.
  • MINIO_* environment variables and existing server options.
  • /minio/* routes, x-minio-* headers, minio_* metrics.
  • Policy-namespace identifiers: arn:minio:* ARNs, minio:s3 and the other service namespaces in IAM policies, notifications, and audit events keep their exact spelling. There is no SILO_* alias namespace — scripts and policies addressing the identifiers above need no change.

There is no data-conversion step. If your MinIO build is years old, validate the version distance itself in staging; it is a large software upgrade, not a format change.

Docker migration

Whichever image you run today, replace it with:

docker.io/pgsty/silo:<RELEASE-tag>

Tags: immutable RELEASE.YYYY-MM-DDTHH-MM-SSZ (pin these), rolling latest, and the -distroless variants below. The old pgsty/minio repository stays published, frozen at its final tag.

In Compose, change only the image line:

services:
  minio:                              # service name may stay "minio"
    image: docker.io/pgsty/silo:<RELEASE-tag>
    command: server /data --console-address ":9001"
    environment:                      # MINIO_* unchanged
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    ports: ["9000:9000", "9001:9001"]
    volumes:
      - minio-data:/data              # same volume, same data
volumes:
  minio-data:
docker compose pull minio && docker compose up -d minio

The entrypoint translates the legacy first argument, so an inherited command: minio server /data keeps working. A hard-coded entrypoint: /usr/bin/minio must change to /usr/bin/silo. Existing mc ready local healthchecks keep working; the native replacement is test: ["CMD", "silo", "healthcheck", "ready"] (reference). Do not run docker compose down -v-v deletes the data volume.

Distroless variant

pgsty/silo:<RELEASE-tag>-distroless ships the silo binary only: no shell, no mc, no curl. It has a built-in HEALTHCHECK (the native probe) and works under any --user:

docker run -d --name silo \
  -p 9000:9000 -p 9001:9001 \
  -e MINIO_ROOT_USER=admin \
  -e MINIO_ROOT_PASSWORD=change-me-long-password \
  -v silo-data:/data \
  docker.io/pgsty/silo:<RELEASE-tag>-distroless \
  server /data --console-address ":9001"

The same deployment as a Compose file:

services:
  silo:
    image: docker.io/pgsty/silo:<RELEASE-tag>-distroless
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: admin
      MINIO_ROOT_PASSWORD: change-me-long-password
    ports: ["9000:9000", "9001:9001"]
    volumes:
      - silo-data:/data
volumes:
  silo-data:

depends_on: condition: service_healthy works against it with no healthcheck: block. The volume format is the same as the classic image and MinIO — the variants are interchangeable over the same data. TLS certificates mount at /tmp/.silo/certs. If command-line flags move the listen address, point the built-in probe with MINIO_HEALTHCHECK_URL. There is no shell inside; debug with docker debug / kubectl debug.

Kubernetes

Kubelet probes are httpGet requests in the pod spec; Docker HEALTHCHECK is ignored, so both image variants are probed identically and existing probe configs keep working. For Helm releases, keep the release identity with nameOverride/fullnameOverride and compare helm template output before applying (details).

Rollback

The disk format is unchanged and works with both servers: set image: back to the recorded MinIO tag and docker compose up -d. The same volume stays attached, and data written by Silo remains readable by MinIO.

Upgrading from RELEASE.2026-08-06

The release after RELEASE.2026-08-06T00-00-00Z tightens several behaviors that 20260806 accepted. Check these before upgrading:

  1. Explicit version deletes need s3:DeleteObjectVersion. DeleteObject and DeleteObjects entries that carry a versionId are authorized as s3:DeleteObjectVersion, as on AWS. Grant it to principals that delete specific versions, and add Deny s3:DeleteObjectVersion next to any Deny s3:DeleteObject that is meant to block permanent deletes.
  2. Enable and disable are separate admin actions. admin:EnableUser / admin:DisableUser and the group equivalents are checked against the requested status; a policy that grants only one of them loses the other operation.
  3. New and updated policies reject bare ARN prefixes such as arn:aws:s3:::, and statements that combine Resource with NotResource. Stored policies keep loading; automation that re-applies such policies fails.
  4. Legacy database notification targets need a connection string. An enabled pre-KV PostgreSQL or MySQL target without connection_string / dsn_string stops startup with a credential-free error; 20260806 silently dropped every notification target in that situation.
  5. Checksum requests are validated. Unknown x-amz-checksum-* algorithms, CRC64NVME combined with COMPOSITE, and checksum-type assertions that contradict the upload are rejected with 400. The default behavior of the AWS SDKs, minio-go, and mcli is unaffected.
  6. Per-bucket CORS is real. A bucket with its own CORS configuration is served by that configuration only; MINIO_API_CORS_ALLOW_ORIGIN applies to buckets without one. In a site-replication group, configure bucket CORS only after every site runs the new release: older peers accept but ignore the configuration and keep reporting a CORS mismatch.
  7. Rollback keeps the data readable. 20260806 ignores bucket CORS configuration and drops it when it rewrites that bucket’s metadata; recreate the configuration after upgrading again.

Console regressions in the September 3 release

RELEASE.2026-09-03T13-18-01Z stopped recognizing forwarded client addresses from unconfigured local proxies and discarded the four CONSOLE_WS_MAX_* connection settings. This can change IP Allow/Deny decisions and prevent operators from raising the eight-connection anonymous per-address limit.

Builds containing the fixes restore loopback TCP-peer trust for embedded Console unless MINIO_API_TRUSTED_PROXIES=none/off, and preserve all four limits from the environment or MINIO_CONFIG_ENV_FILE. Remote proxies still need an explicit IP/CIDR list. Standalone defaults, forwarded-chain trust rules and connection budgets remain unchanged; invalid configuration becomes a startup error. See Console settings for the policy table and configuration constraints. Track availability in #147 and #148; the 0903 image does not include these fixes.

On 0903, explicitly listing the local proxy peer in MINIO_API_TRUSTED_PROXIES restores client attribution, but also switches the S3 listener on port 9000 to listed mode; include its other required proxies too. Custom WebSocket limits require a fixed Server build or standalone Console.

One cluster, one binary

Distributed nodes verify each other’s binary at bootstrap. A node started among peers running a different binary does not fail — it waits indefinitely in activating, logging:

Expected Silo binary checksum: ..., seen: ...
Waiting for at least 1 remote servers with valid configuration to be online

This applies to any pair of different binaries: MinIO next to Silo, and one Silo version next to another. So do not migrate — or later upgrade — a cluster node by node. Switch all nodes in one pass: stop the old binary everywhere, start the new one everywhere (in Compose: change the image for all nodes in one edit, docker compose up -d once). Single-node deployments are unaffected. The same applies to rollback. Rolling restarts of the same binary work normally; gate them with silo healthcheck --maintenance cluster (exit 0 = safe to stop this node).

Verification

Before touching anything, record the artifacts you are leaving behind: the running image digest (or package version and binary checksum), the unit status and enabled state, and the UID/GID that owns the data directory. Rollback is only as precise as that record.

silo healthcheck ready                   # this node serves; exit 0/1
silo healthcheck cluster                 # cluster-wide write quorum
mc admin info <existing-alias>           # all nodes online, new version, old alias

Then download a known object and compare its checksum, exercise one application through its existing SDK, restart the service once, and re-check. One more rollback precondition: do not enable features in the migration window that the old version cannot understand — rolling back means rolling back to what the old binary can parse.

5 - Silo vs. MinIO Server Compatibility

A code-verified compatibility audit of the Silo server fork: binaries, configuration, S3 and admin behavior, storage internals, packaging, containers, and Helm.

Current Console: v2.4.1, with mcli 20260916 and pkg v3.14.1. See the component matrix and password-policy migration.

Silo is a maintained fork of the MinIO server. It preserves MinIO’s S3-facing and on-disk compatibility, but it is not a byte-for-byte, operationally invisible rename. This page is the compatibility contract for moving from the upstream baseline to the Silo source prepared on 2026-08-06.

Warning

Read this before replacing a MinIO deployment. The binary, package, service account, systemd unit, default local configuration directory, container path, Helm resource names, embedded Console, update behavior, several authorization decisions, and some error responses changed. Data disks and the MINIO_* configuration namespace did not receive a matching rename.

Audit scope and method

This is a source audit, not a compilation of release-note claims.

Boundary Audited value
Upstream baseline minio/minio@7aac2a2c5b7c882e68c1ce017d8256be2feea27f, 2026-02-11
Silo snapshot pgsty/silo@219670d3176a5b27ded60914390d5ee7e763cf58, 2026-08-06
Commit set 7aac2a2c..219670d3: 96 reachable commits; 93 were on origin/main and the final three were committed locally at audit time
Net source diff 523 files, 36,715 insertions, 21,450 deletions
Interpretation The behavior of the final snapshot. Intermediate changes later replaced or removed are not presented as current behavior

Every commit in the range was inspected. Release and security posts were used as an index of intended changes, then checked against the final implementation, tests, dependency graph, build recipes, package payloads, container entrypoint, and rendered Helm manifests. The complete commit ledger prevents a documentation-only or CI-only commit from silently falling out of scope.

The range is defined by Git reachability, not author-date sorting. It therefore contains d4cd4b433, authored in December 2025 but joined into the post-baseline graph later; this is not an extra undocumented baseline.

The tagged RELEASE.2026-08-04T00-00-00Z ends at d88f46cce, 18 commits before this audit head. Accordingly, this page records the 2026-08-06 prepared source state; it does not claim that the last 18 changes were already present in a public package, image, tag, or deployed website.

Executive compatibility matrix

Surface Status Practical result
S3 wire API Compatible with documented exceptions Routes, XML/JSON schemas, SigV4, S3 headers, ports, and ordinary error codes retain the MinIO contract. Security and correctness fixes below deliberately reject some requests previously accepted
Data disks Compatible .minio.sys, erasure metadata, bucket/object layout, healing, replication, and encryption formats keep their names and schemas. Poisoned or unusable metadata is now rejected earlier
Configuration Mostly compatible Existing MINIO_* variables, config keys, KMS/KES, IAM, notification, and storage settings remain. The default per-user directory becomes ~/.silo, with a deterministic ~/.minio fallback
Metrics and automation APIs Compatible minio_* Prometheus metrics, /minio/* routes, x-minio-* headers, admin/S3 error identifiers, and release tag syntax stay unchanged
Binary and distribution Renamed minio becomes silo; package, unit, image, chart, archives, checksums, and paths move to the Silo identity. There is no installed server-binary alias
Runtime identity Changed CLI text, banners, HTTP Server, User-Agent application names, FTP banner, log names, support links, and some human-readable errors say Silo
Upstream network services Disabled In-place update, update polling, callhome, SUBNET registration, and diagnostic uploads do not contact MinIO services
Authorization/security Intentionally stricter OIDC HMAC tokens, unsafe LDAP failures, forged replication metadata, object-only grants for protected bucket writes, shadowed policy inputs, ambiguous version IDs, and several malformed internode requests change behavior
Embedded UI and Go dependencies Console and MCLI forked behind compatible import paths; pkg on its own path Silo Console and MCLI are selected with replace directives while github.com/minio/console / github.com/minio/mc paths remain; Silo Pkg is consumed directly under github.com/pgsty/silo-pkg/v3
Mixed-version cluster Not supported for this transition The private ReadMultiple storage-REST operation was removed without bumping storage REST v63. Upgrade all nodes as one build

What deliberately stays compatible

Protocol, storage, and configuration names

The following MinIO identifiers are compatibility identifiers, not unfinished branding work, and must remain visible:

  • the Go module path github.com/minio/minio and the inherited github.com/minio/... imports;
  • the MINIO_* environment namespace, including MINIO_ROOT_USER, MINIO_ROOT_PASSWORD, MINIO_VOLUMES, MINIO_OPTS, and notification variables;
  • S3 and admin routes under /minio/*, x-minio-* headers, MinIO-specific S3 extensions, and established API error codes;
  • Prometheus metric names under minio_*;
  • the .minio.sys internal volume and all existing disk metadata names;
  • the default S3 port 9000, existing --address / --console-address flags, and release tags of the form RELEASE.YYYY-MM-DDTHH-MM-SSZ;
  • configuration KV formats, IAM data, KMS/KES configuration, encryption metadata, bucket metadata, replication state, and healing state.

The automated rebrand baseline records 137 compatible imports, 437 environment names, 19 metric namespaces, 84 headers, 334 routes, one internal root, three Grid namespaces, 15 storage-REST identifiers, 58 policy identifiers, and 9,014 exported symbols. The guard treats an unreviewed change to that manifest as a compatibility failure.

No data copy or metadata rewrite is required when the same disks move from MinIO to Silo. This does not mean every malformed historical object is accepted: the storage hardening described below rejects unsafe paths, invalid erasure geometry, negative part sizes, and poisoned metadata that older code could carry farther into the stack.

Source compatibility

The server module remains github.com/minio/minio, and Console and the client library are selected through replacements without forcing callers to rewrite imports:

replace github.com/minio/console => github.com/pgsty/silo-console ...
replace github.com/minio/mc      => github.com/pgsty/mc ...

The shared package is the deliberate exception. Since pkg v3.13.0 it builds under its own module path, and every maintained component — including the server — consumes it directly:

require github.com/pgsty/silo-pkg/v3 v3.14.0   // no replace arrangement

The historical replace github.com/minio/pkg/v3 => ... arrangement is retired; github.com/minio/pkg/v3 now appears only as a legacy indirect dependency (via colorjson/dperf), separate from the maintained policy implementation. The upstream SDK github.com/minio/minio-go/v7 is the other explicit exception: it is consumed directly from upstream at a verified commit, and the retired silo-go fork is not part of the maintained graph.

This preserves most source compatibility, but it is not an assertion that every private or exported Go symbol is frozen. The internal ReadMultiple storage interface was removed, and the selected silo-pkg release has several developer-visible fixes described in Dependencies.

The maintained source remote is github.com/pgsty/silo on branch main; the former minio branch is archived. go install github.com/minio/minio@... still resolves the upstream project, not Silo, so clone the Silo repository or use an explicit module replace. Contributions no longer require MinIO’s CLA, but commits require DCO sign-off (git commit -s).

Identity, binary, and outbound-service changes

Surface Upstream baseline Silo snapshot Compatibility consequence
Server executable minio / minio.exe silo / silo.exe Scripts and absolute paths must change. Archives, packages, and images do not install a /usr/bin/minio server alias
Version output MinIO identity Silo release/commit/runtime, AGPL, upstream copyright, PGSTY modification copyright, and MinIO technology lineage Parsers should rely on stable fields, not grep for MinIO prose
Local config home ~/.minio ~/.silo for a new home See the fallback rules below; data disks are unrelated
HTTP identity Server: MinIO and MinIO application UAs Server: Silo; internal batch/fan-out/perf UAs use silo-* / Silo names Protocol headers such as x-minio-* remain unchanged; identity-sensitive monitoring may need an update
Human text MinIO banner, help, errors, examples, FTP greeting, support links Silo identity; examples prefer mysilo Exact-string log parsers and snapshots may change, not status/error codes unless listed elsewhere
Integration-visible labels MinIO NATS/Redis connection names and Veeam model NATS name Silo Notification, Redis CLIENT SETNAME Silo, Veeam model "Silo <release>" Broker dashboards, connection-name filters, and Veeam inventory display can change
KMS validation prose MinIO-branded conflict messages Brand-neutral “both KMS/KES/static-key configuration” messages Configuration rules are the same; exact-text automation can change
Updater Release polling and in-place update paths Permanently disabled MINIO_UPDATE is parsed but cannot re-enable it; admin update routes remain and fail stably instead of disappearing
Callhome/SUBNET Registration, callhome, support uploads, embedded MinIO support key Configuration is accepted for migration but forced off; no registration/upload/post; no fallback encryption key Remove automation that expects MinIO-operated services. Requester-key inspect encryption remains
Embedded Console Upstream snapshot had the Console stripped Silo Console v2.1.1, English/Chinese UI, Metrics V3, no SUBNET UI Browser behavior and assets change; the S3/Admin API boundary remains the server contract
Bundled client in OCI No maintained fork contract /usr/bin/mcli plus /usr/bin/mc -> mcli This mc is the client compatibility alias, never a server alias
Warm-tier probe Temporary object contains MinIO Same-length probe contains Silo! Only observable through backend inspection or a failed cleanup; protocol semantics do not change
Log rotation default minio-*.log silo-*.log Log collectors matching filenames must change

The deleted /api/health/upload reference was an outbound SUBNET URL path, not a local Silo HTTP endpoint. The compatibility change is that Silo no longer issues that POST; it is incorrect to describe this as removal of a server route.

Default configuration-directory selection

Unless --config-dir is explicit, Silo makes one decision at startup:

Home-directory state Selected directory Message
Neither directory exists ~/.silo none
Only ~/.silo exists ~/.silo none
Only ~/.minio exists ~/.minio informational legacy notice; no files are moved
Both exist ~/.silo ambiguity warning

--config-dir always wins. Unless --certs-dir is also supplied, the certificate directory follows the selected configuration directory. For deterministic automation, set --config-dir instead of depending on filesystem discovery.

The fork adds only three server configuration controls that materially change compatibility behavior; there is no parallel SILO_* replacement namespace:

Setting Purpose Default
MINIO_API_TRUSTED_PROXIES General source-address trust boundary unset: exact historical trust-any behavior
MINIO_IDENTITY_LDAP_STS_TRUSTED_PROXIES / LDAP key sts_trusted_proxies Source buckets used by LDAP STS failure limiting no trusted proxy; use the socket peer
MINIO_API_LEGACY_BUCKET_RESOURCE_MATCH Temporary rollback for the protected bucket/object IAM boundary off

Notification KV registration fixes do not rename their existing environment variables. MINIO_UPDATE, SUBNET, and callhome inputs are retained only as ignored/migration-compatible inputs as described next. SILO_OPTS appears only inside the generated inspect helper, not as a general replacement for MINIO_OPTS.

Updater, callhome, SUBNET, and inspect

  • Startup never polls dl.min.io; the release URL and upstream minisign root are absent.
  • MINIO_UPDATE values that request updates produce a warning and are ignored. Public and peer admin update handlers remain registered, returning MethodNotAllowed or the stable “in-place updates are disabled” error.
  • Legacy subnet and callhome keys remain parseable so an old config can start. Registration state is always false, Console SUBNET variables are unset, callhome is forced off, and no diagnostic or license payload is posted.
  • Inspect output is encrypted only for a requester-provided public key. It no longer falls back to MinIO’s built-in support key. The helper is start-silo.sh, invokes silo, and includes cluster.info only in the requester-key flow.
  • Upgrade through a package manager, an image rollout, or an orchestrator. Do not call mc admin update / mcli admin update against Silo as an upgrade mechanism.

Installation and deployment compatibility

RPM, DEB, and APK

The package is silo for Linux amd64/arm64. Its relevant payload is:

/usr/bin/silo
/usr/lib/systemd/system/silo.service
/etc/default/silo                 (config, noreplace)
/usr/lib/sysusers.d/silo.conf
/usr/share/doc/silo/LICENSE
/usr/share/doc/silo/NOTICE

The package creates a system silo:silo account without a home. It does not chown existing data, migrate ownership, stop a running MinIO service during installation, or declare package-manager Provides, Obsoletes, Replaces, or Conflicts against the minio package. Both packages can therefore be installed, but their services cannot run together through the shipped units.

silo.service has Conflicts=minio.service, runs as silo:silo, and reads /etc/default/minio first and /etc/default/silo second. The shipped Silo file has no active assignments, so a legacy file continues to work until an administrator overrides it in the later file. It starts:

/usr/bin/silo server $MINIO_OPTS $MINIO_VOLUMES

Before switching the unit, make every data, certificate, KMS credential, and environment-file path readable by silo and every writable path writable by it. An old deployment owned by minio:minio will otherwise fail at startup. Package removal stops/disables silo.service; package upgrades do not deliberately stop the running service in the pre-remove hook.

Container image

The source repository is github.com/pgsty/silo; the intended image name is docker.io/pgsty/silo. There is no registry-level promise that pgsty/minio redirects to it.

  • The server exists only at /usr/bin/silo; an explicit /usr/bin/minio ... command breaks.
  • The entrypoint translates a first argv word of minio to silo, and prepends silo when argv starts with server, fmt-gen, or an option. Consequently the common command: minio server /data form continues to work.
  • An explicitly requested shell or utility is left alone.
  • Every privilege path uses exec, so the server becomes PID 1 and receives SIGTERM for graceful shutdown instead of timing out behind the entrypoint.
  • HOME=/tmp is the image default and is normalized to a writable directory for arbitrary-UID and legacy MINIO_USERNAME drop-user execution.
  • Port 9000, /data, and the MINIO_* interface remain. The amd64/arm64 image manifest also contains checksummed MCLI RELEASE.2026-08-06T00-00-00Z (bumped in the release commit after this audit head) and the client-only mc symlink.
  • OCI license material is under /licenses/{LICENSE,NOTICE,CREDITS}.

Helm chart

The inherited helm/minio chart, helm-releases, root chart index, and reindex helper were removed. The maintained chart is helm/silo; the 20260806 release ships chart version 7.0.1 (the audit head still carried 7.0.0).

Most values deliberately keep their established names, including minioAPIPort, minioConsolePort, and all MINIO_* environment settings. The changes that matter during migration are:

  • image repositories become pgsty/silo;
  • generated resource names and labels follow chart name silo;
  • default service account becomes silo-sa;
  • certificate/client mount paths move from /etc/minio/{certs,mc} to /etc/silo/{certs,mc};
  • no insecure console/console123 user is created by default (users: []);
  • post-job examples prefer alias mysilo, while myminio is also registered so inherited customCommands can still resolve it;
  • the new chart executes silo, so an image-only rollback to an old MinIO image is not safe.

To preserve the old Kubernetes object identities while adopting the new chart, start from the exact old values and set at least:

nameOverride: minio
fullnameOverride: <the-old-full-release-name>   # for example my-release-minio
serviceAccount:
  name: minio-sa
image:
  repository: pgsty/silo
mcImage:
  repository: pgsty/silo

Render both charts and compare Services, selectors, StatefulSets/Deployments, PVC templates, Secrets, service account, storage mounts, environment, and ports before applying. Roll back chart and image together.

Archives, provenance, and legal files

Release archives are named silo_<version>_<os>_<arch> and contain the executable, README, LICENSE, and NOTICE. The checksum manifest is silo_<version>_checksums.txt; each archive receives an SPDX JSON SBOM, and the checksum set is accompanied by a keyless Sigstore bundle. Static, CGO-disabled, kqueue-tagged binaries are published only for Linux, macOS, and Windows on amd64/arm64; formerly compile-checked but unshipped architectures are no longer release gates. RPM/DEB/APK are built for Linux amd64/arm64, with timestamp package versions such as YYYYMMDDHHMMSS.0.0 (RPM release 1) and a separate package checksum manifest. Normal builds no longer stamp the build host’s GOPATH/GOROOT, improving reproducibility and removing path leakage.

Unlike the upstream baseline Docker recipe, which downloaded and verified a prebuilt dl.min.io server, the Silo release image consumes the exact source-built, attested release archive at the selected tag. Image publication is a separate, explicitly dispatched workflow after a GitHub release; build success alone does not publish it.

CREDITS is regenerated from the modules actually linked into the server and guarded in CI. It is included in the OCI image; it is intentionally omitted from packages and archives because of its size. Upstream AGPL and copyright notices remain, alongside PGSTY’s modification notice.

Runtime and security behavior changes

These are user-visible changes even when they close a vulnerability. “Stricter” means a request, policy, token, configuration, or corrupted internal message that formerly succeeded or failed differently can now be rejected.

Authentication, IAM, and request identity

Change Final behavior Who must act
OIDC JWT verification (d24f449e0) The client secret is no longer a verification key. Only asymmetric JWKS algorithms RS256/384/512, ES256/384/512, RS3256/3384/3512, and ES3256/3384/3512 are accepted. HS256/384/512 tokens fail; unknown kid still triggers the established JWKS refresh/retry path An IdP signing Silo tokens with HMAC must migrate to an asymmetric JWKS key
LDAP STS errors (3b950f8fa) Unknown user and bad password share one external InvalidParameterValue authentication failure. LDAP infrastructure failures remain server errors and are logged Clients must not distinguish account existence from response text
LDAP STS rate limiting (18b712d49, 9e10f6d9a, f44110890, 5e40665ac) Per-source, per-node in-memory bucket: burst 10, refill one per 6 seconds, idle TTL 15 minutes. Only authentication failures consume tokens; success and infrastructure failures refund them. Exhaustion returns HTTP 429, ThrottlingException, Retry-After: 6 Proxies should configure MINIO_IDENTITY_LDAP_STS_TRUSTED_PROXIES; it is separate from the general source-address setting
LDAP trusted-proxy source For an allow-listed socket peer, clean X-Real-IP is preferred; otherwise XFF is walked right-to-left past trusted hops. RFC 7239 Forwarded is ignored. A proxy must overwrite X-Real-IP Review ingress header sanitation; the limiter is not a distributed account lockout
LDAP service-account lookup “User DN not found” matching is case-insensitive, preserving the intended Admin no-such-user / login-name error classification across dependency message capitalization Only brittle clients that depended on the accidental misclassification see a difference
Bucket/object IAM boundary (97b7d2804) Twelve protected bucket-write actions no longer inherit an Allow from only arn:aws:s3:::bucket/*; the bare bucket ARN is required. Deny/NotResource and built-in * policies retain their semantics Add arn:aws:s3:::bucket to custom policies that legitimately perform protected bucket writes, or use the temporary global escape hatch MINIO_API_LEGACY_BUCKET_RESOURCE_MATCH=on
Protected actions Delete/ForceDelete bucket; put/delete bucket policy; put replication, lifecycle, object-lock, versioning, or CORS; delete CORS; put bucket QoS or inventory configuration Read/list, create bucket, tags, encryption, and notifications keep the inherited matching behavior
Effective policy inputs (2f55347f7) Server-derived conditions cannot be shadowed by a same-named header/query parameter. Exact keys win in the policy package. Existing/request tags, storage class, content/copy/checksum, object-lock, signature age, and list parameters are sourced from the value actually consumed by the operation Policies that accidentally depended on attacker-controlled shadow values stop matching
Request tags PutObject, CreateMultipartUpload, and PutObjectTagging bind s3:RequestObjectTag/* to the parsed input. ExistingObjectTag comes only from stored metadata. Other operation paths keep the inherited header fallback Re-test tag-conditioned write policies
s3:signatureAge Present only for verified presigned SigV4 requests Raw x-amz-signature-age injection no longer creates the condition
s3:versionid (744a9dcd7) Absent means absent, whitespace is normalized, and DeleteObject/MultiDelete uses the effective per-object version. A URL version cannot decoy a different XML version Re-test version-conditioned delete policies and any policy relying on Null
Replication metadata (56fa63bfd) Ordinary PUT/COPY cannot inject internal replication status/time metadata. An authenticated replica request must have ReplicateObjectAction; multipart and Snowball replication flows retain their legitimate metadata Custom replication callers must use the authorized replication path

The bucket-boundary escape hatch is startup-global and all-or-nothing. It exists for migration, not as a permanent mixed-policy mode. An empty or unparseable LDAP source is deliberately not placed into one shared limiter bucket; it is not throttled until a usable source can be derived.

General source-address trust

fe6dc4780 adds MINIO_API_TRUSTED_PROXIES, because the chosen client address feeds aws:SourceIp, audit remotehost, event Host, admin trace, and node-to-node forwarding.

Value Result
unset Exact inherited behavior: trust source headers from any peer; left-most XFF, then X-Real-IP, then Forwarded
none or off Ignore all three source-address headers and use the TCP peer
IP/CIDR list Trust headers only when the TCP peer is listed; walk XFF right-to-left past trusted hops (maximum 100), then use the last X-Real-IP line, then walk Forwarded right-to-left

Malformed entries, a non-empty list naming no proxy, or a remote env:// read failure fail closed and stop startup rather than reverting to trust-any. Invalid values inside a received chain are skipped; no usable address falls back to the peer. Loopback is implicitly trusted only for the FTP/SFTP peer bridge, not skipped as an arbitrary hop inside a client chain.

The inherited _MINIO_API_XFF_HEADER=off retains its exact old semantics and initialization timing: it disables only XFF parsing, not X-Real-IP or Forwarded, and therefore is not a security boundary. Configure every cluster peer that legitimately forwards requests and ensure edge proxies overwrite or strip client-provided source headers.

S3 request and response behavior

Area Change and compatibility effect
Presigned streaming auth A query/presigned SigV4 request declaring STREAMING-UNSIGNED-PAYLOAD-TRAILER is rejected with SignatureVersionNotSupported; it cannot fall through to anonymous authorization. Header SigV4 is verified before body processing
Snowball Authorization happens before tar extraction; the streaming-trailer bypass cannot write objects before a later failure
S3 Select record limits CSV input, JSON Lines input, and output records over 1 MiB return an OverMaxRecordSize event. JSON Lines always uses the bounded reader (possibly slower on SIMD-capable CPUs), JSON parse errors use JSONParsingError, and already completed records can precede the terminal error
Streaming responses The tracking writer implements Flush; Write/Flush records an implicit HTTP 200. ListenBucketNotification/watch streams and S3 Select keepalives reach clients, while audit/status metrics record the committed status correctly
Multipart full-object checksum FULL_OBJECT CRC32/CRC32C/CRC64NVME completion may omit every per-part checksum. If any are supplied they are still checked; COMPOSITE still requires every part. Explicit completion type is validated even without an object checksum value: mismatch is BadDigest, unknown type is InvalidArgument. CRC64NVME canonicalization is unchanged. A zero-byte multipart object’s checksum is retained correctly
CopyObject checksum response CopyObject XML and HTTP headers report the committed checksum using destination encryption context. SSE-C source key A and destination key B remain separate; same-object key rotation reports checksum fields under the new key. Stored data and checksum format are unchanged
Multipart part ordering Duplicate or non-increasing part numbers fail with InvalidPartOrder before assembly. Gaps and a first part other than 1 remain legal; the upload stays available for retry
Erasure read pooling Correct shard-buffer ownership is restored, avoiding loss of pooling and a wrong-buffer association that could cause hangs, corruption, or severe performance loss
Update buffers Returned download buffers are owned correctly; the public updater was subsequently disabled, so no current supported upgrade path exercises this code

Distributed storage and private APIs

These changes are normally invisible to an S3 client but are compatibility changes for mixed clusters, custom internal callers, corrupted disks, and adversarial peers.

  • ReadMultiple and its private storage-REST /rmpl endpoint, client method, exported Go types, and metric were removed. External S3 List/Get operations are unchanged. storageRESTVersion remains v63, so do not infer mixed-node compatibility from the version number.
  • Every remote StorageAPI path field, nested metadata name, and raw-volume sink is validated at the storage boundary, including peer-S3 Grid messages. Lexical traversal, volume-root aliases, and Windows separator/drive forms are rejected.
  • Erasure geometry, non-positive blocks, negative part sizes, and unusable stored erasure metadata are rejected at all decoded sinks and during CheckParts/VerifyFile.
  • Internode allocation declarations are bounded: AppendFile caps preallocation at 1 MiB while still accepting the body; DeleteVersions grows as it decodes and rejects negative declarations; legacy ReadFile is capped at 5 GiB.
  • Deadline-bounded work converts worker panics into errors and logs a bounded stack instead of taking down the process.
  • ReadParts keeps the actual backend error across keepalive frames. An empty part list returns a successful empty result without a trace panic or leaked goroutine.
  • HTTP stream helpers left orphaned by ReadMultiple were deleted later; that cleanup creates no additional public behavior change.

This containment is lexical. It does not resolve filesystem symlinks, and native Windows server CI was not available for the audit. Upgrade every node together and keep untrusted clients away from the internode port even though root credentials and validation protect it.

Notification configuration and audit output

  • NATS now registers parser-consumed user_credentials, nkey_seed, and tls_handshake_first; AMQP registers immediate. Existing environment variable names stay unchanged.
  • The old literal MINIO_NOTIFY_NATS_USER_CREDENTIALS remains accepted for NATS only. Precedence is environment, new key, then old migration key.
  • AMQP legacy migration maps immediate correctly. Invalid notification errors identify names but no longer echo credential values.
  • PostgreSQL and MySQL notification configuration uses the canonical connection_string or dsn_string; explicit strings pass through unchanged. The retained internal PostgreSQL fallback builder quotes values and uses the correct user keyword, but discrete fields are not a supported KV input.
  • Dangling-object deletion audit events once again include per-drive errors under merrs.

The inherited database-notification migration risk is closed by f1ba68358: migration no longer writes unregistered discrete connection keys. An enabled pre-KV target without connection_string or dsn_string now stops startup with a credential-free error. Convert, disable, or remove such targets before upgrading; diagnostic bundles exported before this fix may contain a historical plaintext database password and should be treated accordingly.

Toolchain, dependencies, and embedded components

The build declaration moved from Go 1.24 plus a 1.24.8 toolchain to go 1.26.5. That can change TLS, HTTP, DNS, scheduler, garbage-collector, and standard-library edge behavior even where no Silo source line changed. Security-sensitive dependencies were also advanced, including Go-Jose, OpenTelemetry, Go crypto/network modules, cloud SDKs, etcd, NATS, and compression libraries.

Observable edge corrections inherited through those updates include Go TLS/X.509/URL/archive fixes; MQTT oversized UTF-8 packet encoding; malformed Azure NTLM challenge handling; Thrift framed transport and 32-bit compilation; NATS authentication, authorization, identity, and denial-of-service fixes; and Prometheus remote-read/write and UI hardening. They are dependency behavior changes, not a promise that every advisory path is reachable from Silo. The jsonparser CVE-2026-32285 investigation produced no patch: the resolved v1.1.2 already contained the fix and no vulnerable reachable symbol was found, so it creates no compatibility delta in this range.

Important deliberate dependency decisions are:

Component Final selection Compatibility rationale / effect
Console pgsty/silo-console v2.1.1 behind github.com/minio/console Restores the embedded UI, applies Silo branding and bilingual text, adds Metrics V3, removes SUBNET flows, and fixes untranslated metric legends
Client library pgsty/mc behind github.com/minio/mc Keeps Console’s import path while consuming the maintained MCLI fork
Shared package pgsty/silo-pkg/v3, consumed directly at its own module path (v3.14.0; behind minio/pkg/v3 via replace up to v3.12.x) Breaking module-path move in v3.13.0; supplies the IAM exact-match half, LDAP TLS/StartTLS/deadline/close fixes, certificate-watcher cleanup, and RNG fixes
Kafka Sarama 1.45.1 Pinned to avoid a breaking broker-negotiation drift
PostgreSQL lib/pq 1.10.9 Pinned to avoid a nil-[]byte / PostgreSQL-before-14 behavior regression; generated DSN quoting is fixed in server code
Compression klauspost/compress 1.18.7 Explicit security/correctness upgrade
Thrift 0.24.0 Fixes 32-bit builds
systemd library require 22.7, replace with 22.6 Retains NetBSD compilation until the monotonic-clock regression is fixed upstream

The LDAP package now honors TLS fields for ldaps://, keeps StartTLS active even with server_insecure, avoids a nil-TLS panic, applies a StartTLS deadline, and closes a connection after failed StartTLS. Certificate-watcher shutdown no longer leaks; on Windows, polling can delay reload by up to about ten seconds. RNG subkey entropy/reset behavior is corrected, although the server does not exercise the reset path.

For external Go consumers of silo-pkg, two changes are broader than this server’s own call paths: xtime.Duration JSON moves from integer nanoseconds to duration strings, and some AIStor action vocabulary / protected-action helpers differ from upstream. In particular, Policy.IsAllowedActions can disagree on protected actions; the server does not call it. The server stores the relevant state through YAML/msgp and does not call the differing AIStor/action helper paths, so no server data migration or authorization delta was found from those library changes.

Generated String() files were regenerated under the new toolchain. Valid enum output remains the same; the diff is generator provenance and invalid-value formatting machinery, not a separately claimed S3 behavior change.

Changes since the 2026-08-06 audit

The sections above describe the 219670d3 snapshot. The table below records the behavior changes merged to main after it and consolidated in the 20260903 release.

Area Change Where
Authorization Explicit version deletes are authorized as s3:DeleteObjectVersion and DeleteObjects authorizes each entry; user and group status changes require the action matching the target status; policy writes reject bare ARN prefixes #104, 58735ee38, 229fe2b3c, eee05a17c (SN-2026-005, -009, -010)
Replication trust Replication-only headers grant replication semantics only with the exact marker and s3:ReplicateObject / s3:ReplicateDelete; otherwise they are removed after signature verification #101 (SN-2026-008)
SSE-C Zero-byte objects and GetObjectAttributes authenticate the customer key; null-version and in-place key-rotation copies no longer rewrite objects into unreadable ciphertext; CopyObject reports checksums under the destination key b73581b05, 474cd5801, 05df6e70d, ffb70eb37, e73436c99 (SN-2026-006, -007)
Checksums Server-side part checksums, federated UploadPartCopy, ChecksumType in CompleteMultipartUpload, AWS-aligned completion errors, rejection of unknown algorithms and of CRC64NVME with COMPOSITE 7fea6d5a5, 8d76a255c, d014a12cf, 5d152416d, 7c103389f, d28885d0e
Listing ListObjects on a missing bucket returns NoSuchBucket on the shortcut paths that previously returned an empty listing e9c5340be
Per-bucket CORS Real ?cors API; a bucket configuration overrides the global policy; the pre-authentication lookup reads resident metadata only and otherwise applies the global policy; site replication converges CORS with a last-writer-wins register #71, #80, #101
Bucket metadata metadata.lock serializes every bucket-configuration writer; ForceCreate and site adoption keep existing configuration; a locked bucket always carries plain Enabled versioning #103, dd3bdb808
Site replication Object Lock configuration replicates in its own field (the legacy Tags carrier is still accepted); status is accounted per site; validity probes verify permissions under the rule prefix 3861f33cb, fb406fdc9, c9ad74673, 5db7be4ee
Configuration Legacy database notification targets require a DSN; MINIO_CONFIG_ENV_FILE uses a dedicated parser that keeps named targets f1ba68358, 6b0998157, 2aea7fe9c
Toolchain and components Go 1.27.1; upstream minio-go at 0e78d3f18efe (silo-go retired); silo-pkg v3.13.2; Console v2.3.0 (see the Console page); bundled mcli 20260903 43f4bb7ed, 4d6e1ea8e, final dependency refresh

The shared package’s own-module move (v3.13.0, a breaking change for Go consumers) was already adopted by published Server 20260903: that tag directly requires github.com/pgsty/silo-pkg/v3 v3.13.2. The September 13 refresh moves the maintained stack to v3.14.0; it is not the first Server adoption of the new path. See the component matrix for each build.

Known residual risks and non-fixes

This audit does not turn inherited limitations into claims of compatibility:

  1. Source IP is still forgeable by default. Unset MINIO_API_TRUSTED_PROXIES deliberately retains upstream trust-any behavior. Set none for direct deployments or an exact proxy allowlist for proxied deployments.
  2. Some version-condition gaps remain. MultiDelete governance-bypass reauthorization still consults the query/absent version rather than each XML entry, and Snowball reads the PAX minio.versionId after per-file authorization. Empty username, userid, signatureversion, and authType condition keys are also still inserted, so Null on them has present-empty semantics.
  3. Multipart parser defense is not complete. Handler-level ordering is fixed, but the object layer has no independent uniqueness defense; XML-root validation and the inherited nonnumeric-part error mapping were not changed.
  4. Legacy notification migration remains risky. Review it as described above.
  5. Storage path validation is lexical. Symlinks are not resolved; native Windows execution was not independently covered.
  6. Private APIs are not a stable compatibility promise. ReadMultiple proves that a same-numbered storage REST protocol can still lose an operation. Do not run a rolling mixed build across this boundary.
  7. A source result is not a released artifact. This page does not assert that GitHub tags, packages, OCI manifests, signatures, or the public site contain the three audit-head-only commits until each channel is verified separately.
  8. Informational HTTP responses remain imperfectly tracked. The response-tracking layer treats a 1xx response as final. The Flush/implicit-200 change did not introduce this behavior and does not claim to fix it.
  9. Conditional delete is not implemented. DeleteObject ignores the HTTP If-Match header and DeleteObjects ignores each <Object><ETag> element; both delete unconditionally (#10).
  10. Historical convergence limit for multi-site bucket-configuration deletion. In the released versions covered by this page, deleting a bucket policy, SSE, tag, or quota configuration on one site can be restored by a peer that still holds it (#77); only per-bucket CORS used a tombstone-aware register at that point. Deployments that rely on multi-site deletion of these configurations must verify every site after a delete. On 2026-09-12, PR #180 merged the repair into main. Full deletion recovery requires every node to be upgraded and deletion export to be enabled consistently; see the bucket configuration convergence record. Check each release artifact’s version for inclusion of the repair.

Migration checklist

For a MinIO-to-Silo move, use this order:

  1. Record the exact MinIO binary/tag, chart and values, image digest, package payload, service unit, environment files, config directory, data ownership, IAM policies, OIDC/LDAP settings, notification targets, and proxy topology.
  2. Back up configuration and IAM metadata. Silo reads existing disks in place, but a rollback still needs the old executable/config/chart and unchanged data ownership available.
  3. Replace invocations of the server binary with silo; do not assume /usr/bin/minio exists. In containers, argv-level minio server is translated, but the absolute path is not.
  4. Decide the config directory explicitly. Reuse --config-dir ~/.minio or let the legacy-only fallback select it; do not create an empty ~/.silo accidentally and then wonder why the old configuration is ignored.
  5. For packages, grant silo:silo access to data, certificates, secrets, and logs. Move deliberate overrides into /etc/default/silo; understand that it overrides /etc/default/minio.
  6. For Helm, render the old and new charts with the complete old values, preserve names with nameOverride / fullnameOverride / serviceAccount.name where required, and change chart plus image atomically.
  7. Remove updater, callhome, SUBNET-registration, and support-upload automation. Replace it with package/image/orchestrator rollout and your own diagnostic transfer path.
  8. Change HMAC-signed OIDC tokens to asymmetric JWKS. Exercise success, bad-password, unknown-user, backend-failure, and rate-limit LDAP paths.
  9. Add bare bucket ARNs for the twelve protected actions. Test effective tag, signature-age, source-IP, and per-version delete conditions. Use the legacy bucket switch only as a temporary rollback lever.
  10. Set MINIO_API_TRUSTED_PROXIES=none or an exact allowlist, sanitize all three source-address headers, and include cluster peers that forward authenticated requests.
  11. Test oversized S3 Select records, streaming notifications, unsigned-trailer rejection, multipart full-object checksums, duplicate parts, replication, healing, KMS, every notification target, audit ingestion, and graceful container shutdown.
  12. Upgrade all distributed nodes as one build. Keep the old chart and image paired for rollback; never roll back only one of them.

Verification evidence

The audit used the final source, not prose alone. At the recorded snapshot:

Check Result / boundary
Commit enumeration 96/96 commits classified in the ledger below; origin/main accounted for 93 and the local prepared head for three more
Net diff review All 523 changed paths classified across server runtime, internal protocol, dependency, delivery, documentation, tests, or superseded changes
Rebrand compatibility guard Passed; compatibility manifest and delivery/runtime assertions unchanged, including Docker argv tests
Go test suite Full go test ./... passed against 219670d31, including cmd, OIDC, LDAP, notify, event targets, Grid, handlers, hash, and all S3 Select packages
Helm migration buildscripts/verify-helm-migration.sh passed: lint, render, legacy upgrade, archive, and identity comparison across seven rendered resources
Package lifecycle buildscripts/package/lifecycle_test.sh passed; package payload/provenance assertions cover empty DEB conflict metadata, unit/default paths, and legal files
Site Strict make check passed: module verification, warning-fatal Hugo render (617 EN / 615 ZH pages), and 388,962 internal references across 1,084 HTML files; git diff --check, bilingual anchors, and 96/96 commit coverage also passed

Security articles contain deeper threat models and test vectors, but their historical “released/unreleased” labels describe their publication date. Where they conflict with the final snapshot, this page’s audited boundary is authoritative.

Complete commit coverage ledger

The hashes are in graph order. Merge, documentation, test, and CI commits are included because delivery behavior and the strength of a compatibility claim are themselves user-relevant; “no independent runtime delta” means exactly that, not “not reviewed.”

Class Commits Verified net effect
Initial fork, Console, CI, dependency base d4cd4b433, 8630937e7, 68521b37f, 00f3cf74f, 5abd9a80f, 377fc616d, f2f9a40dc, ee55e5391, ce1c537eb, 68e0ba997, 1869bd30b, ff58df949, e4fa06394 Go/SDK evolution; embedded Console restoration/fork; MCLI in OCI; replacement CI; LDAP TLS regression fix; security dependency upgrades. The two merge commits add no delta beyond their parents
April security series d24f449e0, 3b950f8fa, 56fa63bfd, 3252d5b7f, f444b6f37, efb6e5b00, db4c0fd5e, 18b712d49, 9e10f6d9a, f44110890, f48dbe777 OIDC, LDAP STS, replication metadata, S3 Select, unsigned-trailer/Snowball, Go 1.26.2, limiter accounting/source hardening, and security documentation
May–June reliability and private API 65795ee1f, 5e40665ac, fd69c89d0, 73ac52472, df627ff89, 3e61b1d3a, d495d30d5 HTTP Flush, final LDAP bucketing, full S3 Select bound, ReadMultiple removal, Go 1.26.4/dependency update, and documentation-link change
Pre-August component integration ce01ccbdc, 4dfc27ce3, b7f52ca43, 7babc0c39, c1aec0518, 15fcc3c8a, 3f192f3f0 Historical chart image switch, security dependency upgrade, notification-stream merge, portable dependency pins, compression, MCLI replacement, Console v2.0
August runtime correctness/security c8590413f, 3e14733f1, 924717926, 89d346bf5, 8069a32ac, a36fd8fff, ca7baa670, 80e8eaa42, b6f70ab08, 1af351a70, 38366f654, 22c1e41fd, 97b7d2804, 2f55347f7, 744a9dcd7, fe6dc4780, 162ded343, 0c14d8151, 9dd1dc172, 2602177ef Multipart, erasure buffers, response commits, panic containment, path/metadata/allocation/ReadParts containment, orphan cleanup, IAM/effective values/version ID/source trust, notification/libpq, and audit details
Chart hardening and audit documentation dfe669862, 5f4513fd4, b42ee4e8a, 8eae745ab Secure chart user default, portal/doc routing, maintainer ignore rules, and advisories; only the chart default changes runtime delivery behavior
Release engineering through the 20260804 tag 9c799f42d, 10c7670b8, cf7df097b, 32863c852, 632ade111, 1814ae52f, 475236c79, 11d79fddc, 3b8a55dee, ca674a696, 4c185d5a6, 2ca4971d9, e064b5555, aa5139369, 021110b45, d88f46cce RPM/DEB/APK, provenance, OCI publishing gates, pinned lint/generation, S3 Select test-race fix, broad CI, PID-1 signal fix, published-target cross-builds, safe release dispatch, reproducibility, stale-config removal, systemd location, honest gates, and runtime-image shutdown assertion
Silo cutover and 2026-08-06 prepared head 15def34dc, 77bdc4c0c, 15ab10833, 30749911b, e071bb77e, bd8df5166, 6613c2a3c, fd2ca1c6d, c46b16ec6, c47733abc, f1c77d5a2, 62717d7bf, 6740e6978, b57275be3, 05be686b8, a6d6d9b02, 6bd9cf77e, 219670d31 Removes unpublished MinIO delivery residue; Silo runtime identity/offline boundary; renamed packages, OCI and Helm with migration guards; pinned fixtures; docs/repository cutover; Console 2.1.0 then 2.1.1; Node 24 actions; DCO/legal/docs polish; regenerated CREDITS; LICENSE/NOTICE delivery

The ledger totals 96 unique commits. Changes replaced inside the range—such as Console 2.0 → 2.1.0 → 2.1.1, the historical pgsty/minio image/chart state, and updater-buffer code after the updater was disabled—are described only where they leave a final compatibility consequence.

See also

6 - MCLI Client Compatibility Notes

Compatibility, behavior changes, installation, and the current pgsty/mc release

mcli is Silo’s build of the MinIO Client (mc). This page records where the two are interchangeable and where they differ.

pgsty/mc forked from the upstream minio/mc at its final commit, 77f82e18 (2025-11-06). The upstream repository was archived in July 2026 without ever cutting a release that contains that commit — so every mcli release is strictly newer than any official mc binary ever published. Documented fork releases: 20260313, 20260321, 20260417, 20260804, 20260806, 20260903 and 20260913. The latest is 20260916; historical notes retain their own comparison baselines.

Tip

Current release: RELEASE.2026-09-16T00-00-00Z, package 20260916000000.0.0, source e952aa78f10a. Six OS/architecture archives, RPM/DEB/APK and multi-architecture images are published. See the release notes and component matrix.

Current release changes

20260916 uses pkg v3.14.1, upstream SDK 32e1f32cb176, JWX v3.3.0 and Go 1.27.1. The SDK retries and reports S3 errors embedded in CopyObject HTTP 200 responses, so a failed copy cannot authorize mv to remove its source. JWX fixes JSON field-name escaping; pkg v3.14.0 password-policy semantics are unchanged.

  • Per-object mirror permission errors are reported and accumulated while later objects continue; finite jobs exit 1. Failed local destination removals are no longer swallowed. These permission failures do not require --skip-errors to continue; watch listing/watcher errors retain their existing cancellation/retry behavior.
  • Failed mirrors suppress normal final success statistics. Explicit --summary still reports statistics, with JSON status: failure; text errors go to stderr and statistics to stdout.
  • Failed legalhold set/clear, partial recursive retention failures, and mv source-deletion failures now return nonzero. Successful objects are not rolled back. Retention reports each failed object once.
  • Empty retention durations and invalid find regexes produce normal errors instead of panics. Very short transfers no longer produce infinite speeds in JSON statistics.

Automation migration: affected failure paths that returned 0 now return 1. Check the final exit code and error records; per-object success start messages and progress bytes are not completion proof. Configuration, aliases and MC_* variables are unchanged. See the release notes.

Changes inherited from 20260913

20260913 uses pkg v3.14.0, upstream SDK 60bd07042d49 and Go 1.27.1, with refreshed Go x/* modules. It preserves mirror destination history and fixes restart dry runs, noninteractive behavior, transfer/SQL failure exits, explicit checksums for empty uploads and quiet JSON output. Boolean environment values accept on/off. Deny/NotResource and bounded wildcard fixes preserve clauses that could previously be lost; recover already-lost clauses from the original policy. The password-permission split requires Console v2.4.1 and a matching Server. Review the existing policy denies as part of the coordinated upgrade.

Changes inherited from 20260903

The 20260903 client keeps the upstream command, configuration, protocol, and JSON contracts, with these deliberate additions and tightenings since 20260806:

  • Read-only checksum audit: mc checksum verify streams object data and compares stored FULL_OBJECT CRC32, CRC32C, CRC64NVME, SHA1, or SHA256 values. It supports one object, recursive prefixes, exact or all versions, JSON Lines manifests, dry runs, bounded workers, size/time filters, SSE-C, machine-readable output, private report files, and explicit failure policies. It never repairs or mutates an object.
  • Credential output is fail-closed: --debug, admin trace, errors, redirects, trailers, JSON documents, SSE-C paths, custom headers, and registered runtime secrets are scrubbed before display. Support and cluster-export artifacts, including rotated copies, are created with mode 0600 on POSIX systems.
  • Correctness repairs: JSON Prometheus metrics no longer panic; empty pipe input uses a regular zero-byte PUT; S3 Select responses are no longer closed twice after valid rows are printed; malformed app-level global flags fail instead of being ignored; and service-account policy clearing works again.
  • Strict writes, permissive reads: named policy and service-account write paths reject empty or malformed policies, bare ARN namespaces, and mixed S3/admin action statements. Existing stored policies remain readable; an empty service-account policy still means “clear the inline policy.”
  • Dependency and toolchain floor: builds require Go 1.27.1 and directly consume github.com/pgsty/silo-pkg/v3 v3.13.2. minio-go is pinned to upstream master commit 0e78d3f18efe; gRPC is 1.83.2 and x/crypto is 0.56.0. Darwin binaries require macOS 13 or newer.
  • Verifiable delivery: the public release is immutable, contains 19 checksum-checked assets, and binds archives plus DEB/APK packages to the signed tag and exact source with Sigstore attestations. RPMs carry the PGSTY GPG signature. The release and latest container tags publish matching linux/amd64 and linux/arm64 manifests.

Two low-level compatibility changes are worth testing in specialized deployments: JWX 3.2 rejects non-compliant JWT crit processing more strictly, and updated Unicode width handling can slightly change table/progress alignment for Indic or ZWJ-heavy strings.

Principles

The fork follows one rule: the shipped artifact and its channels are renamed; the tool you use is not.

  • Renamed / replaced — the artifact name on disk (mcli), the product identity in --version and --help, the distribution channels (GitHub pgsty/mc, the Pigsty repository, docker.io/pgsty/mc), and the signing keys. Not the command syntax, and — depending on how you install it — not even the name you type.
  • Preserved interfaces — familiar command syntax, S3/admin protocols and x-minio-* identifiers; versioned output, authorization and error-handling changes are documented above; the configuration file format and alias semantics; MC_* environment variables (including MC_HOST_<alias>); the .part.minio resume suffix; and the Go module path github.com/minio/mc.
  • Severed — every connection to MinIO-operated services: the release/update feed, the SUBNET support and licensing portal, telemetry, and the pre-seeded play demo alias. Affected commands remain in the CLI for script compatibility and fail with a stable error rather than disappearing.
  • Preserved — upstream copyright and the AGPL-3.0 license. Runtime output credits both MinIO, Inc. and PGSTY.

A configuration written by upstream mc is readable by mcli unchanged, and vice versa. SILO releases test mcli against Silo; operation against upstream MinIO and other S3-compatible endpoints is retained on a best-effort basis and should be verified with the exact versions in use.

What changed

Ordered by how likely each change is to affect you, most likely first.

1. The name — what you type, and where the config lives

For many users nothing changes here: the container image keeps mc as its entrypoint, and a binary installed under the name mc behaves identically to upstream. What changed is what we ship — archives and Linux packages install the binary as /usr/local/bin/mcli (package name mcli).

Neither name is hardcoded anywhere. Since 2016 the upstream client has derived its runtime identity from the name it is invoked as, and mcli is the exact rename upstream’s own CONFLICT.md recommended (issue #873) for the Midnight Commander clash — this fork merely promoted that suggestion to the official shipping name, with zero code changes. What that mechanism means in practice:

Follows the invoked name Fixed, regardless of the name
Configuration directory: ~/.mc vs ~/.mcli (Windows: %USERPROFILE%\mc\ vs …\mcli\) Environment variables: always MC_* — there is no MCLI_CONFIG_DIR
Program name shown in help and usage text config.json format — identical and interchangeable in both directions
Shell-completion registration All commands, flags, JSON output, exit codes
User-Agent application suffix (mc/… vs mcli/…) --config-dir and MC_CONFIG_DIR overrides

The one real trap: run mcli for the first time and your existing mc aliases are not there — it starts from an empty ~/.mcli. Either keep invoking it as mc (a symlink suffices — argv[0] is what counts), or copy the state once with cp -a ~/.mc ~/.mcli. For automation and configuration templates, set MC_CONFIG_DIR explicitly: the environment prefix does not follow the name, so one template serves both. Details in Migration.

Get it from GitHub Releases (SHA-256 mcli_<version>_checksums.txt), the Pigsty repository (RPMs GPG-signed, key fingerprint 9592A7BC7A682E7333376E09E7935D8DB9BD8B20), or docker.io/pgsty/mc. Upstream’s minisign key does not sign these artifacts, and dl.min.io is never contacted. Release tags (RELEASE.YYYY-MM-DDTHH-MM-SSZ) and package versions (YYYYMMDDHHMMSS.0.0) keep their upstream schemes.

2. mcli update always fails — on purpose

Self-update is removed. mcli update never contacts the network and never replaces its binary; it prints an explicit notice and always exits 1. Upstream mc update exited 0 when already current, so any cron job or script that calls it and treats a non-zero exit as failure will start failing — drop the call and upgrade through your package manager or GitHub Releases instead. The per-invocation version probe against upstream release feeds is also gone, and MC_UPDATE / MINIO_UPDATE are no longer consulted.

(mcli admin update ALIAS — updating the server — still exists, but Silo servers reject in-place updates server-side.)

3. SUBNET, licensing, and telemetry commands

Everything that reached MinIO SUBNET is disabled at build time. Affected commands keep their names and flags, print a stable notice — “MinIO SUBNET services (registration, licensing, uploads) are disabled in this Silo build of mc; diagnostics remain available locally.” — and exit 1:

Command Behavior now Use instead
mcli license register notice, exit 1
mcli license update ALIAS (online renewal) notice, exit 1 mcli license update ALIAS license.key (offline, still works)
mcli support upload notice, exit 1 share files through your own channels
mcli support proxy set notice, exit 1 proxy remove still clears a legacy setting
mcli support callhome enable notice, exit 1 disable / status still work

The diagnostics themselves stay: mcli support diag / perf / profile / inspect always run in local (airgap) mode — results are written to local files, nothing is uploaded, and SUBNET registration is no longer a prerequisite. Two related hardening changes: inspect no longer falls back to encrypting output with an embedded MinIO public key (your archives stay decryptable by you), and since 20260804 --debug output redacts SUBNET credentials — if you ever shared debug logs from older builds, rotate the keys in them. mcli license info and unregister work locally.

4. The play demo alias is no longer pre-seeded

Fresh configurations seed local, s3, and gcs — not play. Tutorials and smoke scripts that assume the demo alias need it added explicitly: mcli alias set play https://play.min.io <access-key> <secret-key> restores the old behavior, since nothing blocks deliberate access to any S3 endpoint. Existing configuration files are never modified.

5. Output text carries the Silo identity

mcli --version keeps its machine-readable first line and adds an identity line plus dual copyright; --help says “Silo client” and examples use mysilo. Command syntax is untouched — only scripts that grep for upstream identity strings (e.g. “MinIO Client”) need adjusting.

6. For developers

The module path stays github.com/minio/mc, so imports compile unchanged — but go install github.com/minio/mc@latest installs the archived upstream, not this fork. Build from source (git clone https://github.com/pgsty/mc && cd mc && make) or consume it via a replace directive. Contributions need no CLA but require a DCO sign-off (git commit -s). Upstream being archived also means inherited defects are only ever fixed here — most notably minio/mc#5139 (mirror --remove --watch on versioned buckets).

Migration

Moving from an official mc binary to mcli:

  1. Install mcli from one of the fork’s channels (see §1 for verification): GitHub Releases archive, yum install mcli / apt install mcli from the Pigsty repository, or docker pull pgsty/mc.
  2. Decide what to call it — this determines which configuration it reads:
    • Keep the mc name (least friction): after confirming no upstream binary remains (command -v mc), install it as mc — e.g. ln -s /usr/local/bin/mcli /usr/local/bin/mc. Invoked as mc, it reads your existing ~/.mc untouched; nothing else to migrate.
    • Adopt the mcli name: carry your state over once with cp -a ~/.mc ~/.mcli, or set MC_CONFIG_DIR=~/.mc. Both clients can also coexist side by side, each with its own directory.
  3. Clean up automation:
    • remove mc update calls — they now always exit 1;
    • remove license register, support upload, support callhome enable, and support proxy set — same stable failure;
    • support diag / perf / profile / inspect keep working and write local files; drop any step that expected a SUBNET upload;
    • review anything that greps --version output beyond the first line.
  4. Re-check play usage in tutorials and smoke scripts (§4).
  5. Verify: mcli --version, mcli alias ls, then mcli ls <alias> and mcli ping <alias> against your servers.
  6. Rollback stays trivial: the configuration format is identical in both directions, so keeping the old mc binary around lets you switch back at any time.

See also

7 - Native Package Migration

How the silo RPM/DEB packages differ from the minio packages: file layout, service account, takeover semantics, and caveats.

Silo publishes silo packages for RPM, DEB, and APK on amd64/arm64 via GitHub Releases, with SHA-256 sums and build-provenance attestations. This page records what changes relative to a minio package installation: the file layout, the service account, and the caveats. General migration scope is in the migration guide.

Installing

Install the package that matches your platform from the release assets, then verify the checksum before installing:

curl -fLO https://github.com/pgsty/silo/releases/download/<RELEASE-tag>/silo-<version>.<arch>.rpm
sha256sum --check silo-<version>.<arch>.rpm.sha256sum   # or compare manually
sudo rpm -i silo-<version>.<arch>.rpm                    # Debian/Ubuntu: sudo dpkg -i silo_<version>_<arch>.deb

If you use the Pigsty package repository, dnf install silo / apt install silo resolves the same artifacts (the repository may lag GitHub Releases). The package intentionally provides no minio alias or Provides: relationship — minio and silo are separate packages that coexist, and the takeover happens at the systemd level, not through package replacement (see Takeover).

File layout

MinIO installation Silo package
/usr/bin/minio /usr/bin/silo (also provides silo healthcheck)
minio.service /usr/lib/systemd/system/silo.service
/etc/default/minio Still read, first; /etc/default/silo overrides per variable (noreplace/conffile — upgrades never overwrite edits)
service account minio-user (upstream) / minio (Pigsty) silo:silo, declared in /usr/lib/sysusers.d/silo.conf, created on install
/usr/share/doc/silo/LICENSE, NOTICE (AGPL-3.0-or-later)

Two package properties:

  • Installation never starts or enables the service; postinstall only creates the silo account and reloads systemd.
  • The package installs alongside the minio package — no file conflicts, so the old package stays available for rollback.

Service account

The unit defaults to User=silo, but existing data, TLS keys, and KMS credentials belong to the old MinIO user. Do not chown the data. Run Silo as the current owner via a drop-in:

ls -ld /path/to/your/data              # note the owner, e.g. minio-user
sudo mkdir -p /etc/systemd/system/silo.service.d
sudo tee /etc/systemd/system/silo.service.d/10-legacy-user.conf <<'EOF'
[Service]
User=minio-user
Group=minio-user
EOF
sudo systemctl daemon-reload

This also keeps TLS working: Silo resolves certificates from the runtime user’s home (~/.silo/certs, falling back to the legacy ~/.minio/certs), so the existing public.crt/private.key/CAs/ are found without copying. Without the drop-in, a TLS deployment fails to start:

FATAL Unable to start the server: HTTPS specified in endpoints,
      but no TLS certificate is found on the local machine

Adopting the silo account is an optional later change: move the certificates to a silo-readable path, set --certs-dir in MINIO_OPTS, and transfer data ownership outside the migration window.

Takeover and rollback

The unit is a takeover unit:

[Unit]
After=network-online.target minio.service
Conflicts=minio.service

[Service]
Type=notify
EnvironmentFile=-/etc/default/minio
EnvironmentFile=-/etc/default/silo
ExecStart=/usr/bin/silo server $MINIO_OPTS $MINIO_VOLUMES
Restart=always
  • Conflicts=minio.service: systemd never runs both; starting one stops the other. This implements takeover and rollback in both directions.
  • The EnvironmentFile chain means MINIO_VOLUMES, MINIO_OPTS, credentials, and KMS settings from /etc/default/minio apply unchanged.
  • Type=notify: systemctl start returns success only after the server is actually ready.

Switch over:

sudo cp -a /etc/default/minio /etc/default/minio.migration-backup   # cheap insurance
sudo systemctl disable --now minio.service
sudo systemctl enable  --now silo.service
silo healthcheck --url https://127.0.0.1:9000 ready    # http:// without TLS
mc admin info <existing-alias>

Roll back (nothing to restore — data ownership, certificates, and the old unit were never touched):

sudo systemctl disable --now silo.service
sudo systemctl enable  --now minio.service

After validation completes and the rollback window closes, optionally mask the old unit so nothing but an explicit systemctl unmask can bring it back:

sudo systemctl mask minio.service

Caveats

  • Clusters switch all nodes together. Two different binaries do not form a cluster — MinIO next to Silo, or one Silo version next to another; a mixed node waits indefinitely in activating (details). Prepare every node first (install package, create drop-in), then flip all nodes in quick succession: systemctl disable --now minio && systemctl enable --now --no-block silo. Rollback and later upgrades likewise: all nodes together.
  • Non-packaged installations work the same way. A /usr/local/bin/minio with a custom unit is taken over identically, as long as its configuration lives in /etc/default/minio.
  • Crash loops rate-limit. A misconfigured start (for example, missing certificates) repeats under Restart=always until systemd’s start limit trips (Start request repeated too quickly). Fix the cause, then systemctl reset-failed silo && systemctl start silo.
  • An old minio.service stop can hang. Legacy units commonly set TimeoutSec=infinity. If graceful shutdown remains stuck after traffic is drained and the shutdown allowance expires, an operator can force it with sudo systemctl kill --signal=SIGKILL minio.service, then confirm the old process has exited before starting Silo. This interrupts any remaining requests; plain systemctl kill defaults to another SIGTERM and does not resolve a process that ignores it.
  • The environment chain can surprise you during the bridge period. /etc/default/minio is still read while /etc/default/silo exists: deleting a variable from /etc/default/silo does not disable it — the old value from /etc/default/minio applies again. Remove the variable from both files, or comment it out in the file that still carries it.
  • Keep the rollback window. Leave the minio package, unit, and binary installed until validation completes; a disabled unit costs nothing. Remove the old package afterwards if desired.
  • Rolling restarts after migration: gate each with silo healthcheck --maintenance cluster; exit 0 means stopping this node keeps write quorum, HTTP 412 means it does not.

8 - Console Compatibility Notes

Differences between SILO Console and the upstream MinIO Console

Latest release: Console v2.4.1 (2026-09-16), with restricted shared downloads, password-permission separation, streaming ZIPs and signed release artifacts. See the component matrix.

SILO Console is Silo’s build of the MinIO Console. This page records where the two are interchangeable and where they differ.

pgsty/silo-console continues the upstream minio/console history from its final commit, feff71e4 (2026-04-16); the rebrand begins at 50797deb (2026-08-04). The upstream repository is no longer published — github.com/minio/console now returns 404, where minio/mc was merely archived — so the source lineage survives only in this fork. The Go module path still resolves, because the module proxy continues to serve the versions it already cached. Earlier release records: v2.0.0, v2.1.0, v2.1.1, v2.2.0, and v2.2.1.

Principles

The fork follows the same rule as the rest of Silo: the shipped artifact and its channels are renamed; the interfaces other software depends on are not.

  • Renamed — the artifact on disk (silo-console), the product identity in the interface and in --version, the distribution channels, and the signing keys.
  • Unchanged — the Go module path github.com/minio/console, every CONSOLE_* environment variable (including CONSOLE_MINIO_SERVER and CONSOLE_MINIO_REGION), the REST API shapes the web application calls, and the packaging identifiers minio-console.service, console-user, and /etc/default/console, so an in-place package upgrade keeps working.
  • Severed — automatic self-update, telemetry, analytics, beacons, external scripts and fonts, and call-home. A release catalog is contacted only when one is explicitly configured, through SILO_RELEASE_SERVICE_HOST with RELEASE_SERVICE_HOST retained as a fallback.
  • Preserved — upstream copyright and the AGPL-3.0 license. Runtime output credits both MinIO, Inc. and PGSTY.
Note

SILO Console is not a generic S3 browser. Its administrative features need the MinIO-compatible administration APIs that Silo implements in addition to the S3 API.

What changed

1. The full administration console is retained

This is the largest functional difference, and it runs opposite to the usual direction of a fork. Upstream reduced its community console to an object browser. SILO Console keeps the complete administrative interface: dashboards, health, logs, diagnostics, and speed tests; bucket, object, lifecycle, replication, notification, and tier management; users, groups, service accounts, policies, identity providers, and KMS setup; and server configuration.

2. The dashboard targets Metrics V3

Dashboard widgets query the MinIO Metrics V3 catalog, the metric set current deployments actually scrape, with guards for its zero-value and per-node export semantics so a panel distinguishes a real zero from missing data. The mapping is recorded in docs/metrics-v3.md.

3. A smaller, quieter payload

The embedded frontend went from roughly 10 MB to under 3 MB, rebuilt reproducibly byte for byte and enforced by a release gate. There is no telemetry of any kind, and no external network dependency in the page itself.

4. Bilingual interface

The interface, help content, and documentation links are available in English and Chinese behind a per-page toggle, with no added runtime dependencies.

5. For developers: the module graph

Console v2.4.1 directly requires github.com/pgsty/silo-pkg/v3 v3.14.1 and upstream SDK v7.3.1-0.20260915093545-32e1f32cb176, retaining the historical github.com/minio/console module path. Embedders explicitly select these released sources:

replace github.com/minio/console => github.com/pgsty/silo-console v0.0.0-20260916075814-1360e26d976d
replace github.com/minio/mc => github.com/pgsty/mc v0.0.0-20260916070421-e952aa78f10a

Go does not inherit dependency replacements. Server must select both PGSTY Console and MC. go-systemd v22.6.0 preserves NetBSD compatibility and tablewriter v0.0.5 preserves the MC API. Legacy transitive minio/pkg from colorjson remains separate from the maintained silo-pkg policy implementation. The old minio/pkg => silo-pkg and minio-go => silo-go replacements are unsupported. See the component matrix and embedding guide.

The release-gating target is the coordinated SILO, Console, mcli and pkg stack. Upstream MinIO/MC probes remain non-blocking compatibility signals and do not require pkg downgrades or duplicate APIs.

Migration

The official image is docker.io/pgsty/silo-console. Pin :v2.4.1 for a specific release; latest advances after publication and image verification.

An existing MinIO Console deployment upgrades in place. The service unit, service account, and configuration file keep their names, and every CONSOLE_* variable is read unchanged, so the usual path is to install the silo-console package over the old one and restart.

Two behaviors change on first start and are worth expecting:

  • silo-console will not update itself. Roll out new versions through packages, images, or your orchestrator.
  • Any workflow that relied on the console reaching MinIO-operated services — the update feed, licensing, or telemetry — no longer has anything to reach.

For v2.4.1, also review the password-policy migration. Linux packages use /etc/silo-console/certs; migrate existing certificates or retain their old path in CONSOLE_OPTS in /etc/default/console before restarting. The service and configuration names are retained. Shared downloads require no new setting. See the v2.4.1 release notes.

See also

9 - Feature Notes

Design notes for capabilities Silo adds beyond upstream MinIO — written down before they ship.

The component pages in this section record where Silo matches MinIO. This subsection records the places where Silo deliberately goes beyond it: each page is a design note, written before the implementation lands, and kept afterwards as the authoritative record of what was decided and why.

A note here describes intent, not necessarily shipped behavior — every page carries a status line saying which it is.

9.1 - Native Health Checks and the Distroless Image

Why the silo binary grows a healthcheck subcommand, why mc ready had to be retired as a probe, and how the single-binary distroless image is planned.

Status: P1 (subcommand, 2ff594f4b) and P2 (distroless image + CI gate, 4c34d2309) implemented in pgsty/silo; P3 (Helm probes) and P4 (docs) pending · Decided: 2026-08-06 · Owner: pgsty/silo (command, images, Helm chart), this site (docs)

Silo is getting a native silo healthcheck subcommand and, alongside the existing container image, a new distroless image variant that contains exactly one file that matters: the silo binary. This note records the reasoning and the design decisions before implementation, so the code has a specification to be checked against — and so that “why is it built this way?” has a permanent answer.

Background

Today’s release image (docker.io/pgsty/silo) is built on ubi-micro and ships four moving parts: the silo server, the mcli client (with an mc alias), a statically linked curl, and a POSIX-shell entrypoint script. The compose examples check container health with the bundled client:

healthcheck:
  test: ["CMD", "mc", "ready", "local"]

That arrangement is inherited from upstream MinIO, and its fragility is a matter of record: when mc was briefly missing from the image, users found their health checks failing “with no option but to disable” (#9). Upstream’s own history rhymes — when MinIO moved to ubi-micro in 2023 and lost curl, the maintainers’ answer was to lean harder on mc ready local (minio/minio#18373, #18389), and upstream minio/minio has since been archived with server as the only subcommand its binary ever had. Nobody upstream is going to fix this.

A distroless image forces the question. There is no shell, no curl, no mc — by design. The only program guaranteed to exist inside the container is the server binary itself. If Docker-level health checking is to exist at all in that image, the binary has to provide it.

Why mc ready had to be retired as a probe

Reading the actual mc implementation (cmd/ready-main.go) shows the current health check works by accident, not by design. Four independent defects:

  1. It never fails on its own. mc ready is a wait-until-ready loop: it retries every 5 seconds forever and only ever exits zero, on success. Connection refused does not break the loop. As a Docker healthcheck, the “unhealthy” verdict is produced entirely by Docker’s timeout killing the process — the probe semantics are a side effect of SIGKILL.
  2. It checks the wrong scope. mc ready requests /minio/health/cluster — cluster-wide write quorum. Every container’s “health” therefore reflects the state of the whole cluster, which is precisely the cascading-failure anti-pattern the Kubernetes documentation warns about: lose quorum, and every node is marked unhealthy simultaneously.
  3. It has hidden failure modes. It requires a writable ~/.mc config directory (on a read-only rootfs or an arbitrary OpenShift UID, the probe fails while the server is perfectly healthy), it prints config-creation noise on first run, and its built-in local alias is hardcoded to http://localhost:9000 — wrong the moment TLS is enabled or the port changes, a limitation users complained about upstream.
  4. It is the last functional reason to bundle a second binary. Both mcli and the pinned static curl carry ongoing supply-chain and maintenance cost (the curl pin is stuck on v8.11.0 because a later release dropped the aarch64 build) for what a subcommand of the existing binary can do in ~150 lines.

The decisions

Three tracks, deliberately decoupled:

# Decision
D1 The silo binary gains a healthcheck subcommand — a thin, anonymous HTTP client for the server’s existing /minio/health/* endpoints. It ships in every build, so every image and bare-metal install gains the capability.
D2 The existing image does not change. It keeps mcli, curl, the shell entrypoint, and the mc ready local examples. Users of the current image who want the new probe can opt in by overriding their healthcheck.test — nothing is taken away and no default behavior moves.
D3 A new distroless variant is published alongside it, as a pilot: single binary, no shell, native HEALTHCHECK baked in. If the pilot proves out, it becomes the recommended default later and the switch completes; the classic image remains for compatibility either way.

D2 and D3 answer the obvious “why not just slim the main image?” — because the main image’s contents are a compatibility surface. #9 exists because that surface was changed underneath people once before. The distroless image is a new name with a new contract, so nobody’s existing healthcheck, docker exec mc habit, or entrypoint assumption breaks while it is evaluated.

The silo healthcheck command

silo healthcheck [FLAGS] [CHECK]

CHECK — positional, maps 1:1 onto /minio/health/<path>:
  live          the process is serving (default; touches no external system)
  ready         live + KMS and etcd reachable, when configured
  cluster       cluster-wide write quorum
  cluster-read  cluster-wide read quorum

FLAGS:
  --address value   target host:port  (EnvVar: MINIO_ADDRESS; default ":9000",
                    an empty host is completed to 127.0.0.1)
  --url value       full base URL override (http[s]://host:port); wins over
                    --address and TLS auto-detection (EnvVar: MINIO_HEALTHCHECK_URL)
  --maintenance     cluster only: appends ?maintenance=true — asks "is it safe
                    to take this node down?" (HTTP 412 = no, it would break HA)
  --timeout value   overall deadline; defaults: 5s for live/ready, 15s for cluster*
  plus the inherited global flags: --certs-dir, --config-dir, --json, --quiet

EXIT CODE:  0 = healthy / safe to proceed · 1 = anything else
OUTPUT:     one line, e.g.
  live: ok (200, 2ms)
  cluster: unhealthy (503) server-status=iam-offline write-quorum=3 healing-drives=2

The design principles behind that shape:

  1. Thin client, single source of truth. The command is only ever an HTTP client of the canonical health API. It never re-implements a check in-process, so the semantics of “healthy” live in exactly one place: the server handlers.
  2. CLI vocabulary = API vocabulary. The check names are the endpoint paths. No new concepts to learn, nothing to keep in sync.
  3. Share the server’s own configuration. The port comes from the same --address/MINIO_ADDRESS contract the server uses, and http-vs-https is decided by the same certificate check the server itself performs at startup (public.crt + private.key in the certs directory). This is the Traefik healthcheck pattern — the closest prior art, which resolves its ping endpoint from the same static configuration as its server — with the TLS handling Traefik left as a // TODO actually implemented. It is also the direct fix for mc ready’s port-guessing defect.
  4. The default check is node-local. live answers “is this process serving,” which is the only question a per-container health status should answer. Cluster-scope checks exist, but only behind explicit arguments, mirroring mc ready’s --cluster-read/--maintenance so the operational vocabulary carries over.
  5. Exit codes are 0 and 1, nothing else. The Dockerfile reference explicitly reserves exit code 2 (vault status, which uses 2 for “sealed”, is the cautionary tale). Rich diagnostics belong in the single output line instead — Docker stores the first 4096 bytes of probe output in docker inspect, and the command decodes the server’s diagnostic headers (x-minio-server-status, x-minio-write-quorum, x-minio-healing-drives) into it, which is exactly the detail a bare curl -f throws away.
  6. Skip TLS certificate verification, with no opt-out in v1. This is a loopback self-probe of an anonymous endpoint carrying no data — and the kubelet’s documented behavior for HTTPS httpGet probes is precisely the same. Matching it means one TLS deployment produces one verdict across Docker and Kubernetes; verifying by default would only manufacture false negatives, since self-signed server certs rarely carry a 127.0.0.1 SAN.

Two implementation constraints, discovered in the source, that are load-bearing rather than stylistic:

  • The request must be strictly anonymous. The health routes are exempted from the reserved-path guard only for requests the server classifies as anonymous; attaching an Authorization header reclassifies the request and gets it rejected (ErrAllAccessDisabled) instead of answered.
  • The HTTP transport must set Proxy: nil. Containers routinely inherit HTTP_PROXY without a NO_PROXY entry for 127.0.0.1; a loopback probe must never route through a corporate proxy. (Traefik’s healthcheck does this deliberately, for the same reason.)

And one number that looks arbitrary but is not: the 15-second default timeout for cluster checks exists because the server evaluates cluster health under its own 10-second cluster_deadline — a client that gives up at 5s abandons the request before the server delivers its considered 503, losing every diagnostic header with it. Two details were added after adversarial review: --url is environment-backed (MINIO_HEALTHCHECK_URL) because a probe process cannot see the server’s command line — it is the documented way to point a baked-in HEALTHCHECK at a server whose address or TLS setup comes from CLI arguments; and any outer (Docker) timeout must exceed the probe’s own deadline, or the probe is SIGKILLed before it can print its diagnostic line.

What the endpoints really do

The table below is verified against the handler source, not quoted from documentation — and it corrects a common misreading:

Endpoint Returns 200 when… Fails with… Notes
/minio/health/live almost always — even before the object layer is initialized (that state is only signaled via the x-minio-server-status: offline header) 503 when the request queue is saturated touches no external system; the only endpoint quiet enough for high-frequency probing
/minio/health/ready as live, plus KMS can generate a key and etcd answers a read — each only if configured KMS/etcd failure; queue saturation without KMS or etcd, ready and live are the same code path
/minio/health/cluster object layer, bucket metadata and IAM are initialized, and every erasure set has write quorum 503 with quorum diagnostic headers; with ?maintenance=true, failure is 412 each failed evaluation writes a server-side log line — do not poll it tightly
/minio/health/cluster/read the read-quorum version of the above as above

Consequences worth spelling out: live and ready are liveness-grade signals — they do not tell you the node can serve objects; only the cluster pair does. That is exactly why the cluster pair must stay out of per-container probes (scope, log noise, cascading restarts) and why it is the right tool for operational questions like “may I take this node down?” (--maintenance, where 200 means safe and 412 means you would lose HA).

The distroless variant

Base: gcr.io/distroless/static-debian12 — which is sufficient because silo builds with CGO_ENABLED=0. The base ships the four things the server actually needs from a rootfs: CA certificates (for KMS/webhook/STS egress), tzdata, /tmp, and an /etc/passwd with root/nonroot entries. It ships no shell, no package manager, no libc.

Sketch of the contract:

FROM gcr.io/distroless/static-debian12:latest
COPY silo /usr/bin/silo
COPY LICENSE NOTICE CREDITS /licenses/
ENV HOME=/tmp
# /data is created in the image layer, world-writable — see issue #55:
# there is no entrypoint left to repair ownership at runtime.
VOLUME ["/data"]
EXPOSE 9000
HEALTHCHECK --interval=30s --timeout=10s --start-period=2m --start-interval=2s --retries=3 \
  CMD ["/usr/bin/silo", "healthcheck", "ready"]
ENTRYPOINT ["/usr/bin/silo"]

The decisions folded into that sketch:

  • ENTRYPOINT is the binary itself. docker run pgsty/silo:distroless server /data — no argv-translation script, because there is no shell to run one. The classic image’s MINIO_USERNAME/MINIO_GROUPNAME privilege-drop path (which needs GNU chroot and a writable /etc/passwd) is not supported in this variant; the supported mechanism is --user / Kubernetes runAsUser.
  • /data is created in the layer, mode 0777, and the default user stays root for the pilot. Issue #55 demonstrated that declaring VOLUME ["/data"] without creating it breaks every non-root invocation, and that no entrypoint can repair it after the fact — in distroless there is no entrypoint at all. Creating it world-writable in the layer is the one option that makes all privilege modes work (--user included), keeps drop-in parity with the classic image, and its exposure is bounded by the image running a single process. A nonroot-by-default posture (uid 65532) was considered and deferred: it would break the documented bind-mount workflow on UID mismatch, and the pilot’s job is to measure friction, not maximize it. Revisit at promotion time, possibly as a -nonroot tag.
  • The health check is baked in, exec-form. Shell-form HEALTHCHECK strings need /bin/sh and are impossible here; the JSON-array form is mandatory. Compose inherits an image’s HEALTHCHECK automatically (with disable: true as the escape hatch), so compose users of this variant get working depends_on: condition: service_healthy with zero configuration. ready rather than live because Docker’s health status feeds gating (start ordering), which is readiness semantics — and the two are identical anyway unless KMS/etcd are configured.
  • One release-blocking verification: HEALTHCHECK is a Docker extension, absent from the OCI image spec (opencontainers/image-spec#749 is still open), and OCI-media-type builds drop it silently. The publish pipeline must assert docker inspect shows the Health config on the pushed manifest, or adjust the build’s media types until it does.
  • Naming: docker.io/pgsty/silo:<RELEASE>-distroless, plus a rolling distroless tag. A new Dockerfile.distroless in the server repo — which, having no download stages, is fully offline and can therefore be built and asserted in CI on every release, closing the “the gate tests a synthetic image, not the shipped one” coverage gap that #55 documented for the classic Dockerfile.

What a user gives up in the variant, stated honestly in its docs: no docker exec <container> sh debugging (use docker debug / kubectl debug ephemeral containers), no in-image mc (use the pgsty/mc image or a host-installed mcli), no MINIO_USERNAME path (use --user).

Kubernetes needs no image support at all

Worth stating explicitly, because it bounds the problem: Kubernetes ignores Dockerfile HEALTHCHECK entirely — kubelet probes are configured in the pod spec and executed from outside the container as httpGet requests. Both image variants are therefore probed identically:

startupProbe:            # boot budget: 5s × 60 = 5 minutes for large IAM loads
  httpGet: { path: /minio/health/live, port: 9000 }
  periodSeconds: 5
  failureThreshold: 60
livenessProbe:           # when to restart: process-level signal only
  httpGet: { path: /minio/health/live, port: 9000 }
  periodSeconds: 30
  timeoutSeconds: 5
  failureThreshold: 3
readinessProbe:          # when to unroute: may include hard dependencies (KMS/etcd)
  httpGet: { path: /minio/health/ready, port: 9000 }
  periodSeconds: 15
  timeoutSeconds: 5
  failureThreshold: 3

Three cautions that belong next to any such config: the cluster endpoints must never appear in probes (liveness would restart the whole fleet on quorum loss; readiness would tangle with bootstrap — the chart’s headless service correctly sets publishNotReadyAddresses: true for exactly that reason); live deliberately returns 503 under sustained request-queue saturation, so a saturated node restarts after ~90s by design; and with scheme: HTTPS the kubelet skips certificate verification, so self-signed deployments need nothing extra.

The Silo Helm chart currently ships no probes at all (neither does upstream’s, despite its docs). Adding the three probes above to the chart is planned as an independent follow-up — it depends on neither image track, and it is a differentiator over upstream rather than a compatibility risk.

Rollout

Phase Scope Repo
P1 silo healthcheck subcommand + tests; ships in the next release binary (all images inherit the capability, no image behavior changes) pgsty/silo
P2 Dockerfile.distroless + CI build/health gate + publish -distroless tags as a pilot pgsty/silo
P3 Helm chart: add the three probes; refresh the stale default image tag pgsty/silo
P4 Docs: command reference, probe guide, distroless migration notes; pilot feedback → decide on promoting distroless to the recommended default this site

Compatibility promises across all phases: the classic image’s contents and examples do not change; mc ready local keeps working everywhere it works today; the health HTTP API is untouched (the subcommand is purely additive); and the /minio/health/* paths remain frozen as compatibility surface, same as every other /minio/* route in the fork.

Deferred decisions

Recorded so they are not re-litigated from scratch:

  • --wait mode (block-until-healthy, the one thing mc ready’s loop is genuinely for): deferred — no in-repo consumer needs it yet, and adding it later is backward-compatible. The flag namespace is reserved.
  • Nonroot-by-default for the distroless image: deferred to promotion time, as above.
  • JSON output schema for --json: follows the global-flag convention; the exact schema is fixed at implementation time and documented in the command reference.