Skip to content

Latest commit

 

History

History
172 lines (131 loc) · 7.31 KB

File metadata and controls

172 lines (131 loc) · 7.31 KB

Local HTTPS design

Status: implemented

Reviewed: 2026-08-27

Outcome

A person can run a single-user self-hosted portal with:

wt roost start --https
open https://localhost:8443

or run the gateway separately for a remote wing:

wt serve --local --https

The first invocation creates a localhost-only certificate authority on demand and asks the operating system to trust its public certificate for the current user. WT says what it is doing before the trust command runs. The CA private key never leaves the Wingthing profile.

Why there are two listeners

browser ===== HTTPS :8443 ===== local gateway
                                    |
wing ===== loopback HTTP :8080 =====+

The browser needs a trusted secure origin. A local wing, embedded wing, or remote wing arriving through an SSH reverse forward needs an endpoint it can reach without trusting the browser computer's private CA.

Both listeners use the same relay handler and bind only to loopback in local HTTPS mode. The ordinary HTTP endpoint is therefore a host-local transport, not a LAN service. For a remote wing, SSH authenticates and encrypts the segment between hosts before it reaches that loopback endpoint.

The two-listener design avoids copying the CA certificate or any private key to a remote Linux or WSL machine. It also preserves wt start --local and the existing reverse-forward recipe.

Certificate material

WT creates these files under WINGTHING_DIR/local-tls:

File Contents Mode
ca-key.pem ECDSA P-256 CA private key 0600
ca.pem public self-signed CA certificate 0644
localhost-key.pem ECDSA P-256 server private key 0600
localhost.pem public server certificate 0644
trusted successful, platform-verified user trust-store marker 0600

The directory is mode 0700. Writes use a temporary file and atomic rename. Existing CA material is never silently replaced: incomplete, corrupt, mismatched, not-yet-valid, or expired CA state fails with an explicit error. A corrupt or near-expiry leaf may be regenerated under the same CA.

The CA has a zero-length intermediate path and critical name constraints for:

  • localhost;
  • 127.0.0.0/8; and
  • ::1.

The leaf contains only localhost, 127.0.0.1, and ::1 SANs and server-auth usage. The root is valid for ten years; the leaf rotates when fewer than thirty days remain.

Trust ceremony

The explicit --https flag is consent to create and install this local material. Before installation WT prints:

  • the CA private-key path and mode;
  • the public certificate path;
  • the localhost-only constraint;
  • that only the public certificate enters the trust store; and
  • on macOS, that a native Certificate Trust Settings dialog may appear.

Platform destinations are:

Platform Current-user destination
macOS explicitly selected ~/Library/Keychains/login.keychain-db user trust settings
Windows current-user Root certificate store
Linux Chromium NSS database at ~/.pki/nssdb

Linux needs certutil from libnss3-tools or nss-tools. WT initializes a missing Chromium NSS database without a password and without root.

WT checks the platform destination before writing the successful marker. On macOS it evaluates the generated leaf with the SSL policy, the localhost name, and the explicit login keychain. Windows and Linux confirm the precise root in their current-user browser stores. A failed check is never recorded as trusted. Markers made by older builds are reinstalled and upgraded once. The verified marker then prevents a daemon or temporarily locked macOS login keychain from reopening the ceremony on every start. wt local-cert remove removes this precise public root and clears either marker version. On macOS removal clears both the trust setting and the public certificate. It leaves the keys on the box so a running listener is not broken and WT cannot silently replace an authority that was previously trusted.

Address and mode safety

When --https is selected:

  • the implicit :8080 default becomes 127.0.0.1:8080;
  • both supplied addresses must be explicit loopback addresses with nonzero ports;
  • wildcard, LAN, DNS, and public hosts are rejected before any key is created;
  • HTTP and HTTPS may not resolve to the same loopback socket;
  • the browser-facing base URL becomes https://localhost:8443; and
  • a stale public WT_BASE_URL cannot change that local origin.

The certificate ceremony is deliberately opt-in. Local listener hardening also applies to the HTTP-only form of the same no-login mode:

Deployment Result
Existing wt serve / Fly edge or login node unchanged
Existing single-user local HTTP serve/roost stays HTTP; implicit :8080 becomes 127.0.0.1:8080, explicit non-loopback binds are rejected
OAuth, organization, or public shared roost keeps external HTTPS; local CA mode is rejected
Single-user local serve/roost with --https dual loopback listeners and local trust ceremony

Hosted WT_BASE_URL=https://... remains authoritative whenever local HTTPS is not selected.

The relay independently rejects non-loopback Host headers in local mode, so a DNS rebinding hostname cannot turn the loopback service into its own origin. Browser WebSocket upgrades must be same-origin. Unsafe browser requests must carry the same exact scheme, host, and port when they include Origin, and requests marked cross-site by Sec-Fetch-Site are rejected. Origin-less native wing and CLI requests remain compatible. Authenticated hosted and organization topologies keep their existing cross-host WebSocket behavior.

Security boundary

HTTPS protects browser-to-local-gateway traffic and supplies a conventional secure browser origin. It does not replace Wingthing's browser-to-wing application encryption, wing authentication, SSH authentication, or the egg sandbox.

The CA private key has the power to issue another localhost certificate on this one profile. Protecting the owning OS account and WINGTHING_DIR remains part of the trust boundary. A compromised gateway still serves the browser JavaScript and is therefore inside the client trust boundary even when terminal payloads are application-encrypted.

Regression gates

The automated battery covers certificate constraints, SANs, chain verification, root reuse, leaf rotation, corrupt-state behavior, expiry behavior, permissions, symlink refusal, atomic trust markers, failed install and verification commands, legacy-marker migration, idempotence, macOS trust-rule and certificate removal, all platform command arguments, Linux NSS initialization, and the invariant that no trust command receives a private-key path.

Listener tests cover unsafe-address refusal before key creation, default address rewriting in both HTTP and HTTPS local modes, loopback alias collisions, ordinary HTTP and HTTPS handler parity, trusted and untrusted TLS clients, local passkey origins, Host-header/DNS-rebinding refusal, same-origin mutation and WebSocket rules, hosted base URL preservation, opt-in flags, and mode rejection.

The ordinary full repository gate and Docker-backed shared-roost browser battery remain required. The shared-roost battery exercises multiple users, role paths, ACL denial, persistent terminal replay, mobile views, and existing organization API behavior without selecting local HTTPS.