v0.28.0¶
Release Date: 2026-10-29
A security release that slows abuse, makes incidents traceable and writes the risks down:
- Rate limits in layers, on by default. SystemAuth limits every route
per client address, applies tighter route policies to the token, login,
sign-up and BFF sign-in paths, limits failed client authentication, the
JWT-bearer grant and social login, and limits signed-in callers, all on
one shared storage and answered with
RateLimitheaders. - One client-address rule.
session/clientaddrbelieves a forwarding header only from trusted proxy networks. SystemAuth, the BFF,appauth, rate limits and security events all use it. - Logs and events fit for incidents. Structured security events through
contract/audit, OAuth errors logged without stack traces, tests proving no token or secret reaches a log or event, and no internal error text in API responses. - Static API keys are removed from relying parties.
It also adds a threat model and closes the smaller findings of the launch
review: the BFF session cookie refuses SameSite=None,
EmbeddedOAuthProvider.Revoke revokes, and every server systemforge builds
sets ReadHeaderTimeout.
The release is breaking. Applications that passed APIKeys stop
compiling, rate limits are on without any configuration, and some
organization API status codes and error messages change. A SystemAuth
behind a reverse proxy must set client_address, or every visitor shares
the proxy's address and its limits. Read Upgrading first.
Highlights¶
- Layered rate limits: a default of 300 requests a minute per client
address, route policies for
token,login,signupandbff_auth, handler limits on credential guessing, and a signed-in limit of 1200 a minute per principal or service account. session/clientaddr: trusted proxy networks and the header they write, configured once asclient_addressand shared by every part of the server.- Security events for sign-ins, refusals, sign-outs, token replay, revocations, membership and role changes, service accounts and production waivers, each with the request ID and the visitor's address.
- No internal error text in about 60 API responses.
- A threat model with the trust boundaries, the threats to each and the package that mitigates it.
Security¶
- Rate limits. SystemAuth had no limit on most routes, so credential endpoints could be guessed at and any route flooded. It now applies, by default and outermost:
- a limit per client address on every route (300 a minute);
- route policies:
/oauth/tokenper address (120 a minute) and per OAuth client (1200 a minute),/login*(120),/signup*(60) and/bff/auth/*(120) per address; - exemptions for health checks, static assets and the billing webhook, whose provider calls from shared addresses.
Inside handlers it limits failed client authentication at the token
endpoint, per address and client and then per address; tokens the
JWT-bearer grant issues per service account; and social login start and
callback per address. The login-token, waitlist, sign-up code and
managed-principal code limits move onto the same storage, and the
managed-principal API applies the signed-in limit after authentication.
- Rate limit responses. A refused request gets 429 with Retry-After
in whole seconds, never below 1. Every checked response carries the IETF
RateLimit-Policy and RateLimit headers beside X-RateLimit-*.
- Storage outages. When the storage fails, the default layer and the
signed-in limit let the request through and log the failure; the route
policies and the credential endpoints' handler limits refuse with 503.
A full in-memory storage is always a 429, so a flood of new addresses
cannot switch the limits off.
- Client addresses. Client addresses were found in several ways, some
of which believed forwarding headers from any peer. session/clientaddr
believes X-Forwarded-For, read from the right, or a single-value header
such as CF-Connecting-IP, only when the direct peer is a trusted proxy,
and otherwise uses the peer.
- OAuth error logs. Token, authorize and access-token log records
carried the error value, which slog's text handler formats with its stack
trace. They now carry the RFC 6749 error code, description, hint and
status; the cause is added, by message only, at debug level or for server
errors.
- No secrets in logs or events. Tests run social sign-in, the
authorization code flow with PKCE, refresh, code and refresh-token
replay, client credentials, JWT bearer, revocation, logout and BFF
sign-in and sign-out with logging at debug level, and fail if any token,
code, verifier, client or HMAC secret, DPoP proof, assertion, key or
session cookie appears in a log record or security event.
- Security events. SystemAuth, organizations, platform administration
and service accounts record structured events through an
audit.Emitter, each with the request ID and the visitor's address taken
from the client-address rule. See
Security events.
- Internal error text in responses. About 60 responses across the
organization, marketplace, contract, invite and connections APIs,
session/middleware, managed principals, SystemAuth identity sync and
the sign-up API copied the underlying error, such as a database error,
into the body. Known errors now map to fixed messages; anything else is
logged and answered with a generic 500.
- BFF session cookie. bff.CookieConfig accepted SameSite=None,
which lets the browser send the session cookie on cross-site
subrequests; the origin check that protects state-changing requests
relies on the browser withholding it. Validate, and so NewHandler,
now refuse it.
- EmbeddedOAuthProvider.Revoke passed the raw token where storage
expects a request ID, so it reported success without revoking anything.
It now revokes the access and refresh tokens of the token's family.
- ReadHeaderTimeout. The multiapp server and the publicclient
loopback callback server set none, so a client could hold a connection by
sending headers slowly. Every server systemforge builds now sets it.
- Static API keys were a second, weaker way into a relying party:
long-lived bearer secrets outside the short-lived, revocable access
tokens every application already verifies. They are removed.
Breaking Changes¶
- API keys removed from relying parties:
appauth.Options.APIKeys,relyingparty.BearerConfig.APIKeys,relyingparty.APIKeyValidator,relyingparty.AuthMethodAPIKeyandrelyingparty.AuthenticatedPrincipal.APIKeyID. ABearervalue that is not a JWT is answered401withinvalid_token, andX-API-Keyis ignored.identity/apikeyand thesession/middlewareAPI key middleware are unchanged. - Rate limits are on by default. Set
rate_limit.disabled: trueto turn off the default layer and the route policies when the host applies its ownratelimit.Policyin front of SystemAuth; the handler limits stay on. - Organization API status codes:
- the personal-organization conflict is
409on every path, including create and remove; - a storage error while reading a member is
500instead of404; - an unexpected error on create or remove is
500instead of400. - Fixed error messages. The APIs listed under Security answer fixed
messages instead of the underlying error text, and the bearer and DPoP
middlewares answer the matching sentinel's text or
authentication failed. contract.ToContractErrorno longer copies a non-contract error's text into its message.ratelimit.Limiter.Middlewareanswers429instead of503when aMemoryStorageis full.bff.CookieConfig.ValidaterejectsSameSite=None.EmbeddedOAuthProvider.Revokereturns an error wrappingErrInvalidTokenfor an unknown, expired or already revoked token.
Added¶
session/clientaddr:Resolver,Config(TrustedProxies,Header),New,MustNew,LoopbackProxy,Peer,PeerAddr,PeerIPandParseNetworks.- Client address settings:
client_address.trusted_proxiesandclient_address.headerin SystemAuth's configuration,systemauth.WithClientAddressResolver,scimtoken.WithClientAddress,bff.ClientIPConfig.Resolver,appauth.Options.ClientAddressandAuthenticator.ClientAddress. session/ratelimit:Policy,PolicyConfigandNewPolicy, withRoute,RouteLimit,DefaultRoutes,MergeRoutes, the route names (RouteToken,RouteLogin,RouteSignup,RouteBFFAuth,RouteHealth,RouteStatic,RoutePaymentWebhook),KeyAddress,KeyClientandAddressKey;NewSignedInLimiterandSignedInKey;Check,ExhaustedandRecordfor handler limits;WriteHeaders,WriteTooManyRequestsandRetryAfterSeconds;WithName,WithSkip, andPeekonMemoryStorageandRedisStorage.- Rate limit settings:
rate_limit.disabled,redis_url,single_instance,default,signed_in,exempt_networksandroutes(name,paths,methods,per_address,per_client,exempt,fail_open) in SystemAuth's configuration;systemauth.WithRateLimitStorage,Server.RateLimitStorage,Server.RateLimitPolicy,Server.SignedInRateLimitandServer.ClientAddress;systemauthsvc.Options.RateLimitStorage;appauth.Options.SignedInRateLimit. contract/audit:Context.RequestID,RequestID,NopEmitterandNewLogEmitter;requestprincipal.AuditActor.- Security event sources:
systemauth.WithAuditEmitter,organization.WithAuditEmitter,principal.WithAuditEmitterandserviceaccount.Config.Audit.systemauthsvc.Options.AuditEmitterreaches SystemAuth and the services the package builds, and each production check waived at startup is recorded as asystemauth.production.waiverevent. Thesystemauthcommand writes its security events to its log. - Sentinel errors:
organization.ErrOwnerRequired,ErrAlreadyMember,ErrNewOwnerNotMemberandErrInvalidOwnerRole;marketplace.ErrInvalidListingandErrListingNotDeletable;invite.ErrInviteNotFound,ErrPendingInviteExists,ErrAlreadyMember,ErrAppRolesNotConfiguredandErrCollaboratorAppRoles.
Deprecated¶
Each still works and is built on session/clientaddr:
ratelimit.ForwardedClientIPandratelimit.ForwardedIPKey: use aclientaddr.Resolverwithratelimit.AddressKey.systemauth.WithLoginTokenClientIP: configureclient_address, or useWithClientAddressResolver.bff.CloudflareClientIPConfigand theClientIPConfigfieldsTrustCloudflare,TrustProxy,TrustedProxiesandCloudflareIPRanges: setClientIPConfig.Resolver.
Upgrading¶
New SystemAuth settings. Values reach the YAML through ${VAR} references,
so the variable name is a convention.
| YAML key | Suggested variable | Needed when |
|---|---|---|
client_address.trusted_proxies |
— | SystemAuth is behind a reverse proxy |
client_address.header |
— | The proxy writes a header other than X-Forwarded-For |
rate_limit.redis_url |
SYSTEMAUTH_RATE_LIMIT_REDIS_URL |
Several instances serve SystemAuth |
rate_limit.single_instance: true |
— | One instance serves SystemAuth |
rate_limit.exempt_networks |
— | Back-channel callers share one address |
rate_limit.routes |
— | A route policy needs other limits |
-
Set the client address behind a proxy. Without it every visitor has the proxy's address, so the per-address limits apply to all of them together. For a reverse proxy on the same host that replaces
X-Forwarded-For:See Client addresses and Deploying SystemAuth. 2. Share the counters. With several SystemAuth instances, set
rate_limit.redis_url; with one, setrate_limit.single_instance: trueto silence the startup warning. See Rate limits. 3. Exempt or raise limits for back-channel callers. A relying party that calls/oauth/tokenfrom one server address shares thetokenroute's per-address limit across all its people. Add its network torate_limit.exempt_networks, or raise thetokenroute's limits: -
Move off static API keys. Remove
APIKeysfromappauth.Optionsandrelyingparty.BearerConfig, move command-line tools toidentity/publicclientsign-in and automation to service accounts, then revoke the keys. See Upgrading: static API keys removed. - Record security events. Set
systemauthsvc.Options.AuditEmitter, orsystemauth.WithAuditEmitter,organization.WithAuditEmitter,principal.WithAuditEmitterandserviceaccount.Config.Audit, for example withaudit.NewLogEmitter(logger), and ship the events off the host. Without an emitter they are discarded. See Recording events. - Expect
429responses. Clients should honourRetry-Afterand may read theRateLimitheaders. - Check API clients that relied on the old organization status codes, or on error text in responses, which is now a fixed message.
- Use
SameSiteStrictorLaxfor the BFF session cookie, and handle the errorEmbeddedOAuthProvider.Revokenow returns for a token it cannot revoke.
Walk the production checklist before going live.
Documentation¶
- Threat model: scope and assumptions, assets, actors, trust boundaries, threats and mitigations per boundary, and residual risks
- Security events: how to record them, their common fields and each event type
- Client addresses and rate limiting, with
the
client_addressandrate_limitsettings in Deploying SystemAuth and the production checklist - Upgrading from static API keys
For release history, see the changelog.