Repository navigation
nginx vhost for the app-service HTTPS proxy - #15
Conversation
Adds a vhost for job-<id>.services.int.janelia.org. It resolves the compute node and port for each job through Fileglancer's /api/apps/resolve endpoint via auth_request, then proxies straight to the node, so no proxied bytes pass through the application server -- which matters because the desktop app is effectively a video stream. The resolve endpoint is marked internal here and returns 404 on the primary hostname, which is what keeps it off the network: it is unauthenticated and will resolve any running service's host:port for a guessable job id. Inert until a wildcard DNS record and a *.services.int.janelia.org certificate exist, and until apps.service_proxy_domain is set in the Fileglancer config. The existing *.int.janelia.org certificate does not cover these names: a wildcard matches exactly one label. Validated with nginx -t against a wrapper config, since this file is an http fragment rather than a standalone config.
|
Application half: JaneliaSciComp/fileglancer#440 — the resolve endpoint, settings and operator guide this block depends on. This vhost 502s until a |
neomorphic
left a comment
There was a problem hiding this comment.
Looks good to me. There are a few minor quibbles that I noted.
Follow-up:
|
The regex only matched job-N.services.int.janelia.org, so requests to job-N.services-dev.int.janelia.org fell through to the default_server block and got served the wrong certificate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…d service Three things the fileglancer side changed under this config. The server_name regex still matched `job-<id>.services...`, so every signed hostname fell through to the default server and got Fileglancer's own 404. The MAC is matched loosely here on purpose -- Fileglancer verifies it -- but the shape has to allow it at all. proxy_pass hardcoded http://, so an app that fronts itself with TLS (marimo and OpenVSCode do) was dialed as cleartext, which its listener refuses outright rather than merely serving unencrypted. The scheme now comes from X-Fg-Upstream-Scheme. proxy_ssl_verify stays off because the certificate is generated on the compute node at launch and signed by nobody; this buys confidentiality on the node hop, not authentication of the upstream. A refused resolution -- the job finished, was cancelled, or has not published its address yet -- rendered nginx's stock 403, which tells a user nothing. error_page now serves Fileglancer's explanation at the URL they clicked. It has to be a prefix location: nginx refuses a proxy_pass with a URI part inside a named location. proxy_intercept_errors must stay off, or a 403 from the app itself would be replaced by the same page. README: the hostname now carries the MAC, and FGC_SESSION_SECRET_KEY is required once the proxy domain is set -- Fileglancer refuses to start without it, since the labels are signed with it. Verified with `nginx -t` and a live request against a stub resolver: a signed hostname whose resolution is refused returns 503 with the page, an unsigned one does not match the block, and /_fg_unavailable is not reachable directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit folded in the scheme plumbing from fileglancer#443 / fileglancer-hub#16 along with the signed-hostname and error-page changes. That PR is not being accepted, so those directives have to come back out -- and not merely as tidying. `proxy_pass $upscheme://$upstream` reads a header that a Fileglancer without #443 never sends, which leaves the scheme empty and fails every proxied request rather than falling back to http. What stays is what this branch is actually for: the server_name regex that matches signed hostnames, the error page for a service that is no longer running, and the README notes on both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deployment half of the app-service HTTPS proxy. The application half is JaneliaSciComp/fileglancer#440 — both must land, and this one is inert on its own.
What it does
Adds a vhost for
job-<id>.services.int.janelia.org. A browser opens a running app service (JupyterLab, marimo, OpenVSCode, TensorBoard, the noVNC desktop) at that name over HTTPS; nginx asks Fileglancer which compute node and port the job is serving on, then proxies straight to the node.The upstream is dynamic — a new node and port on every launch — so it is resolved per request through
auth_requestagainst Fileglancer's/api/apps/resolve, which answers204withX-Fg-Upstream: <host>:<port>or403. Fileglancer never carries the proxied traffic itself, which matters because the desktop app is effectively a video stream.Why the browser can't just keep using the direct link
Today a service publishes
http://<node>:<port>/...?tkn=<token>. Fileglancer is served over HTTPS, so that link is mixed content with the token in cleartext, and the host and port change on every launch. Reachability is not the problem being solved — nodes stay directly reachable.Two directives are load-bearing, please don't drop them in review
internal;onlocation = /_fg_resolve. The resolve endpoint is unauthenticated by design (the session cookie is scoped to the Fileglancer hostname and never reaches an app subdomain, so there is no user to check). It will resolve any running service'shost:portfor a guessable, sequential job id.internalis what makes it reachable only from nginx's own subrequest.location = /api/apps/resolve { return 404; }in the primary server block, so the same endpoint is not exposed onfileglancer.int.janelia.orgeither.Also worth knowing:
resolveris mandatory, not decorative. nginx refuses to start whenproxy_passtargets a variable and no resolver is configured. It is set to127.0.0.53(systemd-resolved's stub) — change it if this host runs something else.proxy_set_header Host $hostis what makes JupyterLab's WebSocket origin check pass: the app then seesHostandOriginas the same value, so no per-app configuration is needed.proxy_buffering offand the 3600s read timeout are for long-lived WebSocket sessions; notebooks and the desktop hold connections open for hours.Verified
nginx -tpasses with this block in place. Becausenginx.confhere is anhttp{}fragment rather than a standalone config, I validated it by including it in a wrapper config with throwaway self-signed certs and unprivileged ports — so theauth_request,internalandresolverdirectives are confirmed valid in their contexts, not merely plausible.Not verified, and not verifiable until DNS and the certificate exist: whether each app actually behaves behind the proxy. That is step 5 below.
Deployment checklist
int.janelia.orgzone is enough:*.services IN A <fileglancer host IP>A wildcard never matches its own parent, so this does not make
services.int.janelia.orgitself resolve. Add a plainservices IN Arecord if we want the bare alias; the proxy does not need it.*.services.int.janelia.orgcertificate. The existing*.int.janelia.orgwildcard does not coverjob-1.services.int.janelia.org— a wildcard matches exactly one label and browsers enforce that strictly. Confirm with whoever issues certs that a nested wildcard is acceptable under our PKI policy; public CAs issue them routinely, internal policies are sometimes fussier about depth./etc/nginx/certs/services-wildcard.{crt,key}on the dev server, or change the paths in this block to match wherever it lands.resolveraddress matches what the host actually runs.fileglancerversion containing #440 and bump the pin inpixi.toml(currently==3.1.0, which has neither the endpoint nor the settings). This block 502s until that is done, since/api/apps/resolvewill not exist.apps.service_proxy_domain: services.int.janelia.orgin the Fileglancer config.apps.service_proxy_upstream_zoneand/orapps.service_proxy_upstream_networks. Worth thinking about: our nodes are<host>.int.janelia.org, so the zone setting would have to beint.janelia.org, which permits every internal host rather than just compute nodes. The CIDR setting is probably the more useful control here. This matters because a service publishes its own upstream from a file its job writes — see the security notes on #440.nginx -t, then reload.grep 'service proxy resolve totals'. A misconfigured zone or CIDR shows up there asrefused_no_upstreamrather than as user complaints.If an app rejects the proxied origin, fix it in that app's manifest (most servers have an allowed-origin or base-URL option) rather than weakening this configuration.
Related
docs/superpowers/specs/2026-08-28-app-service-https-proxy-design.mdin that PRdocs/ServiceProxy.mdin that PR@StephanPreibisch @JaneliaSciComp/fileglancer