Skip to content

Reverse proxy

View as Markdown

The built-in reverse proxy makes other services reachable under their own domains on ports 443 and 80: a request for media.home.arpa goes to the service behind it, with a certificate for that name. The box itself stays on port 7484; every name you did not set up goes there as before (or gets 404, if you choose so). Streams forward a TCP or UDP port of the box to another host. Everything is in Settings > Reverse proxy (administrators only), applies to the next request without a restart and is part of the configuration backup.

Point each domain at the box in your DNS server or router (an A or AAAA record with the box’s address, or a wildcard record for *.apps.home.arpa). The proxy only answers names that reach it. Without HTTPS on the box (development or simulation) it answers plain HTTP only, and a development daemon listens on the proxy ports only when PITHYX_DEV_PROXY_PORTS names them.

Add proxy host opens the settings of a host:

  • Domains: up to 50 names, separated by commas or spaces. A leading *. covers any one label in front (*.apps.home.arpa answers photos.apps.home.arpa, not a.b.apps.home.arpa); an exact name wins over a wildcard. A name belongs to one host only (proxy.domain.taken). A switched-off host answers nothing; its names count as unknown.
  • Target: protocol, host and port of the service. For https choose how its certificate is checked: against the trusted authorities of the system, against the box’s own authority (devices with a certificate from Settings > Certificates), or not at all for a self-signed certificate.
  • The target is a service on this box itself: needed for a target such as localhost, the box’s name or one of its addresses (proxy.target.on_box), for example to reach the web interface on 7484 under a domain. Ports 80 and 443 of the box are always refused, because the request would come back to the proxy (proxy.target.loop).
  • Pass WebSockets on: on by default; switched off, upgrades get 403 (proxy.websockets.disabled).

Answers stream through without buffering, so server-sent events and large downloads reach the browser as they come. Targets are spoken to with HTTP/1.1.

  • Certificate: From the box’s certificate authority makes a certificate for the host’s domains in memory (397 days, renewed 30 days before its end and whenever the authority changes). Every domain must lie inside the authority’s scope (tls.name.not_permitted); add your own domain to the scope with a new authority in Settings > Certificates. Upload my own takes a certificate with its chain and unencrypted key in PEM that covers every domain (proxy.certificate.name_mismatch), for example one for a public domain. An expired or broken upload is replaced by the box’s own certificate, and the host shows No certificate when it has none of its own.
  • The box picks the certificate by the name the browser asks for in the TLS handshake, on 443 and on 7484; a name without a host gets the box’s own certificate.
  • Redirect HTTP to HTTPS (on by default) answers plain HTTP on 80 with a redirect to 443 (301 for GET and HEAD, 308 otherwise). Switched off, the host also answers on 80.
  • Offer HTTP/2 (on by default) lets browsers use HTTP/2 on 443.
  • Send HSTS adds Strict-Transport-Security: max-age=31536000 to HTTPS answers.

The proxy checks, in this order: the allowed and denied addresses, Basic Auth, then the Pithyx sign-in.

  • Allowed addresses and Denied addresses: IP addresses or ranges such as 192.168.1.0/24. A denied address never gets in; with allowed addresses, only those do (proxy.address.denied). The box sees the address the connection comes from.
  • Basic Auth: users with a password the browser asks for before anything else. Passwords are stored as Argon2id hashes and never shown; leave the field empty to keep a stored password. Failed attempts count towards the same limits as failed sign-ins. The target does not get the Authorization header.
  • Require Pithyx sign-in: a page of the host sends a visitor without access to the box’s sign-in, by default on the requested name and port 7484 (or the Sign-in address of the page). After signing in there, the browser comes back with a one-time ticket (30 seconds) to /.pithyx/auth on the host, which sets a cookie of the host and continues to the page. Requests that are no page load get 401 (proxy.auth.required). The cookie belongs to the box session: signing out of the box, revoking the session, losing the right to sign in to the desktop or being removed from the host’s users and groups ends the access at the next request. Users and Groups limit who gets in (proxy.access.denied); with neither, everybody who may sign in does, and administrators always do. The target gets the user in X-Pithyx-User, X-Pithyx-User-Id, X-Pithyx-Groups and X-Pithyx-Admin. The path /.pithyx/auth of such a host belongs to the proxy. Users and groups are stored by name: after renaming a group, update the host.
  • The target never gets incoming X-Pithyx-*, X-Forwarded-*, Forwarded or trace headers, nor the box’s own cookies (pithyx_*); an answer cannot set those cookies either.
  • Send X-Forwarded-For, -Proto and -Host (on by default) tells the target the client’s address, the protocol and the requested name.
  • Send the requested name as Host passes the browser’s Host on; otherwise the target gets its own host and port. Host header sets a Host of your own instead.
  • Request headers and Response headers: one per line as Name: value; Name: alone removes the header. Headers the proxy controls itself (Host, Connection, X-Forwarded-*, X-Pithyx-*, Strict-Transport-Security and others) are refused (proxy.header.invalid).
  • Largest upload in MB: empty for no limit.
  • Connect timeout (1 to 300 seconds, default 10) and Answer timeout (1 to 86400 seconds, default 100: how long the target may stay silent). An idle WebSocket stays open for up to a day. A target that cannot be reached gets 502 (proxy.target.unavailable), one that is too slow 504 (proxy.target.timeout).
  • Write an access log (on by default): every request in the Log Center under the source proxy/<first domain>, with method, name, path (without the query, which may hold secrets), status, duration, client address and, behind a sign-in, the user.

The list shows for each host whether its target answered when the page opened; Check target looks again.

A redirect host sends browsers from its domains to another address with 301, 302, 307 or 308. With Keep path and query a request for /a?b=1 goes to the target address followed by /a?b=1. It presents a certificate on 443 like a proxy host.

Names without a host decides what 443 and 80 do with a name no enabled host has: Redirect to the web interface on port 7484 (the default, as without a reverse proxy) or Answer “not found” (404) (proxy.host.unknown). The installer’s check /api/install-id on port 80 always answers.

A stream forwards a port of the box, TCP, UDP or both, byte for byte to a host and port, for services that do not speak HTTP. The firewall opens the port while the stream is on. The box’s own ports are refused (21, 22, 80, 443, 445, 7484 and FTP’s passive ports 50000 to 50099, proxy.stream.port_reserved), so are ports another program holds (proxy.stream.port_in_use) and ports below 1024 unless you confirm them. A UDP client keeps its own connection to the target until it is silent for a minute; a stream takes up to 512 TCP connections and 1024 UDP clients at a time.

The proxy runs inside the daemon, which runs as root, and only administrators may set targets. Requests the proxy itself sent are recognized and refused with 508 (proxy.loop.detected). The settings are in the configuration backup as the part Reverse proxy; uploaded certificates come with the part Certificates (files proxy-host-<id>.pem and .key next to the box’s own).

Load balancing with health checks, rules per path, rate limits and Let’s Encrypt come later.