Files
rustfs/helm/README.md
T
Zhengchao An b2a376c2d2 Merge commit from fork
* fix(admin): bound IAM import archive expansion

MAX_IAM_IMPORT_SIZE caps the compressed upload at 10 MB, but every member of the
archive was then read with read_to_end into an unbounded Vec. Deflate ratios well
above 100:1 are easy to construct, so a small authorized upload could expand
without limit across the seven members ImportIam reads.

Add a shared expansion budget (MAX_IAM_IMPORT_EXPANDED_SIZE, 10x the compressed
cap) drawn down by every member, and route all seven reads through one helper
that reads a byte past the remaining budget to detect overrun. Sharing the budget
bounds the archive as a whole rather than letting each member spend the full
limit independently.

Covers R03-CAN-024 through R03-CAN-030 plus R04-CAN-077 (backlog #1471) — one
fix rather than seven, since all seven call sites were byte-identical.

* fix(kms): confine local key paths and refuse silent key replacement

Local KMS key identifiers arrive from request input — the `name` tag on CreateKey,
the `keyId` body field or query parameter on DeleteKey — and were joined onto
`key_dir` with no validation. An identifier such as `../../tmp/evil` escaped the
configured directory, making key creation a constrained arbitrary-file write and
`DeleteKey` with `force_immediate` a cross-directory delete.

Validate in `master_key_path` and make it fallible, so every filesystem path in
this backend inherits the guard: decode_stored_key, load_master_key,
save_master_key, create_key and delete_key all derive their paths there. The rule
is containment rather than a character allowlist, so identifiers already in use
keep resolving; only separators, NUL, absolute paths and non-single-component
forms are refused. Note `.` and `..` are contained rather than refused — the
`.key` suffix turns them into the ordinary filenames `..key` and `...key`.

Separately, `LocalKmsBackend::create_key` had no existence check, while the
sibling `KmsClient::create_key` has always had one. Since `save_master_key`
renames over its destination, creating a key under an existing name silently
replaced its material and destroyed the ability to decrypt everything wrapped
under it — and the backend path is the one the admin API uses. It now returns
KeyAlreadyExists, matching StaticKmsBackend.

Covers R03-CAN-072, R03-CAN-073 and R07-CAN-103 (backlog #1475). R03-CAN-073
needed no separate change: delete_key routes both its load and its remove_file
through master_key_path.

* fix(swift): bound SLO manifest reads to the 2 MiB manifest limit

The three Swift SLO handlers that load a stored manifest (handle_slo_get,
handle_slo_get_manifest, handle_slo_delete) read the `<object>.slo-manifest`
object to EOF with AsyncReadExt::read_to_end. That key is predictable and
writable through the ordinary object PUT path, so a tenant can replace the
manifest with an arbitrarily large object and then make the server allocate
its full size on every SLO GET, multipart-manifest=get, or
multipart-manifest=delete request - a memory amplification bounded only by
the stored object size (CWE-400 / CWE-770). The 2 MiB manifest limit that
handle_slo_put enforces was not applied on the read side.

Introduce MAX_SLO_MANIFEST_SIZE (the existing 2 MiB PUT limit, now a named
constant) and a shared read_manifest_bytes helper that reads through a
`take(limit + 1)` and rejects anything larger, so an oversized manifest is
refused instead of being buffered first. All three call sites go through the
helper. handle_slo_put now checks the size before parsing the JSON.

Regression tests: test_read_manifest_bytes_rejects_oversized_manifest and
test_read_manifest_bytes_stops_reading_oversized_manifest (which asserts the
reader is not consumed past the limit), plus a boundary test that a manifest
at exactly 2 MiB is still accepted.

* fix(protocols): authorize every object in FTPS/WebDAV recursive deletes

The FTPS and WebDAV gateways authorized only the container before a
recursive delete and then destroyed everything inside it without a
further check:

- FTPS RMD (and DELE on a bucket path ending in '/') cleared
  s3:DeleteBucket, then delete_bucket_recursively listed the bucket and
  deleted every object.
- WebDAV DELETE on a bucket did the same via its own
  delete_bucket_recursively.
- WebDAV DELETE on a directory cleared s3:DeleteObject for the directory
  marker key ("dir/") only, then listed that prefix and deleted every
  child under it.

A principal holding s3:DeleteBucket (or s3:DeleteObject on a single
marker key) could therefore erase objects it had no s3:DeleteObject
permission for, and the operation reported success.

Deletion stays recursive - that is the expected behaviour for these
protocols - but each object now clears s3:DeleteObject on its own key
before it is removed, and the enumeration clears s3:ListBucket. A denial
aborts the whole operation with access denied rather than being skipped,
so the caller can never be told the delete succeeded while objects were
left behind or removed without authorization.

The test double gained shared-state cloning, delete_object/delete_bucket
call logs, and list/delete queue helpers so the regression tests can
observe that nothing is deleted once a deny lands.

* fix(server,ecstore): bound TLS handshakes and remote volume RPC waits

Three call sites let an unauthenticated client or a misbehaving peer hold
server resources with no deadline.

TLS listener (R03-CAN-035): process_connection awaited
`acceptor.accept(socket)` with no bound. A client that opens a TCP
connection and never finishes the handshake parks a Tokio task and a socket
forever, and the connection cap (RUSTFS_API_MAX_CONNECTIONS) is unlimited by
default, so nothing else sheds it. The handshake now runs under
accept_tls_with_deadline(), reusing the existing HTTP/1 header-read budget —
the established slow-client bound for the pre-request phase — and the
expiry is recorded through the same log/metric path as a handshake error,
under a new TIMEOUT failure kind.

Remote disk RPCs (R03-CAN-049, R03-CAN-050): list_volumes and delete_volume
passed Duration::ZERO, which execute_with_timeout treats as "no deadline",
so a peer that accepts the request and never answers stalls the coordinator
(and, for delete_volume, the bucket-deletion workflow). Both now pass
get_max_timeout_duration(), matching every sibling method in the file.

Regression tests: a silent TLS peer must be shed by the handshake deadline;
list_volumes/delete_volume against a peer that completes the TCP connect and
then goes silent must fail with DiskError::Timeout instead of hanging.

* fix(security): stop leaking signed headers and bound OIDC/KMS credentials

Three independent hygiene fixes found by the security review.

R03-CAN-018 (crates/signer): try_get_canonical_headers and get_signed_headers
logged the complete header map at DEBUG before signing. Runtime callers pass
session credentials and SSE-C key material through these headers, so anyone
able to raise the log level (or read DEBUG logs) recovered
X-Amz-Security-Token and SSE-C keys verbatim. The statements were debugging
leftovers with no operational value and are deleted rather than redacted.

R03-CAN-014 (crates/iam): the OIDC HTTP adapter buffered provider responses
with an unbounded Response::bytes(), so a configured, compromised or
attacker-pointed IdP endpoint could stream an arbitrarily large or endless
body into memory (the ValidateOidcConfig admin handler lets a ServerInfo
caller choose the endpoint). Responses are now read incrementally and fail
closed past MAX_OIDC_RESPONSE_SIZE, and the already SSRF-hardened client
builder gains request and connect timeouts so a stalled provider cannot pin
the calling task indefinitely.

R07-CAN-105 (helm): the Vault KMS token was serialized into the chart
ConfigMap, exposing it to every subject allowed to get ConfigMaps in the
namespace. It now renders into a dedicated Secret that the Deployment and
StatefulSet consume via envFrom; the Secret is separate from the main
credentials Secret so it also works when secret.existingSecret is set.

Regression tests:
- rustfs-signer: signing_never_logs_signed_header_material
- rustfs-iam: oidc_response_body_past_the_limit_is_rejected,
  oidc_response_body_at_the_limit_is_accepted
- scripts/test_helm_templates.sh: KMS token must never render in plaintext

* fix(webdav): enforce body limit, request timeout and connection cap

The configured WebDAV maximum body size was enforced from Content-Length, so a
chunked request declared no length and bypassed it entirely. The configured
request timeout was never applied to the connection at all, and the accept loop
spawned a task per connection with no bound, so an unauthenticated client could
hold resources indefinitely and in unbounded number.

Enforce the limit on bytes actually read rather than the declared length, apply
the configured timeout to the request, and bound accepted connections with a new
RUSTFS_WEBDAV_MAX_CONNECTIONS (default 1024) surfaced in the config report.

Covers R03-CAN-051, R03-CAN-052, R03-CAN-067, R04-CAN-089, R05-CAN-094 and
R05-CAN-097 (backlog #1471, #1474).

* fix(security): stop STS credentials from crossing the parent trust boundary

Two related credential-boundary holes let a short-lived STS credential act
with the full, unrestricted authority of the long-term user it was minted
from.

AddUser (R03-CAN-021, CWE-269/863): should_check_deny_only relaxes the admin
policy check to deny-only when a Console/STS session targets the IAM user it
represents. Nothing then stopped that session from calling AddUser with its
own parent's access key, so the handler wrote an attacker-chosen secret key
and status over the parent's stored Credentials via create_user ->
save_user_identity. A session that expires in minutes became permanent
control of the account. AddUser now rejects any temp or service-account
requester whose resolved parent equals the target access key, resolving the
parent the same way should_check_deny_only does (parent_user field, else the
JWT `parent` claim, since some stores persist the parent only in the token).

FTPS/SFTP/WebDAV password auth (R04-CAN-086, CWE-287/862): these protocols
looked the access key up with check_key, which falls back to the STS account
cache, and then compared only the stored secret. An STS access key plus
secret therefore authenticated with no session token presented and no
session-policy claims applied - the holder got the parent's full permissions.
Password authentication now rejects temporary credentials before the secret
comparison. The discriminator is is_temp() && !is_service_account(), the same
one IamCache::update_user_with_claims uses to route an identity into the STS
cache, so service accounts - which resolve policy from stored IAM state
rather than a client-presented token - keep working over these protocols.

Regression tests cover both predicates and pin the guards to their call
sites so neither can be dropped without a test failure.
2026-07-27 00:22:50 +08:00

24 KiB
Raw Blame History

RustFS Helm Mode

RustFS helm chart supports standalone and distributed mode.

  • Standalone mode: one pod with one PVC (single node, single disk).
  • Distributed mode (default): multiple pods with multiple PVCs (multiple nodes, multiple disks).

Distributed topology

The distributed topology is defined by two parameters:

  • replicaCount — number of pods (nodes) in the StatefulSet.
  • drivesPerNode — number of data PVCs mounted on each pod.

Total drives in the cluster = replicaCount * drivesPerNode.

When drivesPerNode is left unset, the chart automatically infers a backward-compatible value from each pool's replica count (with pools disabled there is a single pool driven by the top-level replicaCount):

replicaCount Inferred drivesPerNode Legacy equivalent
4 4 old default 4×4
anything else 1 old 16×1, etc.

You can override the inference by setting drivesPerNode explicitly, e.g. --set drivesPerNode=2 for an 8×2 cluster.

IMPORTANT: Kubernetes does not allow changes to volumeClaimTemplates in an existing StatefulSet. If you want to change drivesPerNode after installation you must delete the StatefulSet (with --cascade=orphan to keep pods and PVCs) and recreate it, or perform a full reinstall.


Upgrade notes

Upgrading from chart versions that did not have drivesPerNode is safe without manual intervention:

  • Existing 4×4 deployments (default replicaCount=4) continue to receive 4 drives per node because the chart infers drivesPerNode=4.
  • Existing 16×1 deployments (replicaCount=16) continue to receive 1 drive per node because the chart infers drivesPerNode=1.

If you previously set replicaCount=16 and now want a different topology, set both replicaCount and drivesPerNode explicitly.


Parameters Overview

Parameter Type Default value Description
affinity.nodeAffinity object {}
affinity.podAntiAffinity.enabled bool true
affinity.podAntiAffinity.topologyKey string "kubernetes.io/hostname"
clusterDomain string "cluster.local" Kubernetes cluster DNS domain used to build in-cluster FQDNs for RUSTFS_VOLUMES (distributed mode) and mTLS server certificate SANs. Override for clusters not using the default cluster.local. Provide the DNS root only, without a svc. prefix or leading/trailing dots.
commonLabels object {} Labels to add to all deployed objects.
config.rustfs.address string ":9000"
config.rustfs.console_address string ":9001"
config.rustfs.console_enable string "true"
config.rustfs.domains string "" Enable virtual host mode.
config.rustfs.log_level string "info"
config.rustfs.obs_environment string "development"
config.rustfs.obs_log_directory string "/logs" Log directory inside the RustFS container. Set to "" to disable log PVCs and mounts.
config.rustfs.region string "us-east-1"
config.rustfs.volumes string ""
config.rustfs.log_rotation.size int "100" Default log rotation size mb for rustfs.
config.rustfs.log_rotation.time string "hour" Default log rotation time for rustfs.
config.rustfs.log_rotation.keep_files int "30" Default log keep files for rustfs.
config.rustfs.metrics.enabled bool false Toggle metrics export.
config.rustfs.metrics.endpoint string "" Dedicated metrics endpoint.
config.rustfs.scanner.speed string "" Scanner speed preset: fastest, fast, default, slow, slowest.
config.rustfs.scanner.delay string "" Override scanner sleep multiplier with RUSTFS_SCANNER_DELAY (0 through 10000).
config.rustfs.scanner.max_wait_secs string "" Override maximum scanner sleep in seconds with RUSTFS_SCANNER_MAX_WAIT_SECS.
config.rustfs.scanner.cycle_secs string "" Override scanner cycle interval in seconds with RUSTFS_SCANNER_CYCLE.
config.rustfs.scanner.start_delay_secs string "" Override scanner cycle interval in seconds with RUSTFS_SCANNER_START_DELAY_SECS.
config.rustfs.scanner.cycle_max_duration_secs string "" Cap one scanner cycle's runtime in seconds with RUSTFS_SCANNER_CYCLE_MAX_DURATION_SECS (0 disables).
config.rustfs.scanner.cycle_max_objects string "" Cap objects processed by one scanner cycle with RUSTFS_SCANNER_CYCLE_MAX_OBJECTS (0 disables).
config.rustfs.scanner.cycle_max_directories string "" Cap directories entered by one scanner cycle with RUSTFS_SCANNER_CYCLE_MAX_DIRECTORIES (0 disables).
config.rustfs.scanner.bitrot_cycle_secs string "" Override periodic deep bitrot cycle with RUSTFS_SCANNER_BITROT_CYCLE_SECS; false, off, no, or disabled disables it.
config.rustfs.scanner.idle_mode string "" Override scanner idle throttling flag (RUSTFS_SCANNER_IDLE_MODE).
config.rustfs.scanner.cache_save_timeout_secs string "" Override scanner cache save timeout in seconds with RUSTFS_SCANNER_CACHE_SAVE_TIMEOUT_SECS (minimum 1).
config.rustfs.scanner.max_concurrent_set_scans string "" Cap concurrent scanner set tasks with RUSTFS_SCANNER_MAX_CONCURRENT_SET_SCANS (0 keeps topology-derived concurrency).
config.rustfs.scanner.max_concurrent_disk_scans string "" Cap concurrent scanner disk bucket walks per set with RUSTFS_SCANNER_MAX_CONCURRENT_DISK_SCANS (0 keeps disk-count-derived concurrency).
config.rustfs.scanner.yield_every_n_objects string "" Yield to the async runtime every N scanned objects with RUSTFS_SCANNER_YIELD_EVERY_N_OBJECTS (0 disables extra yield).
config.rustfs.scanner.alert_excess_versions string "" Set version count threshold for scanner alerts with RUSTFS_SCANNER_ALERT_EXCESS_VERSIONS.
config.rustfs.scanner.alert_excess_version_size string "" Set retained version byte threshold for scanner alerts with RUSTFS_SCANNER_ALERT_EXCESS_VERSION_SIZE.
config.rustfs.scanner.alert_excess_folders string "" Set direct subfolder threshold for scanner alerts with RUSTFS_SCANNER_ALERT_EXCESS_FOLDERS.
config.rustfs.obs_endpoint.enabled bool false Whether to send metrics/logs/traces/profilings to remote endpoint, eg, OLTP.
config.rustfs.obs_endpoint.base_endpoint string "" Root OTLP/HTTP endpoint, e.g. http://otel-collector:4318.
config.rustfs.obs_endpoint.use_stdout bool false Whether to output logs to stdout in addition the OLTP.
config.rustfs.obs_endpoint.metrics.enabled bool false Whether to send metrics to remote endpoint.
config.rustfs.obs_endpoint.metrics.endpoint string "" Remote endpoint url for metrics.
config.rustfs.obs_endpoint.trace.enabled bool false Whether to send trace to remote endpoint.
config.rustfs.obs_endpoint.trace.endpoint string "" Remote endpoint url for trace.
config.rustfs.obs_endpoint.logs.enabled bool false Whether to send logs to remote endpoint.
config.rustfs.obs_endpoint.logs.endpoint string "" Remote endpoint url for logs.
config.rustfs.obs_endpoint.profiling.enabled bool false Whether to send profiling to remote endpoint.
config.rustfs.obs_endpoint.profiling.endpoint string "" Remote endpoint url for profiling.
config.rustfs.kms.enabled bool false Whether to enable kms.
config.rustfs.kms.type string vault The kms type that RustFS supported.
config.rustfs.kms.vault.vault_backend string "" The vault backend, vault-kv2 or vault-transit.
config.rustfs.kms.vault.vault_address string "" The vault address.
config.rustfs.kms.vault.vault_token string "" The vault token. Rendered into a dedicated Secret (<fullname>-kms-secret), never into the ConfigMap.
config.rustfs.kms.vault.vault_mount_path string "transit" The vault mount path, only works if vault_backend equals vault-transit .
config.rustfs.kms.vault.default_key string "transit" The master key id for RustFS.
extraEnv map [] Extra environment variables for RustFS container.
extraVolumes list [] Extra volumes to add to the pod spec. Supported in both standalone (Deployment) and distributed (StatefulSet) modes.
extraVolumeMounts list [] Extra volume mounts to add to the RustFS container. Supported in both standalone (Deployment) and distributed (StatefulSet) modes.
containerSecurityContext.capabilities.drop[0] string "ALL"
containerSecurityContext.readOnlyRootFilesystem bool true
containerSecurityContext.runAsNonRoot bool true
priorityClassName string ""
enableServiceLinks bool false
extraManifests list [] List of additional k8s manifests.
fullnameOverride string ""
image.rustfs.pullPolicy string "IfNotPresent"
image.rustfs.repository string "rustfs/rustfs" RustFS docker image repository.
image.rustfs.tag string "" Chart appVersion default if unset.
imagePullSecrets list [] A List of secrets to pull image from private registry.
imageRegistryCredentials.email string "" The email to pull rustfs image from private registry.
imageRegistryCredentials.enabled bool false To indicate whether pull image from private registry.
imageRegistryCredentials.password string "" The password to pull rustfs image from private registry.
imageRegistryCredentials.registry string "" Private registry url to pull rustfs image.
imageRegistryCredentials.username string "" The username to pull rustfs image from private registry.
ingress.className string "nginx" Specify the ingress class, traefik or nginx.
ingress.enabled bool true
ingress.hosts[0].host string "example.rustfs.com"
ingress.hosts[0].paths[0].path string "/"
ingress.hosts[0].paths[0].pathType string "ImplementationSpecific"
ingress.nginxAnnotations."nginx.ingress.kubernetes.io/affinity" string "cookie"
ingress.nginxAnnotations."nginx.ingress.kubernetes.io/session-cookie-expires" string "3600"
ingress.nginxAnnotations."nginx.ingress.kubernetes.io/session-cookie-hash" string "sha1"
ingress.nginxAnnotations."nginx.ingress.kubernetes.io/session-cookie-max-age" string "3600"
ingress.nginxAnnotations."nginx.ingress.kubernetes.io/session-cookie-name" string "rustfs"
ingress.customAnnotations dict {} Additional custom annotations, merged with class-specific stickiness annotations.
ingress.traefikAnnotations."traefik.ingress.kubernetes.io/service.sticky.cookie" string "true"
ingress.traefikAnnotations."traefik.ingress.kubernetes.io/service.sticky.cookie.httponly" string "true"
ingress.traefikAnnotations."traefik.ingress.kubernetes.io/service.sticky.cookie.name" string "rustfs"
ingress.traefikAnnotations."traefik.ingress.kubernetes.io/service.sticky.cookie.samesite" string "none"
ingress.traefikAnnotations."traefik.ingress.kubernetes.io/service.sticky.cookie.secure" string "true"
ingress.tls.enabled bool false Enable tls and access rustfs via https.
ingress.tls.certManager.enabled string false Enable cert manager support to generate certificate automatically.
ingress.tls.crt string "" The content of certificate file.
ingress.tls.key string "" The content of key file.
livenessProbe.failureThreshold int 3
livenessProbe.httpGet.path string "/health"
livenessProbe.httpGet.port string "endpoint"
livenessProbe.initialDelaySeconds int 10
livenessProbe.periodSeconds int 5
livenessProbe.successThreshold int 1
livenessProbe.timeoutSeconds int 3
mode.distributed.enabled bool true RustFS distributed mode support, namely multiple pod multiple pvc.
mode.standalone.enabled bool false RustFS standalone mode support, namely one pod one pvc.
mode.standalone.existingClaim.dataClaim string "" Whether to use existing pvc claim for data storage.
mode.standalone.existingClaim.logsClaim string "" Whether to use existing pvc claim for logs storage.
mtls.enabled bool false Enable mtls betweens pods.
mtls.clientCertPath string /opt/tls/client_cert.pem The path for client cert.
mtls.clientKeyPath string /opt/tls/client_key.pem The path for client key.
mtls.existingIssuerRef.enabled bool false Enable to use external/existing certificate issuer.
mtls.existingIssuerRef.name string "" The name of external/existing certificate issuer.
mtls.existingIssuerRef.kind string "" The kind of external/existing certificate iss
uer. ClusterIssuer or Issuer.
mtls.existingIssuerRef.group string "" The group of external/existing certificate issuer.
nameOverride string ""
nodeSelector object {}
pdb.create bool false Enable/disable a Pod Disruption Budget creation
pdb.maxUnavailable string 1
pdb.minAvailable string ""
podAnnotations object {}
pools.enabled bool false Enable multiple server pools (capacity expansion, distributed mode only).
pools.list list [] One entry per pool; entries may set replicaCount (>= 2) and storageclass, omitted fields inherit top-level values. Append-only.
podLabels object {}
podSecurityContext.fsGroup int 10001
podSecurityContext.runAsGroup int 10001
podSecurityContext.runAsUser int 10001
readinessProbe.failureThreshold int 3
readinessProbe.httpGet.path string "/health/ready"
readinessProbe.httpGet.port string "endpoint"
readinessProbe.initialDelaySeconds int 30
readinessProbe.periodSeconds int 5
readinessProbe.successThreshold int 1
readinessProbe.timeoutSeconds int 3
replicaCount int 4 Number of cluster nodes. Distributed mode requires >= 2.
drivesPerNode int null Number of data PVCs per pod. Inferred from replicaCount when unset (see Distributed topology above).
resources.limits.cpu string "200m"
resources.limits.memory string "512Mi"
resources.requests.cpu string "100m"
resources.requests.memory string "128Mi"
secret.existingSecret string "" Use existing secret with a credentials.
secret.rustfs.access_key string "rustfsadmin" RustFS Access Key ID
secret.rustfs.secret_key string "rustfsadmin" RustFS Secret Key ID
service.type string "ClusterIP"
service.console.nodePort int 32001
service.console.port int 9001
service.endpoint.nodePort int 32000
service.endpoint.port int 9000
serviceAccount.annotations object {}
serviceAccount.automount bool true
serviceAccount.create bool true
serviceAccount.name string ""
storageclass.dataStorageSize string "256Mi" The storage size for data PVC.
storageclass.logStorageSize string "256Mi" The storage size for logs PVC.
storageclass.name string "local-path" The name for StorageClass.
storageclass.pvcAnnotations.data map {} Data pvc customized annotations.
storageclass.pvcAnnotations.logs map {} Logs pvc customized annotations.
tolerations list []
topologySpreadConstraints.enabled bool false Enable custom topology spread constraints on distributed-mode StatefulSet pods.
topologySpreadConstraints.constraints list [] Raw spec.template.spec.topologySpreadConstraints entries applied to the distributed StatefulSet when enabled.
gatewayApi.enabled bool false To enable/disable gateway api support.
gatewayApi.gatewayClass string traefik Gateway class implementation.
gatewayApi.listeners.http.name string web Gateway API http listener name.
gatewayApi.listeners.http.port int 8000 Gateway API http listener port.
gatewayApi.listeners.https.name string websecure Gateway API https listener name.
gatewayApi.listeners.https.port int 8443 Gateway API https listener port.
gatewayApi.hostname string Hostname to access RustFS via gateway api.
gatewayApi.secretName string Secret tls to via RustFS using HTTPS.
gatewayApi.existingGateway.name string "" The existing gateway name, instead of creating a new one.
gatewayApi.existingGateway.namespace string "" The namespace of the existing gateway, if not the local namespace.

Scanner values map directly to scanner environment variables. For tuning workflow and /v3/scanner/status interpretation, see Scanner Runtime Controls. For repeatable scanner-pressure validation, see Scanner Benchmark Runbook.


NOTE:

The chart pulls the rustfs image from Docker Hub by default. For private registries, provide either:

  • Existing secrets: Set imagePullSecrets with an array of secret names

    imagePullSecrets:
      - name: my-existing-secret
    
  • Auto-generated secret: Enable imageRegistryCredentials.enabled: true and specify credentials plus your image details

    imageRegistryCredentials:
      enabled: true
      registry: myregistry.com
      username: myuser
      password: mypass
      email: user@example.com
    

Both approaches support pulling from private registries seamlessly and you can also combine them.

  • The chart default pull rustfs image from dockerhub, if your rustfs image stores in private registry, you can use either existing image Pull secrets with parameter imagePullSecrets or create one setting imageRegistryCredentials.enabled to true,and then specify the imageRegistryCredentials.registry/username/password/email as well as image.rustfs.repository,image.rustfs.tag to pull rustfs image from your private registry.

  • The default storageclass is local-path,if you want to specify your own storageclass, try to set parameter storageclass.name.

  • The default size for data and logs dir is 256Mi which must satisfy the production usage,you should specify storageclass.dataStorageSize and storageclass.logStorageSize to change the size, for example, 1Ti for data and 1Gi for logs.

Server pools (capacity expansion)

In distributed mode the chart can run multiple server pools — independent StatefulSets whose drives together form one cluster, the same expansion model the RustFS server already supports via space-separated RUSTFS_VOLUMES expressions (rc admin pool ls / expand / rebalance / decommission).

With pools.enabled=false (default) the chart behaves exactly as before: one StatefulSet driven by the top-level replicaCount/storageclass.

To expand an existing deployment, enable pools and describe the current layout as pool 0 plus your new capacity:

pools:
  enabled: true
  list:
    - {}                  # pool 0: inherits top-level values and keeps the
                          # existing StatefulSet/pod/PVC names and data
    - replicaCount: 4     # pool 1: new capacity
      storageclass:
        dataStorageSize: 10Gi

Each entry may set replicaCount (>= 2) and/or a storageclass block; omitted fields inherit the top-level values. Additional pools render as <fullname>-pool<N> StatefulSets; all pools share the headless service, the main service, the configuration and the credentials.

Notes:

  • Pools are append-only. The list index determines the StatefulSet name — never remove or reorder entries. Retire a pool with rc admin decommission before removing it from the list.
  • During the expansion rollout, pods restart until every pod of every pool is resolvable — the server refuses to start with unresolvable peers, so expect a few crash/restart cycles before the cluster converges. This is harmless.
  • After the cluster converges, run rc admin rebalance start <alias> to spread existing objects across the new pool.
  • Pod anti-affinity in pool mode is scoped per pool and preferred (soft), not required: two pools can share nodes, and each pool's own pods spread across distinct nodes when capacity allows. Soft affinity is load-bearing for in-place expansion — with required rules, the not-yet-rolled pods of the existing pool block the new pool's pods from their nodes while the rolled pods crash on the unresolvable (Pending) peers, deadlocking the rollout on any cluster with fewer nodes than the total pod count. Single-pool deployments (pools.enabled=false) keep the chart's existing required anti-affinity unchanged.
  • The PodDisruptionBudget spans all pools: with the default pdb.maxUnavailable: 1, at most one pod of the whole cluster may be evicted at a time. This is deliberately conservative — quorum safety matters across the union of all pools.

Installation

Requirement

  • Helm V3
  • RustFS >= 1.0.0-alpha.69

Due to the traefik and ingress has different session sticky/affinity annotations, and rustfs support both those two controller, you should specify parameter ingress.className to select the right one which suits for you.

Installation with traefik controller

If your ingress class is traefik, running the command:

helm install rustfs -n rustfs --create-namespace ./ --set ingress.className="traefik"

Installation with nginx controller

If your ingress class is nginx, running the command:

helm install rustfs -n rustfs --create-namespace ./ --set ingress.className="nginx"

Installation check and rustfs login

Check the pod status

kubectl -n rustfs get pods -w
NAME       READY   STATUS    RESTARTS        AGE
rustfs-0   1/1     Running   0               2m27s
rustfs-1   1/1     Running   0               2m27s
rustfs-2   1/1     Running   0               2m27s
rustfs-3   1/1     Running   0               2m27s

Check the ingress status

kubectl -n rustfs get ing
NAME     CLASS   HOSTS            ADDRESS         PORTS     AGE
rustfs   nginx   example.rustfs.com   10.43.237.152   80, 443   29m

Access the rustfs cluster via https://example.rustfs.com with the default username and password rustfsadmin.

Replace the example.rustfs.com with your own domain as well as the certificates.

TLS configuration

By default, tls is not enabled. If you want to enable tls(recommendated),you can follow below steps:

  • Step 1: Certification generation

You can request cert and key from CA or use the self-signed cert(not recommendated on prod), and put those two files(eg, tls.crt and tls.key) under some directory on server, for example tls directory.

  • Step 2: Certification specifying

You should use --set-file parameter when running helm install command, for example, running the below command can enable ingress tls and generate tls secret:

helm install rustfs rustfs/rustfs -n rustfs --set tls.enabled=true,--set-file tls.crt=./tls.crt,--set-file tls.key=./tls.key

Gateway API support (alpha)

Due to ingress nginx retirement in March 2026, so RustFS adds support for gateway api. Currently, RustFS only supports traefik as gateway class, more and more gateway class support will be added in the future after those classes are tested. If you want to enable gateway api, specify gatewayApi.enabled to true while specify ingress.enabled to false. After installation, you can find the Gateway and HttpRoute resources,

$ kubectl -n rustfs get gateway
NAME             CLASS     ADDRESS   PROGRAMMED   AGE
rustfs-gateway   traefik             True         169m

$ kubectl -n rustfs get httproute
NAME           HOSTNAMES            AGE
rustfs-route   ["example.rustfs.com"]   172m

Then, via RustFS instance via https://example.rustfs.com or http://example.rustfs.com.

Uninstall

Uninstalling the rustfs installation with command,

helm uninstall rustfs -n rustfs