Files
minio/docs/security/source-address-trust.md
T
Feng Ruohang fd2ca1c6d2 docs: rebrand the repository documentation, templates and dashboards
README, README_ZH, SECURITY, COMPLIANCE, CONTRIBUTING, NOTICE,
code_of_conduct, the vulnerability and PR-etiquette documents, the GitHub issue
and pull request templates, and the docs/ tree all present Silo as the product.
The Grafana dashboards under docs/metrics/prometheus/grafana/ have their panel
titles and descriptions rebranded while every minio_* query, label and
expression is left alone, so existing alerts and recording rules keep matching.

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 08:49:30 +08:00

12 KiB

Client source address trust

Silo decides where a request came from, and that decision is load-bearing. The address it settles on becomes:

  • aws:SourceIp, so it decides IpAddress and NotIpAddress policy conditions
  • the audit log's remotehost field, so it decides who every logged action is attributed to
  • the Host field of S3 event notifications, and the client shown by mc admin trace

None of that is derived from the TCP connection by default. It is read out of the X-Forwarded-For, X-Real-IP and RFC 7239 Forwarded request headers, which any client can set to any value. This document states which peers Silo believes, how to change that, and what each choice costs.

The three modes

One setting selects all three. It is read from the environment, not from mc admin config.

Mode MINIO_API_TRUSTED_PROXIES Source address
Untrusted (default) unset left-most X-Forwarded-For entry, else X-Real-IP, else Forwarded, else the TCP peer — from any client
Trust nobody none always the TCP peer
Allow-listed a list of addresses and CIDR blocks forwarded headers, but only from listed peers

off is accepted as a synonym for none, and the value is matched case-insensitively.

The setting is read once at startup, after MINIO_CONFIG_ENV_FILE is loaded, so an environment file is a valid place to put it. A malformed value stops the server rather than silently changing what every policy condition resolves to.

_MINIO_API_XFF_HEADER is not this setting. It suppresses parsing of X-Forwarded-For and nothing else, leaving X-Real-IP and Forwarded honoured, so it cannot stop a client naming its own address — a client refused one header simply sends another. It keeps its original upstream meaning, and applies within whichever mode above is in force. If you set it hoping to stop source-address forgery, that is what MINIO_API_TRUSTED_PROXIES is for.

It also keeps upstream's read timing, which is earlier than the one described above: it is taken at package initialisation, before environment files are read, so writing it into MINIO_CONFIG_ENV_FILE has no effect. That quirk is deliberately left in place rather than repaired, because repairing it would make an already-deployed setting start taking effect. Set it in the process environment if you want it honoured.

Untrusted (default)

Every deployment behaves exactly as it did before this setting existed. Any client that can open a connection to the S3 API port can name its own address, so under this mode:

An IpAddress policy condition is not enforceable, and audit client addresses are not evidence. Both are attacker-chosen for anyone with direct network access to the API port.

This mode is sound only when every route to the API port passes through a proxy that overwrites all three headers. Two things commonly break that assumption:

  • A second way in. On Kubernetes an Ingress and a ClusterIP Service usually coexist. The Ingress sanitises headers; the Service does not, and any pod in the cluster can reach it. The boundary is assumed to be the Ingress but is actually the pod network.
  • Appending proxies. The stock nginx recipe proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; appends to what the client sent. A client sending X-Forwarded-For: 1.2.3.4 produces 1.2.3.4, <real client> at Silo, and this mode reads the left-most entry — the client's. Overwriting with $remote_addr avoids this; so does the allow-listed mode below, which reads the chain from the other end.

Trust nobody

MINIO_API_TRUSTED_PROXIES=none

No forwarded header is believed. The source address is the TCP peer, always.

Use this when Silo is reached directly and you would rather have a correct address that is sometimes a proxy than a plausible one that is sometimes a lie. Behind a proxy, every request will be attributed to the proxy.

On a multi-node deployment this also applies to Silo's own nodes. Some requests are forwarded between nodes (see Multi-node), and the receiving node's peer is the forwarding node, so those requests are attributed to it rather than to the client. Nothing corrects this under this mode, because it believes nothing. Multi-node clusters that want direct requests protected and internally-forwarded ones attributed correctly should use the allow-listed mode with the node addresses included.

The scheme headers (X-Forwarded-Proto, X-Forwarded-Scheme) are deliberately unaffected by any of this. They feed the Location URL in S3 responses, not a policy decision, and suppressing them would hand http:// URLs to every deployment terminating TLS at a proxy.

Allow-listed

MINIO_API_TRUSTED_PROXIES=10.0.0.1,10.0.0.2

A comma-, semicolon- or whitespace-separated list of addresses and CIDR blocks. Forwarded headers are believed only when the request's TCP peer matches. Every other peer is attributed to itself, whatever its headers claim.

Write entries in their plain form. An address written in IPv4-mapped notation (::ffff:192.168.1.10) is accepted but matches nothing, because peers are reduced to plain form before matching — so the entry grants no trust and the proxy is treated as any other client. This is long-standing behaviour shared with MINIO_IDENTITY_LDAP_STS_TRUSTED_PROXIES, and it fails closed.

This is the only mode under which aws:SourceIp is enforceable against a client that can reach the API port directly.

Catch-all ranges (0.0.0.0/0, ::/0) are rejected. They would trust every peer and quietly reinstate the untrusted mode under a name that suggests otherwise. Note that only /0 itself is rejected — a pair like 0.0.0.0/1,128.0.0.0/1 covers the same ground and is accepted, so the check is a guardrail, not a proof.

List proxies, not the subnet they sit in. This is the one way to configure the setting so that it makes things worse rather than better, so it is worth stating plainly.

Entries on the list are skipped during the chain walk described below. If the list covers addresses that clients also occupy — 10.0.0.0/8 when the load balancer is 10.0.0.1 and application servers are elsewhere in 10/8 — then those clients' addresses are skipped too, and the walk continues past the real client into whatever it placed to the left. A client at 10.5.5.5 sending X-Forwarded-For: 8.8.8.8 would then be recorded as 8.8.8.8.

A broad list does not merely trust more peers; it lets those peers forge. Prefer exact host entries or the narrowest prefix that contains only proxies.

Chain handling. X-Forwarded-For and Forwarded are read right-to-left, stepping over entries that name a listed proxy, and the first remaining address wins. Each proxy appends the peer it actually saw, so an entry the client injected sits to the left of the one its proxy wrote, and the walk stops before reaching it. Appending proxies are therefore safe here. List every proxy hop's address so intermediate hops are skipped. Repeated header lines are handled as one chain, so proxies that add a second X-Forwarded-For line rather than extending the first — HAProxy's option forwardfor — work correctly.

X-Real-IP is a single value with no chain, so it cannot be checked against the list. It is trusted verbatim, and only when the chain headers yield nothing. The deployment contract is that a listed proxy overwrites whichever headers it sets — for nginx, proxy_set_header X-Real-IP $remote_addr;. A proxy that relays the client's copy instead is choosing to let the client answer the question, and nothing Silo does can undo that.

Strip at the edge every source-address header your proxy does not itself write. This is the one rule that covers every case, and it is worth following even if the rest of this section is skipped.

Listing a peer means believing all three headers from it, and they are consulted in order: X-Forwarded-For, then X-Real-IP, then Forwarded. A header your proxy does not write is entirely under the client's control, and if it is consulted before the one your proxy does write, the client wins.

The common way to get bitten: a proxy that authors only X-Real-IP (some nginx configurations) or only Forwarded (RFC 7239-native proxies) relays the client's X-Forwarded-For untouched — and X-Forwarded-For is read first, so a client sending X-Forwarded-For: 8.8.8.8 is recorded as 8.8.8.8 despite the proxy having correctly written the real address elsewhere.

(MINIO_IDENTITY_LDAP_STS_TRUSTED_PROXIES orders the first two the other way round. Neither order is safe for every proxy — the mirror hazard is a proxy that authors only X-Forwarded-For and relays a client's X-Real-IP, which is what AWS ALB does. This setting prefers the header it can chain-validate, because it decides access control rather than rate-limit bucketing. Stripping what your proxy does not write makes the ordering irrelevant, which is why it is the rule worth remembering.)

Loopback is always trusted as a peer, even when not listed. The FTP and SFTP front-ends reach the S3 layer over 127.0.0.1 and declare their session's client with X-Forwarded-For; excluding loopback would attribute every FTP and SFTP request to the server itself. A 127.0.0.1 entry appearing inside a chain is not skipped — only peers are exempt, not chain entries.

Multi-node deployments

A cluster must list its own nodes. Silo forwards some requests between nodes — bucket-DNS and site-replication routing, listing continuation, heal-by-token, batch jobs and pool decommissioning. The receiving node's peer is the forwarding node, so unless the cluster's own addresses are on the list, those requests resolve to the forwarding node rather than to the client. Include the node addresses alongside your proxy's.

This is not a rare path. A ListObjectsV2 continuation token carries the node index, so any client can cause its own request to be forwarded. If the nodes are not listed, those requests are evaluated with aws:SourceIp set to an internal node address and audited against one — which an IpAddress condition that allows internal ranges would treat as a pass.

Choosing

Behind a proxy you control, with no other route to the API port, the default is fine and you need not set anything — but confirm the proxy overwrites rather than appends, or you are in the second case below. Otherwise:

  • Direct exposure, no proxy: MINIO_API_TRUSTED_PROXIES=none.
  • Behind a proxy, but the port is also reachable directly (the usual Kubernetes Ingress-plus-Service case, and the usual Pigsty case): set MINIO_API_TRUSTED_PROXIES to the proxy addresses plus the Silo node addresses. This is the configuration that makes an IpAddress condition mean something.
  • Multi-node clusters: use the allow-list with node addresses included, not none.
  • Behind a proxy you are not sure about: allow-list it, and strip at the edge every source-address header it does not itself write.

Whichever applies, two rules govern every allow-list deployment: name proxies, not the subnet they occupy; and strip what your proxy does not write.

Relationship to LDAP STS trusted proxies

MINIO_IDENTITY_LDAP_STS_TRUSTED_PROXIES is a separate allow-list governing which peers may supply the client address used for LDAP STS login rate-limit bucketing only. It does not affect aws:SourceIp or audit addresses, and this setting does not affect STS rate limiting. The two use the same list syntax and the same chain-walking rules, and in most deployments should be set to the same value. See docs/sts/ldap.md.