feat(server): present Silo identity and close the inherited upstream services

Two coupled changes that must land together, because the same files carry both:
the server now identifies itself as Silo, and every path that would have called
home to a MinIO-operated service is closed.

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Feng Ruohang
2026-08-06 08:46:58 +08:00
parent 15def34dce
commit 77bdc4c0cd
50 changed files with 469 additions and 581 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ var (
config.HelpKV{
Key: Enable,
Type: "on|off",
Description: "set to enable callhome" + defaultHelpPostfix(Enable),
Description: "legacy callhome setting; retained for compatibility but unsupported by Silo" + defaultHelpPostfix(Enable),
Optional: true,
},
config.HelpKV{
+13 -13
View File
@@ -21,8 +21,8 @@ package config
var (
ErrInvalidXLValue = newErrFn(
"Invalid drive path",
"Please provide a fresh drive for single drive MinIO setup",
"MinIO only supports fresh drive paths",
"Please provide a fresh drive for a single-drive Silo setup",
"Silo only supports fresh drive paths",
)
ErrInvalidBrowserValue = newErrFn(
@@ -118,14 +118,14 @@ var (
ErrStorageClassValue = newErrFn(
"Invalid storage class value",
"Please check the value",
`MINIO_STORAGE_CLASS_STANDARD: Format "EC:<Default_Parity_Standard_Class>" (e.g. "EC:3"). This sets the number of parity drives for MinIO server in Standard mode. Objects are stored in Standard mode, if storage class is not defined in Put request
MINIO_STORAGE_CLASS_RRS: Format "EC:<Default_Parity_Reduced_Redundancy_Class>" (e.g. "EC:3"). This sets the number of parity drives for MinIO server in Reduced Redundancy mode. Objects are stored in Reduced Redundancy mode, if Put request specifies RRS storage class
Refer to the link https://github.com/minio/minio/tree/master/docs/erasure/storage-class for more information`,
`MINIO_STORAGE_CLASS_STANDARD: Format "EC:<Default_Parity_Standard_Class>" (e.g. "EC:3"). This sets the number of parity drives for Silo in Standard mode. Objects are stored in Standard mode if no storage class is defined in the Put request.
MINIO_STORAGE_CLASS_RRS: Format "EC:<Default_Parity_Reduced_Redundancy_Class>" (e.g. "EC:3"). This sets the number of parity drives for Silo in Reduced Redundancy mode. Objects are stored in Reduced Redundancy mode if the Put request specifies the RRS storage class.
See https://silo.pgsty.com/operations/concepts/erasure-coding/ for more information.`,
)
ErrUnexpectedBackendVersion = newErrFn(
"Backend version seems to be too recent",
"Please update to the latest MinIO version",
"Please update to the latest Silo version",
"",
)
@@ -143,8 +143,8 @@ Refer to the link https://github.com/minio/minio/tree/master/docs/erasure/storag
"Please check the endpoint",
`Single-Node modes requires absolute path without hostnames:
Examples:
$ minio server /data/minio/ #Single Node Single Drive
$ minio server /data-{1...4}/minio # Single Node Multi Drive`,
$ silo server /data/silo/ # Single Node Single Drive
$ silo server /data-{1...4}/silo # Single Node Multi Drive`,
)
ErrUnsupportedBackend = newErrFn(
@@ -155,8 +155,8 @@ Examples:
ErrUnableToWriteInBackend = newErrFn(
"Unable to write to the backend",
"Please ensure MinIO binary has write permissions for the backend",
`Verify if MinIO binary is running as the same user who has write permissions for the backend`,
"Please ensure the Silo binary has write permissions for the backend",
`Verify that the Silo binary is running as the same user who has write permissions for the backend`,
)
ErrPortAlreadyInUse = newErrFn(
@@ -167,8 +167,8 @@ Examples:
ErrPortAccess = newErrFn(
"Unable to use specified port",
"Please ensure MinIO binary has 'cap_net_bind_service=+ep' permissions",
`Use 'sudo setcap cap_net_bind_service=+ep /path/to/minio' to provide sufficient permissions`,
"Please ensure the Silo binary has 'cap_net_bind_service=+ep' permissions",
`Use 'sudo setcap cap_net_bind_service=+ep /path/to/silo' to provide sufficient permissions`,
)
ErrTLSReadError = newErrFn(
@@ -209,7 +209,7 @@ Examples:
ErrUnexpectedError = newErrFn(
"Unexpected error",
"Please contact MinIO at https://slack.min.io",
"Please report this Silo error at https://github.com/pgsty/minio/issues",
"",
)
+1 -1
View File
@@ -69,7 +69,7 @@ var (
},
config.HelpKV{
Key: Vendor,
Description: `Specify vendor type for vendor specific behavior to checking validity of temporary credentials and service accounts on MinIO` + defaultHelpPostfix(Vendor),
Description: `Specify vendor type for vendor-specific behavior when checking temporary credentials and service accounts on Silo` + defaultHelpPostfix(Vendor),
Optional: true,
Type: "string",
},
+15 -31
View File
@@ -29,11 +29,6 @@ import (
xnet "github.com/minio/pkg/v3/net"
)
const (
baseURL = "https://subnet.min.io"
baseURLDev = "http://localhost:9000"
)
// DefaultKVS - default KV config for subnet settings
var DefaultKVS = config.KVS{
config.KV{
@@ -70,29 +65,21 @@ type Config struct {
var configLock sync.RWMutex
// Registered indicates if cluster is registered or not
// Registered reports false because Silo does not use MinIO SUBNET. The
// inherited credentials remain parseable for configuration compatibility.
func (c *Config) Registered() bool {
configLock.RLock()
defer configLock.RUnlock()
return len(c.APIKey) > 0
return false
}
// ApplyEnv - applies the current subnet config to Console UI specific environment variables.
func (c *Config) ApplyEnv() {
configLock.RLock()
defer configLock.RUnlock()
if c.License != "" {
os.Setenv("CONSOLE_SUBNET_LICENSE", c.License)
}
if c.APIKey != "" {
os.Setenv("CONSOLE_SUBNET_API_KEY", c.APIKey)
}
if c.Proxy != "" {
os.Setenv("CONSOLE_SUBNET_PROXY", c.Proxy)
}
os.Setenv("CONSOLE_SUBNET_URL", c.BaseURL)
// Do not expose inherited commercial-service credentials or an endpoint to
// the embedded Console. These names are outputs, not the MINIO_* input
// compatibility surface.
os.Unsetenv("CONSOLE_SUBNET_LICENSE")
os.Unsetenv("CONSOLE_SUBNET_API_KEY")
os.Unsetenv("CONSOLE_SUBNET_PROXY")
os.Unsetenv("CONSOLE_SUBNET_URL")
}
// Update - in-place update with new license and registration information.
@@ -103,15 +90,12 @@ func (c *Config) Update(ncfg Config, isDevEnv bool) {
c.License = ncfg.License
c.APIKey = ncfg.APIKey
c.Proxy = ncfg.Proxy
c.transport = ncfg.transport
c.BaseURL = baseURL
c.transport = nil
c.BaseURL = ""
if isDevEnv {
c.BaseURL = os.Getenv("_MINIO_SUBNET_URL")
if c.BaseURL == "" {
c.BaseURL = baseURLDev
}
}
// Retain the hidden compatibility input but deliberately ignore its value.
_ = os.Getenv("_MINIO_SUBNET_URL")
_ = isDevEnv
}
// LookupConfig - lookup config and override with valid environment settings if any.
+6 -76
View File
@@ -18,97 +18,27 @@
package subnet
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"time"
xhttp "github.com/minio/minio/internal/http"
)
const (
respBodyLimit = 1 << 20 // 1 MiB
// LoggerWebhookName - subnet logger webhook target
LoggerWebhookName = "subnet"
)
var errSiloSubnetDisabled = errors.New("MinIO SUBNET integration is disabled in Silo")
// Upload given file content (payload) to specified URL
func (c Config) Upload(reqURL string, filename string, payload []byte) (string, error) {
if !c.Registered() {
return "", errors.New("Deployment is not registered with SUBNET. Please register the deployment via 'mc license register ALIAS'")
}
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, e := writer.CreateFormFile("file", filename)
if e != nil {
return "", e
}
if _, e = part.Write(payload); e != nil {
return "", e
}
writer.Close()
r, e := http.NewRequest(http.MethodPost, reqURL, &body)
if e != nil {
return "", e
}
r.Header.Add("Content-Type", writer.FormDataContentType())
return c.submitPost(r)
return "", errSiloSubnetDisabled
}
func (c Config) submitPost(r *http.Request) (string, error) {
configLock.RLock()
r.Header.Set(xhttp.SubnetAPIKey, c.APIKey)
configLock.RUnlock()
r.Header.Set(xhttp.MinioDeploymentID, xhttp.GlobalDeploymentID)
client := &http.Client{
Timeout: 10 * time.Second,
Transport: c.transport,
}
resp, err := client.Do(r)
if err != nil {
return "", err
}
defer xhttp.DrainBody(resp.Body)
respBytes, err := io.ReadAll(io.LimitReader(resp.Body, respBodyLimit))
if err != nil {
return "", err
}
respStr := string(respBytes)
if resp.StatusCode == http.StatusOK {
return respStr, nil
}
return respStr, fmt.Errorf("SUBNET request failed with code %d and error: %s", resp.StatusCode, respStr)
func (c Config) submitPost(_ *http.Request) (string, error) {
return "", errSiloSubnetDisabled
}
// Post submit 'payload' to specified URL
func (c Config) Post(reqURL string, payload any) (string, error) {
if !c.Registered() {
return "", errors.New("Deployment is not registered with SUBNET. Please register the deployment via 'mc license register ALIAS'")
}
body, err := json.Marshal(payload)
if err != nil {
return "", err
}
r, err := http.NewRequest(http.MethodPost, reqURL, bytes.NewReader(body))
if err != nil {
return "", err
}
r.Header.Set("Content-Type", "application/json")
return c.submitPost(r)
return "", errSiloSubnetDisabled
}
+41
View File
@@ -0,0 +1,41 @@
// Copyright 2026 PGSTY contributors.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
package subnet
import (
"errors"
"os"
"testing"
)
func TestSiloSubnetIsPermanentlyDisabled(t *testing.T) {
cfg := Config{License: "legacy-license", APIKey: "legacy-key", BaseURL: "https://example.invalid"}
if cfg.Registered() {
t.Fatal("legacy SUBNET credentials must not register a Silo deployment")
}
for _, name := range []string{"CONSOLE_SUBNET_LICENSE", "CONSOLE_SUBNET_API_KEY", "CONSOLE_SUBNET_PROXY", "CONSOLE_SUBNET_URL"} {
t.Setenv(name, "must-be-cleared")
}
cfg.ApplyEnv()
for _, name := range []string{"CONSOLE_SUBNET_LICENSE", "CONSOLE_SUBNET_API_KEY", "CONSOLE_SUBNET_PROXY", "CONSOLE_SUBNET_URL"} {
if _, ok := os.LookupEnv(name); ok {
t.Fatalf("%s remains set", name)
}
}
if _, err := cfg.Upload("https://example.invalid", "health.json", nil); !errors.Is(err, errSiloSubnetDisabled) {
t.Fatalf("Upload error = %v, want %v", err, errSiloSubnetDisabled)
}
if _, err := cfg.Post("https://example.invalid", struct{}{}); !errors.Is(err, errSiloSubnetDisabled) {
t.Fatalf("Post error = %v, want %v", err, errSiloSubnetDisabled)
}
if _, err := cfg.submitPost(nil); !errors.Is(err, errSiloSubnetDisabled) {
t.Fatalf("submitPost error = %v, want %v", err, errSiloSubnetDisabled)
}
}