Repository navigation
feat(cli): support fixed callback port for browser login in containers - #5065
Conversation
Browser login (OIDC Authorization Code + PKCE and the edge/mTLS browser flow) binds its callback listener to an ephemeral port on 127.0.0.1. When the CLI runs inside a container, the wrapper cannot know a random port in advance, so it cannot publish it to the host and the browser never reaches the callback. Add a shared bind helper honoring two environment variables: - OPENSHELL_CALLBACK_PORT pins the listener to a fixed port (validated integer 1..=65535) so a wrapper can publish -p 127.0.0.1:<port>:<port> before login starts. - OPENSHELL_CALLBACK_BIND_ADDRESS overrides the bind address (validated IP address) so user-mode container networking (pasta/slirp4netns), which can only reach 0.0.0.0 listeners, can forward to it. Defaults are unchanged: with both variables unset the listener binds 127.0.0.1:0 exactly as before, and the bind address never widens to 0.0.0.0 without the explicit variable, since a wildcard bind on a host machine would expose the callback server to the local network. The browser-facing redirect URL stays http://127.0.0.1:<port>/callback even for a wildcard bind, so no identity-provider allowlist change is needed. A busy fixed port or an invalid value fails with a clear error instead of falling back silently. Signed-off-by: Jesse Jaggars <jjaggars@redhat.com>
dfb3c9b to
a9426dc
Compare
|
Thanks for reviewing! Two maintainer actions are needed to unblock CI on this fork PR:
The |
TaylorMutch
left a comment
There was a problem hiding this comment.
Reviewed locally at a9426dc. The shared helper preserves existing defaults and authentication checks. I recommend addressing the IPv6 callback mismatch noted inline before merging. Validation: cargo test -p openshell-cli --lib callback passed 23 tests; cargo test -p openshell-cli --lib browser_flow passed 6 tests. A separate connection probe using the exact binding helper confirmed the IPv6 mismatch. Full container/IdP login was not exercised.
|
/ok to test a9426dc |
An IPv6-only callback bind (OPENSHELL_CALLBACK_BIND_ADDRESS=::1) was accepted but both browser flows still directed callbacks at 127.0.0.1, so the listener was unreachable and login timed out. Route callbacks to the address the listener actually bound: - The CLI derives the browser-facing callback target from the bound address. Wildcard binds map to the matching loopback (0.0.0.0 to 127.0.0.1, :: to [::1]) because a wildcard listener accepts loopback connections of its family; IPv6 addresses render bracketed per RFC 3986/RFC 8252. - The OIDC redirect URI therefore becomes http://[::1]:<port>/callback for ::1-bound listeners, and the bundled Keycloak realm now allows http://[::1]:* redirects for the CLI client alongside the existing IPv4 entries. - The edge flow passes the unbracketed loopback literal to the gateway connect page as callback_host. The page strictly validates it to a loopback IP literal (anything missing, non-loopback, or malformed falls back to 127.0.0.1) before embedding it in the fetch URL and CSP connect-src, so a crafted parameter can never direct the embedded edge token to an external host. The host is preserved across the waiting-page refresh, and older gateways that ignore the new parameter keep the historical 127.0.0.1 behavior. Defaults are unchanged: the listener still binds 127.0.0.1 on an ephemeral port unless the environment variables say otherwise. Adds a callback-reachability test (connecting to the advertised [::1] target succeeds while 127.0.0.1 is refused for a ::1 bind), host-derivation table tests, OIDC redirect tests for ::1 and :: binds, edge URL tests, and server-side validation and render tests. Signed-off-by: Jesse Jaggars <jjaggars@redhat.com>
|
Good catch — the PR description was accidentally overwritten with unrelated content from a local temp-file collision; it's now restored with Still needing a maintainer |
TaylorMutch
left a comment
There was a problem hiding this comment.
Reviewed the update at b676200. The earlier IPv6 listener/redirect mismatch is addressed, and the IPv4 fixed-port/container path looks reasonable. One remaining IPv6 edge-login limitation is noted inline. IPv6 edge login can be out of scope for this PR if acknowledged explicitly and the documentation/PR description reflects that limitation; fixing it here is not required on that basis. The linked issue #5066 is now state:accepted, and the PR description has been corrected. Validation: local Chrome CSP reproduction with an IPv4 control; full container/IdP login was not exercised and Rust tests did not complete locally.
|
/ok to test b676200 |
Review feedback on b676200: Chrome does not support IPv6 literals in CSP host-source expressions, so the edge flow's connect page blocks the callback POST to http://[::1]:<port> with a connect-src violation even though the policy allows it. The CLI listener is reachable; the browser refuses to fetch. Keep the callback routing as-is (correct per the CSP specification, and the failure mode is an explicit page error rather than a hang) and scope the claim precisely instead: - The connect page CSP always keeps the IPv4 loopback source so a dropped IPv6 source expression cannot degrade the policy to empty. - The auth reference and PR description now state that IPv6 callback binds are fully supported for OIDC login but are best-effort for edge login until browsers implement IPv6 CSP source expressions; all IPv4 paths, including the container use case, are unaffected. Signed-off-by: Jesse Jaggars <jjaggars@redhat.com>
|
/ok to test 2fee68f |
TaylorMutch
left a comment
There was a problem hiding this comment.
Final review at 2fee68f: looks good for the documented scope. The IPv4 fixed-port/container login path preserves the existing defaults and authentication checks, and IPv6 callback routing is supported for OIDC. The remaining Chrome CSP limitation for IPv6 edge login is explicitly acknowledged and documented as out of scope, which is acceptable for this PR. No new blocking findings. Local validation passed: 27 CLI callback tests, 9 browser-flow tests, and 21 gateway auth HTTP tests, including the updated CSP test. Full container/IdP login was not exercised. The linked issue #5066 is accepted. Merge remains conditional on required CI passing.
Summary
Both browser login flows (OIDC Authorization Code + PKCE and the edge/mTLS browser flow) bind their callback listener to an ephemeral port on
127.0.0.1. When the CLI runs inside a container — for example the hypershell installer's wrapper — the wrapper cannot know a randomly chosen port in advance, so it cannot publish the port to the host and the browser can never reach the callback. This PR adds two environment variables that make browser login work in that setting, with defaults unchanged.Related Issue
Fixes #5066
Changes
crates/openshell-cli/src/callback.rsmodule with a sharedbind_callback_listener()helper used by both browser login flows. It honors:OPENSHELL_CALLBACK_PORT— pin the callback listener to a fixed TCP port (validated integer from 1 to 65535) so a container wrapper can publish-p 127.0.0.1:<port>:<port>before login starts.OPENSHELL_CALLBACK_BIND_ADDRESS— override the listener bind address (validated IP address, IPv4 or IPv6; e.g.0.0.0.0for pasta/slirp4netns user-mode container networking,::1for IPv6-only hosts).oidc_browser_auth_flow(crates/openshell-cli/src/oidc_auth.rs) andbrowser_auth_flow(crates/openshell-cli/src/auth.rs) now bind through the shared helper; the port used in the redirect URI /callback_portURL parameter is still derived from the bound listener.0.0.0.0→127.0.0.1,::→[::1]), and IPv6 addresses render bracketed (http://[::1]:<port>/callback). The edge flow passes a strictly loopback-validatedcallback_hostparameter to the gateway connect page (crates/openshell-server/src/auth/http.rs), which brackets IPv6 in the fetch URL and CSPconnect-src, falls back to127.0.0.1for anything malformed or non-loopback (so the embedded edge token can never be directed to an external host), and preserves the host across the waiting-page refresh. The bundled Keycloak realm (scripts/keycloak-realm.json) now also allowshttp://[::1]:*redirects for the CLI client.docs/how-it-works/gateways/authentication.mdxwith the env-var table, container example, IPv6 behavior, and security notes.Design choices
127.0.0.1:0exactly as before — no behavior change for host users.0.0.0.0is never implied by setting a fixed port. Binding0.0.0.0on a host machine would expose the short-lived callback server to the local network, so widening the bind address requires the explicitOPENSHELL_CALLBACK_BIND_ADDRESS=0.0.0.0, which only a container wrapper that also publishes the port to host loopback (-p 127.0.0.1:<port>:<port>) should set. The port is never exposed beyond host loopback.0.0.0.0in a container, where the wrapper publishes the port to host loopback) usehttp://127.0.0.1:<port>/callback; IPv6 binds use the bracketed RFC 8252 formhttp://[::1]:<port>/callback. The bundled Keycloak realm allows all three loopback redirect prefixes (127.0.0.1,localhost,[::1]). IPv6 callback binds are fully supported for OIDC login.connect-srcsource expressions, so an IPv6-bound CLI cannot complete edge login in Chrome (the page's POST is blocked by CSP and it reports it could not reach the CLI). This is a browser limitation, documented in the auth reference; the connect page keeps the IPv4 loopback source in its policy so it never degrades to empty. IPv6 edge login is out of scope here; all IPv4 paths, including the container use case, are unaffected.--network=host:--network=hostworks on Linux but not with a podman machine VM on macOS, so the fixed published port is the portable approach.How the hypershell installer will use it
The wrapper will set
OPENSHELL_CALLBACK_PORT=<fixed>andOPENSHELL_CALLBACK_BIND_ADDRESS=0.0.0.0for the CLI inside the container and run the container with-p 127.0.0.1:<port>:<port>, exposing the callback only on host loopback. That installer change is intentionally out of scope here and waits for this to ship in a release.Testing
mise run pre-commitpasses (the only failure is a pre-existingopenshell-supervisor-processclippy lint that fails identically onmain; verified viagit stash)Unit tests cover the shared helper and both flows:
callback.rs: default ephemeral loopback bind; fixed port; fixed port already in use (clear error namingOPENSHELL_CALLBACK_PORT); valid/invalid port values (0, negative, 65536, non-numeric, whitespace); wildcard0.0.0.0bind; fixed port + wildcard combined; IPv6::1and::binds; invalid bind addresses (hostname,ip:port, garbage); callback host/target derivation table; callback reachability probe (connecting to the advertised[::1]target succeeds while127.0.0.1is refused on a::1bind)oidc_auth.rs: default bind produceshttp://127.0.0.1:<ephemeral>/callback; fixed port +0.0.0.0bind keeps the redirect URI on127.0.0.1with the fixed port;::1and::binds producehttp://[::1]:<port>/callback; invalid port value failsauth.rs: default bind advertises the ephemeral port in the/auth/connectURL; fixed port +0.0.0.0bind advertises the fixed port;::1bind advertisescallback_host=::1; invalid port value failsopenshell-server/src/auth/http.rs:sanitize_callback_hostaccepts only loopback IP literals (IPv4 and IPv6; hostnames, non-loopback IPs, brackets, and garbage fall back to127.0.0.1); connect page targets127.0.0.1by default and bracketed[::1]when provided; waiting page preservescallback_hostacross refreshVerification run on top of current
main:cargo test -p openshell-cli -p openshell-serverall green (405 + 2522 lib tests plus integration targets, including 33 callback/flow tests),cargo clippy -p openshell-cli -p openshell-server --all-targets -- -D warningsclean,cargo fmt --checkclean, markdownlint clean.Checklist
docs/how-it-works/gateways/authentication.mdx