Summary
Authsome's proxy currently uses a single hardcoded behavior: build routes from connected providers only, and pass through any unmatched request. This should become a named, persistent configuration instead of an implicit default.
ADR 0003 explicitly deferred the full policy model. This issue specifies the first step: four named modes covering two independent axes — what traffic the proxy attempts to intercept, and what it does with traffic that doesn't match any route.
The two axes
Intercept scope — what goes into the route table:
connected: only providers with an active connection in the vault (current behavior)
configured: all registered providers (bundled + custom), regardless of connection status
Unmatched policy — what happens to a request that doesn't match any route entry:
allow: forward unchanged (current behavior)
deny: block the request — return HTTP 403 for plain HTTP; kill the TCP connection for CONNECT tunnels (HTTPS)
The four modes
| Mode |
Intercept scope |
Unmatched policy |
Typical use |
connected_allow |
Connected providers |
Pass through |
Default — ergonomic local dev |
connected_deny |
Connected providers |
Block |
Lock down agent egress to only actively-credentialed providers |
configured_allow |
All configured providers |
Pass through |
Surface requests to known-but-unauthenticated providers in the audit log |
configured_deny |
All configured providers |
Block |
Strict egress: agent can only reach providers that are registered and credentialed |
Configuration
The mode is stored in GlobalConfig as a new proxy sub-block, parallel to the existing encryption block:
class ProxyConfig(BaseModel):
mode: Literal[
"connected_allow",
"connected_deny",
"configured_allow",
"configured_deny",
] = "connected_allow"
class GlobalConfig(BaseModel):
...
proxy: ProxyConfig | None = Field(default_factory=ProxyConfig)
It is not a CLI flag — it is persisted state, set via something like authsome config set proxy.mode configured_deny.
Behavior details
Intercept scope:
connected — route table built from list_connections(); only providers with stored, active credentials are matched (same as today)
configured — route table built from list_providers() (bundled + custom); a request that matches a configured provider host but where credential resolution fails is logged as proxy_no_credentials and then subject to the unmatched policy
Unmatched policy:
allow — existing pass-through behavior
deny — applies to both plain HTTP and HTTPS (CONNECT tunnels); a denied request is audit-logged as proxy_deny with host and reason
Invariants that hold across all four modes:
- Loopback hosts (
127.0.0.1, localhost, ::1) always pass through — blocking them would break proxy-internal communication
- Provider OAuth/auth endpoints always pass through — blocking them would break the login flow itself
Implementation sketch
ProxyRouter.resolve() already returns RouteResolution with a miss_reason. The deny policy acts on miss_reason == "no_match" cases. (ambiguous already errors today.)
configured scope requires proxy_routes() in AuthService to optionally draw from list_providers() instead of list_connections() as the host-matching source.
- Proxy mode should be read at proxy startup / router build time, not per-request.
AuthProxyAddon needs the current proxy mode to decide whether to call flow.kill() on misses. The ProxyClient protocol (in both runner.py and server.py) should expose a proxy_mode() method so the addon doesn't couple directly to GlobalConfig.
- Default connections only — non-default connections are out of scope.
Out of scope for this issue
- Per-provider allowlist/denylist overrides
- "warn" mode (log + pass through)
- Non-default connections
- Hosted/multi-user mode
Related
- ADR 0003: Unmatched Proxy Requests Pass Through In Local Mode — this issue supersedes the deferred decision recorded in its Consequences section. If implemented, ADR 0003 should be updated to reference this feature.
Summary
Authsome's proxy currently uses a single hardcoded behavior: build routes from connected providers only, and pass through any unmatched request. This should become a named, persistent configuration instead of an implicit default.
ADR 0003 explicitly deferred the full policy model. This issue specifies the first step: four named modes covering two independent axes — what traffic the proxy attempts to intercept, and what it does with traffic that doesn't match any route.
The two axes
Intercept scope — what goes into the route table:
connected: only providers with an active connection in the vault (current behavior)configured: all registered providers (bundled + custom), regardless of connection statusUnmatched policy — what happens to a request that doesn't match any route entry:
allow: forward unchanged (current behavior)deny: block the request — return HTTP 403 for plain HTTP; kill the TCP connection for CONNECT tunnels (HTTPS)The four modes
connected_allowconnected_denyconfigured_allowconfigured_denyConfiguration
The mode is stored in
GlobalConfigas a newproxysub-block, parallel to the existingencryptionblock:It is not a CLI flag — it is persisted state, set via something like
authsome config set proxy.mode configured_deny.Behavior details
Intercept scope:
connected— route table built fromlist_connections(); only providers with stored, active credentials are matched (same as today)configured— route table built fromlist_providers()(bundled + custom); a request that matches a configured provider host but where credential resolution fails is logged asproxy_no_credentialsand then subject to the unmatched policyUnmatched policy:
allow— existing pass-through behaviordeny— applies to both plain HTTP and HTTPS (CONNECT tunnels); a denied request is audit-logged asproxy_denywithhostandreasonInvariants that hold across all four modes:
127.0.0.1,localhost,::1) always pass through — blocking them would break proxy-internal communicationImplementation sketch
ProxyRouter.resolve()already returnsRouteResolutionwith amiss_reason. Thedenypolicy acts onmiss_reason == "no_match"cases. (ambiguousalready errors today.)configuredscope requiresproxy_routes()inAuthServiceto optionally draw fromlist_providers()instead oflist_connections()as the host-matching source.AuthProxyAddonneeds the current proxy mode to decide whether to callflow.kill()on misses. TheProxyClientprotocol (in bothrunner.pyandserver.py) should expose aproxy_mode()method so the addon doesn't couple directly toGlobalConfig.Out of scope for this issue
Related