docs(knowledge-base): prune stale content and add agent-facing index (#7035)
This commit is contained in:
@@ -1,120 +1,62 @@
|
||||
# Outbound Connection Policy
|
||||
|
||||
This document describes the outbound connection policy that RustFS applies to
|
||||
server-initiated HTTP(S) requests, and the `RUSTFS_OUTBOUND_ALLOW_ORIGINS`
|
||||
allowlist operators can use to reach endpoints on private or container networks.
|
||||
**Use this when:** a webhook, audit target, OIDC provider, or object-lambda endpoint on a private or container network (Compose service names, `host.docker.internal`, RFC 1918 addresses) is not being reached, or you need to know which server-initiated connections RustFS restricts and how to allowlist one.
|
||||
**Source of truth:** `crates/utils/src/egress.rs` (`OutboundPolicy`, `OutboundDnsResolver`, `validate_outbound_url`, `ENV_OUTBOUND_ALLOW_ORIGINS`).
|
||||
|
||||
It is written for operators whose outbound integrations stopped reaching
|
||||
endpoints after an upgrade — typically Docker Compose service names,
|
||||
`host.docker.internal`, or RFC 1918 addresses. Webhook and audit clients adopted
|
||||
this policy in `1.0.0-beta.11`; OIDC provider requests adopted it in
|
||||
`1.0.0-beta.12`.
|
||||
RustFS validates every operator-configured outbound destination to close a server-side request forgery (SSRF) class. Two layers exist:
|
||||
|
||||
## Background: what the policy protects
|
||||
| Layer | What it checks | Escape hatch |
|
||||
| --- | --- | --- |
|
||||
| Literal URL check (`validate_outbound_url`) | Scheme is `http`/`https`; the host is not `localhost` or a loopback, private, shared, reserved, link-local, unspecified, or metadata address (IPv4-mapped and embedded IPv6 forms are classified by the embedded IPv4) | None |
|
||||
| Full policy (`OutboundPolicy` + `OutboundDnsResolver`) | The literal check, plus re-validation of every address DNS returns on each new connection, so a hostname cannot be rebound to a restricted address after it was accepted | `RUSTFS_OUTBOUND_ALLOW_ORIGINS` for the loopback, private, shared, and reserved classes |
|
||||
|
||||
Several RustFS subsystems open connections to operator-configured URLs. To close
|
||||
a server-side request forgery (SSRF) class of problem, RustFS validates every such
|
||||
destination and re-checks the addresses returned by DNS on each new connection,
|
||||
so a hostname cannot be rebound to a restricted address after it is first
|
||||
accepted.
|
||||
## Which subsystem uses which layer
|
||||
|
||||
The policy governs the outbound clients used by:
|
||||
| Subsystem | Layer | Notes |
|
||||
| --- | --- | --- |
|
||||
| Event-notification webhooks (`RUSTFS_NOTIFY_WEBHOOK_*`) and audit webhooks (`RUSTFS_AUDIT_WEBHOOK_*`) | Full policy | Proxies disabled and redirects not followed, so the endpoint must be reachable directly (`crates/targets/src/target/webhook.rs`) |
|
||||
| Target configuration validation (startup and admin API) | Full policy | `crates/targets/src/config/common.rs` `validate_outbound_http_url`; `rustfs/src/admin/handlers/target_descriptor.rs` |
|
||||
| OIDC discovery, JWKS, and token requests | Full policy | A blocked provider logs `OIDC provider discovery blocked by outbound policy` naming the origin to allowlist (`crates/iam/src/oidc.rs`) |
|
||||
| Object Lambda targets | Full policy | `rustfs/src/admin/router.rs` `outbound_policy` |
|
||||
| Bucket replication targets | Literal check, relaxed | Private addresses are always allowed; loopback only with `RUSTFS_REPLICATION_ALLOW_LOOPBACK_TARGET=true` (`crates/ecstore/src/bucket/bucket_target_sys.rs` `validate_replication_target_endpoint`) |
|
||||
| Site replication peers | Literal check | `rustfs/src/site_replication/mod.rs` |
|
||||
| Tiering warm backends (S3, MinIO, RustFS, Azure, GCS, Aliyun, Tencent, Huawei, R2) | Literal check | `crates/ecstore/src/services/tier/warm_backend.rs` `validate_endpoint`; the RustFS provider adds a debug-only, env-gated loopback exception for e2e tests |
|
||||
| Keystone `auth_url` | Literal check | `crates/keystone/src/config.rs` |
|
||||
|
||||
- event-notification webhooks (`RUSTFS_NOTIFY_WEBHOOK_*`);
|
||||
- audit webhooks (`RUSTFS_AUDIT_WEBHOOK_*`);
|
||||
- OIDC identity-provider discovery, JWKS, and token requests (since `1.0.0-beta.12`);
|
||||
- S3 tiering (warm-backend) endpoints;
|
||||
- Keystone auth URLs.
|
||||
|
||||
The webhook and audit outbound clients also **disable proxies and do not follow
|
||||
redirects**, so the destination must be reachable directly at the configured URL.
|
||||
|
||||
## What changed in beta.11 (and for OIDC in beta.12)
|
||||
|
||||
For webhook and audit clients:
|
||||
|
||||
| | beta.10 | beta.11+ |
|
||||
|---|---|---|
|
||||
| Literal `localhost` / private / loopback IPs | Rejected | Rejected |
|
||||
| Hostnames that resolve to private/loopback addresses (`logstash`, `host.docker.internal`, Compose service DNS, …) | Allowed | **Blocked at DNS/connect time** unless allowlisted |
|
||||
| Escape hatch for private destinations | None | `RUSTFS_OUTBOUND_ALLOW_ORIGINS` |
|
||||
| Proxies / redirects for outbound clients | Followed | Disabled |
|
||||
|
||||
Before beta.11 a webhook endpoint whose hostname happened to resolve to a
|
||||
private address was accepted. Beta.11 fails that resolution check unless the
|
||||
exact origin is on the allowlist. This is why a Compose setup that delivered
|
||||
events on beta.10 can go silent after the upgrade even though the configuration
|
||||
is unchanged.
|
||||
|
||||
OIDC joined the same policy in beta.12. An internal identity provider that
|
||||
worked in beta.11 can therefore fail discovery after upgrading to beta.12 unless
|
||||
its exact origin is allowlisted. The policy remains active for discovery, JWKS,
|
||||
and token requests.
|
||||
The allowlist affects only the "Full policy" rows. A literal-check subsystem rejects a hostname that is itself a restricted IP literal, does not re-check what a hostname resolves to, and cannot be widened by `RUSTFS_OUTBOUND_ALLOW_ORIGINS`.
|
||||
|
||||
## Symptoms
|
||||
|
||||
- Bucket event rules and webhook configuration look correct.
|
||||
- Uploads and audited API calls succeed.
|
||||
- No HTTP POST reaches the internal webhook receiver.
|
||||
- The target may appear offline or fail activation when its endpoint resolves to
|
||||
a loopback, private, shared, or reserved address.
|
||||
- Startup or target validation reports `webhook endpoint is not allowed: ...`
|
||||
with a reason such as `private address` or `loopback host`.
|
||||
- An OIDC provider or login button is missing, and startup reports
|
||||
`OIDC provider discovery blocked by outbound policy` with the exact origin to
|
||||
allowlist.
|
||||
- Bucket event rules and webhook configuration look correct and uploads succeed, but no POST reaches the receiver.
|
||||
- Target validation reports `<field> is not allowed: ...` with a reason such as `private address` or `loopback host`; when an exact-origin allowlist entry would fix it, the message says so.
|
||||
- An OIDC login button is missing and startup logs `OIDC provider discovery blocked by outbound policy`.
|
||||
|
||||
## `RUSTFS_OUTBOUND_ALLOW_ORIGINS`
|
||||
|
||||
`RUSTFS_OUTBOUND_ALLOW_ORIGINS` is a comma-separated list of exact HTTP(S)
|
||||
origins that are permitted to resolve to otherwise-restricted addresses. It is an
|
||||
operator-owned process setting read once at startup; individual target
|
||||
configuration cannot extend it.
|
||||
A comma-separated list of exact HTTP(S) origins permitted to resolve to otherwise-restricted addresses. It is a process-level setting read once at startup; individual target configuration cannot extend it.
|
||||
|
||||
```bash
|
||||
# exact scheme://host:port — comma-separate multiple origins
|
||||
RUSTFS_OUTBOUND_ALLOW_ORIGINS=http://logstash:8080,http://host.docker.internal:3020
|
||||
```
|
||||
|
||||
### Origin format rules
|
||||
|
||||
Each entry is matched as an **exact origin** (`scheme://host:port`):
|
||||
|
||||
- The scheme must be `http` or `https`.
|
||||
- The host and port must match the destination exactly. An allowlisted
|
||||
`http://logstash:8080` does **not** authorize `http://logstash:9090` or
|
||||
`https://logstash:8080`.
|
||||
- If the port is omitted, the scheme's default is used (`80` for `http`, `443`
|
||||
for `https`); the destination must then use that same default port.
|
||||
- Entries must be origins only. A trailing `/` is accepted, but a path, query,
|
||||
or fragment (for example `http://logstash:8080/events`) is **rejected** as an
|
||||
invalid origin — the process fails closed rather than silently ignoring the
|
||||
path.
|
||||
- Userinfo (`http://user:pass@host`) is not allowed.
|
||||
- An empty entry (for example a trailing or doubled comma) is rejected.
|
||||
|
||||
An invalid list fails closed: the affected subsystem reports an
|
||||
`invalid outbound policy` / `invalid origin at position N` error instead of
|
||||
starting with a partially applied allowlist.
|
||||
| Rule | Detail |
|
||||
| --- | --- |
|
||||
| Exact origin | `scheme://host:port`. `http://logstash:8080` does not authorize `http://logstash:9090` or `https://logstash:8080` |
|
||||
| Scheme | `http` or `https` only |
|
||||
| Default port | If omitted, the scheme default (`80` / `443`) applies and the destination must use that port |
|
||||
| Origin only | A trailing `/` is accepted; any path, query, or fragment (`http://logstash:8080/events`) is rejected |
|
||||
| No userinfo | `http://user:pass@host` is rejected |
|
||||
| No empty entries | A trailing or doubled comma is rejected |
|
||||
| Fail closed | An invalid list yields `invalid outbound policy` / `invalid origin at position N` and the affected subsystem does not start with a partially applied allowlist |
|
||||
|
||||
### What stays blocked even when allowlisted
|
||||
|
||||
Allowlisting an origin only relaxes the loopback, private, shared, and reserved
|
||||
address classes for that exact origin. The following remain forbidden for every
|
||||
origin, allowlisted or not:
|
||||
- Cloud metadata endpoints (`169.254.169.254` and the other well-known IMDS addresses).
|
||||
- Link-local addresses (`169.254.0.0/16`, `fe80::/10`) and the unspecified address (`0.0.0.0`, `::`).
|
||||
- IPv4-mapped, IPv4-compatible, and NAT64/6to4-embedded forms of the above; the embedded IPv4 address is what gets classified, so `::ffff:127.0.0.1` cannot bypass the policy.
|
||||
|
||||
- cloud metadata endpoints (for example `169.254.169.254` and the other
|
||||
well-known IMDS addresses);
|
||||
- link-local addresses (`169.254.0.0/16`, `fe80::/10`);
|
||||
- the unspecified address (`0.0.0.0`, `::`);
|
||||
- IPv4-mapped, IPv4-compatible, and NAT64/6to4-embedded forms of any of the
|
||||
above (RustFS classifies the embedded IPv4 destination, so `::ffff:127.0.0.1`
|
||||
and similar cannot be used to bypass the policy).
|
||||
|
||||
The allowlist authorizes only the exact host you name. A DNS answer for a
|
||||
different hostname that points at a private address is still rejected, and each
|
||||
new connection re-validates the resolved addresses so a rebinding answer fails
|
||||
closed.
|
||||
The allowlist authorizes only the exact host named. A DNS answer for a different hostname that points at a private address is still rejected, and each new connection re-validates the resolved addresses.
|
||||
|
||||
## Docker Compose example
|
||||
|
||||
@@ -128,25 +70,11 @@ services:
|
||||
RUSTFS_NOTIFY_WEBHOOK_ENDPOINT_PRIMARY: "http://logstash:8080/events"
|
||||
RUSTFS_NOTIFY_WEBHOOK_QUEUE_DIR_PRIMARY: "/tmp/rustfs-events"
|
||||
# Allow the webhook host to resolve to the Compose private network.
|
||||
# Note: the allowlist takes the origin only, without the /events path.
|
||||
# The allowlist takes the origin only, without the /events path.
|
||||
RUSTFS_OUTBOUND_ALLOW_ORIGINS: "http://logstash:8080"
|
||||
logstash:
|
||||
image: docker.elastic.co/logstash/logstash:8.15.0
|
||||
# ...
|
||||
```
|
||||
|
||||
The endpoint keeps its full path (`/events`); the allowlist entry is the origin
|
||||
(`http://logstash:8080`) only.
|
||||
|
||||
## Upgrade checklist (beta.10 → beta.11+, or OIDC beta.11 → beta.12+)
|
||||
|
||||
1. List every outbound endpoint whose hostname resolves to a loopback, private,
|
||||
shared, or reserved address: notification webhooks, audit webhooks, OIDC
|
||||
providers, tiering endpoints, and Keystone auth URLs.
|
||||
2. Add each one to `RUSTFS_OUTBOUND_ALLOW_ORIGINS` as an exact
|
||||
`scheme://host:port` origin (no path).
|
||||
3. Ensure the endpoint is reachable directly — for webhook and audit targets,
|
||||
proxies are disabled and redirects are not followed.
|
||||
4. Restart RustFS; the policy is read at startup.
|
||||
5. Confirm delivery, and check the logs for `... is not allowed` messages if a
|
||||
target still fails to activate.
|
||||
The endpoint keeps its full path (`/events`); the allowlist entry is the origin only. Restart RustFS after changing the variable — the policy is read at startup — and check the logs for `is not allowed` messages if a target still fails to activate.
|
||||
|
||||
Reference in New Issue
Block a user