Documentation
¶
Overview ¶
Package proxy is the hub's transparent front door to kcp, plus the identity helpers and static-token login that share its auth dispatch.
Index ¶
- Constants
- Variables
- func ClientIP(r *http.Request, trusted []netip.Prefix) string
- func ParseTrustedProxyCIDRs(values []string) ([]netip.Prefix, error)
- type BoundedKeys
- type IPRateLimiter
- type KCPProxy
- func (p *KCPProxy) BrowserIdentity(r *http.Request) (browsersession.Identity, error)
- func (p *KCPProxy) HandleTokenLogin(w http.ResponseWriter, r *http.Request)
- func (p *KCPProxy) HandleTokenLoginRateLimited(w http.ResponseWriter, r *http.Request)
- func (p *KCPProxy) IdentifyUser(r *http.Request) (string, error)
- func (p *KCPProxy) ScrubStaticTokenUsers(ctx context.Context) error
- func (p *KCPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (p *KCPProxy) SetBrowserSessionStore(store *browsersession.Store)
- func (p *KCPProxy) SetTrustedProxies(prefixes []netip.Prefix)
Constants ¶
const DefaultMaxLimiterEntries = 10000
DefaultMaxLimiterEntries bounds how many distinct client addresses a per-address rate limiter tracks at once. Past it the least recently seen address is dropped; an address that comes back simply starts a fresh bucket. The bound exists so a flood of distinct peers cannot grow the hub's memory without limit — it is not a security boundary in itself, since every key is a real connection peer once ClientIP has done its job.
Variables ¶
var ErrIdentifyNoBearer = errors.New("no Authorization: Bearer token")
ErrIdentifyNoBearer is returned by IdentifyUser when the request carries no Authorization: Bearer header. Callers (e.g. the tenant middleware) translate this into a 401.
ErrUserRecordUnavailable wraps failures that happen AFTER the caller's credential has been accepted — the static token matched, or the OIDC token verified — but the backing User record could not be read or created.
It exists so callers can tell "this caller is not authenticated" from "this caller is authenticated and the hub is having trouble". Both surface as an error from IdentifyUser, and conflating them forces a choice between serving unauthenticated callers and 500-ing authenticated ones during a hiccup. Endpoints that only need to know the caller is legitimate (not who they are) can proceed on this; anything that acts as the user must still fail.
Functions ¶
func ClientIP ¶
ClientIP derives the address the hub keys its pre-authentication rate limits on. The rule is the one every reverse proxy follows when it forwards: the proxy APPENDS the peer it saw to X-Forwarded-For, so the only hop the hub can vouch for is the right-most one that a trusted proxy wrote. Every hop to the left of that is whatever the client chose to send.
- The connection peer (RemoteAddr) is always the starting point.
- When trusted is empty, X-Forwarded-For and X-Real-IP are ignored entirely and the peer address is the answer. A hub deployed behind a proxy without --trusted-proxy-cidrs therefore sees every client as the proxy's own address and throttles them as one; that is the safe way to fail (too much throttling, never too little).
- When the peer is inside a trusted prefix, X-Forwarded-For is walked from the right, skipping hops that are themselves trusted proxies, and the first untrusted hop is the client. X-Real-IP is consulted only when that walk yields nothing.
- A hop that is not a valid IP ends the walk and the peer address is used, so a malformed header can never mint a fresh bucket.
The result is a canonical IP string (IPv4-mapped IPv6 unmapped, zone dropped); when RemoteAddr itself is not an IP it is returned as-is, which only happens for non-TCP listeners.
func ParseTrustedProxyCIDRs ¶
ParseTrustedProxyCIDRs parses the --trusted-proxy-cidrs values. Each entry may be a CIDR ("10.0.0.0/8", "fd00::/8") or a bare address, which is taken as a single-host prefix. Entries may themselves be comma-separated; blanks are ignored.
Types ¶
type BoundedKeys ¶
type BoundedKeys[V any] struct { // contains filtered or unexported fields }
BoundedKeys is a fixed-capacity, least-recently-used map from a client key to a limiter state, with idle eviction. It is not safe for concurrent use; the limiter that owns it holds the lock.
Two things keep it bounded: Put evicts the least recently used entry when the map is at capacity, and any Put that lands more than idle after the previous sweep first drops every entry that has not been touched within idle. The sweep walks from the least recent end and stops at the first live entry, so it costs no more than the number of entries it removes.
func NewBoundedKeys ¶
func NewBoundedKeys[V any](maxEntries int, idle time.Duration) *BoundedKeys[V]
NewBoundedKeys returns an empty store holding at most max entries and forgetting entries not touched for idle. Non-positive max falls back to DefaultMaxLimiterEntries; non-positive idle disables idle eviction.
type IPRateLimiter ¶
type IPRateLimiter struct {
// contains filtered or unexported fields
}
IPRateLimiter is the per-client-address token bucket the hub uses on its pre-authentication endpoints (token-login, the aggregate MCP bearer check). It is the same limiter HandleTokenLoginRateLimited applies, exposed so other packages can share one implementation without importing its internals.
func NewIPRateLimiter ¶
func NewIPRateLimiter(interval time.Duration, burstSize int) *IPRateLimiter
NewIPRateLimiter returns a limiter that admits burstSize requests per client address immediately and refills one token per interval up to burstSize.
func (*IPRateLimiter) Allow ¶
func (l *IPRateLimiter) Allow(clientIP string) bool
Allow reports whether a request from clientIP may proceed, consuming one token when it may.
type KCPProxy ¶
type KCPProxy struct {
// contains filtered or unexported fields
}
KCPProxy is the hub's front door to kcp. It forwards every kube path (/clusters/{id}/…, /services/apiexport/…, bare /api and /apis) to kcp byte-for-byte: path, query, Authorization and the WebSocket bearer subprotocol all arrive as the caller sent them, and the hub adds no credential of its own. kcp authenticates the bearer (static token, OIDC, ServiceAccount) and authorizes the request with its own RBAC — the workspace `access` gate, the bound tenant-admin role and the organization boundary guard — so the hub makes no access decision on this path. The type also hosts the identity helpers (BrowserIdentity, IdentifyUser) and the static-token login the rest of the hub shares with that front door.
func NewKCPProxy ¶
func NewKCPProxy(kcpConfig *rest.Config, verifier *oidc.IDTokenVerifier, railgridClient *railgridclient.Client, staticAuthTokens []string, hubExternalURL string, devMode bool) (*KCPProxy, error)
NewKCPProxy creates the transparent front door to kcp. verifier is used only by BrowserIdentity and may be nil when only static token auth is used.
func (*KCPProxy) BrowserIdentity ¶
BrowserIdentity validates a portal bearer and resolves it to the stable platform user identity used by the shared browser-session store. The returned value contains no bearer, kubeconfig, or OIDC token.
func (*KCPProxy) HandleTokenLogin ¶
func (p *KCPProxy) HandleTokenLogin(w http.ResponseWriter, r *http.Request)
HandleTokenLogin handles static token login requests. POST /auth/token-login with Authorization: Bearer <token> Returns a LoginResponse with kubeconfig pointing to the user's workspace. Note: This handler should be wrapped with rate limiting using HandleTokenLoginRateLimited when registering routes to prevent brute force attacks.
func (*KCPProxy) HandleTokenLoginRateLimited ¶
func (p *KCPProxy) HandleTokenLoginRateLimited(w http.ResponseWriter, r *http.Request)
HandleTokenLoginRateLimited wraps HandleTokenLogin with rate limiting. This should be used when registering the route to protect against brute force attacks. In devMode the limiter is bypassed: dev clusters share a single client IP (localhost) across many test/CLI invocations, which trivially exhausts the per-IP bucket and is not a realistic threat model for dev.
func (*KCPProxy) IdentifyUser ¶
IdentifyUser extracts the caller's User CR name from r's bearer token (static token or OIDC). Used by hub REST endpoints behind the tenant middleware so they can identify the caller with one auth dispatch.
Returns ErrIdentifyNoBearer for missing/unparseable Authorization headers, and other errors for verification failures. kcp ServiceAccount tokens are intentionally not accepted here — REST endpoints are addressed by humans (or by their portal session) and not by edge-side bots.
func (*KCPProxy) ScrubStaticTokenUsers ¶
ScrubStaticTokenUsers removes the token text from the Users (and their personal Organizations) of every configured static token; see scrubStaticTokenUser. Login does the same, but a token nobody uses after an upgrade would otherwise stay readable in member lists indefinitely. It only updates Users that exist; it never creates one.
func (*KCPProxy) ServeHTTP ¶
func (p *KCPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP forwards the request to kcp unchanged. There is no bearer check, cluster check or path rewrite here: a request without a credential gets kcp's 401, a workspace the caller holds no RBAC in gets kcp's 403, and a path kcp does not serve gets kcp's 404.
func (*KCPProxy) SetBrowserSessionStore ¶
func (p *KCPProxy) SetBrowserSessionStore(store *browsersession.Store)
SetBrowserSessionStore wires the hub-wide opaque browser-session store used by static-token portal login. The proxy never places the static bearer in that store or in the cookie; it records only the resolved User identity.
func (*KCPProxy) SetTrustedProxies ¶
SetTrustedProxies tells the token-login rate limiter which connection peers are reverse proxies whose X-Forwarded-For may be believed (see ClientIP). Without it every request is keyed on the connection peer, so a hub behind a proxy throttles all of its clients as one address.