diff --git a/.gitignore b/.gitignore index 78303e3..92458f7 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ node_modules /docs/.vitepress/dist/ /docs/.vitepress/cache/ /.direnv +/vendor diff --git a/CHANGELOG.md b/CHANGELOG.md index c72df9d..016a15d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,15 @@ -# 0.7.0-beta.1 (March 27, 2026) +# 0.7.0-beta.2 (April 7, 2026) > This is a beta release. Please report any issues you encounter. - **Migrated all UI components from react-aria/react-stately to @base-ui-components/react.** - Removed `react-aria`, `react-stately`, and `tailwindcss-react-aria-components` as dependencies. -- **Rearchitected the Headplane Agent** from a long-running stdin/stdout daemon to a one-shot sync model (closes [#350](https://github.com/tale/headplane/issues/350), closes [#455](https://github.com/tale/headplane/issues/455)). - - The Go binary connects to the Tailnet, fetches all peer hostinfo as JSON, and exits. +- **Replaced `openid-client` with a clean-room OIDC implementation.** + - Removed the `openid-client` dependency entirely. + - Fixed `client_secret_basic` auth method not working with Google SSO and other providers (closes [#493](https://github.com/tale/headplane/issues/493)). + - Fixed OIDC connector initialization failures on beta.1 (closes [#516](https://github.com/tale/headplane/issues/516)). +- **Rearchitected the Headplane Agent** with a new sync model (closes [#350](https://github.com/tale/headplane/issues/350), closes [#455](https://github.com/tale/headplane/issues/455)). + - The Go binary connects to the Tailnet and fetches all peer hostinfo as JSON. - The Node.js manager auto-generates ephemeral tag-only pre-auth keys (requires Headscale 0.28+). - Deprecated `integration.agent.pre_authkey` and `integration.agent.cache_path` config fields. - Added `integration.agent.executable_path` config field. @@ -16,15 +20,23 @@ - Added an agent status page at `/settings/agent` showing sync status, node count, errors, and a "Sync Now" button. - Added additional machine list filters for user, tag, status, and route (via [#507](https://github.com/tale/headplane/pull/507), closes [#506](https://github.com/tale/headplane/issues/506)). - Added self-service pre-auth key creation for auditor role users (via [#478](https://github.com/tale/headplane/pull/478), closes [#453](https://github.com/tale/headplane/issues/453)). -- Fetch OIDC profile pictures server-side when the URL requires authentication (via [#510](https://github.com/tale/headplane/pull/510), closes [#326](https://github.com/tale/headplane/issues/326)). +- Store OIDC profile pictures in the database to prevent cookie header overload (closes [#326](https://github.com/tale/headplane/issues/326), via [#510](https://github.com/tale/headplane/pull/510)). +- Fixed pre-auth key expiration on Headscale 0.28+ (closes [#519](https://github.com/tale/headplane/issues/519)). +- Fixed OIDC subject matching for providers that use special characters in user IDs (e.g. Auth0 `github|12345`) (closes [#428](https://github.com/tale/headplane/issues/428)). +- Fixed `headscale.api_key` not being used consistently across all code paths. +- Fixed intermittent SSR crash on the Access Control page caused by client-only CodeMirror imports. - Fixed first user not being assigned the owner role on OIDC login (via [#480](https://github.com/tale/headplane/pull/480), closes [#266](https://github.com/tale/headplane/issues/266)). - Fixed login errors throwing an unexpected server error instead of showing form validation (via [#475](https://github.com/tale/headplane/pull/475), closes [#474](https://github.com/tale/headplane/issues/474)). - Fixed agent HostInfo not refreshing periodically using `cache_ttl` (via [#477](https://github.com/tale/headplane/pull/477), closes [#427](https://github.com/tale/headplane/issues/427)). +- Fixed agent working directory being wiped on restart. - Fixed a race condition where the SSE controller could be used after being closed. +- Fixed WebSSH dropping DERP port information on non-standard ports (e.g. `:8443`), which caused connections to fail (closes [#515](https://github.com/tale/headplane/issues/515)). - Fixed WebSSH WASM prefix paths for correct asset loading (closes [#386](https://github.com/tale/headplane/issues/386)). - Fixed Dockerfile WASM copy paths. - Fixed CodeMirror version mismatch override in the ACL editor. - Fixed cookie secret generation using incorrect byte length (via [#501](https://github.com/tale/headplane/pull/501)). +- Fixed OIDC configuration error troubleshooting link (via [#518](https://github.com/tale/headplane/pull/518), closes [#517](https://github.com/tale/headplane/issues/517)). +- Fixed deprecated Nix package attributes (via [#521](https://github.com/tale/headplane/pull/521)). - Detect unsupported Docker API versions early with a clear error message (via [#497](https://github.com/tale/headplane/pull/497)). - Updated NixOS module options: removed deprecated agent fields, added `headscale.api_key_path` and `integration.agent.executable_path`. diff --git a/Dockerfile b/Dockerfile index 6fa6c54..6a1c3ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,7 @@ FROM --platform=$BUILDPLATFORM golang:1.25.1 AS go-base WORKDIR /run COPY go.mod go.sum build.sh ./ +COPY patches/ ./patches/ RUN go mod download COPY cmd/ ./cmd/ diff --git a/build.sh b/build.sh index 00ec6f4..5f67f18 100755 --- a/build.sh +++ b/build.sh @@ -137,7 +137,21 @@ build_wasm() { cat "$(go env GOROOT)/lib/wasm/wasm_exec.js" >> \ "$(dirname "$WASM_OUTPUT")/wasm_exec.js" - GOOS=js GOARCH=wasm go build -o "$WASM_OUTPUT" ./cmd/hp_ssh + # Vendor dependencies and apply the DERP port patch. + # Tailscale's derphttp WebSocket URL builder ignores DERPPort, + # which breaks WASM connections to non-443 DERP servers. + echo "==> Vendoring Go dependencies for WASM patch" + go mod vendor + + DERP_PATCH="$ROOT_DIR/patches/tailscale-derp-port.patch" + if [ -f "$DERP_PATCH" ]; then + echo "==> Applying DERP port patch" + patch -d vendor/tailscale.com -p1 < "$DERP_PATCH" || \ + die "failed to apply DERP port patch" + fi + + GOOS=js GOARCH=wasm go build -mod=vendor -o "$WASM_OUTPUT" ./cmd/hp_ssh + rm -rf vendor } build_app() { diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 7383321..c00456a 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -45,7 +45,8 @@ export default defineConfig({ text: "Features", items: [ { text: "Single Sign-On (SSO)", link: "/features/sso" }, - { text: "Headplane Agent / SSH", link: "/features/agent" }, + { text: "Headplane Agent", link: "/features/agent" }, + { text: "WebSSH", link: "/features/ssh" }, ], }, { diff --git a/docs/features/ssh.md b/docs/features/ssh.md new file mode 100644 index 0000000..a3ff5bf --- /dev/null +++ b/docs/features/ssh.md @@ -0,0 +1,73 @@ +--- +title: WebSSH +description: Open SSH sessions to your Tailnet nodes directly from the browser. +--- + +# WebSSH + +
+ +
SSH access via the browser
+
+ +WebSSH lets you open an SSH session to any node directly from the browser. It +uses a Go-based WASM shim that creates an ephemeral Tailscale node in the +browser and connects to the target node over the Tailnet. + +## Prerequisites + +- **Headscale 0.28 or newer** is required. +- Target nodes must have **Tailscale SSH** enabled. +- Users must be authenticated via **OIDC** (API key logins cannot use WebSSH). +- The **Headplane Agent** must be + [enabled and configured](/features/agent) so that ephemeral node cleanup + works correctly. +- The WASM assets (`hp_ssh.wasm` and `wasm_exec.js`) must be present in the + build. These are built automatically by `./build.sh --wasm`. + +## How It Works + +When a user opens an SSH session from the UI, the browser: + +1. Loads a Go WASM binary that implements a minimal Tailscale node. +2. Authenticates to the Tailnet using an ephemeral pre-auth key generated + server-side. +3. Connects to the target node over the Tailnet using DERP relay servers. +4. Opens an SSH session and renders it in an [xterm.js](https://xtermjs.org) + terminal. + +The ephemeral node is automatically cleaned up after the session ends. + +## DERP Servers on Non-Standard Ports + +In the browser, Tailscale connects to DERP relay servers via WebSockets. If +your DERP servers run on a non-standard port (e.g. `:8443` instead of `:443`), +Headplane includes a patch to Tailscale's DERP client that preserves the port +in WebSocket URLs. This patch is applied automatically during the WASM build. + +::: warning +If you are building the WASM module manually (outside of `build.sh`), make sure +to apply `patches/tailscale-derp-port.patch` to the vendored Tailscale source +before compiling. See the `build_wasm()` function in `build.sh` for reference. +::: + +## Troubleshooting + +### "WebSSH is not configured in this build" + +The WASM assets are missing. Rebuild with `./build.sh --wasm` or ensure your +Docker image was built with the `--wasm` flag. + +### "Only OAuth users are allowed to use WebSSH" + +WebSSH requires OIDC authentication to generate pre-auth keys tied to a +Headscale user. API key logins do not have an associated Headscale user +identity. + +### Connection hangs or fails to reach the node + +- Verify that the target node has Tailscale SSH enabled. +- If using custom DERP servers on non-standard ports, ensure you are running + a build that includes the DERP port patch (any build from `build.sh` or + Docker includes it automatically). +- Check the browser console for WASM errors or DERP connection failures. diff --git a/nix/ssh-wasm.nix b/nix/ssh-wasm.nix index 9c977fd..959de87 100644 --- a/nix/ssh-wasm.nix +++ b/nix/ssh-wasm.nix @@ -22,7 +22,14 @@ in buildPhase = '' export GOOS=js export GOARCH=wasm - go build -o hp_ssh.wasm ./cmd/hp_ssh + + # Patch Tailscale's derphttp to include DERPPort in WebSocket URLs. + # Without this, DERP servers on non-443 ports fail in WASM builds. + if [ -f patches/tailscale-derp-port.patch ]; then + patch -d vendor -p1 < patches/tailscale-derp-port.patch + fi + + go build -mod=vendor -o hp_ssh.wasm ./cmd/hp_ssh ''; installPhase = '' diff --git a/patches/tailscale-derp-port.patch b/patches/tailscale-derp-port.patch new file mode 100644 index 0000000..46d262c --- /dev/null +++ b/patches/tailscale-derp-port.patch @@ -0,0 +1,30 @@ +Fix DERP WebSocket URL to include non-standard ports. + +Tailscale's derphttp client ignores DERPPort when building WebSocket +URLs, causing connections to fail when DERP servers run on non-443 +ports (e.g. :8443). The TCP dial path correctly handles DERPPort +but the WebSocket path (used in WASM/browser builds) does not. + +--- a/derp/derphttp/derphttp_client.go ++++ b/derp/derphttp/derphttp_client.go +@@ -279,10 +279,19 @@ + return c.url.String() + } + proto := "https" ++ var port string + if debugUseDERPHTTP() { + proto = "http" ++ port = "3340" + } +- return fmt.Sprintf("%s://%s/derp", proto, node.HostName) ++ if node != nil && node.DERPPort != 0 { ++ port = fmt.Sprint(node.DERPPort) ++ } ++ host := node.HostName ++ if port != "" { ++ host = net.JoinHostPort(node.HostName, port) ++ } ++ return fmt.Sprintf("%s://%s/derp", proto, host) + } + + // AddressFamilySelector decides whether IPv6 is preferred for