Skip to content

Commit d439318

Browse files
committed
docs(spec,adr-0069): server.trustProxy names both client-address consumers; D5's mechanism cell amended
The `server.trustProxy` describe named only the inbound rate limiter; since the auth allow-list fix, `objectstack serve` passes it to AuthPlugin too, whose D5 IP allow-list and D7 `last_login_ip` audit use the same client-address rule (`@objectstack/plugin-auth/client-address`). The generated reference is regenerated with gen:docs. ADR-0069 D5 gains a dated amendment note: forwarded headers (X-Forwarded-For, then X-Real-IP) only under server.trustProxy, otherwise the transport peer; CF-Connecting-IP no longer read; an undetermined address refused (403 IP_NOT_ALLOWED) while ranges are set. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
1 parent 41646a5 commit d439318

4 files changed

Lines changed: 35 additions & 5 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): `server.trustProxy`'s description names both consumers of the client-address rule
6+
7+
Clause-②: no
8+
9+
The `.describe()` on `server.trustProxy` named only the inbound rate limiter. `objectstack serve` also passes the value to the authentication plugin, whose IP allow-list (`allowed_ip_ranges`) judges, and whose login audit stamps as `last_login_ip`, the address that the same rule establishes (`@objectstack/plugin-auth/client-address`). The description now names both. No key, type, default or accepted value changes.

‎content/docs/references/system/stack-server.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ const result = ServerRateLimitConfigSchema.parse(data);
101101
| Property | Type | Required | Description |
102102
| :--- | :--- | :--- | :--- |
103103
| **security** | `{ rateLimit?: object }` | optional | Server-level security configuration. Today: the global inbound rate limit. |
104-
| **trustProxy** | `boolean` | optional (default: `false`) | Believe `X-Forwarded-For` / `X-Real-IP` when identifying a caller. Declare `true` ONLY when a reverse proxy you control overwrites those headers on every inbound request. Left `false` (the default) the caller IP is the transport's own peer address, which a client cannot forge. Consumed by the inbound rate limiter when `server.security.rateLimit.enabled` is set. |
104+
| **trustProxy** | `boolean` | optional (default: `false`) | Believe `X-Forwarded-For` / `X-Real-IP` when identifying a caller. Declare `true` ONLY when a reverse proxy you control overwrites those headers on every inbound request. Left `false` (the default) the caller IP is the transport's own peer address, which a client cannot forge. One client-address rule, two consumers: the inbound rate limiter when `server.security.rateLimit.enabled` is set, and the authentication plugin, whose IP allow-list (`allowed_ip_ranges`) judges and whose login audit stamps (`last_login_ip`) the same address. |
105105

106106
### Nested Shape: `StackServerConfig.security`
107107

‎docs/adr/0069-enterprise-authentication-hardening.md‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0069: Enterprise authentication hardening — password policy, enforced MFA, SSO, session controls, network gating, and anti-brute-force, all enforcement-wired
22

3-
**Status**: Accepted — **P1 + P2 implemented** (2026-07-04); P3 partially landed (see Addendum). Original proposal 2026-06-24. Session-of-record question settled 2026-08-04 (#4785): sessions live in `sys_session`; the kernel `cache` service backs D2's rate-limit counters only. · **Amended** (2026-10-14, ruling C′ on [#23287](https://github.com/objectstack-ai/objectstack/issues/23287), [`6159450584`](https://github.com/objectstack-ai/objectstack/issues/23287#issuecomment-6159450584), maintainer 「同意」) — D3 states what the gate holds for an API key, for password expiry and for a forced reset; see the note under D3.
3+
**Status**: Accepted — **P1 + P2 implemented** (2026-07-04); P3 partially landed (see Addendum). Original proposal 2026-06-24. Session-of-record question settled 2026-08-04 (#4785): sessions live in `sys_session`; the kernel `cache` service backs D2's rate-limit counters only. · **Amended** (2026-10-14, ruling C′ on [#23287](https://github.com/objectstack-ai/objectstack/issues/23287), [`6159450584`](https://github.com/objectstack-ai/objectstack/issues/23287#issuecomment-6159450584), maintainer 「同意」) — D3 states what the gate holds for an API key, for password expiry and for a forced reset; see the note under D3. · **Amended** (2026-10-20, PR [#24795](https://github.com/objectstack-ai/objectstack/pull/24795), no decision changes) — D5's client-address mechanism follows the shared `server.trustProxy` rule; see the note under D5.
44
**Deciders**: ObjectStack Protocol Architects
55
**Builds on**: [ADR-0049](./0049-no-unenforced-security-properties.md) (**the governing constraint** — a security property that isn't enforced at runtime is forbidden; no toggle may be a "false surface"), [ADR-0007](./0007-settings-manifest-and-kv-store.md) (settings manifest + cascade KV store), [ADR-0057](./0057-erp-authorization-core-business-units-and-scope-depth.md) (`sys_role` is platform-native, decoupled from better-auth; org scoping), [ADR-0066](./0066-unified-authorization-model.md) (capability/assignment split), [ADR-0068](./0068-unified-user-context-and-built-in-identity-roles.md) (`current_user` contract, built-in roles)
66
**Consumers**: `@objectstack/plugin-auth` (better-auth wiring, `bindAuthSettings`/`applyConfigPatch`, auth route middleware), `@objectstack/service-settings` (`auth.manifest.ts`), `@objectstack/platform-objects` (identity objects `sys_user`/`sys_session`/`sys_account`), `@objectstack/rest` (auth request middleware seam), `../objectui` (settings UI rendering)
@@ -132,11 +132,24 @@ Legend: **[native]** = a better-auth config/plugin does the enforcing; **[custom
132132

133133
| Setting | Default | Seam | Mechanism |
134134
|---|---|---|---|
135-
| `sys_organization.allowed_ip_ranges` (CIDR[]) | — | auth request middleware (Hono, **before** the better-auth handler in `plugin-auth` route registration) | **[custom]** extract client IP (`x-forwarded-for` / `cf-connecting-ip`, trust-proxy aware); reject login/session outside the org's ranges with `IP_NOT_ALLOWED` |
135+
| `sys_organization.allowed_ip_ranges` (CIDR[]) | — | auth request middleware (Hono, **before** the better-auth handler in `plugin-auth` route registration) | **[custom]** extract client IP (`x-forwarded-for` / `cf-connecting-ip`, trust-proxy aware); reject login/session outside the org's ranges with `IP_NOT_ALLOWED` *(client-address mechanism amended 2026-10-20 — see the note below)* |
136136
| `sys_user.allowed_ip_ranges` (optional override) | — | same | **[custom]** per-user override, evaluated before org |
137137
| `require_ip_match` (toggle) | off | session refresh | **[custom]** detect `request IP != session.ip_address` (already captured) -> re-verify or revoke |
138138

139139
> IP is already captured on `sys_session.ip_address`; the only missing piece is the **gate** — a middleware seam exists in the auth route registration, before requests reach better-auth.
140+
>
141+
> **How the client address is established (amended 2026-10-20: PR [#24795](https://github.com/objectstack-ai/objectstack/pull/24795),
142+
> `25e16ab00c`, for [#24739](https://github.com/objectstack-ai/objectstack/issues/24739)).** The header list in the
143+
> first row's mechanism cell no longer describes the code. The address the allow-list judges is the one the server
144+
> establishes by the platform's single client-address rule, `resolveClientAddress`, published as
145+
> `@objectstack/plugin-auth/client-address` and imported back by the runtime's inbound rate limiter, so both apply it
146+
> identically: `X-Forwarded-For` (its leftmost entry), then `X-Real-IP`, are believed **only** when the deployment
147+
> declares `server.trustProxy` (`objectstack serve` passes the authored value to `AuthPlugin`); otherwise the address is
148+
> the transport's own peer address. `CF-Connecting-IP` is no longer read. An address that cannot be established is
149+
> refused like one outside the ranges — `403 IP_NOT_ALLOWED` while any range is set (with no ranges set, nothing is
150+
> judged). The D7 login audit stamps that same established address as `last_login_ip`, and stamps none when it is
151+
> undetermined. The decision itself — a gate ahead of the better-auth handler, refusing with `IP_NOT_ALLOWED` — is
152+
> unchanged.
140153
141154
### D6 — Enterprise SSO (P2/P3)
142155

‎packages/spec/src/system/stack-server.zod.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,12 +209,20 @@ export const StackServerConfigSchema = lazySchema(() => strictObject(
209209
* address (limit weaponised). Trusting the header therefore has to be an act
210210
* of authorship — the operator asserting "a proxy I control rewrites this
211211
* header on every request" — and never a default.
212+
*
213+
* Both consumers apply one rule, `resolveClientAddress` from
214+
* `@objectstack/plugin-auth/client-address`: the runtime's inbound rate
215+
* limiter, and `AuthPlugin` (passed this value by `objectstack serve`),
216+
* whose ADR-0069 D5 IP allow-list and D7 `last_login_ip` audit use the
217+
* address it establishes — so a header believed without such a proxy is
218+
* an allow-list bypass as well as a rate-limit one.
212219
*/
213220
trustProxy: z.boolean().default(false).describe(
214221
'Believe `X-Forwarded-For` / `X-Real-IP` when identifying a caller. Declare `true` ONLY when a reverse proxy '
215222
+ 'you control overwrites those headers on every inbound request. Left `false` (the default) the caller IP is '
216-
+ "the transport's own peer address, which a client cannot forge. Consumed by the inbound rate limiter when "
217-
+ '`server.security.rateLimit.enabled` is set.',
223+
+ "the transport's own peer address, which a client cannot forge. One client-address rule, two consumers: the "
224+
+ 'inbound rate limiter when `server.security.rateLimit.enabled` is set, and the authentication plugin, whose IP '
225+
+ 'allow-list (`allowed_ip_ranges`) judges and whose login audit stamps (`last_login_ip`) the same address.',
218226
),
219227
},
220228
));

0 commit comments

Comments
 (0)