Compare commits

...

28 Commits

Author SHA1 Message Date
Aarnav Tale 28ad1d439f Merge branch 'main' into next 2025-10-30 23:52:35 -04:00
Aarnav Tale 135d2940f1 chore: fix hash 2025-10-30 23:38:54 -04:00
Aarnav Tale 497a782f17 docs: fix link typo for configuration 2025-10-30 17:30:13 -04:00
Aarnav Tale 6d5e44f585 fix: pnpm lifecycle scripts broke nix 2025-10-28 09:58:53 -04:00
Aarnav Tale 94e5cb95c6 Merge pull request #342 from tale/update_flake_lock_action 2025-10-27 22:00:56 -04:00
Aarnav Tale 800d73248d Merge pull request #349 from tituspijean/patch-1 2025-10-27 22:00:14 -04:00
tituspijean 66b6dea068 fix: write_ahead_log in sqlite config schema 2025-10-27 21:57:52 +01:00
github-actions[bot] 489b12460f flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/362791944032cb532aabbeed7887a441496d5e6e?narHash=sha256-gKl2Gtro/LNf8P%2B4L3S2RsZ0G390ccd5MyXYrTdMCFE%3D' (2025-10-11)
  → 'github:nixos/nixpkgs/02f2cb8e0feb4596d20cc52fda73ccee960e3538?narHash=sha256-tH3wHnOJms%2BU4k/rK2Nn1RfBrhffX92jLP/2VndSn0w%3D' (2025-10-24)
2025-10-26 08:23:59 +00:00
Aarnav Tale ef8dcec9c8 chore: changelog 2025-10-19 15:35:41 -04:00
Aarnav Tale f66f778398 chore: add sig handlers for the server 2025-10-19 15:29:40 -04:00
Aarnav Tale 6eabfb7610 feat: support headplane compiled without ssh 2025-10-19 15:29:13 -04:00
Aarnav Tale a68df033a2 chore: switch to build script 2025-10-19 14:09:56 -04:00
Aarnav Tale 53b8f76ac6 Merge remote-tracking branch 'origin/main' into next 2025-10-19 13:05:36 -04:00
Aarnav Tale df4d864838 docs: man why is this case sensitive 2025-10-16 22:30:15 -04:00
Aarnav Tale c9bae42622 chore: ok mise be stupid 2025-10-16 22:30:12 -04:00
Aarnav Tale a2a28e747b chore: nix hash 2025-10-16 22:16:26 -04:00
Aarnav Tale d175171efc chore: document agent procedure 2025-10-16 22:16:11 -04:00
Aarnav Tale ac9ce72078 feat: automatically build go modules during pnpm build 2025-10-16 22:01:46 -04:00
Aarnav Tale f44881b399 docs: update images 2025-10-16 21:56:04 -04:00
Aarnav Tale 3b93f2fb61 chore: more docs cleanup/unification 2025-10-16 21:18:27 -04:00
Aarnav Tale 1b2d7e305a docs: new installation docs! 2025-10-16 21:03:40 -04:00
Aarnav Tale 639e7e9c6b fix: reorganize vitepress docs into /docs 2025-10-16 18:29:51 -04:00
Aarnav Tale 5d94667186 fix: update docs build script 2025-10-16 13:30:14 -04:00
Aarnav Tale 225dfceddb chore: changelog 2025-10-15 18:18:26 -04:00
Aarnav Tale cd379cd10d Merge remote-tracking branch 'origin/main' into next 2025-10-15 18:17:09 -04:00
Aarnav Tale 8f260b79f1 feat: bundle all node_modules to reduce size 2025-10-15 18:14:14 -04:00
Aarnav Tale eaffcd7b62 chore: refactor vitepress docs location 2025-10-12 22:03:13 -04:00
Aarnav Tale 4ccc73d7e4 chore: cleanup docs a bit 2025-10-12 15:50:20 -04:00
48 changed files with 2514 additions and 1915 deletions
+8 -4
View File
@@ -1,7 +1,11 @@
node_modules
.dockerignore
Dockerfile
/.cache
/.github
/.react-router
/node_modules
/build
/docs
/nix
.dockerignore
.env
Dockerfile
+1 -1
View File
@@ -55,7 +55,7 @@ jobs:
key: ${{ runner.os }}-go-${{ hashFiles('**/go.mod', '**/go.sum') }}
- name: CI pipeline
run: mise run ci
run: ./build.sh --skip-pnpm-prune
nix:
name: nix
+6 -3
View File
@@ -4,9 +4,12 @@ node_modules
/build
/test
.env
app/hp_ssh.wasm
app/wasm_exec.js
/app/hp_ssh.wasm
/app/wasm_exec.js
/public/hp_ssh.wasm
/public/wasm_exec.js
/docs/.vitepress/dist/
/docs/.vitepress/cache/
/.direnv
-25
View File
@@ -1,25 +0,0 @@
{
"formatter": {
"language_server": {
"name": "biome"
}
},
"code_actions_on_format": {
"source.fixAll.biome": true,
"source.organizeImports.biome": true,
"source.organizeImports": true
},
"languages": {
"YAML": {
"tab_size": 2,
"hard_tabs": false
},
"Go": {
"formatter": {
"language_server": {
"name": "gopls"
}
}
}
}
}
+119 -38
View File
@@ -1,4 +1,11 @@
### 0.6.1 (October 12, 2025)
# 0.6.2 (Next)
- Bundle all `node_modules` aside from native ones to reduce bundle and container size (closes [#331](https://github.com/tale/headplane/issues/331)).
- Allow conditionally compiling the SSH WASM integration when building (closes [#337](https://github.com/tale/headplane/issues/337)).
- Implemented the ability to customize the build with a custom script (see `./build.sh --help` for more information).
---
# 0.6.1 (October 12, 2025)
- **Headplane now supports connecting to machines via SSH in the web browser.**
- This is an experimental feature and requires the `integration.agent` section to be set up in the config file.
- This is built on top of a Go binary that runs in WebAssembly, using Xterm.js for the terminal interface.
@@ -27,7 +34,9 @@
- `oidc.scope` can be set to specify custom scopes (defaults to `openid email profile`).
- `oidc.extra_params` can be set to pass arbitrary query parameters to the authorization endpoint (closes [#197](https://github.com/tale/headplane/issues/197)).
### 0.6.0 (May 25, 2025)
---
# 0.6.0 (May 25, 2025)
- Headplane 0.6.0 now requires **Headscale 0.26.0** or newer.
- Breaking API changes with routes and pre auth keys are now supported (closes [#204](https://github.com/tale/headplane/issues/204)).
- Older versions of Headscale will not work with Headplane.
@@ -52,15 +61,21 @@
- Tighten `proc` integration logic by checking for the `headscale serve` command (via #[195](https://github.com/tale/headplane/pull/195)).
- Strip newlines in the OIDC `client_secret_path` file if provided (closes [#199](https://github.com/tale/headplane/issues/199)).
### 0.5.10 (April 4, 2025)
---
# 0.5.10 (April 4, 2025)
- Fix an issue where other preferences to skip onboarding affected every user.
### 0.5.9 (April 3, 2025)
---
# 0.5.9 (April 3, 2025)
- Filter out empty users from the pre-auth keys page which could possibly cause a crash with unmigrated users.
- OIDC users cannot be renamed, so that functionality has been disabled in the menu options.
- Suppress hydration errors for any fields with a date in it.
### 0.5.8 (April 3, 2025)
---
# 0.5.8 (April 3, 2025)
- You can now skip the onboarding page if desired.
- Added the UI to change user roles in the dashboard.
- Fixed an issue where integrations would throw instead of loading properly.
@@ -68,10 +83,14 @@
- Automatically create `/var/lib/headplane` in the Docker container (fixes [#166](https://github.com/tale/headplane/issues/166))
- OIDC logout with `disable_api_key_login` set to true will not automatically login again (fixes [#149](https://github.com/tale/headplane/issues/149))
### 0.5.7 (April 2, 2025)
---
# 0.5.7 (April 2, 2025)
- Hotfix an issue where assets aren't served under `/admin` or the prefix.
### 0.5.6 (April 2, 2025)
---
# 0.5.6 (April 2, 2025)
### IMPORTANT
> **PLEASE** update to this ASAP if you were using Google OIDC. This is because previously *ANY* accounts have admin access to your Tailnet if they discover the URL that Headplane is being hosted on. This new change enforces that new logins by default are not given any permissions. You will need to re-login to Headplane to generate an owner account and prevent unauthorized access.
@@ -91,10 +110,14 @@ If there are no users, the first user will be given an `owner` role which cannot
- Unknown values in the Headplane config no longer cause a crash.
- Fixed an issue that caused copied commands to have a random space (fixes [#161](https://github.com/tale/headplane/issues/161))
### 0.5.5 (March 18, 2025)
---
# 0.5.5 (March 18, 2025)
- Hotfix an issue that caused Headplane to crash if no agents are available
### 0.5.4 (March 18, 2025)
---
# 0.5.4 (March 18, 2025)
- Fixed a typo in the Kubernetes documentation
- Handle split and global DNS records not being set in the Headscale config (via [#129](https://github.com/tale/headplane/pull/129))
- Stop checking for the `mkey:` prefix on machine registration (via [#131](https://github.com/tale/headplane/pull/131))
@@ -108,20 +131,28 @@ If there are no users, the first user will be given an `owner` role which cannot
- You can now pass in an OIDC client secret through `oidc.client_secret_path` in the config (fixes [#126](https://github.com/tale/headplane/issues/126))
- Correctly handle differently localized number inputs (fixes [#125](https://github.com/tale/headplane/issues/125))
### 0.5.3 (March 1, 2025)
---
# 0.5.3 (March 1, 2025)
- Fixed an issue where Headplane expected the incorrect config value for OIDC scope (fixes [#111](https://github.com/tale/headplane/issues/111))
- Added an ARIA indicator for when an input is required and fixed the confirm buttons (fixed [#116](https://github.com/tale/headplane/issues/116))
- Fixed a typo in the docs that defaulted to `/var/run/docker.dock` for the Docker socket (via [#112](https://github.com/tale/headplane/pull/112))
### 0.5.2 (February 28, 2025)
---
# 0.5.2 (February 28, 2025)
- Hotfixed an issue where the server bundle got reloaded on each request
### 0.5.1 (February 28, 2025)
---
# 0.5.1 (February 28, 2025)
- Fixed an issue that caused the entire server to crash on start
- Fixed the published semver tags from Docker
- Fixed the Kubernetes integration not reading the config
### 0.5 (February 27, 2025)
---
# 0.5 (February 27, 2025)
> This release is a major overhaul and contains a significant breaking change.
> We now use a config file for all settings instead of environment variables.
> Please see [config.example.yaml](/config.example.yaml) for the new format.
@@ -136,10 +167,14 @@ If there are no users, the first user will be given an `owner` role which cannot
- Settings that were previously shared like `public_url` or `oidc` are now separate within Headplane/Headscale. This is a rather large breaking change but fixes cases where a user may choose to utilize Headscale OIDC for Tailscale but not for the Headplane UI.
- Deprecate the `latest` tag in Docker for explicit versioning and `edge` for nightly builds.
### 0.4.1 (January 18, 2025)
---
# 0.4.1 (January 18, 2025)
- Fixed an urgent issue where the OIDC redirect URI would mismatch.
### 0.4.0 (January 18, 2025)
---
# 0.4.0 (January 18, 2025)
- Switched from Remix.run to React-Router
- Fixed an issue where some config fields were marked as required even if they weren't (fixes [#66](https://github.com/tale/headplane/issues/66))
- Fixed an issue where the toasts would be obscured by the footer (fixes [#68](https://github.com/tale/headplane/issues/68))
@@ -152,29 +187,41 @@ If there are no users, the first user will be given an `owner` role which cannot
- Reusable Pre-Auth Keys no longer show expired when used (PR [#88](https://github.com/tale/headplane/pull/88))
- Tweaked some CSS issues in the UI
### 0.3.9 (December 6, 2024)
---
# 0.3.9 (December 6, 2024)
- Fixed a race condition bug in the OIDC validation code
### 0.3.8 (December 6, 2024)
---
# 0.3.8 (December 6, 2024)
- Added a little HTML footer to show the login page and link to a donation page.
- Allow creating pre-auth keys that expire past 90 days (fixes [#58](https://github.com/tale/headplane/issues/58))
- Validates OIDC config and ignores validation if specified via variables or Headscale config (fixes [#63](https://github.com/tale/headplane/issues/63))
### 0.3.7 (November 30, 2024)
---
# 0.3.7 (November 30, 2024)
- Allow customizing the OIDC token endpoint auth method via `OIDC_CLIENT_SECRET_METHOD` (fixes [#57](https://github.com/tale/headplane/issues/57))
- Added a `/healthz` endpoint for Kubernetes and other health checks (fixes [#59](https://github.com/tale/headplane/issues/59))
- Allow `HEADSCALE_PUBLIC_URL` to be set if `HEADSCALE_URL` points to a different internal address (fixes [#60](https://github.com/tale/headplane/issues/60))
- Fixed an issue where the copy machine registration command had a typo.
### 0.3.6 (November 20, 2024)
---
# 0.3.6 (November 20, 2024)
- Fixed an issue where select dropdowns would not scroll (fixes [#53](https://github.com/tale/headplane/issues/53))
- Added a button to copy the machine registration command to the clipboard (fixes [#52](https://github.com/tale/headplane/issues/52))
### 0.3.5 (November 8, 2024)
---
# 0.3.5 (November 8, 2024)
- Quickfix a bug where environment variables are ignored on the server.
- Remove a nagging error about missing cookie since that happens when signed out.
### 0.3.4 (November 7, 2024)
---
# 0.3.4 (November 7, 2024)
- Clicking on the machine name in the users page now takes you to the machine overview page.
- Completely rebuilt the production server to work better outside of Docker and be lighter. More specifically, we've switched from the `@remix-run/serve` package to our own custom built server.
- Fixed a bunch of silly issues introduced by me not typechecking the codebase.
@@ -184,70 +231,94 @@ If there are no users, the first user will be given an `owner` role which cannot
- Added full support for Exit Nodes in the UI and redesigned the machines page (fixes [#36](https://github.com/tale/headplane/issues/36))
- Added a basic check to see if the API keys passed via cookies are invalid.
### 0.3.3 (October 28, 2024)
---
# 0.3.3 (October 28, 2024)
- Added the ability to load a `.env` file from the PWD when `LOAD_ENV_FILE=true` is set as an environment variable.
- Fixed an issue where non-English languages could not create Pre-auth keys due to a localization error
- Improved ACL editor performance by switching back to CodeMirror 6
- Fixed an issue where editing the ACL policy would cause it to revert on the UI (fixes [#34](https://github.com/tale/headplane/issues/34))
- Updated to the next stable beta of the React 19 Compiler ([See More](https://react.dev/learn/react-compiler))
### 0.3.2 (October 11, 2024)
---
# 0.3.2 (October 11, 2024)
- Implement the ability to create and expire pre-auth keys (fixes [#22](https://github.com/tale/headplane/issues/22))
- Fix machine registration not working as expected (fixes [#27](https://github.com/tale/headplane/issues/27))
- Removed more references to usernames in MagicDNS hostnames (fixes [#35](https://github.com/tale/headplane/issues/35))
- Handle `null` values on machine expiry when using a database like PostgreSQL.
- Use `X-Forwarded-Proto` and `Host` headers for building the OIDC callback URL.
### 0.3.1 (October 3, 2024)
---
# 0.3.1 (October 3, 2024)
- Fixed the Docker integration to properly support custom socket paths. This regressed at some point previously.
- Allow you to register a machine using machine keys (`nodekey:...`) on the machines page.
- Added the option for debug logs with the `DEBUG=true` environment variable.
### 0.3.0 (September 25, 2024)
---
# 0.3.0 (September 25, 2024)
- Bumped the minimum supported version of Headscale to 0.23.
- Updated the UI to respect `dns.use_username_in_magic_dns`.
### 0.2.4 (August 24, 2024)
---
# 0.2.4 (August 24, 2024)
- Removed ACL management from the integration since Headscale 0.23-beta2 now supports it natively.
- Removed the `ACL_FILE` environment variable since it's no longer needed.
- Introduce a `COOKIE_SECURE=false` environment variable to disable HTTPS requirements for cookies.
- Fixed a bug where removing Split DNS configurations would crash the UI.
### 0.2.3 (August 23, 2024)
---
# 0.2.3 (August 23, 2024)
- Change the minimum required version of Headscale to 0.23-beta2
- Support the new API policy mode for Headscale 0.23-beta1
- Switch to the new DNS configuration in Headscale 0.23-beta2 (fixes [#29](https://github.com/tale/headplane/issues/29))
- If OIDC environment variables are defined, don't use configuration file values (fixes [#24](https://github.com/tale/headplane/issues/24))
### 0.2.2 (August 2, 2024)
---
# 0.2.2 (August 2, 2024)
- Added a proper Kubernetes integration which utilizes `shareProcessNamespace` for PIDs.
- Added a new logger utility that shows categories, levels, and timestamps.
- Reimplemented the integration system to be more resilient and log more information.
- Fixed an issue where the /proc integration found `undefined` PIDs.
### 0.2.1 (July 7, 2024)
---
# 0.2.1 (July 7, 2024)
- Added the ability to manage custom DNS records on your Tailnet.
- ACL tags for machines are now able to be changed via the machine menu.
- Fixed a bug where the ACL editor did not show the diffs correctly.
- Fixed an issue that stopped the "Discard changes" button in the ACL editor from working.
### 0.2.0 (June 23, 2024)
---
# 0.2.0 (June 23, 2024)
- Fix the dropdown options for machines not working on the machines page.
- Add an option to change the machine owner in the dropdown (aside from the users page).
### 0.1.9 (June 2, 2024)
---
# 0.1.9 (June 2, 2024)
- Switch to Monaco editor with proper HuJSON and YAML syntax highlighting.
- Utilize magic DNS hostnames for the machine overview page.
- Fixed the expiry issue once and for all.
- Add a nightly build with the `ghcr.io/tale/headplane:edge` tag
### 0.1.8 (June 2, 2024)
---
# 0.1.8 (June 2, 2024)
- Built basic functionality for the machine overview page (by machine ID).
- Possibly fixed an issue where expiry disabled machines' timestamps weren't handled correctly.
- Prevent users from being deleted if they still have ownership of machines.
- Fixed some type issues where `Date` was being used instead of `string` for timestamps.
### 0.1.7 (May 30, 2024)
---
# 0.1.7 (May 30, 2024)
- Added support for the `HEADSCALE_INTEGRATION` variable to allow for advanced integration without Docker.
- Fixed a bug where the `expiry` field on the Headscale configuration could cause crashes.
- Made the strict configuration loader more lenient to allow for more flexibility.
@@ -255,19 +326,25 @@ If there are no users, the first user will be given an `owner` role which cannot
- Headplane's context now only loads once at start instead of being lazy-loaded.
- Improved logging and error propagation so that it's easier to debug issues.
### 0.1.6 (May 22, 2024)
---
# 0.1.6 (May 22, 2024)
- Added experimental support for advanced integration without Docker.
- Fixed a crash where the Docker integration tried to use `process.env.API_KEY` instead of context.
- Fixed a crash where `ROOT_API_KEY` was not respected in the OIDC flow.
### 0.1.5 (May 20, 2024)
---
# 0.1.5 (May 20, 2024)
- Robust configuration handling with fallbacks based on the headscale source.
- Support for `client_secret_path` on configuration file based OIDC.
- `DISABLE_API_KEY_LOGIN` now works as expected (non 'true' values work).
- `API_KEY` is renamed to `ROOT_API_KEY` for better clarity (old variable still works).
- Fixed button responders not actually being invoked (should fix the ACL page).
### 0.1.4 (May 15, 2024)
---
# 0.1.4 (May 15, 2024)
- Users can now be created, renamed, and deleted on the users page.
- Machines can be dragged between users to change their ownership.
@@ -276,13 +353,17 @@ If there are no users, the first user will be given an `owner` role which cannot
- Upgrade to React 19 beta to take advantage of the compiler (may revert if it causes issues).
- Upgrade other dependencies
### 0.1.3 (May 4, 2024)
---
# 0.1.3 (May 4, 2024)
- Switched to a better icon set for the UI.
- Support stable scrollbar gutter if supported by the browser.
- Cleaned up the header which fixed a bug that could crash the entire application on fetch errors.
### 0.1.2 (May 1, 2024)
---
# 0.1.2 (May 1, 2024)
- Added support for renaming, expiring, removing, and managing the routes of a machine.
- Implemented an expiry check for machines which now reflect on the machine table.
+41 -38
View File
@@ -1,11 +1,7 @@
FROM --platform=$BUILDPLATFORM jdxcode/mise:latest AS mise-context
COPY mise.toml ./
RUN --mount=type=secret,id=gh_token,env=MISE_GITHUB_TOKEN mise install
FROM --platform=$BUILDPLATFORM golang:1.25.1 AS go-base
WORKDIR /run
FROM --platform=$BUILDPLATFORM mise-context AS go-build
WORKDIR /build/
COPY go.mod go.sum ./
COPY go.mod go.sum build.sh ./
RUN go mod download
COPY cmd/ ./cmd/
@@ -14,50 +10,57 @@ COPY internal/ ./internal/
ARG TARGETOS
ARG TARGETARCH
ARG IMAGE_TAG
RUN mkdir -p /build/app/ && \
GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 IMAGE_TAG=$IMAGE_TAG \
mise run wasm ::: agent ::: fake-shell
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 IMAGE_TAG=$IMAGE_TAG \
./build.sh --wasm --agent --fake-shell \
--wasm-output /bin/hp_ssh.wasm \
--agent-output /bin/hp_agent \
--fake-shell-output /bin/fake-sh
RUN chmod +x /build/build/hp_agent
RUN chmod +x /build/build/sh
RUN chmod +x /bin/hp_ssh.wasm
RUN chmod +x /bin/hp_agent
RUN chmod +x /bin/fake-sh
FROM --platform=$BUILDPLATFORM mise-context AS js-build
WORKDIR /build
COPY patches ./patches
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN mise trust
COPY --from=go-build /build/app/hp_ssh.wasm /build/app/hp_ssh.wasm
COPY --from=go-build /build/app/wasm_exec.js /build/app/wasm_exec.js
ARG IMAGE_TAG
RUN IMAGE_TAG=$IMAGE_TAG pnpm run build
# Folder needs to exist for later stages
RUN mkdir -p /var/lib/headplane/agent
FROM --platform=$BUILDPLATFORM node:22.16-slim AS js-base
WORKDIR /run
RUN corepack enable
COPY patches ./patches
COPY package.json pnpm-lock.yaml build.sh ./
COPY --from=go-base /bin/hp_ssh.wasm /run/app/hp_ssh.wasm
COPY --from=go-base /bin/wasm_exec.js /run/app/wasm_exec.js
RUN ./build.sh --app --app-install-only
COPY . .
RUN ./build.sh --app
FROM gcr.io/distroless/nodejs22-debian12:latest AS final
COPY --from=js-build /build/build/ /app/build/
COPY --from=js-build /build/drizzle /app/drizzle/
COPY --from=js-build /var/lib/headplane /var/lib/headplane
COPY --from=js-build /build/node_modules/ /app/node_modules/
COPY --from=go-build /build/build/hp_agent /usr/libexec/headplane/agent
COPY --from=js-base /run/build /app/build
COPY --from=js-base /run/drizzle /app/drizzle
COPY --from=js-base /run/node_modules /app/node_modules
COPY --from=go-base /bin/hp_agent /usr/libexec/headplane/agent
COPY --from=go-base /var/lib/headplane /var/lib/headplane
# Fake shell to inform the user that they should use the debug image
COPY --from=go-build /build/build/sh /bin/sh
COPY --from=go-build /build/build/sh /bin/bash
COPY --from=go-base /bin/fake-sh /bin/sh
COPY --from=go-base /bin/fake-sh /bin/bash
WORKDIR /app
CMD [ "/app/build/server/index.js" ]
FROM node:22-alpine AS debug-shell
RUN apk add --no-cache bash curl git
RUN apk add --no-cache bash curl
COPY --from=js-build /build/build/ /app/build/
COPY --from=js-build /build/drizzle /app/drizzle/
COPY --from=js-build /var/lib/headplane /var/lib/headplane
COPY --from=js-build /build/node_modules/ /app/node_modules/
COPY --from=go-build /build/build/hp_agent /usr/libexec/headplane/agent
COPY --from=js-base /run/build /app/build
COPY --from=js-base /run/drizzle /app/drizzle
COPY --from=js-base /run/node_modules /app/node_modules
COPY --from=go-base /bin/hp_agent /usr/libexec/headplane/agent
COPY --from=go-base /var/lib/headplane /var/lib/headplane
WORKDIR /app
CMD [ "node", "/app/build/server/index.js" ]
+1 -11
View File
@@ -32,17 +32,7 @@ These are some of the features that Headplane offers:
- Configurability for Headscale's settings
## Deployment
Headplane runs as a server-based web-application, meaning you'll need a server to run it.
It's available as a Docker image (recommended) or through a manual installation.
There are 2 ways to deploy Headplane:
- ### [Integrated Mode (Recommended)](/docs/Integrated-Mode.md)
Integrated mode unlocks all the features of Headplane and is the most
feature-complete deployment method. It communicates with Headscale directly.
- ### [Simple Mode](/docs/Simple-Mode.md)
Simple mode does not include the automatic management of DNS and Headplane
settings, requiring manual editing and reloading when making changes.
Refer to the [website](https://headplane.net) for detailed installation instructions.
## Versioning
Headplane uses [semantic versioning](https://semver.org/) for its releases (since v0.6.0).
-15
View File
@@ -1,15 +0,0 @@
FROM golang:1.23 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY agent/ ./agent
RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath \
-ldflags "-s -w" \
-o /app/hp_agent ./agent/cmd/hp_agent
FROM scratch
COPY --from=builder /app/hp_agent /hp_agent
ENTRYPOINT ["/hp_agent"]
+4 -2
View File
@@ -8,6 +8,7 @@ import {
useNavigation,
} from 'react-router';
import '@fontsource-variable/inter';
import { ExternalScripts } from 'remix-utils/external-scripts';
import { ErrorPopup } from '~/components/Error';
import ProgressBar from '~/components/ProgressBar';
import ToastProvider from '~/components/ToastProvider';
@@ -38,16 +39,17 @@ export function Layout({ children }: { readonly children: React.ReactNode }) {
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta content="width=device-width, initial-scale=1" name="viewport" />
<Meta />
<Links />
<link rel="icon" href="favicon.ico" />
<link href="favicon.ico" rel="icon" />
</head>
<body className="overscroll-none dark:bg-headplane-900 dark:text-headplane-50">
{children}
<ToastProvider queue={toastQueue} />
<ScrollRestoration />
<Scripts />
<ExternalScripts />
</body>
</html>
</LiveDataProvider>
+76 -41
View File
@@ -1,4 +1,5 @@
/** biome-ignore-all lint/correctness/noNestedComponentDefinitions: Wtf? */
import { faker } from '@faker-js/faker';
import { eq } from 'drizzle-orm';
import { Loader2 } from 'lucide-react';
@@ -6,17 +7,17 @@ import { useEffect, useState } from 'react';
import {
ActionFunctionArgs,
data,
LinksFunction,
LoaderFunctionArgs,
ShouldRevalidateFunction,
useLoaderData,
useSubmit,
} from 'react-router';
import wasm from '~/hp_ssh.wasm?url';
import { ExternalScriptsHandle } from 'remix-utils/external-scripts';
import { LoadContext } from '~/server';
import { EphemeralNodeInsert, ephemeralNodes } from '~/server/db/schema';
import { Machine, PreAuthKey, User } from '~/types';
import { useLiveData } from '~/utils/live-data';
import '~/wasm_exec';
import UserPrompt from './user-prompt';
import XTerm from './xterm.client';
@@ -28,6 +29,19 @@ export async function loader({
request,
context,
}: LoaderFunctionArgs<LoadContext>) {
const origin = new URL(request.url).origin;
const assets = ['/wasm_exec.js', '/hp_ssh.wasm'];
const missing: string[] = [];
for (const file of assets) {
const res = await fetch(`${origin}${file}`, { method: 'HEAD' });
if (!res.ok) missing.push(file);
}
if (missing.length > 0) {
throw data('WebSSH is not configured in this build.', 405);
}
if (!context.agents?.agentID()) {
throw data(
'WebSSH is only available with the Headplane agent integration',
@@ -200,6 +214,26 @@ export async function action({
.where(eq(ephemeralNodes.auth_key, authKey));
}
export const links: LinksFunction = () => [
{
rel: 'preload',
href: '/hp_ssh.wasm',
as: 'fetch',
type: 'application/wasm',
crossOrigin: 'anonymous',
},
];
export const handle: ExternalScriptsHandle = {
scripts: [
{
src: '/wasm_exec.js',
crossOrigin: 'anonymous',
preload: true,
},
],
};
export default function Page() {
const submit = useSubmit();
const { pause } = useLiveData();
@@ -215,46 +249,47 @@ export default function Page() {
pause();
const go = new Go(); // Go is defined by wasm_exec.js
WebAssembly.instantiateStreaming(fetch(wasm), go.importObject).then(
(value) => {
go.run(value.instance);
const handle = TsWasmNet(ipnDetails, {
NotifyState: (state) => {
console.log('State changed:', state);
if (state === 'Running') {
setIpn(handle);
}
},
NotifyNetMap: (netmap) => {
// Only set NodeKey if it is not already set and then
// also dispatch that to the backend to track the
// ephemeral node.
//
// We open an SSE connection to the backend
// so that when the connection is closed,
// the backend can delete the ephemeral node.
if (nodeKey === null) {
setNodeKey(netmap.NodeKey);
submit(
{
node_key: netmap.NodeKey,
auth_key: ipnDetails.PreAuthKey,
},
{ method: 'POST' },
);
}
},
NotifyBrowseToURL: (url) => {
console.log('Browse to URL:', url);
},
NotifyPanicRecover: (message) => {
console.error('Panic recover:', message);
},
});
WebAssembly.instantiateStreaming(
fetch('/hp_ssh.wasm'),
go.importObject,
).then((value) => {
go.run(value.instance);
const handle = TsWasmNet(ipnDetails, {
NotifyState: (state) => {
console.log('State changed:', state);
if (state === 'Running') {
setIpn(handle);
}
},
NotifyNetMap: (netmap) => {
// Only set NodeKey if it is not already set and then
// also dispatch that to the backend to track the
// ephemeral node.
//
// We open an SSE connection to the backend
// so that when the connection is closed,
// the backend can delete the ephemeral node.
if (nodeKey === null) {
setNodeKey(netmap.NodeKey);
submit(
{
node_key: netmap.NodeKey,
auth_key: ipnDetails.PreAuthKey,
},
{ method: 'POST' },
);
}
},
NotifyBrowseToURL: (url) => {
console.log('Browse to URL:', url);
},
NotifyPanicRecover: (message) => {
console.error('Panic recover:', message);
},
});
handle.Start();
},
);
handle.Start();
});
}, []);
if (!sshDetails.username) {
+9 -9
View File
@@ -1,9 +1,6 @@
import { Icon } from '@iconify/react';
import { ArrowRight } from 'lucide-react';
import { useEffect } from 'react';
import { GrApple } from 'react-icons/gr';
import { ImFinder } from 'react-icons/im';
import { MdAndroid } from 'react-icons/md';
import { PiTerminalFill, PiWindowsLogoFill } from 'react-icons/pi';
import { LoaderFunctionArgs, NavLink, useLoaderData } from 'react-router';
import Button from '~/components/Button';
import Card from '~/components/Card';
@@ -132,7 +129,7 @@ export default function Page() {
key="linux"
title={
<div className="flex items-center gap-1">
<PiTerminalFill className="ml-1 w-4" />
<Icon className="ml-1 w-4" icon="ion:terminal" />
<span>Linux</span>
</div>
}
@@ -163,7 +160,7 @@ export default function Page() {
key="windows"
title={
<div className="flex items-center gap-1">
<PiWindowsLogoFill className="ml-1 w-4" />
<Icon className="ml-1 w-4" icon="mdi:microsoft" />
<span>Windows</span>
</div>
}
@@ -186,7 +183,10 @@ export default function Page() {
key="macos"
title={
<div className="flex items-center gap-1">
<ImFinder className="ml-1 w-4" />
<Icon
className="ml-1 w-4"
icon="streamline-logos:mac-finder-logo-solid"
/>
<span>macOS</span>
</div>
}
@@ -218,7 +218,7 @@ export default function Page() {
key="ios"
title={
<div className="flex items-center gap-1">
<GrApple className="ml-1 w-4" />
<Icon className="ml-1 w-4" icon="grommet-icons:apple" />
<span>iOS</span>
</div>
}
@@ -241,7 +241,7 @@ export default function Page() {
key="android"
title={
<div className="flex items-center gap-1">
<MdAndroid className="ml-1 w-4" />
<Icon className="ml-1 w-4" icon="material-symbols:android" />
<span>Android</span>
</div>
}
+1 -1
View File
@@ -14,7 +14,7 @@ const databaseConfig = type({
type: '"sqlite" | "sqlite3"',
sqlite: {
path: 'string',
write_head_log: goBool.default(true),
write_ahead_log: goBool.default(true),
wal_autocheckpoint: 'number = 1000',
},
})
+10
View File
@@ -106,3 +106,13 @@ export default createHonoServer({
log.info('server', 'Running on %s:%s', info.address, info.port);
},
});
process.on('SIGINT', () => {
log.info('server', 'Received SIGINT, shutting down...');
process.exit(0);
});
process.on('SIGTERM', () => {
log.info('server', 'Received SIGTERM, shutting down...');
process.exit(0);
});
Executable
+166
View File
@@ -0,0 +1,166 @@
#!/bin/sh
# This is a general purpose build script for Headplane to be used across many
# different environments such as CI, Docker, Nix, and locally. For specific
# usage instructions, run `./build.sh --help`.
set -eu
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$ROOT_DIR" || exit 1
APP_DIR="$ROOT_DIR/app"
BUILD_DIR="$ROOT_DIR/build"
PUBLIC_DIR="$ROOT_DIR/public"
BUILD_WASM=0
BUILD_APP=0
BUILD_AGENT=0
BUILD_FAKE_SHELL=0
SKIP_PATH_CHECKS=0
SKIP_PNPM_PRUNE=0
APP_INSTALL_ONLY=0
WASM_OUTPUT="$PUBLIC_DIR/hp_ssh.wasm"
AGENT_OUTPUT="$BUILD_DIR/hp_agent"
FAKE_SHELL_OUTPUT="$BUILD_DIR/hp_fake_sh"
die() { echo "error: $*" >&2; exit 1; }
run() { echo ">> $*"; "$@"; }
while [ $# -gt 0 ]; do
case "$1" in
--wasm) BUILD_WASM=1 ;;
--app) BUILD_APP=1 ;;
--agent) BUILD_AGENT=1 ;;
--fake-shell) BUILD_FAKE_SHELL=1 ;;
--skip-path-checks) SKIP_PATH_CHECKS=1 ;;
--skip-pnpm-prune) SKIP_PNPM_PRUNE=1 ;;
--app-install-only) APP_INSTALL_ONLY=1 ;;
--wasm-output)
shift
[ $# -gt 0 ] || die "--wasm-output requires a path"
WASM_OUTPUT=$1
;;
--agent-output)
shift
[ $# -gt 0 ] || die "--agent-output requires a path"
AGENT_OUTPUT=$1
;;
--fake-shell-output)
shift
[ $# -gt 0 ] || die "--fake-shell-output requires a path"
FAKE_SHELL_OUTPUT=$1
;;
--help)
cat <<EOF
Usage: $0 [flags]
--wasm build wasm module
--app build react-router app
--agent build tailscale agent
--fake-shell build fake shell binary (for Docker)
--skip-path-checks skip safety checks (ie. checking PATH)
--skip-pnpm-prune skip pruning devDependencies from node_modules
--app-install-only only install app dependencies, skip build
--wasm-output <path> override wasm output path
--agent-output <path> override agent output path
--fake-shell-output <path> override fake shell output path
EOF
exit 0
;;
*)
die "unknown flag: $1"
;;
esac
shift
done
# By default build everything except for the fake shell
if [ "$BUILD_WASM" -eq 0 ] && [ "$BUILD_APP" -eq 0 ] && \
[ "$BUILD_AGENT" -eq 0 ] && [ "$BUILD_FAKE_SHELL" -eq 0 ]; then
BUILD_WASM=1
BUILD_APP=1
BUILD_AGENT=1
fi
if [ "$SKIP_PATH_CHECKS" -eq 0 ]; then
[ -d "$ROOT_DIR" ] || die "missing project root"
need_go=0
need_pnpm=0
[ "$BUILD_WASM" -eq 1 ] && need_go=1
[ "$BUILD_APP" -eq 1 ] && need_pnpm=1
[ "$BUILD_AGENT" -eq 1 ] && need_go=1
[ "$BUILD_FAKE_SHELL" -eq 1 ] && need_go=1
if [ $need_go -eq 1 ]; then
echo "==> Checking for Go toolchain"
command -v go >/dev/null 2>&1 || die "go not installed"
go version >/dev/null 2>&1 || die "go not working"
fi
if [ $need_pnpm -eq 1 ]; then
echo "==> Checking for pnpm"
command -v pnpm >/dev/null 2>&1 || die "pnpm not installed"
pnpm --version >/dev/null 2>&1 || die "pnpm not working"
fi
fi
build_wasm() {
echo "==> Building SSH WASM module → $WASM_OUTPUT"
mkdir -p "$(dirname "$WASM_OUTPUT")"
echo "// $(go version)" > "$(dirname "$WASM_OUTPUT")/wasm_exec.js"
# This depends on Go 1.23+ since the path is different in earlier versions
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
}
build_app() {
echo "==> Building React Router app → $BUILD_DIR"
[ -f "$WASM_OUTPUT" ] || echo "warning: Building without SSH WASM module"
pnpm install --frozen-lockfile
if [ "$APP_INSTALL_ONLY" -eq 1 ]; then
echo "==> Skipping app build (install only)"
return
fi
pnpm run build
if [ "$SKIP_PNPM_PRUNE" -eq 0 ]; then
echo "==> Pruning devDependencies from node_modules"
pnpm prune --prod
fi
}
build_agent() {
echo "==> Building Tailscale agent → $AGENT_OUTPUT"
mkdir -p "$(dirname "$AGENT_OUTPUT")"
go build -o "$AGENT_OUTPUT" ./cmd/hp_agent
}
build_fake_shell() {
[ -n "${IMAGE_TAG:-}" ] || die \
"\$IMAGE_TAG is required to build fake shell binary"
echo "==> Building fake shell binary → $FAKE_SHELL_OUTPUT"
mkdir -p "$(dirname "$FAKE_SHELL_OUTPUT")"
go build -ldflags="-s -w -X main.imageTag=${IMAGE_TAG}" \
-o "$FAKE_SHELL_OUTPUT" ./cmd/fake_sh
}
[ "$BUILD_FAKE_SHELL" = 1 ] && build_fake_shell
[ "$BUILD_WASM" = 1 ] && build_wasm
[ "$BUILD_APP" = 1 ] && build_app
[ "$BUILD_AGENT" = 1 ] && build_agent
[ "$BUILD_FAKE_SHELL" = 1 ] && build_fake_shell
echo "✅ Build complete."
+32 -11
View File
@@ -3,33 +3,54 @@ import { defineConfig } from 'vitepress';
export default defineConfig({
title: 'Headplane',
description: 'The missing dashboard for Headscale',
ignoreDeadLinks: ['/docs/Integrated-Mode', '/docs/Simple-Mode'],
cleanUrls: true,
head: [['link', { rel: 'icon', href: '/favicon.ico' }]],
themeConfig: {
logo: '/logo.svg',
nav: [
{ text: 'Home', link: '/' },
{ text: 'Sponsor Headplane', link: 'https://github.com/sponsors/tale' },
{ text: 'Changelog', link: '/CHANGELOG' },
],
sidebar: [
{
text: 'Chapters',
text: 'Getting Started',
items: [
{ text: 'Getting Started', link: '/README' },
{ text: 'Configuration', link: '/Configuration' },
{ text: 'Bare-Metal Mode', link: '/Bare-Metal' },
{ text: 'Integrated Mode', link: '/Integrated-Mode' },
{ text: 'Simple Mode', link: '/Simple-Mode' },
{ text: 'What is Headplane?', link: '/introduction' },
{
text: 'Installation',
link: '/install',
items: [
{ text: 'Limited Mode', link: '/install/limited-mode' },
{ text: 'Native Mode', link: '/install/native-mode' },
{ text: 'Docker', link: '/install/docker' },
],
},
{ text: 'Configuration', link: '/configuration' },
{ text: 'Nix', link: '/Nix' },
{ text: 'NixOS', link: '/NixOS-options' },
{ text: 'Security', link: '/SECURITY' },
{ text: 'Contributing', link: '/CONTRIBUTING' },
{ text: 'Changelog', link: '/CHANGELOG' },
{
text: 'Features',
items: [
{ text: 'Single Sign-On (SSO)', link: '/features/sso' },
{ text: 'Headplane Agent / SSH', link: '/features/agent' },
],
},
{
text: 'Development',
collapsed: true,
items: [
{ text: 'Contributing', link: '/CONTRIBUTING' },
{ text: 'Security', link: '/SECURITY' },
],
},
],
},
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/tale/headplane' },
{ icon: 'githubsponsors', link: 'https://github.com/sponsors/tale' },
{ icon: 'kofi', link: 'https://ko-fi.com/atale' },
],
lastUpdated: {
+17
View File
@@ -0,0 +1,17 @@
html.dark .light-only {
display: none !important;
}
html:not(.dark) .dark-only {
display: none !important;
}
figure {
padding: 1em;
}
figcaption {
text-align: center;
font-size: 0.9em;
margin-top: 0.5em;
}
+4
View File
@@ -0,0 +1,4 @@
import DefaultTheme from 'vitepress/theme';
import './custom.css';
export default DefaultTheme;
-92
View File
@@ -1,92 +0,0 @@
# Bare-Metal Mode
Bare-Metal mode is the most flexible way to deploy Headplane. It allows you to
run Headplane on any system without the need for Docker or any other container
runtime. This is not recommended, but I understand that everyone has
different needs.
> It works with both the **Simple** and **Integrated** deployment modes. Refer
> to the section below for instructions on configuring Integrated mode.
## Deployment
Requirements:
- Headscale 0.26 or newer (already deployed)
- Node.js **22.16** LTS or newer
- [PNPM](https://pnpm.io/installation) 10.x
- A finished configuration file (config.yaml)
Before installing Headplane, ensure that `/var/lib/headplane` exists and is
writable by the user that will run the Headplane service. You can create this
directory with the following command:
```sh
sudo mkdir -p /var/lib/headplane
# Replace headplane:headplane with the appropriate user and group if not root.
sudo chown -R headplane:headplane /var/lib/headplane
```
Clone the Headplane repository, install dependencies, and build the project:
```sh
git clone https://github.com/tale/headplane
cd headplane
git checkout v0.6.0 # Or whatever tag you want to use
pnpm install
pnpm build
```
## Running Headplane
You can start headplane through `pnpm start` or `node build/headplane/server.js`.
Headplane expects the `build` directory to be present when running the server.
The structure of this folder is very important and should not be tampered with.
### Integrated Mode
Since you are running Headplane in Bare-Metal, you most likely also are running
Headscale in Bare-Metal. Refer to the [**Integrated Mode**](./Integrated-Mode.md)
guide for instructions on setting up the integrated mode in Native Linux (/proc).
### Changing the Admin Path
Since you are building Headplane yourself, you are able to configure the admin
path to be anything you want. When running `pnpm build`, you can pass the
`__INTERNAL_PREFIX` environment variable to change the admin path. For example:
```sh
__INTERNAL_PREFIX=/admin2 pnpm build
```
Just keep in mind that the admin path is not configurable at runtime, so you
will need to rebuild the project if you want to change it. Also, anything aside
from `/admin` is not officially supported and could break in future versions.
> Refer to the [**Configuration**](./Configuration.md) guide for help with
> setting up your `config.yaml` file to the appropriate values.
### Systemd Unit
Here is an example of a systemd unit file that you can use to manage the
Headplane service:
```ini
[Unit]
Description=Headplane
# Decomment the following line if running on bare metal with integrated mode (/proc integration)
# PartOf=headscale.service
[Service]
Type=simple
User=headplane
Group=headplane
WorkingDirectory=/path/to/headplane
ExecStart=/usr/bin/node /path/to/headplane/build/server/index.js
Restart=always
[Install]
WantedBy=multi-user.target
```
You will need to replace `/path/to/headplane` with the actual path to the
Headplane repository on your system. Save this file as `headplane.service` in
`/etc/systemd/system/` and run `systemctl enable headplane` to enable the service.
Other fields may also need some configuration, as this unit expects a user and a
group called `headplane` to exist on the system. You can change these values to
match your system's configuration.
-199
View File
@@ -1,199 +0,0 @@
# Integrated Mode
<picture>
<source
media="(prefers-color-scheme: dark)"
srcset="./assets/dns-dark.png"
>
<source
media="(prefers-color-scheme: light)"
srcset="./assets/dns-light.png"
>
<img
alt="Integration Preview"
src="./assets/dns-dark.png"
>
</picture>
Integrated mode is a deployment method that allows you to deploy Headplane with
automatic management of DNS and Headplane settings. This is the recommended
deployment method for most users, as it provides a more feature-complete
experience.
## Deployment
> If you are not looking to deploy with Docker, follow the [**Bare-Metal**](./Bare-Metal.md) deployment guide.
> Refer to the `Integrated Mode` section at the bottom for caveats.
Requirements:
- Docker and Docker Compose
- Headscale 0.26 or newer
- A finished configuration file (config.yaml)
Here is what a sample Docker Compose deployment would look like:
```yaml
services:
headplane:
# I recommend you pin the version to a specific release
image: ghcr.io/tale/headplane:0.6.0
container_name: headplane
restart: unless-stopped
ports:
- '3000:3000'
volumes:
- './config.yaml:/etc/headplane/config.yaml'
# This should match headscale.config_path in your config.yaml
- './headscale-config/config.yaml:/etc/headscale/config.yaml'
# If using dns.extra_records in Headscale (recommended), this should
# match the headscale.dns_records_path in your config.yaml
- './headscale-config/dns_records.json:/etc/headscale/dns_records.json'
# Headplane stores its data in this directory
- './headplane-data:/var/lib/headplane'
# If you are using the Docker integration, mount the Docker socket
- '/var/run/docker.sock:/var/run/docker.sock:ro'
headscale:
image: headscale/headscale:0.26.0
container_name: headscale
restart: unless-stopped
command: serve
labels:
# This is needed for Headplane to find it and signal it
me.tale.headplane.target: headscale
ports:
- '8080:8080'
volumes:
- './headscale-data:/var/lib/headscale'
- './headscale-config:/etc/headscale'
```
This will result in the Headplane UI being available at the `/admin` path of the
server you deployed it on. The `/admin` path is currently not configurable unless
you build the container yourself or run Headplane in Bare-Metal mode.
> Refer to the [**Configuration**](./Configuration.md) guide for help with
> setting up your `config.yaml` file to the appropriate values.
## Docker Integration
The Docker integration is the easiest to set up, as it only requires mounting the
Docker socket into the container along with some basic configuration. Headplane
uses Docker labels to discover the Headscale container. As long as Headplane has
access to the Docker socket and can identify the Headscale container—either by
label or name—it will automatically propagate configuration and DNS changes to
Headscale without any additional setup. Alternatively, instead of using a label
to dynamically determine the container name, it is possible to directly specify
the container name.
## Native Linux (/proc) Integration
The `proc` integration is used when you are running Headscale and Headplane on
non-Docker environments. Headplane will attempt to locate the Headscale process
PID through the `/proc` filesystem and communicate with it directly. In order for
this to work, the Headplane process must have permission to do the following:
- Read the `/proc` filesystem
- Send signals to the Headscale process (`SIGTERM`)
The best way to ensure this is to run Headplane as the same user as Headscale
(or optionally just run them both as `root`). Due to the way the integration is
currently configured, Headplane will not re-check the Headscale process PID if
it changes. This means that if you restart Headscale manually, you will need to
restart Headplane as well.
## Kubernetes Integration
The Kubernetes integration is the most complex to setup, as it requires a
service account with the appropriate permissions to be created. The service
account must have the following permissions and looks like this:
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: headplane-agent
namespace: default # Adjust namespace as needed
rules:
- apiGroups: ['']
resources: ['pods']
verbs: ['get', 'list']
- apiGroups: ['apps']
resources: ['deployments']
verbs: ['get', 'list']
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: headplane-agent
namespace: default # Adjust namespace as needed
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: headplane-agent
subjects:
- kind: ServiceAccount
name: default # If you use a different service account, change this
namespace: default # Adjust namespace as needed
```
To successfully deploy Headplane in Kubernetes, you will need to run both the
Headplane and Headscale containers in the same pod. This is because Headplane
needs access to Headscale's PID in order to communicate with it. Here is an
example, note the **`shareProcessNamespace: true`** field:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: headplane
namespace: default # Adjust namespace as needed
labels:
app: headplane
spec:
replicas: 1
selector:
matchLabels:
app: headplane
template:
metadata:
labels:
app: headplane
spec:
shareProcessNamespace: true
serviceAccountName: default
containers:
- name: headplane
image: ghcr.io/tale/headplane:0.6.0
env:
# Set these if the pod name for Headscale is not static
# We will use the downward API to get the pod name instead
- name: HEADPLANE_LOAD_ENV_OVERRIDES
value: 'true'
- name: 'HEADPLANE_INTEGRATION__KUBERNETES__POD_NAME'
valueFrom:
fieldRef:
fieldPath: metadata.name
volumeMounts:
- name: headscale-config
mountPath: /etc/headscale
- name: headplane-data
mountPath: /var/lib/headplane
- name: headscale
image: headscale/headscale:0.26.0
command: ['serve']
volumeMounts:
- name: headscale-data
mountPath: /var/lib/headscale
- name: headscale-config
mountPath: /etc/headscale
volumes:
- name: headscale-data
persistentVolumeClaim:
claimName: headscale-data
- name: headplane-data
persistentVolumeClaim:
claimName: headplane-data
- name: headscale-config
persistentVolumeClaim:
claimName: headscale-config
```
-1
View File
@@ -1 +0,0 @@
./../README.md
-37
View File
@@ -1,37 +0,0 @@
# Simple Mode
Simple mode enables you to quickly deploy Headplane and is recommended for any
testing or simple environments. It does not include the automatic management of
DNS and Headplane settings, requiring manual editing and reloading when making
changes. If you're looking for a more feature-complete deployment method, check
out [**Integrated Mode**](./Integrated-Mode.md).
## Deployment
> If you are not looking to deploy with Docker, follow the [**Bare-Metal**](./Bare-Metal.md) deployment guide.
Requirements:
- Docker and Docker Compose
- Headscale 0.26 or newer (already deployed)
- A finished configuration file (config.yaml)
Here is what a sample Docker Compose deployment would look like:
```yaml
services:
headplane:
# I recommend you pin the version to a specific release
image: ghcr.io/tale/headplane:0.6.0
container_name: headplane
restart: unless-stopped
ports:
- '3000:3000'
volumes:
- './config.yaml:/etc/headplane/config.yaml'
- './headplane-data:/var/lib/headplane'
```
This will result in the Headplane UI being available at the `/admin` path of the
server you deployed it on. The `/admin` path is currently not configurable unless
you build the container yourself or run Headplane in Bare-Metal mode.
> Refer to the [**Configuration**](./Configuration.md) guide for help with
> setting up your `config.yaml` file to the appropriate values.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

+67
View File
@@ -0,0 +1,67 @@
---
title: Headplane Agent
description: Configure the Headplane Agent for enhanced functionality.
---
# Headplane Agent
<figure>
<img src="../assets/ssh.png" />
<figcaption>SSH access via the browser</figcaption>
</figure>
The Headplane Agent is an optional component that can be enabled on your
Headplane instance in order to unlock additional features such as remote
web-based SSH and detailed machine information.
## Configuration
To enable the Headplane Agent, you'll need to modify the following fields in
your Headplane configuration file. For more information on configuring Headplane
please refer to the
[example configuration](https://github.com/tale/headplane/blob/main/config.example.yaml)
for details.
| Field | Description |
|---------------------|--------------------------------------------------------|
| **`integration.agent.enabled`** | Set to `true` to enable the agent. |
| **`integration.agent.pre_authkey`** | A reusable pre-auth-key (see below). |
| `integration.agent.host_name` | *Optional*. Host name to register as. |
| `integration.agent.cache_path` | *Optional*. Cache path for the agent. |
| `integration.agent.work_dir` | *Optional*. Working directory for the agent. |
## Native Mode Configuration
Once you've built Headplane locally, there will be a binary in the `./build`
folder called `hp_agent`. Please move this binary to
`/usr/libexec/headplane/agent` and ensure that it is executable.
::: tip
If for some reason you cannot move the binary to the intended location, you can
define **`integration.agent.executable_path`** in your Headplane configuration
file to point to the correct location of the agent binary.
:::
The agent will also use `/var/lib/headplane/agent` as its data directory by
default. You can change this location by defining
**`integration.agent.work_dir`** in your Headplane configuration file. Ensure
that the specified directory exists and is writable by the user running
Headplane.
## Usage
<figure>
<img class="dark-only" src="../assets/preview-dark.png" />
<img class="light-only" src="../assets/preview-light.png" />
<figcaption>Headplane Dashboard</figcaption>
</figure>
After enabling and configuring the Headplane Agent, restart your Headplane
instance. You should now see additional options in the UI, such as host
information about each node and the ability to open SSH sessions directly from
the browser if the nodes have Tailscale SSH enabled.
<figure>
<img class="dark-only" src="../assets/machine-dark.png" />
<img class="light-only" src="../assets/machine-light.png" />
<figcaption>Machine page</figcaption>
</figure>
+14
View File
@@ -0,0 +1,14 @@
---
title: Single Sign-On (SSO)
description: Configure Single Sign-On (SSO) authentication for Headplane.
---
# Single Sign-On (SSO)
<figure>
<img class="dark-only" src="../assets/sso-dark.png" />
<img class="light-only" src="../assets/sso-light.png" />
<figcaption>SSO Configuration Page</figcaption>
</figure>
TODO
+25 -12
View File
@@ -3,20 +3,33 @@
layout: home
hero:
name: "A feature-complete Web UI for Headscale"
text: "Documentation"
tagline: "A feature-complete Web UI for Headscale"
name: "Headplane"
tagline: "A full-featured admin interface for Headscale"
image:
src: "/logo.svg"
alt: "Headplane"
actions:
- theme: brand
text: Getting Started
link: /README
- text: "Getting Started"
link: "/introduction"
- text: "Installation"
link: "/install"
theme: "alt"
- text: "GitHub"
link: "https://github.com/tale/headplane"
theme: "alt"
features:
- title: Web SSH
details: Connect to machines directly from your browser
- title: OIDC
details: Login through your favourite SSO provider
- title: And many more
details: Setup ACLs, DNS, pre-auth keys, etc.
- title: "Remote web-based SSH"
details: "Connect to and manage machines via SSH directly through your web browser"
icon: "🌐"
- title: "Single-sign-on"
details: "Bring your own auth via OpenID Connect, including popular solutions such as Google Workspace, Azure/Entra, Okta, etc."
icon: "👥"
- title: "Detailed Tailnet Overview"
details: "Get access to detailed host and network information from each device residing within your Tailnet"
icon: "🔎"
- title: "Configure Headscale Settings"
details: "Manage settings hidden behind Headscale configuration such as DNS, networking, auth controls, etc."
icon: "📝"
---
+262
View File
@@ -0,0 +1,262 @@
---
title: Docker
description: Install Headplane with Docker.
outline: [2, 3]
---
# Docker Installation
::: tip
If you are not looking to deploy with Docker, follow the
[**Native Mode**](./native-mode.md) deployment guide.
:::
The recommended way to deploy Headplane is through Docker. This method is quick,
easy, and works in most environments. It requires that Headscale is also running
with Docker.
## Prerequisites
- Docker and Docker Compose
- Headscale version 0.26.0 or later installed and running
- A [completed configuration file](/index.md#configuration) for Headplane.
## Installation
Running Headplane in with Docker is as simple as applying 1 compose file:
```yaml
services:
headplane:
image: ghcr.io/tale/headplane:latest
container_name: headplane
restart: unless-stopped
ports:
- '3000:3000'
volumes:
- './config.yaml:/etc/headplane/config.yaml'
- './headplane-data:/var/lib/headplane'
```
It's important to mount your configuration file and also provide a persistent
storage location for Headplane to store its own data. You can also change the
port mapping if you want to run it on a different port.
## Accessing Headplane
After starting the container, you can access the Headplane web interface by
navigating to `http://localhost:3000/admin` in your web browser (replace
`localhost` with your server's IP address or domain name if not running locally).
In order to log in, you'll need to supply a Headscale API key. You can create
one by running the following command within your Headscale environment:
```bash
# You may want to tweak the expiration duration as needed
headscale apikeys create --expiration 90d
```
## Enabling advanced features
You've technically completed the installation, but read on if you would like
to enable advanced features like the ability to edit network settings from the
UI or remote SSH from the browser.
### Network Management
Network management allows you to configure Tailnet settings such as DNS servers,
custom A records, the tailnet domain name, and MagicDNS from the Headplane UI.
#### Prerequisites
Network management (and other configurable Headscale features) requires that
Headplane and Headscale both run together in the same Docker machine. This is
because Headplane needs the following permissions:
- Access to read and write the Head**scale** configuration file through a shared
volume used by both Headscale and Headplane.
- Access to the Docker socket (usually `/var/run/docker.sock`, you may also use
a proxy such as [Tecnativa/docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy)).
#### Configuration
First you'll need to run both Headscale and Headplane in the same Docker
environment. Here is an example `compose.yaml` file that accomplishes this:
```yaml
services:
headplane:
image: ghcr.io/tale/headplane:latest
container_name: headplane
restart: unless-stopped
ports:
- '3000:3000'
volumes:
# Same as before
- '/path/to/your/config.yaml:/etc/headplane/config.yaml'
- '/path/to/data/storage:/var/lib/headplane'
# A shared path to the Headscale config file. It is important that the
# path you mount this on matches `headscale.config_path` in your
# Headplane config.yaml file.
- '/path/to/headscale/config.yaml:/etc/headscale/config.yaml'
# If you are using dns.extra_records in Headscale (recommended), you
# should also mount that file here so Headplane can read and write it.
# Ensure that the path matches `headscale.dns_records_path` in your
# Headplane config.yaml file.
- '/path/to/headscale/dns_records.json:/etc/headscale/dns_records.json'
# Read-only access to the Docker socket (or a proxy)
- '/var/run/docker.sock:/var/run/docker.sock:ro'
headscale:
image: headscale/headscale:0.26.0
container_name: headscale
restart: unless-stopped
command: serve
labels:
# This label is absolutely necessary to help Headplane find Headscale.
me.tale.headplane.target: headscale
ports:
- '8080:8080'
volumes:
# Notice how these are on the exact same path as the host for both
# Headscale and Headplane! This is very important.
- '/path/to/headscale/config.yaml:/etc/headscale/config.yaml'
- '/path/to/headscale/dns_records.json:/etc/headscale/dns_records.json'
- '/path/to/headscale/data/storage:/var/lib/headscale'
```
::: info
With some effort, you can technically run Headscale and Headplane in separate
Docker hosts and remotely connect to a Docker daemon. This is an advanced setup
that is not covered in this documentation. Refer to the
[example configuration](https://github.com/tale/headplane/blob/main/config.example.yaml)
for more details on setting it up.
:::
You'll also need to enable a few fields in your Headplane configuration file:
| Field | Description |
|---------------------|--------------------------------------------------------|
| **`integration.docker.enabled`** | Set to `true` to enable Docker integration. |
| **`headscale.config_path`** | Path to your Head**scale** configuration file within the container (e.g., `/etc/headscale/config.yaml`). |
| `headscale.dns_records_path` | *Optional*. Refer to the [example configuration](https://github.com/tale/headplane/blob/main/config.example.yaml) for details. |
With these settings in place, restart Headplane. You should now see additional
options in the UI navbar such as "DNS" and "Settings" where you can manage your
Tailnet configuration.
### Remote Web SSH
Remote Web SSH allows you to open a terminal session to your Tailscale nodes
directly from the Headplane web interface via
[Tailscale SSH](https://tailscale.com/kb/1193/tailscale-ssh). This feature
requires that Tailscale SSH is running on your nodes (done via
`tailscale up --ssh`).
This feature uses the [Headplane Agent](../features/agent.md) to facilitate the
SSH connections. Refer to the [Agent documentation](../features/agent.md) for
setup instructions.
### Single Sign-On (SSO)
Single Sign-On (SSO) authentication allows users to log in to Headplane using
external identity providers such as Google, GitHub, or any provider that
supports OpenID Connect (OIDC).
To get started with SSO, refer to the [SSO documentation](../features/sso.md)
for detailed setup instructions.
## Reverse Proxying
You *should* run Headplane behind a reverse proxy such as Nginx or Caddy in
production. Additionally, putting Headscale beind the reverse proxy allows
you to access both services via the same domain and TLS certificate.
#### Configuration
Headscale supports integrating with
[several reverse proxies](https://headscale.net/stable/ref/integration/reverse-proxy/)
such as Nginx, Caddy, Apache, etc. Deploying Headplane is as simple as adding
a handler to route any requests to `/admin` to the Headplane service. Refer
to the Traefik example below for a reference configuration. A similar setup via
Nginx without Docker is available in the
[Native Mode](./native-mode.md#reverse-proxying) installation documentation.
#### Example Traefik Configuration
The following configuration will set up Traefik to proxy all Headscale requests
on `headscale.example.com` and serve the Headplane UI under the `/admin` path.
This is identical to how Tailscale's own admin console is served.
Keep in mind this won't work on its own as you'll need to configure Traefik
and TLS certificates as needed. This is just a snippet to show how to configure
the routing for Headplane and Headscale.
```yaml
services:
# Same as before
headplane:
image: ghcr.io/tale/headplane:latest
container_name: headplane
restart: unless-stopped
ports:
- '3000:3000'
volumes:
- '/path/to/your/config.yaml:/etc/headplane/config.yaml'
- '/path/to/data/storage:/var/lib/headplane'
- '/path/to/headscale/config.yaml:/etc/headscale/config.yaml'
- '/path/to/headscale/dns_records.json:/etc/headscale/dns_records.json'
- '/var/run/docker.sock:/var/run/docker.sock:ro'
labels:
# Expose the admin UI at /admin
- 'traefik.enable=true'
- 'traefik.http.routers.headplane.rule=Host(`headscale.example.com`) && PathPrefix(`/admin`)'
- 'traefik.http.routers.headplane.entrypoints=websecure'
- 'traefik.http.routers.headplane.tls=true'
headscale:
image: headscale/headscale:0.26.0
container_name: headscale
restart: unless-stopped
command: serve
ports:
- '8080:8080'
volumes:
- '/path/to/headscale/config.yaml:/etc/headscale/config.yaml'
- '/path/to/headscale/dns_records.json:/etc/headscale/dns_records.json'
- '/path/to/headscale/data/storage:/var/lib/headscale'
labels:
- 'me.tale.headplane.target=headscale'
# Traefik labels to expose Headscale at headscale.example.com
- 'traefik.enable=true'
- 'traefik.http.routers.headscale.rule=Host(`headscale.example.com`)'
- 'traefik.http.routers.headscale.entrypoints=websecure'
- 'traefik.http.routers.headscale.tls=true'
# This middleware is essential to ensuring Headplane works correctly
- 'traefik.http.routers.headscale.middlewares=cors'
- 'traefik.http.middlewares.cors.headers.accesscontrolallowheaders=*'
- 'traefik.http.middlewares.cors.headers.accesscontrolallowmethods=GET,POST,PUT'
- 'traefik.http.middlewares.cors.headers.accesscontrolalloworiginlist=https://headscale.example.com'
- 'traefik.http.middlewares.cors.headers.accesscontrolmaxage=100'
- 'traefik.http.middlewares.cors.headers.addvaryheader=true'
# If you would optionally like to automatically redirect / to /admin
- 'traefik.http.routers.rewrite.rule=Host(`headscale.example.com`) && Path(`/`)'
- 'traefik.http.routers.rewrite.service=headscale'
- 'traefik.http.routers.rewrite.middlewares=rewrite'
- 'traefik.http.middlewares.rewrite.addprefix.prefix=/admin'
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
ports:
- '80:80'
- '443:443'
volumes:
# Example volumes/setup, please configure Traefik as needed
- '/var/run/docker.sock:/var/run/docker.sock:ro'
- '/path/to/certs/storage:/certs'
```
+51
View File
@@ -0,0 +1,51 @@
---
title: Installation
description: Get started with Headplane.
outline: [2, 3]
---
# Installation
Headplane is designed to be deployed within several different environments to
ensure that it can seamlessly integrate into your existing infrastructure. First
set up your configuration file and then pick the installation method that best
suits your needs.
## Configuration
Headplane requires a configuration file to operate. A
[sample file](https://github.com/tale/headplane/blob/main/config.example.yaml)
is available to use as a starting point. Some of the important fields include:
| Field | Description |
|---------------------|--------------------------------------------------------|
| **`headscale.url`** | Point to your Headscale server (e.g., `http://headscale.example.com` or `http://headscale:8080` in Docker). |
| **`server.cookie_secret`** | Used to encrypt cookies. You can generate a random string using a command like `openssl rand -base64 32`. |
| **`server.data_path`** | Just a path to keep in mind, especially if you're using Docker. |
The configuration file is rather complicated and has many more options. Refer to
the [Configuration](../configuration.md) guide for a detailed explanation of all
the available options, as well as guidance on securely setting up your values
through secret path options and environment variables.
## Deployment Methods
Headplane can be deployed in several different ways, each with its own set of
advantages and trade-offs. Choose the method that best fits your needs:
### [Docker](./docker.md): Fast and easy deployment using Docker
- Recommended for most users due to its simplicity and ease of use.
- Allows for advanced features like network management and remote web SSH.
- Requires Docker and Docker Compose to be installed.
---
### [Native Mode](./native-mode.md): Direct installation on a server
- Suitable for users who prefer not to use Docker.
- Allows for advanced features like network management and remote web SSH.
- Requires manual setup of dependencies and environment.
---
### [Limited Mode](./limited-mode.md): Quick and easy deployment with minimal features
- Ideal for testing or simple environments and not intended for production use.
- Lacks any advanced functionality or integrations such as network management
or remote web SSH.
+86
View File
@@ -0,0 +1,86 @@
---
title: Limited Mode
description: Install Headplane in Limited Mode.
---
# Limited Mode
::: warning
**Limited Mode is not recommended for production use.**
Please consider using one of the other installation methods for a production
deployment. Limited mode lacks advanced features such as network management,
remote web SSH, and more.
:::
Limited Mode is good for users who want to test out the *basic* functionality
provided by Headplane. It only interacts with the Headplane API and lacks all
advanced features, making it suitable for local testing and development.
## Prerequisites
- Docker (and optionally Docker Compose)
- Headscale version 0.26.0 or later installed and running
- A [completed configuration file](/index.md#configuration) for Headplane.
## Installation
::: tip
If you want to test Limited Mode without Docker, you can follow the
[Native Mode](./native-mode.md) installation guide and simply avoid setting
up any of the advanced features.
:::
Running Headplane in Limited Mode is as simple as running 1 command:
```bash
docker run -d \
-p 3000:3000 \
-v /path/to/your/config.yaml:/etc/headplane/config.yaml \
-v /path/to/data/storage:/var/lib/headplane \
--name headplane
--restart unless-stopped
ghcr.io/tale/headplane:latest
```
It's important to mount your configuration file and also provide a persistent
storage location for Headplane to store its own data. You can also change the
port mapping if you want to run it on a different port.
### Optional: Docker Compose
If you prefer using Docker Compose, here is a minimal example of a
`compose.yaml` file that runs Headplane in Limited Mode:
```yaml
services:
headplane:
image: ghcr.io/tale/headplane:latest
container_name: headplane
restart: unless-stopped
ports:
- '3000:3000'
volumes:
- '/path/to/your/config.yaml:/etc/headplane/config.yaml'
- '/path/to/data/storage:/var/lib/headplane'
```
## Accessing Headplane
After starting the container, you can access the Headplane web interface by
navigating to `http://localhost:3000/admin` in your web browser (replace
`localhost` with your server's IP address or domain name if not running locally).
In order to log in, you'll need to supply a Headscale API key. You can create
one by running the following command within your Headscale environment:
```bash
# You may want to tweak the expiration duration as needed
headscale apikeys create --expiration 90d
```
Limited Mode is intended for testing and development purposes, so please avoid
using it in a production environment. For production deployments, consider using
one of the other installation methods that will provide both the advanced
features of Headplane and a more robust deployment.
Limited Mode also technically supports
[Single Sign-On (SSO) authentication](../features/sso.md), but some parts of it
may not work as expected. For a full-featured experience with SSO, please use
one of the other installation methods.
+245
View File
@@ -0,0 +1,245 @@
---
title: Native Mode
description: Install Headplane without Docker.
outline: [2, 3]
---
# Native Mode
::: tip
If you are looking to deploy with Docker, follow the
[**Docker**](./docker.md) deployment guide.
:::
Headplane can be installed and run directly on your host system without the need
for Docker. This method is suitable for users who already run Headscale natively
or prefer to avoid containers.
## Prerequisites
- A Linux-based operating system (e.g, Ubuntu, Debian, CentOS, Fedora)
- Go version 1.25.1 installed (only needed to build Headplane)
- Node.js version 22.16.x and [pnpm](https://pnpm.io/) version 10.4.x installed
- Headscale version 0.26.0 or later installed and running
- A [completed configuration file](./index.md#configuration) for Headplane.
Before building and running Headplane, ensure that the directory defined in
`server.data_path` in your configuration exists and is writable by the user who
will run Headplane.
```bash
# Adjust as needed and set a custom user if you desire
sudo mkdir -p /var/lib/headplane
sudo chown -R $(whoami):$(whoami) /var/lib/headplane
```
## Building Headplane
Clone the Headplane repository, install dependencies, and build the project:
```bash
# You can optionally checkout a specific release tag.
git clone https://github.com/tale/headplane.git
cd headplane
pnpm install
pnpm build
```
## Running Headplane
Running Headplane is as straightforward as running `pnpm start` (or also
directly with `node build/server/index.js`). Headplane will look for a config
file at `/etc/headplane/config.yaml` by default, but you can specify a different
path by setting the `HEADPLANE_CONFIG_PATH` environment variable.
> Ensure that the `build/` directory exists relative to where the start command
> is run, otherwise Headplane will not be able to find the frontend assets.
### Example systemd Service
Slotting this file into `/etc/systemd/system/headplane.service` will allow
you to manage Headplane via systemd. Adjust the paths and user as needed,
run `sudo systemctl daemon-reload`, and then enable/start the service.
```ini
[Unit]
Description=Headplane Service
After=network.target # (or headscale.service if it runs via systemd)
Requires=network.target # (or headscale.service if it runs via systemd)
StartLimitIntervalSec=0
[Service]
Type=simple
User=your-username
WorkingDirectory=/path/to/your/cloned/headplane
ExecStart=/usr/bin/node /path/to/your/cloned/headplane/build/server/index.js
Restart=on-failure
RestartSec=5s
# Uncomment and set if using a custom config path
# Environment=HEADPLANE_CONFIG_PATH=/path/to/your/config.yaml
[Install]
WantedBy=multi-user.target
```
To access Headplane, navigate to `http://localhost:3000/admin` in your web
browser (replace `localhost` with your server's IP address or domain name if
not running locally).
In order to log in, you'll need to supply a Headscale API key. You can create
one by running the following command within your Headscale environment:
```bash
# You may want to tweak the expiration duration as needed
headscale apikeys create --expiration 90d
```
## Enabling advanced features
You've technically completed the installation, but read on if you would like
to enable advanced features like the ability to edit network settings from the
UI or remote SSH from the browser.
### Network Management
Network management allows you to configure Tailnet settings such as DNS servers,
custom A records, the tailnet domain name, and MagicDNS from the Headplane UI.
#### Prerequisites
Network management (and other configurable Headscale features) requires that
Headplane and Headscale both run on the same machine because Headplane needs
- Access to read and write the Head**scale** configuration file
- Access to read the `/proc` filesystem on Linux to locate Headscale's process
#### Configuration
Enabling network management is as simple as setting a few additional fields in
your Headplane configuration file:
| Field | Description |
|---------------------|--------------------------------------------------------|
| **`integration.proc.enabled`** | Set to `true` to enable process inspection. |
| **`headscale.config_path`** | Path to your Head**scale** configuration file (e.g., `/etc/headscale/config.yaml`). |
| `headscale.dns_records_path` | *Optional*. Refer to the [example configuration](https://github.com/tale/headplane/blob/main/config.example.yaml) for details. |
With these settings in place, restart Headplane. You should now see additional
options in the UI navbar such as "DNS" and "Settings" where you can manage your
Tailnet configuration.
### Remote Web SSH
Remote Web SSH allows you to open a terminal session to your Tailscale nodes
directly from the Headplane web interface via
[Tailscale SSH](https://tailscale.com/kb/1193/tailscale-ssh). This feature
requires that Tailscale SSH is running on your nodes (done via
`tailscale up --ssh`).
This feature uses the [Headplane Agent](../features/agent.md) to facilitate the
SSH connections. Refer to the [Agent documentation](../features/agent.md) for
setup instructions and specifically follow the
[native mode configuration](../features/agent.md#native-mode-configuration)
section to point Headplane to the correct agent location.
### Single Sign-On (SSO)
Single Sign-On (SSO) authentication allows users to log in to Headplane using
external identity providers such as Google, GitHub, or any provider that
supports OpenID Connect (OIDC).
To get started with SSO, refer to the [SSO documentation](../features/sso.md)
for detailed setup instructions.
## Reverse Proxying
You *should* run Headplane behind a reverse proxy such as Nginx or Caddy in
production. Additionally, putting Headscale beind the reverse proxy allows
you to access both services via the same domain and TLS certificate.
#### Configuration
::: tip
If you are using a [custom path prefix](#custom-path-prefix) for Headplane,
adjust the `/admin` paths in the examples below accordingly.
:::
Headscale supports integrating with
[several reverse proxies](https://headscale.net/stable/ref/integration/reverse-proxy/)
such as Nginx, Caddy, Apache, etc. Deploying Headplane is as simple as adding
a handler to route any requests to `/admin` to the Headplane service. Refer
to the Nginx example below for a reference configuration. A similar setup via
Traefik in Docker is available in the [Docker](./docker.md#reverse-proxying)
installation documentation.
#### Example Nginx Configuration
The following configuration will set up Nginx to proxy all Headscale requests
on `headscale.example.com` and serve the Headplane UI under the `/admin` path.
This is identical to how Tailscale's own admin console is served.
```nginx
server {
listen 80;
listen [::]:80;
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name headscale.example.com;
# Or use LetsEncrypt with Certbot (up to you)
ssl_certificate /path/to/your/fullchain.pem;
ssl_certificate_key /path/to/your/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / { # Headscale runs on the root path
proxy_pass http://localhost:8080/; # Adjust if Headscale runs on a different port
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_redirect http:// https://;
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
}
location /admin/ { # Headplane is served under /admin
proxy_pass http://localhost:3000/; # Adjust if Headplane runs on a different port
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_redirect http:// https://;
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
## Custom Path Prefix
::: warning
The only officially supported path prefix for Headplane is `/admin`. Using a
custom path prefix may lead to unexpected issues and is not recommended.
:::
If for whatever reason you do not want to serve Headplane under `/admin`
(e.g., you want to serve it under `/headplane`), you can set the prefix
while building Headplane via the `__INTERNAL_PREFIX` environment variable.
```bash
# Example for /headplane prefix
git clone
cd headplane
pnpm install
# Set the prefix here
__INTERNAL_PREFIX=/headplane pnpm build
```
When running Headplane, all requests will only be served under the specified
path. Make sure to also adjust your reverse proxy configuration accordingly if
you are using one. Additionally, if you want to change the path prefix again,
you will need to rebuild Headplane with the new prefix.
+50
View File
@@ -0,0 +1,50 @@
---
title: Introduction
description: What is Headplane?
---
# What is Headplane (and Headscale)?
Headplane is a web-based UI that bridges the transforms Headscale into a
feature-rich VPN platform that rivals Tailscale's official offering. Headscale
is a self-hosted implementation of the Tailscale control server, enabling users
to create and manage their own private VPN networks via the Tailscale client.
<figure>
<img class="dark-only" src="./assets/preview-dark.png" />
<img class="light-only" src="./assets/preview-light.png" />
<figcaption>Headplane Dashboard</figcaption>
</figure>
Headscale does not ship with any web UI by default, which is where Headplane
comes in. Headplane provides a comprehensive web interface for managing your
Headscale instance, allowing you to easily administer your nodes, networks, and
ACLs.
It goes beyond basic management features, offering advanced capabilities such as
remote web-based SSH access to your nodes, single sign-on (SSO) via OpenID
Connect (OIDC), and detailed insights into your Tailnet's configuration and
status. Things like DNS management, ACL editing, and Headscale configuration can
all be done directly from the Headplane UI when compared to other Headscale UIs.
<figure>
<img class="dark-only" src="./assets/dns-dark.png" />
<img class="light-only" src="./assets/dns-light.png" />
<figcaption>DNS management in Headplane</figcaption>
</figure>
Headplane aims to replicate the functionality offered by the official Tailscale
product and dashboard, being one of the most feature complete Headscale UIs available.
These are some of the features that Headplane offers:
- Machine management, including expiry, network routing, name, and owner management
- Access Control List (ACL) and tagging configuration for ACL enforcement
- Support for OpenID Connect (OIDC) as a login provider
- The ability to edit DNS settings and automatically provision Headscale
- Configurability for Headscale's settings
<figure>
<img class="dark-only" src="./assets/machine-dark.png" />
<img class="light-only" src="./assets/machine-light.png" />
<figcaption>Machine management in Headplane</figcaption>
</figure>
+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 30 30">
<g transform="translate(-142.5 -229.5)">
<path d="M120 147.5h20a5 5 45 0 1 5 5v20a5 5 135 0 1-5 5h-20a5 5 45 0 1-5-5v-20a5 5 135 0 1 5-5" style="fill:#181717" transform="translate(27.5 82)"/>
<circle cx="150" cy="237" r="2.5" style="fill:#f7f5f4"/>
<circle cx="157.5" cy="237" r="2.5" style="fill:#444343"/>
<circle cx="165" cy="237" r="2.5" style="fill:#f7f5f4"/>
<circle cx="150" cy="244.5" r="2.5" style="fill:#f7f5f4"/>
<circle cx="157.5" cy="244.5" r="2.5" style="fill:#f7f5f4"/>
<circle cx="165" cy="244.5" r="2.5" style="fill:#f7f5f4"/>
<circle cx="150" cy="252" r="2.5" style="fill:#f7f5f4"/>
<circle cx="157.5" cy="252" r="2.5" style="fill:#444343"/>
<circle cx="165" cy="252" r="2.5" style="fill:#f7f5f4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 901 B

Generated
+3 -3
View File
@@ -40,11 +40,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1760164275,
"narHash": "sha256-gKl2Gtro/LNf8P+4L3S2RsZ0G390ccd5MyXYrTdMCFE=",
"lastModified": 1761349956,
"narHash": "sha256-tH3wHnOJms+U4k/rK2Nn1RfBrhffX92jLP/2VndSn0w=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "362791944032cb532aabbeed7887a441496d5e6e",
"rev": "02f2cb8e0feb4596d20cc52fda73ccee960e3538",
"type": "github"
},
"original": {
-31
View File
@@ -3,37 +3,6 @@ go = "1.25.1"
pnpm = "10.4.0"
node = "22.16"
[tasks.copy-wasm-shim]
alias = ["gojs"]
description = "Copies Go's wasm_exec.js to the public directory"
run = [
"echo // $(go version) > app/wasm_exec.js",
"cat $(go env GOROOT)/lib/wasm/wasm_exec.js >> app/wasm_exec.js",
]
[tasks.build-go-wasm]
alias = ["wasm"]
depends = ["copy-wasm-shim"]
description = "Builds the Go WebAssembly module for Tailscale SSH"
env = { GOOS = "js", GOARCH = "wasm" }
run = "go build -o app/hp_ssh.wasm ./cmd/hp_ssh"
[tasks.build-go-agent]
alias = ["agent"]
description = "Builds the Go agent for HostInfo"
run = "go build -o build/hp_agent ./cmd/hp_agent"
# env = { CGO_ENABLED = "1" }
# run = "go build -o build/hp_agent.so -buildmode=c-shared ./cmd/hp_agent"
[tasks.build-fake-shell]
alias = ["fake-shell"]
description = "Builds the fake shell for Distroless docker images"
run = [
'test -n "$IMAGE_TAG" || (echo "IMAGE_TAG is not set" && exit 1)',
'go build -ldflags="-s -w -X main.imageTag=$IMAGE_TAG" -o build/sh ./cmd/fake_sh'
]
[tasks.build-nixos-docs]
alias = ["nixos-docs"]
description = "Builds NixOS module documentation"
+2 -2
View File
@@ -28,7 +28,7 @@ in
pnpmDeps = pnpm_10.fetchDeps {
inherit (finalAttrs) pname version src;
hash = "sha256-KyUcaR2Lvu5kT8arr4ZO8rCa5HWXTqmk8C7P8WoYK+c=";
hash = "sha256-KniPa9RMQAy5TY7H/TmETQNjqnuJv/kdFuRQdIoE/gQ=";
fetcherVersion = 1;
};
@@ -36,7 +36,7 @@ in
runHook preBuild
cp ${headplane-ssh-wasm}/hp_ssh.wasm app/hp_ssh.wasm
cp ${headplane-ssh-wasm}/wasm_exec.js app/wasm_exec.js
pnpm build
pnpm react-router build
runHook postBuild
'';
+28 -28
View File
@@ -16,20 +16,28 @@
"docs:preview": "vitepress preview docs"
},
"dependencies": {
"@libsql/client": "0.15.12"
},
"devDependencies": {
"@biomejs/biome": "^2.2.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@faker-js/faker": "9.9.0",
"@fontsource-variable/inter": "^5.2.6",
"@fontsource-variable/inter": "^5.2.8",
"@iconify/react": "^6.0.2",
"@kubernetes/client-node": "^1.3.0",
"@libsql/client": "0.15.12",
"@react-aria/toast": "3.0.6",
"@react-aria/toast": "3.0.8",
"@react-router/dev": "^7.8.1",
"@react-router/node": "^7.8.1",
"@react-stately/toast": "3.1.2",
"@shopify/lang-jsonc": "^1.0.1",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@tailwindcss/vite": "^4.1.12",
"@types/node": "^24.3.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@typescript/native-preview": "7.0.0-dev.20250821.1",
"@uiw/codemirror-theme-github": "4.25.1",
"@uiw/codemirror-theme-xcode": "4.25.1",
"@uiw/react-codemirror": "4.25.1",
@@ -41,48 +49,40 @@
"arktype": "^2.1.20",
"clsx": "^2.1.1",
"dotenv": "17.2.1",
"drizzle-kit": "^0.31.4",
"drizzle-orm": "0.44.4",
"isbot": "5.1.30",
"jose": "6.1.0",
"js-yaml": "^4.1.0",
"lefthook": "^1.12.3",
"lucide-react": "^0.540.0",
"mime": "^4.0.7",
"openid-client": "6.7.0",
"react": "19.1.1",
"react-aria": "3.42.0",
"postcss": "^8.5.6",
"react": "19.2.0",
"react-aria": "3.44.0",
"react-codemirror-merge": "4.25.1",
"react-dom": "19.1.1",
"react-dom": "19.2.0",
"react-error-boundary": "^6.0.0",
"react-icons": "^5.5.0",
"react-router": "^7.8.1",
"react-router-dom": "^7.8.1",
"react-router-hono-server": "2.21.0",
"react-stately": "3.40.0",
"react-scan": "^0.4.3",
"react-stately": "3.42.0",
"remix-utils": "^8.8.0",
"tailwind-merge": "3.3.1",
"ulidx": "2.4.1",
"undici": "7.14.0",
"usehooks-ts": "^3.1.1",
"yaml": "2.8.1"
},
"devDependencies": {
"@biomejs/biome": "^2.2.0",
"@react-router/dev": "^7.8.1",
"@tailwindcss/vite": "^4.1.12",
"@types/node": "^24.3.0",
"@typescript/native-preview": "7.0.0-dev.20250821.1",
"drizzle-kit": "^0.31.4",
"js-yaml": "^4.1.0",
"lefthook": "^1.12.3",
"postcss": "^8.5.6",
"react-router-dom": "^7.8.1",
"react-scan": "^0.4.3",
"tailwindcss": "^4.1.12",
"tailwindcss-animate": "^1.0.7",
"tailwindcss-react-aria-components": "^2.0.0",
"typescript": "^5.9.2",
"ulidx": "2.4.1",
"undici": "7.14.0",
"usehooks-ts": "^3.1.1",
"vite": "npm:rolldown-vite@7.1.4",
"vite-tsconfig-paths": "^5.1.4",
"vitepress": "next",
"vitest": "^3.1.3"
"vitest": "^3.1.3",
"yaml": "2.8.1"
},
"packageManager": "pnpm@10.4.0",
"engines": {
+1158 -1310
View File
File diff suppressed because it is too large Load Diff
+14 -1
View File
@@ -37,10 +37,23 @@ export default defineConfig(({ command, isSsrBuild }) => ({
},
build: {
target: 'esnext',
sourcemap: true,
rolldownOptions:
command === 'build'
? {
// Exclude libsql from the server side since it's a native module
// Exclude WASM from the client since it fetches from the server
external: isSsrBuild ? [/^@libsql\//] : [/\.wasm(\?url)?$/],
output: {
manualChunks: undefined,
inlineDynamicImports: isSsrBuild,
},
}
: undefined,
},
ssr: {
target: 'node',
noExternal: isSsrBuild ? ['@libsql/client'] : undefined,
noExternal: command === 'build' ? true : undefined,
},
optimizeDeps: {
include: ['@libsql/client'],