// Package config loads process-level gateway configuration from environment // variables. package config import ( "fmt" "net/url" "os" "strconv" "strings" "time" ) const ( // shutdownTimeoutEnvVar names the environment variable that controls the // maximum time granted to each component shutdown call. shutdownTimeoutEnvVar = "GATEWAY_SHUTDOWN_TIMEOUT" // logLevelEnvVar names the environment variable that configures the process // log level used by structured JSON logging. logLevelEnvVar = "GATEWAY_LOG_LEVEL" // publicHTTPAddrEnvVar names the environment variable that configures the // public REST listener address. publicHTTPAddrEnvVar = "GATEWAY_PUBLIC_HTTP_ADDR" // publicHTTPReadHeaderTimeoutEnvVar names the environment variable that // configures the maximum time allowed to read public REST request headers. publicHTTPReadHeaderTimeoutEnvVar = "GATEWAY_PUBLIC_HTTP_READ_HEADER_TIMEOUT" // publicHTTPReadTimeoutEnvVar names the environment variable that configures // the maximum time allowed to read the full public REST request. publicHTTPReadTimeoutEnvVar = "GATEWAY_PUBLIC_HTTP_READ_TIMEOUT" // publicHTTPIdleTimeoutEnvVar names the environment variable that configures // the keep-alive idle timeout for the public REST listener. publicHTTPIdleTimeoutEnvVar = "GATEWAY_PUBLIC_HTTP_IDLE_TIMEOUT" // publicAuthUpstreamTimeoutEnvVar names the environment variable that // configures the timeout budget used for public auth upstream calls. publicAuthUpstreamTimeoutEnvVar = "GATEWAY_PUBLIC_AUTH_UPSTREAM_TIMEOUT" // authServiceBaseURLEnvVar names the environment variable that configures // the optional Auth / Session Service public HTTP base URL used by gateway // public-auth delegation. authServiceBaseURLEnvVar = "GATEWAY_AUTH_SERVICE_BASE_URL" // userServiceBaseURLEnvVar names the environment variable that configures // the optional User Service internal HTTP base URL used by authenticated // gateway self-service delegation. userServiceBaseURLEnvVar = "GATEWAY_USER_SERVICE_BASE_URL" // adminHTTPAddrEnvVar names the environment variable that configures the // private admin HTTP listener address. When it is empty, the admin listener // remains disabled. adminHTTPAddrEnvVar = "GATEWAY_ADMIN_HTTP_ADDR" // adminHTTPReadHeaderTimeoutEnvVar names the environment variable that // configures the maximum time allowed to read admin listener request // headers. adminHTTPReadHeaderTimeoutEnvVar = "GATEWAY_ADMIN_HTTP_READ_HEADER_TIMEOUT" // adminHTTPReadTimeoutEnvVar names the environment variable that configures // the maximum time allowed to read one admin listener request. adminHTTPReadTimeoutEnvVar = "GATEWAY_ADMIN_HTTP_READ_TIMEOUT" // adminHTTPIdleTimeoutEnvVar names the environment variable that configures // the keep-alive idle timeout for the admin listener. adminHTTPIdleTimeoutEnvVar = "GATEWAY_ADMIN_HTTP_IDLE_TIMEOUT" // authenticatedGRPCAddrEnvVar names the environment variable that configures // the authenticated gRPC listener address. authenticatedGRPCAddrEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ADDR" // authenticatedGRPCConnectionTimeoutEnvVar names the environment variable // that configures the inbound connection handshake timeout for the // authenticated gRPC listener. authenticatedGRPCConnectionTimeoutEnvVar = "GATEWAY_AUTHENTICATED_GRPC_CONNECTION_TIMEOUT" // authenticatedGRPCDownstreamTimeoutEnvVar names the environment variable // that configures the timeout budget used for downstream unary execution. authenticatedGRPCDownstreamTimeoutEnvVar = "GATEWAY_AUTHENTICATED_DOWNSTREAM_TIMEOUT" // authenticatedGRPCFreshnessWindowEnvVar names the environment variable that // configures the accepted client timestamp skew window for authenticated // gRPC requests. authenticatedGRPCFreshnessWindowEnvVar = "GATEWAY_AUTHENTICATED_GRPC_FRESHNESS_WINDOW" // authenticatedGRPCIPRateLimitRequestsEnvVar names the environment // variable that configures the authenticated gRPC per-IP request budget per // window. authenticatedGRPCIPRateLimitRequestsEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_IP_RATE_LIMIT_REQUESTS" // authenticatedGRPCIPRateLimitWindowEnvVar names the environment variable // that configures the authenticated gRPC per-IP rate-limit window. authenticatedGRPCIPRateLimitWindowEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_IP_RATE_LIMIT_WINDOW" // authenticatedGRPCIPRateLimitBurstEnvVar names the environment variable // that configures the authenticated gRPC per-IP rate-limit burst. authenticatedGRPCIPRateLimitBurstEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_IP_RATE_LIMIT_BURST" // authenticatedGRPCSessionRateLimitRequestsEnvVar names the environment // variable that configures the authenticated gRPC per-session request // budget per window. authenticatedGRPCSessionRateLimitRequestsEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_SESSION_RATE_LIMIT_REQUESTS" // authenticatedGRPCSessionRateLimitWindowEnvVar names the environment // variable that configures the authenticated gRPC per-session rate-limit // window. authenticatedGRPCSessionRateLimitWindowEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_SESSION_RATE_LIMIT_WINDOW" // authenticatedGRPCSessionRateLimitBurstEnvVar names the environment // variable that configures the authenticated gRPC per-session rate-limit // burst. authenticatedGRPCSessionRateLimitBurstEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_SESSION_RATE_LIMIT_BURST" // authenticatedGRPCUserRateLimitRequestsEnvVar names the environment // variable that configures the authenticated gRPC per-user request budget // per window. authenticatedGRPCUserRateLimitRequestsEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_USER_RATE_LIMIT_REQUESTS" // authenticatedGRPCUserRateLimitWindowEnvVar names the environment // variable that configures the authenticated gRPC per-user rate-limit // window. authenticatedGRPCUserRateLimitWindowEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_USER_RATE_LIMIT_WINDOW" // authenticatedGRPCUserRateLimitBurstEnvVar names the environment variable // that configures the authenticated gRPC per-user rate-limit burst. authenticatedGRPCUserRateLimitBurstEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_USER_RATE_LIMIT_BURST" // authenticatedGRPCMessageClassRateLimitRequestsEnvVar names the // environment variable that configures the authenticated gRPC per-message // class request budget per window. authenticatedGRPCMessageClassRateLimitRequestsEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_REQUESTS" // authenticatedGRPCMessageClassRateLimitWindowEnvVar names the environment // variable that configures the authenticated gRPC per-message-class // rate-limit window. authenticatedGRPCMessageClassRateLimitWindowEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_WINDOW" // authenticatedGRPCMessageClassRateLimitBurstEnvVar names the environment // variable that configures the authenticated gRPC per-message-class // rate-limit burst. authenticatedGRPCMessageClassRateLimitBurstEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_BURST" // sessionCacheRedisAddrEnvVar names the environment variable that configures // the Redis address used for SessionCache lookups. sessionCacheRedisAddrEnvVar = "GATEWAY_SESSION_CACHE_REDIS_ADDR" // sessionCacheRedisUsernameEnvVar names the environment variable that // configures the Redis username used for SessionCache lookups. sessionCacheRedisUsernameEnvVar = "GATEWAY_SESSION_CACHE_REDIS_USERNAME" // sessionCacheRedisPasswordEnvVar names the environment variable that // configures the Redis password used for SessionCache lookups. sessionCacheRedisPasswordEnvVar = "GATEWAY_SESSION_CACHE_REDIS_PASSWORD" // sessionCacheRedisDBEnvVar names the environment variable that configures // the Redis logical database used for SessionCache lookups. sessionCacheRedisDBEnvVar = "GATEWAY_SESSION_CACHE_REDIS_DB" // sessionCacheRedisKeyPrefixEnvVar names the environment variable that // configures the Redis key prefix used for SessionCache records. sessionCacheRedisKeyPrefixEnvVar = "GATEWAY_SESSION_CACHE_REDIS_KEY_PREFIX" // sessionCacheRedisLookupTimeoutEnvVar names the environment variable that // configures the timeout used for SessionCache Redis lookups and startup // connectivity checks. sessionCacheRedisLookupTimeoutEnvVar = "GATEWAY_SESSION_CACHE_REDIS_LOOKUP_TIMEOUT" // sessionCacheRedisTLSEnabledEnvVar names the environment variable that // configures whether SessionCache Redis connections use TLS. sessionCacheRedisTLSEnabledEnvVar = "GATEWAY_SESSION_CACHE_REDIS_TLS_ENABLED" // replayRedisKeyPrefixEnvVar names the environment variable that configures // the Redis key prefix used for authenticated replay reservations. replayRedisKeyPrefixEnvVar = "GATEWAY_REPLAY_REDIS_KEY_PREFIX" // replayRedisReserveTimeoutEnvVar names the environment variable that // configures the timeout used for authenticated replay reservations and // startup connectivity checks. replayRedisReserveTimeoutEnvVar = "GATEWAY_REPLAY_REDIS_RESERVE_TIMEOUT" // sessionEventsRedisStreamEnvVar names the environment variable that // configures the Redis Stream key consumed for session lifecycle updates. sessionEventsRedisStreamEnvVar = "GATEWAY_SESSION_EVENTS_REDIS_STREAM" // sessionEventsRedisReadBlockTimeoutEnvVar names the environment variable // that configures the blocking read timeout used by the session event // subscriber. sessionEventsRedisReadBlockTimeoutEnvVar = "GATEWAY_SESSION_EVENTS_REDIS_READ_BLOCK_TIMEOUT" // clientEventsRedisStreamEnvVar names the environment variable that // configures the Redis Stream key consumed for client-facing push events. clientEventsRedisStreamEnvVar = "GATEWAY_CLIENT_EVENTS_REDIS_STREAM" // clientEventsRedisReadBlockTimeoutEnvVar names the environment variable // that configures the blocking read timeout used by the client-event // subscriber. clientEventsRedisReadBlockTimeoutEnvVar = "GATEWAY_CLIENT_EVENTS_REDIS_READ_BLOCK_TIMEOUT" // responseSignerPrivateKeyPEMPathEnvVar names the environment variable that // configures the path to the PKCS#8 PEM-encoded Ed25519 private key used to // sign authenticated unary responses and stream events. responseSignerPrivateKeyPEMPathEnvVar = "GATEWAY_RESPONSE_SIGNER_PRIVATE_KEY_PEM_PATH" // publicAuthMaxBodyBytesEnvVar names the environment variable that // configures the maximum accepted request body size for public_auth. publicAuthMaxBodyBytesEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_MAX_BODY_BYTES" // publicAuthRateLimitRequestsEnvVar names the environment variable that // configures the public_auth request budget per window. publicAuthRateLimitRequestsEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_RATE_LIMIT_REQUESTS" // publicAuthRateLimitWindowEnvVar names the environment variable that // configures the public_auth rate-limit window. publicAuthRateLimitWindowEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_RATE_LIMIT_WINDOW" // publicAuthRateLimitBurstEnvVar names the environment variable that // configures the public_auth rate-limit burst. publicAuthRateLimitBurstEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_RATE_LIMIT_BURST" // browserBootstrapMaxBodyBytesEnvVar names the environment variable that // configures the maximum accepted request body size for browser_bootstrap. browserBootstrapMaxBodyBytesEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_BOOTSTRAP_MAX_BODY_BYTES" // browserBootstrapRateLimitRequestsEnvVar names the environment variable // that configures the browser_bootstrap request budget per window. browserBootstrapRateLimitRequestsEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_BOOTSTRAP_RATE_LIMIT_REQUESTS" // browserBootstrapRateLimitWindowEnvVar names the environment variable that // configures the browser_bootstrap rate-limit window. browserBootstrapRateLimitWindowEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_BOOTSTRAP_RATE_LIMIT_WINDOW" // browserBootstrapRateLimitBurstEnvVar names the environment variable that // configures the browser_bootstrap rate-limit burst. browserBootstrapRateLimitBurstEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_BOOTSTRAP_RATE_LIMIT_BURST" // browserAssetMaxBodyBytesEnvVar names the environment variable that // configures the maximum accepted request body size for browser_asset. browserAssetMaxBodyBytesEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_ASSET_MAX_BODY_BYTES" // browserAssetRateLimitRequestsEnvVar names the environment variable that // configures the browser_asset request budget per window. browserAssetRateLimitRequestsEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_ASSET_RATE_LIMIT_REQUESTS" // browserAssetRateLimitWindowEnvVar names the environment variable that // configures the browser_asset rate-limit window. browserAssetRateLimitWindowEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_ASSET_RATE_LIMIT_WINDOW" // browserAssetRateLimitBurstEnvVar names the environment variable that // configures the browser_asset rate-limit burst. browserAssetRateLimitBurstEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_ASSET_RATE_LIMIT_BURST" // publicMiscMaxBodyBytesEnvVar names the environment variable that // configures the maximum accepted request body size for public_misc. publicMiscMaxBodyBytesEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_MISC_MAX_BODY_BYTES" // publicMiscRateLimitRequestsEnvVar names the environment variable that // configures the public_misc request budget per window. publicMiscRateLimitRequestsEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_MISC_RATE_LIMIT_REQUESTS" // publicMiscRateLimitWindowEnvVar names the environment variable that // configures the public_misc rate-limit window. publicMiscRateLimitWindowEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_MISC_RATE_LIMIT_WINDOW" // publicMiscRateLimitBurstEnvVar names the environment variable that // configures the public_misc rate-limit burst. publicMiscRateLimitBurstEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_MISC_RATE_LIMIT_BURST" // sendEmailCodeIdentityRateLimitRequestsEnvVar names the environment // variable that configures the send-email-code identity request budget per // window. sendEmailCodeIdentityRateLimitRequestsEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_SEND_EMAIL_CODE_IDENTITY_RATE_LIMIT_REQUESTS" // sendEmailCodeIdentityRateLimitWindowEnvVar names the environment variable // that configures the send-email-code identity rate-limit window. sendEmailCodeIdentityRateLimitWindowEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_SEND_EMAIL_CODE_IDENTITY_RATE_LIMIT_WINDOW" // sendEmailCodeIdentityRateLimitBurstEnvVar names the environment variable // that configures the send-email-code identity rate-limit burst. sendEmailCodeIdentityRateLimitBurstEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_SEND_EMAIL_CODE_IDENTITY_RATE_LIMIT_BURST" // confirmEmailCodeIdentityRateLimitRequestsEnvVar names the environment // variable that configures the confirm-email-code identity request budget // per window. confirmEmailCodeIdentityRateLimitRequestsEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_CONFIRM_EMAIL_CODE_IDENTITY_RATE_LIMIT_REQUESTS" // confirmEmailCodeIdentityRateLimitWindowEnvVar names the environment // variable that configures the confirm-email-code identity rate-limit // window. confirmEmailCodeIdentityRateLimitWindowEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_CONFIRM_EMAIL_CODE_IDENTITY_RATE_LIMIT_WINDOW" // confirmEmailCodeIdentityRateLimitBurstEnvVar names the environment // variable that configures the confirm-email-code identity rate-limit burst. confirmEmailCodeIdentityRateLimitBurstEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_CONFIRM_EMAIL_CODE_IDENTITY_RATE_LIMIT_BURST" // defaultShutdownTimeout is applied when shutdownTimeoutEnvVar is absent. defaultShutdownTimeout = 5 * time.Second // defaultLogLevel is applied when logLevelEnvVar is absent. defaultLogLevel = "info" // defaultPublicHTTPAddr is applied when publicHTTPAddrEnvVar is absent. defaultPublicHTTPAddr = ":8080" defaultPublicHTTPReadHeaderTimeout = 2 * time.Second defaultPublicHTTPReadTimeout = 10 * time.Second defaultPublicHTTPIdleTimeout = time.Minute defaultPublicAuthUpstreamTimeout = 3 * time.Second defaultAdminHTTPReadHeaderTimeout = 2 * time.Second defaultAdminHTTPReadTimeout = 10 * time.Second defaultAdminHTTPIdleTimeout = time.Minute // defaultAuthenticatedGRPCAddr is applied when // authenticatedGRPCAddrEnvVar is absent. defaultAuthenticatedGRPCAddr = ":9090" defaultAuthenticatedGRPCConnectionTimeout = 5 * time.Second defaultAuthenticatedGRPCDownstreamTimeout = 5 * time.Second defaultAuthenticatedGRPCFreshnessWindow = 5 * time.Minute defaultAuthenticatedGRPCIPRateLimitRequests = 120 defaultAuthenticatedGRPCIPRateLimitBurst = 40 defaultAuthenticatedGRPCSessionRateLimitRequests = 60 defaultAuthenticatedGRPCSessionRateLimitBurst = 20 defaultAuthenticatedGRPCUserRateLimitRequests = 120 defaultAuthenticatedGRPCUserRateLimitBurst = 40 defaultAuthenticatedGRPCMessageClassRateLimitRequests = 60 defaultAuthenticatedGRPCMessageClassRateLimitBurst = 20 defaultSessionCacheRedisDB = 0 defaultSessionCacheRedisKeyPrefix = "gateway:session:" defaultSessionCacheRedisLookupTimeout = 250 * time.Millisecond defaultReplayRedisKeyPrefix = "gateway:replay:" defaultReplayRedisReserveTimeout = 250 * time.Millisecond defaultSessionEventsRedisReadBlockTimeout = time.Second defaultClientEventsRedisReadBlockTimeout = time.Second defaultPublicAuthMaxBodyBytes = int64(8192) defaultPublicAuthRateLimitRequests = 30 defaultPublicAuthRateLimitBurst = 10 defaultBrowserBootstrapRateLimitRequests = 60 defaultBrowserBootstrapRateLimitBurst = 20 defaultBrowserAssetRateLimitRequests = 300 defaultBrowserAssetRateLimitBurst = 80 defaultPublicMiscRateLimitRequests = 30 defaultPublicMiscRateLimitBurst = 10 defaultSendEmailCodeIdentityRateLimitRequests = 3 defaultSendEmailCodeIdentityRateLimitBurst = 1 defaultConfirmEmailCodeIdentityRateLimitRequests = 6 defaultConfirmEmailCodeIdentityRateLimitBurst = 2 ) var ( defaultClassRateLimitWindow = time.Minute defaultIdentityRateLimitWindow = 10 * time.Minute ) // RateLimitConfig describes a single rate-limit budget. type RateLimitConfig struct { // Requests is the number of accepted requests replenished per Window. Requests int // Window is the interval over which Requests are replenished. Window time.Duration // Burst is the maximum number of immediately available tokens. Burst int } // PublicRateLimitConfig identifies the generic rate-limit budget shape used by // public REST policy. type PublicRateLimitConfig = RateLimitConfig // AuthenticatedRateLimitConfig identifies the generic rate-limit budget shape // used by authenticated gRPC policy. type AuthenticatedRateLimitConfig = RateLimitConfig // PublicRoutePolicyConfig describes the anti-abuse policy enforced for one // stable public REST traffic class. type PublicRoutePolicyConfig struct { // MaxBodyBytes is the maximum accepted request body size. Zero means that // the request must not carry a body. MaxBodyBytes int64 // RateLimit configures the per-IP budget for the route class. RateLimit PublicRateLimitConfig } // PublicAuthIdentityPolicyConfig describes the additional identity-based // limiter applied to one public auth command. type PublicAuthIdentityPolicyConfig struct { // RateLimit configures the accepted request budget for one normalized public // auth identity key. RateLimit PublicRateLimitConfig } // PublicHTTPAntiAbuseConfig describes the public REST anti-abuse policy used // before route handling. type PublicHTTPAntiAbuseConfig struct { // PublicAuth applies to the stable public_auth route class. PublicAuth PublicRoutePolicyConfig // BrowserBootstrap applies to the stable browser_bootstrap route class. BrowserBootstrap PublicRoutePolicyConfig // BrowserAsset applies to the stable browser_asset route class. BrowserAsset PublicRoutePolicyConfig // PublicMisc applies to the stable public_misc route class. PublicMisc PublicRoutePolicyConfig // SendEmailCodeIdentity applies the additional identity limiter for // send-email-code. SendEmailCodeIdentity PublicAuthIdentityPolicyConfig // ConfirmEmailCodeIdentity applies the additional identity limiter for // confirm-email-code. ConfirmEmailCodeIdentity PublicAuthIdentityPolicyConfig } // AuthenticatedGRPCAntiAbuseConfig describes the authenticated gRPC // rate-limit budgets enforced after request authenticity has been established. type AuthenticatedGRPCAntiAbuseConfig struct { // IP applies to the transport peer IP derived from the gRPC connection. IP AuthenticatedRateLimitConfig // Session applies to the authenticated device_session_id. Session AuthenticatedRateLimitConfig // User applies to the authenticated user_id resolved from SessionCache. User AuthenticatedRateLimitConfig // MessageClass applies to the current authenticated message class. The // gateway uses the full message_type literal as the stable v1 class key. MessageClass AuthenticatedRateLimitConfig } // PublicHTTPConfig describes the public unauthenticated REST listener exposed // by the gateway. type PublicHTTPConfig struct { // Addr is the TCP listen address used by the public REST server. Addr string // ReadHeaderTimeout bounds how long the listener may spend reading request // headers before the gateway rejects the connection. ReadHeaderTimeout time.Duration // ReadTimeout bounds how long the listener may spend reading one public // request. ReadTimeout time.Duration // IdleTimeout bounds how long the listener keeps an idle keep-alive // connection open. IdleTimeout time.Duration // AuthUpstreamTimeout bounds one public auth adapter call. AuthUpstreamTimeout time.Duration // AntiAbuse configures the public REST anti-abuse middleware. AntiAbuse PublicHTTPAntiAbuseConfig } // AuthServiceConfig describes the optional public-auth upstream used by the // gateway runtime. type AuthServiceConfig struct { // BaseURL is the absolute base URL of the Auth / Session Service public // HTTP API. When BaseURL is empty, the gateway keeps using its built-in // unavailable public-auth adapter. BaseURL string } // UserServiceConfig describes the optional authenticated self-service upstream // used by the gateway runtime. type UserServiceConfig struct { // BaseURL is the absolute base URL of the User Service internal HTTP API. // When BaseURL is empty, the gateway keeps using its built-in unavailable // downstream adapter for the reserved `user.*` routes. BaseURL string } // AdminHTTPConfig describes the private operational HTTP listener used for // metrics exposure. The listener remains disabled when Addr is empty. type AdminHTTPConfig struct { // Addr is the TCP listen address used by the admin HTTP server. An empty // value disables the listener. Addr string // ReadHeaderTimeout bounds how long the listener may spend reading request // headers before the gateway rejects the connection. ReadHeaderTimeout time.Duration // ReadTimeout bounds how long the listener may spend reading one admin // request. ReadTimeout time.Duration // IdleTimeout bounds how long the listener keeps an idle keep-alive // connection open. IdleTimeout time.Duration } // AuthenticatedGRPCConfig describes the authenticated gRPC listener exposed by // the gateway. type AuthenticatedGRPCConfig struct { // Addr is the TCP listen address used by the authenticated gRPC server. Addr string // ConnectionTimeout bounds one inbound connection handshake. ConnectionTimeout time.Duration // DownstreamTimeout bounds one downstream unary execution after the request // has passed the full authenticated ingress pipeline. DownstreamTimeout time.Duration // FreshnessWindow is the accepted skew window around current server time // used for client request timestamps. FreshnessWindow time.Duration // AntiAbuse configures the authenticated gRPC rate limits enforced after // the request passes the transport authenticity checks. AntiAbuse AuthenticatedGRPCAntiAbuseConfig } // SessionCacheRedisConfig describes the Redis connection used for authenticated // SessionCache lookups. type SessionCacheRedisConfig struct { // Addr is the Redis endpoint used for SessionCache requests. Addr string // Username is the optional Redis ACL username used for authentication. Username string // Password is the optional Redis password used for authentication. Password string // DB is the Redis logical database number used for SessionCache keys. DB int // KeyPrefix is prepended to every SessionCache Redis key. KeyPrefix string // LookupTimeout bounds individual SessionCache Redis operations. LookupTimeout time.Duration // TLSEnabled reports whether SessionCache Redis connections should use TLS. TLSEnabled bool } // ReplayRedisConfig describes the Redis namespace and timeout used for // authenticated replay reservations. type ReplayRedisConfig struct { // KeyPrefix is prepended to every ReplayStore Redis key. KeyPrefix string // ReserveTimeout bounds individual ReplayStore Redis operations. ReserveTimeout time.Duration } // SessionEventsRedisConfig describes the Redis Stream consumed by the gateway // to keep the process-local session cache synchronized with session lifecycle // updates. type SessionEventsRedisConfig struct { // Stream is the Redis Stream key carrying full session snapshot events. Stream string // ReadBlockTimeout bounds one blocking XREAD call so shutdown remains // responsive even when the stream is idle. ReadBlockTimeout time.Duration } // ClientEventsRedisConfig describes the Redis Stream consumed by the gateway // to deliver client-facing events to active push streams. type ClientEventsRedisConfig struct { // Stream is the Redis Stream key carrying client-facing event entries. Stream string // ReadBlockTimeout bounds one blocking XREAD call so shutdown remains // responsive even when the stream is idle. ReadBlockTimeout time.Duration } // ResponseSignerConfig describes the private-key material used to sign // authenticated unary responses and stream events. type ResponseSignerConfig struct { // PrivateKeyPEMPath is the filesystem path to the PKCS#8 PEM-encoded // Ed25519 private key loaded during startup. PrivateKeyPEMPath string } // LoggingConfig describes the process-wide structured logging settings. type LoggingConfig struct { // Level is the configured minimum log level literal. Level string } // Config describes process-wide settings required to start and stop the // gateway safely. type Config struct { // ShutdownTimeout limits how long each component may spend in Shutdown // before the gateway reports a timeout. ShutdownTimeout time.Duration // Logging configures the process-wide structured logger. Logging LoggingConfig // PublicHTTP configures the public unauthenticated REST listener. PublicHTTP PublicHTTPConfig // AuthService configures the optional public-auth delegation to the Auth / // Session Service. AuthService AuthServiceConfig // UserService configures the optional authenticated self-service // delegation to User Service. UserService UserServiceConfig // AdminHTTP configures the optional private admin listener used for metrics // exposure. AdminHTTP AdminHTTPConfig // AuthenticatedGRPC configures the authenticated gRPC listener. AuthenticatedGRPC AuthenticatedGRPCConfig // SessionCacheRedis configures the Redis-backed authenticated SessionCache. SessionCacheRedis SessionCacheRedisConfig // ReplayRedis configures the Redis-backed authenticated ReplayStore. ReplayRedis ReplayRedisConfig // SessionEventsRedis configures the Redis Stream consumed for session cache // updates and revocations. SessionEventsRedis SessionEventsRedisConfig // ClientEventsRedis configures the Redis Stream consumed for client-facing // push delivery. ClientEventsRedis ClientEventsRedisConfig // ResponseSigner configures the authenticated response and event signer // loaded during startup. ResponseSigner ResponseSignerConfig } // DefaultPublicHTTPConfig returns the default listener and anti-abuse settings // for the public REST surface. func DefaultPublicHTTPConfig() PublicHTTPConfig { return PublicHTTPConfig{ Addr: defaultPublicHTTPAddr, ReadHeaderTimeout: defaultPublicHTTPReadHeaderTimeout, ReadTimeout: defaultPublicHTTPReadTimeout, IdleTimeout: defaultPublicHTTPIdleTimeout, AuthUpstreamTimeout: defaultPublicAuthUpstreamTimeout, AntiAbuse: PublicHTTPAntiAbuseConfig{ PublicAuth: PublicRoutePolicyConfig{ MaxBodyBytes: defaultPublicAuthMaxBodyBytes, RateLimit: PublicRateLimitConfig{ Requests: defaultPublicAuthRateLimitRequests, Window: defaultClassRateLimitWindow, Burst: defaultPublicAuthRateLimitBurst, }, }, BrowserBootstrap: PublicRoutePolicyConfig{ RateLimit: PublicRateLimitConfig{ Requests: defaultBrowserBootstrapRateLimitRequests, Window: defaultClassRateLimitWindow, Burst: defaultBrowserBootstrapRateLimitBurst, }, }, BrowserAsset: PublicRoutePolicyConfig{ RateLimit: PublicRateLimitConfig{ Requests: defaultBrowserAssetRateLimitRequests, Window: defaultClassRateLimitWindow, Burst: defaultBrowserAssetRateLimitBurst, }, }, PublicMisc: PublicRoutePolicyConfig{ RateLimit: PublicRateLimitConfig{ Requests: defaultPublicMiscRateLimitRequests, Window: defaultClassRateLimitWindow, Burst: defaultPublicMiscRateLimitBurst, }, }, SendEmailCodeIdentity: PublicAuthIdentityPolicyConfig{ RateLimit: PublicRateLimitConfig{ Requests: defaultSendEmailCodeIdentityRateLimitRequests, Window: defaultIdentityRateLimitWindow, Burst: defaultSendEmailCodeIdentityRateLimitBurst, }, }, ConfirmEmailCodeIdentity: PublicAuthIdentityPolicyConfig{ RateLimit: PublicRateLimitConfig{ Requests: defaultConfirmEmailCodeIdentityRateLimitRequests, Window: defaultIdentityRateLimitWindow, Burst: defaultConfirmEmailCodeIdentityRateLimitBurst, }, }, }, } } // DefaultAdminHTTPConfig returns the default settings for the optional private // admin listener. The zero address keeps the listener disabled by default. func DefaultAdminHTTPConfig() AdminHTTPConfig { return AdminHTTPConfig{ ReadHeaderTimeout: defaultAdminHTTPReadHeaderTimeout, ReadTimeout: defaultAdminHTTPReadTimeout, IdleTimeout: defaultAdminHTTPIdleTimeout, } } // DefaultAuthenticatedGRPCConfig returns the default listener, freshness, and // anti-abuse settings for the authenticated gRPC surface. func DefaultAuthenticatedGRPCConfig() AuthenticatedGRPCConfig { return AuthenticatedGRPCConfig{ Addr: defaultAuthenticatedGRPCAddr, ConnectionTimeout: defaultAuthenticatedGRPCConnectionTimeout, DownstreamTimeout: defaultAuthenticatedGRPCDownstreamTimeout, FreshnessWindow: defaultAuthenticatedGRPCFreshnessWindow, AntiAbuse: AuthenticatedGRPCAntiAbuseConfig{ IP: AuthenticatedRateLimitConfig{ Requests: defaultAuthenticatedGRPCIPRateLimitRequests, Window: defaultClassRateLimitWindow, Burst: defaultAuthenticatedGRPCIPRateLimitBurst, }, Session: AuthenticatedRateLimitConfig{ Requests: defaultAuthenticatedGRPCSessionRateLimitRequests, Window: defaultClassRateLimitWindow, Burst: defaultAuthenticatedGRPCSessionRateLimitBurst, }, User: AuthenticatedRateLimitConfig{ Requests: defaultAuthenticatedGRPCUserRateLimitRequests, Window: defaultClassRateLimitWindow, Burst: defaultAuthenticatedGRPCUserRateLimitBurst, }, MessageClass: AuthenticatedRateLimitConfig{ Requests: defaultAuthenticatedGRPCMessageClassRateLimitRequests, Window: defaultClassRateLimitWindow, Burst: defaultAuthenticatedGRPCMessageClassRateLimitBurst, }, }, } } // DefaultLoggingConfig returns the default structured logging settings. func DefaultLoggingConfig() LoggingConfig { return LoggingConfig{Level: defaultLogLevel} } // DefaultSessionCacheRedisConfig returns the default optional settings for the // Redis-backed authenticated SessionCache. Addr remains empty and must be // supplied explicitly. func DefaultSessionCacheRedisConfig() SessionCacheRedisConfig { return SessionCacheRedisConfig{ DB: defaultSessionCacheRedisDB, KeyPrefix: defaultSessionCacheRedisKeyPrefix, LookupTimeout: defaultSessionCacheRedisLookupTimeout, } } // DefaultReplayRedisConfig returns the default Redis key namespace and timeout // used for authenticated replay reservations. func DefaultReplayRedisConfig() ReplayRedisConfig { return ReplayRedisConfig{ KeyPrefix: defaultReplayRedisKeyPrefix, ReserveTimeout: defaultReplayRedisReserveTimeout, } } // DefaultSessionEventsRedisConfig returns the default optional settings for the // session lifecycle event subscriber. Stream remains empty and must be // supplied explicitly. func DefaultSessionEventsRedisConfig() SessionEventsRedisConfig { return SessionEventsRedisConfig{ ReadBlockTimeout: defaultSessionEventsRedisReadBlockTimeout, } } // DefaultClientEventsRedisConfig returns the default optional settings for the // client-facing event subscriber. Stream remains empty and must be supplied // explicitly. func DefaultClientEventsRedisConfig() ClientEventsRedisConfig { return ClientEventsRedisConfig{ ReadBlockTimeout: defaultClientEventsRedisReadBlockTimeout, } } // DefaultResponseSignerConfig returns the default response-signer settings. // The private key path remains empty and must be supplied explicitly. func DefaultResponseSignerConfig() ResponseSignerConfig { return ResponseSignerConfig{} } // DefaultAuthServiceConfig returns the default public-auth upstream settings. // The zero value keeps the built-in unavailable adapter active. func DefaultAuthServiceConfig() AuthServiceConfig { return AuthServiceConfig{} } // DefaultUserServiceConfig returns the default authenticated self-service // upstream settings. The zero value keeps the built-in unavailable adapter // active for reserved `user.*` routes. func DefaultUserServiceConfig() UserServiceConfig { return UserServiceConfig{} } // LoadFromEnv loads Config from the process environment, applies defaults for // omitted settings, and validates the resulting values. func LoadFromEnv() (Config, error) { cfg := Config{ ShutdownTimeout: defaultShutdownTimeout, Logging: DefaultLoggingConfig(), PublicHTTP: DefaultPublicHTTPConfig(), AuthService: DefaultAuthServiceConfig(), UserService: DefaultUserServiceConfig(), AdminHTTP: DefaultAdminHTTPConfig(), AuthenticatedGRPC: DefaultAuthenticatedGRPCConfig(), SessionCacheRedis: DefaultSessionCacheRedisConfig(), ReplayRedis: DefaultReplayRedisConfig(), SessionEventsRedis: DefaultSessionEventsRedisConfig(), ClientEventsRedis: DefaultClientEventsRedisConfig(), ResponseSigner: DefaultResponseSignerConfig(), } rawShutdownTimeout, ok := os.LookupEnv(shutdownTimeoutEnvVar) if ok { shutdownTimeout, err := time.ParseDuration(rawShutdownTimeout) if err != nil { return Config{}, fmt.Errorf("load gateway config: parse %s: %w", shutdownTimeoutEnvVar, err) } cfg.ShutdownTimeout = shutdownTimeout } rawLogLevel, ok := os.LookupEnv(logLevelEnvVar) if ok { cfg.Logging.Level = rawLogLevel } rawPublicHTTPAddr, ok := os.LookupEnv(publicHTTPAddrEnvVar) if ok { cfg.PublicHTTP.Addr = rawPublicHTTPAddr } publicHTTPReadHeaderTimeout, err := loadDurationEnvWithDefault(publicHTTPReadHeaderTimeoutEnvVar, cfg.PublicHTTP.ReadHeaderTimeout) if err != nil { return Config{}, err } cfg.PublicHTTP.ReadHeaderTimeout = publicHTTPReadHeaderTimeout publicHTTPReadTimeout, err := loadDurationEnvWithDefault(publicHTTPReadTimeoutEnvVar, cfg.PublicHTTP.ReadTimeout) if err != nil { return Config{}, err } cfg.PublicHTTP.ReadTimeout = publicHTTPReadTimeout publicHTTPIdleTimeout, err := loadDurationEnvWithDefault(publicHTTPIdleTimeoutEnvVar, cfg.PublicHTTP.IdleTimeout) if err != nil { return Config{}, err } cfg.PublicHTTP.IdleTimeout = publicHTTPIdleTimeout publicAuthUpstreamTimeout, err := loadDurationEnvWithDefault(publicAuthUpstreamTimeoutEnvVar, cfg.PublicHTTP.AuthUpstreamTimeout) if err != nil { return Config{}, err } cfg.PublicHTTP.AuthUpstreamTimeout = publicAuthUpstreamTimeout rawAuthServiceBaseURL, ok := os.LookupEnv(authServiceBaseURLEnvVar) if ok { cfg.AuthService.BaseURL = rawAuthServiceBaseURL } rawUserServiceBaseURL, ok := os.LookupEnv(userServiceBaseURLEnvVar) if ok { cfg.UserService.BaseURL = rawUserServiceBaseURL } rawAdminHTTPAddr, ok := os.LookupEnv(adminHTTPAddrEnvVar) if ok { cfg.AdminHTTP.Addr = rawAdminHTTPAddr } adminHTTPReadHeaderTimeout, err := loadDurationEnvWithDefault(adminHTTPReadHeaderTimeoutEnvVar, cfg.AdminHTTP.ReadHeaderTimeout) if err != nil { return Config{}, err } cfg.AdminHTTP.ReadHeaderTimeout = adminHTTPReadHeaderTimeout adminHTTPReadTimeout, err := loadDurationEnvWithDefault(adminHTTPReadTimeoutEnvVar, cfg.AdminHTTP.ReadTimeout) if err != nil { return Config{}, err } cfg.AdminHTTP.ReadTimeout = adminHTTPReadTimeout adminHTTPIdleTimeout, err := loadDurationEnvWithDefault(adminHTTPIdleTimeoutEnvVar, cfg.AdminHTTP.IdleTimeout) if err != nil { return Config{}, err } cfg.AdminHTTP.IdleTimeout = adminHTTPIdleTimeout rawAuthenticatedGRPCAddr, ok := os.LookupEnv(authenticatedGRPCAddrEnvVar) if ok { cfg.AuthenticatedGRPC.Addr = rawAuthenticatedGRPCAddr } authenticatedGRPCConnectionTimeout, err := loadDurationEnvWithDefault(authenticatedGRPCConnectionTimeoutEnvVar, cfg.AuthenticatedGRPC.ConnectionTimeout) if err != nil { return Config{}, err } cfg.AuthenticatedGRPC.ConnectionTimeout = authenticatedGRPCConnectionTimeout authenticatedGRPCDownstreamTimeout, err := loadDurationEnvWithDefault(authenticatedGRPCDownstreamTimeoutEnvVar, cfg.AuthenticatedGRPC.DownstreamTimeout) if err != nil { return Config{}, err } cfg.AuthenticatedGRPC.DownstreamTimeout = authenticatedGRPCDownstreamTimeout authenticatedGRPCFreshnessWindow, err := loadDurationEnvWithDefault(authenticatedGRPCFreshnessWindowEnvVar, cfg.AuthenticatedGRPC.FreshnessWindow) if err != nil { return Config{}, err } cfg.AuthenticatedGRPC.FreshnessWindow = authenticatedGRPCFreshnessWindow authenticatedGRPCIPRateLimit, err := loadRateLimitConfigFromEnv( cfg.AuthenticatedGRPC.AntiAbuse.IP, authenticatedGRPCIPRateLimitRequestsEnvVar, authenticatedGRPCIPRateLimitWindowEnvVar, authenticatedGRPCIPRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.AuthenticatedGRPC.AntiAbuse.IP = authenticatedGRPCIPRateLimit authenticatedGRPCSessionRateLimit, err := loadRateLimitConfigFromEnv( cfg.AuthenticatedGRPC.AntiAbuse.Session, authenticatedGRPCSessionRateLimitRequestsEnvVar, authenticatedGRPCSessionRateLimitWindowEnvVar, authenticatedGRPCSessionRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.AuthenticatedGRPC.AntiAbuse.Session = authenticatedGRPCSessionRateLimit authenticatedGRPCUserRateLimit, err := loadRateLimitConfigFromEnv( cfg.AuthenticatedGRPC.AntiAbuse.User, authenticatedGRPCUserRateLimitRequestsEnvVar, authenticatedGRPCUserRateLimitWindowEnvVar, authenticatedGRPCUserRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.AuthenticatedGRPC.AntiAbuse.User = authenticatedGRPCUserRateLimit messageClassRateLimit, err := loadRateLimitConfigFromEnv( cfg.AuthenticatedGRPC.AntiAbuse.MessageClass, authenticatedGRPCMessageClassRateLimitRequestsEnvVar, authenticatedGRPCMessageClassRateLimitWindowEnvVar, authenticatedGRPCMessageClassRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.AuthenticatedGRPC.AntiAbuse.MessageClass = messageClassRateLimit rawSessionCacheRedisAddr, ok := os.LookupEnv(sessionCacheRedisAddrEnvVar) if ok { cfg.SessionCacheRedis.Addr = rawSessionCacheRedisAddr } rawSessionCacheRedisUsername, ok := os.LookupEnv(sessionCacheRedisUsernameEnvVar) if ok { cfg.SessionCacheRedis.Username = rawSessionCacheRedisUsername } rawSessionCacheRedisPassword, ok := os.LookupEnv(sessionCacheRedisPasswordEnvVar) if ok { cfg.SessionCacheRedis.Password = rawSessionCacheRedisPassword } sessionCacheRedisDB, err := loadIntEnvWithDefault(sessionCacheRedisDBEnvVar, cfg.SessionCacheRedis.DB) if err != nil { return Config{}, err } cfg.SessionCacheRedis.DB = sessionCacheRedisDB rawSessionCacheRedisKeyPrefix, ok := os.LookupEnv(sessionCacheRedisKeyPrefixEnvVar) if ok { cfg.SessionCacheRedis.KeyPrefix = rawSessionCacheRedisKeyPrefix } sessionCacheRedisLookupTimeout, err := loadDurationEnvWithDefault(sessionCacheRedisLookupTimeoutEnvVar, cfg.SessionCacheRedis.LookupTimeout) if err != nil { return Config{}, err } cfg.SessionCacheRedis.LookupTimeout = sessionCacheRedisLookupTimeout sessionCacheRedisTLSEnabled, err := loadBoolEnvWithDefault(sessionCacheRedisTLSEnabledEnvVar, cfg.SessionCacheRedis.TLSEnabled) if err != nil { return Config{}, err } cfg.SessionCacheRedis.TLSEnabled = sessionCacheRedisTLSEnabled rawReplayRedisKeyPrefix, ok := os.LookupEnv(replayRedisKeyPrefixEnvVar) if ok { cfg.ReplayRedis.KeyPrefix = rawReplayRedisKeyPrefix } replayRedisReserveTimeout, err := loadDurationEnvWithDefault(replayRedisReserveTimeoutEnvVar, cfg.ReplayRedis.ReserveTimeout) if err != nil { return Config{}, err } cfg.ReplayRedis.ReserveTimeout = replayRedisReserveTimeout rawSessionEventsRedisStream, ok := os.LookupEnv(sessionEventsRedisStreamEnvVar) if ok { cfg.SessionEventsRedis.Stream = rawSessionEventsRedisStream } sessionEventsRedisReadBlockTimeout, err := loadDurationEnvWithDefault(sessionEventsRedisReadBlockTimeoutEnvVar, cfg.SessionEventsRedis.ReadBlockTimeout) if err != nil { return Config{}, err } cfg.SessionEventsRedis.ReadBlockTimeout = sessionEventsRedisReadBlockTimeout rawClientEventsRedisStream, ok := os.LookupEnv(clientEventsRedisStreamEnvVar) if ok { cfg.ClientEventsRedis.Stream = rawClientEventsRedisStream } clientEventsRedisReadBlockTimeout, err := loadDurationEnvWithDefault(clientEventsRedisReadBlockTimeoutEnvVar, cfg.ClientEventsRedis.ReadBlockTimeout) if err != nil { return Config{}, err } cfg.ClientEventsRedis.ReadBlockTimeout = clientEventsRedisReadBlockTimeout rawSignerKeyPath, ok := os.LookupEnv(responseSignerPrivateKeyPEMPathEnvVar) if ok { cfg.ResponseSigner.PrivateKeyPEMPath = rawSignerKeyPath } publicAuthPolicy, err := loadPublicRoutePolicyConfigFromEnv( cfg.PublicHTTP.AntiAbuse.PublicAuth, publicAuthMaxBodyBytesEnvVar, publicAuthRateLimitRequestsEnvVar, publicAuthRateLimitWindowEnvVar, publicAuthRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.PublicHTTP.AntiAbuse.PublicAuth = publicAuthPolicy browserBootstrapPolicy, err := loadPublicRoutePolicyConfigFromEnv( cfg.PublicHTTP.AntiAbuse.BrowserBootstrap, browserBootstrapMaxBodyBytesEnvVar, browserBootstrapRateLimitRequestsEnvVar, browserBootstrapRateLimitWindowEnvVar, browserBootstrapRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.PublicHTTP.AntiAbuse.BrowserBootstrap = browserBootstrapPolicy browserAssetPolicy, err := loadPublicRoutePolicyConfigFromEnv( cfg.PublicHTTP.AntiAbuse.BrowserAsset, browserAssetMaxBodyBytesEnvVar, browserAssetRateLimitRequestsEnvVar, browserAssetRateLimitWindowEnvVar, browserAssetRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.PublicHTTP.AntiAbuse.BrowserAsset = browserAssetPolicy publicMiscPolicy, err := loadPublicRoutePolicyConfigFromEnv( cfg.PublicHTTP.AntiAbuse.PublicMisc, publicMiscMaxBodyBytesEnvVar, publicMiscRateLimitRequestsEnvVar, publicMiscRateLimitWindowEnvVar, publicMiscRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.PublicHTTP.AntiAbuse.PublicMisc = publicMiscPolicy sendIdentityPolicy, err := loadPublicAuthIdentityPolicyConfigFromEnv( cfg.PublicHTTP.AntiAbuse.SendEmailCodeIdentity, sendEmailCodeIdentityRateLimitRequestsEnvVar, sendEmailCodeIdentityRateLimitWindowEnvVar, sendEmailCodeIdentityRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.PublicHTTP.AntiAbuse.SendEmailCodeIdentity = sendIdentityPolicy confirmIdentityPolicy, err := loadPublicAuthIdentityPolicyConfigFromEnv( cfg.PublicHTTP.AntiAbuse.ConfirmEmailCodeIdentity, confirmEmailCodeIdentityRateLimitRequestsEnvVar, confirmEmailCodeIdentityRateLimitWindowEnvVar, confirmEmailCodeIdentityRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.PublicHTTP.AntiAbuse.ConfirmEmailCodeIdentity = confirmIdentityPolicy if cfg.ShutdownTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", shutdownTimeoutEnvVar) } if err := validateLogLevel(cfg.Logging.Level); err != nil { return Config{}, fmt.Errorf("load gateway config: %w", err) } if strings.TrimSpace(cfg.PublicHTTP.Addr) == "" { return Config{}, fmt.Errorf("load gateway config: %s must not be empty", publicHTTPAddrEnvVar) } if cfg.PublicHTTP.ReadHeaderTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", publicHTTPReadHeaderTimeoutEnvVar) } if cfg.PublicHTTP.ReadTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", publicHTTPReadTimeoutEnvVar) } if cfg.PublicHTTP.IdleTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", publicHTTPIdleTimeoutEnvVar) } if cfg.PublicHTTP.AuthUpstreamTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", publicAuthUpstreamTimeoutEnvVar) } cfg.AuthService.BaseURL = strings.TrimSpace(cfg.AuthService.BaseURL) if cfg.AuthService.BaseURL != "" { parsedAuthServiceBaseURL, err := url.Parse(cfg.AuthService.BaseURL) if err != nil { return Config{}, fmt.Errorf("load gateway config: parse %s: %w", authServiceBaseURLEnvVar, err) } if parsedAuthServiceBaseURL.Scheme == "" || parsedAuthServiceBaseURL.Host == "" { return Config{}, fmt.Errorf("load gateway config: %s must be an absolute URL", authServiceBaseURLEnvVar) } cfg.AuthService.BaseURL = strings.TrimRight(parsedAuthServiceBaseURL.String(), "/") } cfg.UserService.BaseURL = strings.TrimSpace(cfg.UserService.BaseURL) if cfg.UserService.BaseURL != "" { parsedUserServiceBaseURL, err := url.Parse(cfg.UserService.BaseURL) if err != nil { return Config{}, fmt.Errorf("load gateway config: parse %s: %w", userServiceBaseURLEnvVar, err) } if parsedUserServiceBaseURL.Scheme == "" || parsedUserServiceBaseURL.Host == "" { return Config{}, fmt.Errorf("load gateway config: %s must be an absolute URL", userServiceBaseURLEnvVar) } cfg.UserService.BaseURL = strings.TrimRight(parsedUserServiceBaseURL.String(), "/") } if addr := strings.TrimSpace(cfg.AdminHTTP.Addr); addr != "" { cfg.AdminHTTP.Addr = addr } if cfg.AdminHTTP.ReadHeaderTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", adminHTTPReadHeaderTimeoutEnvVar) } if cfg.AdminHTTP.ReadTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", adminHTTPReadTimeoutEnvVar) } if cfg.AdminHTTP.IdleTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", adminHTTPIdleTimeoutEnvVar) } if strings.TrimSpace(cfg.AuthenticatedGRPC.Addr) == "" { return Config{}, fmt.Errorf("load gateway config: %s must not be empty", authenticatedGRPCAddrEnvVar) } if cfg.AuthenticatedGRPC.ConnectionTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", authenticatedGRPCConnectionTimeoutEnvVar) } if cfg.AuthenticatedGRPC.DownstreamTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", authenticatedGRPCDownstreamTimeoutEnvVar) } if cfg.AuthenticatedGRPC.FreshnessWindow <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", authenticatedGRPCFreshnessWindowEnvVar) } if err := validateRateLimitConfig( cfg.AuthenticatedGRPC.AntiAbuse.IP, authenticatedGRPCIPRateLimitRequestsEnvVar, authenticatedGRPCIPRateLimitWindowEnvVar, authenticatedGRPCIPRateLimitBurstEnvVar, ); err != nil { return Config{}, err } if err := validateRateLimitConfig( cfg.AuthenticatedGRPC.AntiAbuse.Session, authenticatedGRPCSessionRateLimitRequestsEnvVar, authenticatedGRPCSessionRateLimitWindowEnvVar, authenticatedGRPCSessionRateLimitBurstEnvVar, ); err != nil { return Config{}, err } if err := validateRateLimitConfig( cfg.AuthenticatedGRPC.AntiAbuse.User, authenticatedGRPCUserRateLimitRequestsEnvVar, authenticatedGRPCUserRateLimitWindowEnvVar, authenticatedGRPCUserRateLimitBurstEnvVar, ); err != nil { return Config{}, err } if err := validateRateLimitConfig( cfg.AuthenticatedGRPC.AntiAbuse.MessageClass, authenticatedGRPCMessageClassRateLimitRequestsEnvVar, authenticatedGRPCMessageClassRateLimitWindowEnvVar, authenticatedGRPCMessageClassRateLimitBurstEnvVar, ); err != nil { return Config{}, err } if strings.TrimSpace(cfg.SessionCacheRedis.Addr) == "" { return Config{}, fmt.Errorf("load gateway config: %s must not be empty", sessionCacheRedisAddrEnvVar) } if cfg.SessionCacheRedis.DB < 0 { return Config{}, fmt.Errorf("load gateway config: %s must not be negative", sessionCacheRedisDBEnvVar) } if cfg.SessionCacheRedis.LookupTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", sessionCacheRedisLookupTimeoutEnvVar) } if strings.TrimSpace(cfg.ReplayRedis.KeyPrefix) == "" { return Config{}, fmt.Errorf("load gateway config: %s must not be empty", replayRedisKeyPrefixEnvVar) } if cfg.ReplayRedis.ReserveTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", replayRedisReserveTimeoutEnvVar) } if strings.TrimSpace(cfg.SessionEventsRedis.Stream) == "" { return Config{}, fmt.Errorf("load gateway config: %s must not be empty", sessionEventsRedisStreamEnvVar) } if cfg.SessionEventsRedis.ReadBlockTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", sessionEventsRedisReadBlockTimeoutEnvVar) } if strings.TrimSpace(cfg.ClientEventsRedis.Stream) == "" { return Config{}, fmt.Errorf("load gateway config: %s must not be empty", clientEventsRedisStreamEnvVar) } if cfg.ClientEventsRedis.ReadBlockTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", clientEventsRedisReadBlockTimeoutEnvVar) } if strings.TrimSpace(cfg.ResponseSigner.PrivateKeyPEMPath) == "" { return Config{}, fmt.Errorf("load gateway config: %s must not be empty", responseSignerPrivateKeyPEMPathEnvVar) } if err := validatePublicRoutePolicyConfig(cfg.PublicHTTP.AntiAbuse.PublicAuth, publicAuthMaxBodyBytesEnvVar, publicAuthRateLimitRequestsEnvVar, publicAuthRateLimitWindowEnvVar, publicAuthRateLimitBurstEnvVar); err != nil { return Config{}, err } if err := validatePublicRoutePolicyConfig(cfg.PublicHTTP.AntiAbuse.BrowserBootstrap, browserBootstrapMaxBodyBytesEnvVar, browserBootstrapRateLimitRequestsEnvVar, browserBootstrapRateLimitWindowEnvVar, browserBootstrapRateLimitBurstEnvVar); err != nil { return Config{}, err } if err := validatePublicRoutePolicyConfig(cfg.PublicHTTP.AntiAbuse.BrowserAsset, browserAssetMaxBodyBytesEnvVar, browserAssetRateLimitRequestsEnvVar, browserAssetRateLimitWindowEnvVar, browserAssetRateLimitBurstEnvVar); err != nil { return Config{}, err } if err := validatePublicRoutePolicyConfig(cfg.PublicHTTP.AntiAbuse.PublicMisc, publicMiscMaxBodyBytesEnvVar, publicMiscRateLimitRequestsEnvVar, publicMiscRateLimitWindowEnvVar, publicMiscRateLimitBurstEnvVar); err != nil { return Config{}, err } if err := validatePublicAuthIdentityPolicyConfig(cfg.PublicHTTP.AntiAbuse.SendEmailCodeIdentity, sendEmailCodeIdentityRateLimitRequestsEnvVar, sendEmailCodeIdentityRateLimitWindowEnvVar, sendEmailCodeIdentityRateLimitBurstEnvVar); err != nil { return Config{}, err } if err := validatePublicAuthIdentityPolicyConfig(cfg.PublicHTTP.AntiAbuse.ConfirmEmailCodeIdentity, confirmEmailCodeIdentityRateLimitRequestsEnvVar, confirmEmailCodeIdentityRateLimitWindowEnvVar, confirmEmailCodeIdentityRateLimitBurstEnvVar); err != nil { return Config{}, err } return cfg, nil } func loadPublicRoutePolicyConfigFromEnv(defaults PublicRoutePolicyConfig, maxBodyEnvVar string, requestsEnvVar string, windowEnvVar string, burstEnvVar string) (PublicRoutePolicyConfig, error) { policy := defaults maxBodyBytes, err := loadInt64EnvWithDefault(maxBodyEnvVar, defaults.MaxBodyBytes) if err != nil { return PublicRoutePolicyConfig{}, err } policy.MaxBodyBytes = maxBodyBytes rateLimit, err := loadRateLimitConfigFromEnv(defaults.RateLimit, requestsEnvVar, windowEnvVar, burstEnvVar) if err != nil { return PublicRoutePolicyConfig{}, err } policy.RateLimit = rateLimit return policy, nil } func loadPublicAuthIdentityPolicyConfigFromEnv(defaults PublicAuthIdentityPolicyConfig, requestsEnvVar string, windowEnvVar string, burstEnvVar string) (PublicAuthIdentityPolicyConfig, error) { rateLimit, err := loadRateLimitConfigFromEnv(defaults.RateLimit, requestsEnvVar, windowEnvVar, burstEnvVar) if err != nil { return PublicAuthIdentityPolicyConfig{}, err } return PublicAuthIdentityPolicyConfig{RateLimit: rateLimit}, nil } func loadRateLimitConfigFromEnv(defaults RateLimitConfig, requestsEnvVar string, windowEnvVar string, burstEnvVar string) (RateLimitConfig, error) { cfg := defaults requests, err := loadIntEnvWithDefault(requestsEnvVar, defaults.Requests) if err != nil { return RateLimitConfig{}, err } cfg.Requests = requests window, err := loadDurationEnvWithDefault(windowEnvVar, defaults.Window) if err != nil { return RateLimitConfig{}, err } cfg.Window = window burst, err := loadIntEnvWithDefault(burstEnvVar, defaults.Burst) if err != nil { return RateLimitConfig{}, err } cfg.Burst = burst return cfg, nil } func validateLogLevel(level string) error { switch strings.ToLower(strings.TrimSpace(level)) { case "debug", "info", "warn", "error", "dpanic", "panic", "fatal": return nil default: return fmt.Errorf("%s must be one of debug, info, warn, error, dpanic, panic, fatal", logLevelEnvVar) } } func loadIntEnvWithDefault(envVar string, fallback int) (int, error) { rawValue, ok := os.LookupEnv(envVar) if !ok { return fallback, nil } value, err := strconv.Atoi(rawValue) if err != nil { return 0, fmt.Errorf("load gateway config: parse %s: %w", envVar, err) } return value, nil } func loadInt64EnvWithDefault(envVar string, fallback int64) (int64, error) { rawValue, ok := os.LookupEnv(envVar) if !ok { return fallback, nil } value, err := strconv.ParseInt(rawValue, 10, 64) if err != nil { return 0, fmt.Errorf("load gateway config: parse %s: %w", envVar, err) } return value, nil } func loadDurationEnvWithDefault(envVar string, fallback time.Duration) (time.Duration, error) { rawValue, ok := os.LookupEnv(envVar) if !ok { return fallback, nil } value, err := time.ParseDuration(rawValue) if err != nil { return 0, fmt.Errorf("load gateway config: parse %s: %w", envVar, err) } return value, nil } func loadBoolEnvWithDefault(envVar string, fallback bool) (bool, error) { rawValue, ok := os.LookupEnv(envVar) if !ok { return fallback, nil } value, err := strconv.ParseBool(rawValue) if err != nil { return false, fmt.Errorf("load gateway config: parse %s: %w", envVar, err) } return value, nil } func validatePublicRoutePolicyConfig(cfg PublicRoutePolicyConfig, maxBodyEnvVar string, requestsEnvVar string, windowEnvVar string, burstEnvVar string) error { if cfg.MaxBodyBytes < 0 { return fmt.Errorf("load gateway config: %s must not be negative", maxBodyEnvVar) } return validateRateLimitConfig(cfg.RateLimit, requestsEnvVar, windowEnvVar, burstEnvVar) } func validatePublicAuthIdentityPolicyConfig(cfg PublicAuthIdentityPolicyConfig, requestsEnvVar string, windowEnvVar string, burstEnvVar string) error { return validateRateLimitConfig(cfg.RateLimit, requestsEnvVar, windowEnvVar, burstEnvVar) } func validateRateLimitConfig(cfg RateLimitConfig, requestsEnvVar string, windowEnvVar string, burstEnvVar string) error { if cfg.Requests <= 0 { return fmt.Errorf("load gateway config: %s must be positive", requestsEnvVar) } if cfg.Window <= 0 { return fmt.Errorf("load gateway config: %s must be positive", windowEnvVar) } if cfg.Burst <= 0 { return fmt.Errorf("load gateway config: %s must be positive", burstEnvVar) } return nil }