Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f846436
feat(i18n): Add internationalization support
serein-213 Feb 9, 2026
ac08af5
feat: Adapt for Headscale v0.28.0 and add ACL management
serein-213 Feb 9, 2026
b6c54b5
feat: enhance security and fix navigation reactivity issues
serein-213 Feb 9, 2026
33302e1
feat: add export functionality for users, nodes, preAuthKeys, and pol…
serein-213 Feb 14, 2026
cfd66a8
feat: improve node route handling and enhance UI responsiveness
serein-213 Feb 14, 2026
3847818
feat(i18n): enhance internationalization support and improve user fee…
serein-213 Feb 25, 2026
da501db
feat(i18n): add localized error messages for tag management and impro…
serein-213 Feb 25, 2026
7da5aa3
feat: enhance pre-auth key handling with improved mapping and validation
serein-213 Feb 25, 2026
52d099a
Align Headscale 0.28 API and add first-run language gate
tcpqueue Apr 19, 2026
3449b1f
Merge pull request #1 from tcpqueue/codex/headscale-028-i18n-boot-fixes
serein-213 Apr 30, 2026
b388ca9
fix: rebase i18n branch onto latest main
serein-213 Mar 21, 2026
994c3bc
chore: add i18n sync and validation scripts
Apr 30, 2026
b38acd6
fix: improve dark/light mode handling and migrate legacy user prefere…
serein-213 Apr 30, 2026
07cf626
fix: preserve legacy locale preferences during upgrade
serein-213 Apr 30, 2026
d492e68
fix: show remember api key after authorization
serein-213 May 24, 2026
1551889
feat: upgrade to Headscale v0.29.1 API
serein-213 Jun 26, 2026
c138e1b
fix: align Headscale 0.29.1 API update
serein-213 Jun 26, 2026
788ac5f
feat: add Headscale 0.29 policy and auth controls
serein-213 Jun 26, 2026
19166c3
fix: update version display from 0.28.0 to 0.29.1
serein-213 Jun 28, 2026
42f032b
fix: align UI with Headscale 0.29.2
serein-213 Jul 8, 2026
03e8f70
fix: improve mobile language selection
serein-213 Jul 8, 2026
7dbd3c8
perf: lazy load heavy UI modules
serein-213 Jul 8, 2026
3f4d846
fix: harden polling and lazy-load failures
serein-213 Jul 8, 2026
6106104
chore: align formatting and lint tooling
serein-213 Jul 8, 2026
5b65f1a
fix: stabilize ACL auth approvals
serein-213 Jul 8, 2026
6e0c7b2
fix: improve mobile ACL tabs
serein-213 Jul 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions .eslintignore

This file was deleted.

30 changes: 0 additions & 30 deletions .eslintrc.cjs

This file was deleted.

1 change: 0 additions & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
engine-strict=true
resolution-mode=highest
1 change: 0 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@
"trailingComma": "all",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}
7 changes: 6 additions & 1 deletion Caddyfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
:{$PORT:80}
root * /app
encode gzip zstd
try_files {path}.html {path}

# SPA fallback for /admin path
handle /admin* {
try_files {path} /admin/index.html
}

file_server
50 changes: 30 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# headscale-admin

headscale-admin is meant to be a simple, modern, and useful web interface for [juanfont/headscale](https://github.com/juanfont/headscale) - *"An open source, self-hosted implementation of the Tailscale control server."*
headscale-admin is meant to be a simple, modern, and useful web interface for [juanfont/headscale](https://github.com/juanfont/headscale) - _"An open source, self-hosted implementation of the Tailscale control server."_

headscale-admin is still in active development and will evolve in tandem with headscale. It should not be treated as a final product, but when used properly, it should be safe to run in a production environment.

Expand All @@ -13,16 +13,17 @@ headscale-admin is still in active development and will evolve in tandem with he
### Securing headscale-admin

Please note that headscale-admin is an entirely stateless application. The static files hosted on a server do not perform any interaction with the headscale API or backend. headscale-admin only provides the application scaffolding which facilitates interactions from the client's browser to the headscale API. There are no sessions, API tokens, or any other sensitive information passed to or from the web server hosting headscale-admin. For this reason, security beyond SSL certificates is unnecessary, though you may choose to do so simply for the sake of hiding the application. Any static headscale UI offers functionality that can be trivially replicated with cURL or other web request utilities. **The security of headscale lies in your API token being securely preserved.** That said, here are some common recommendations:

- Using proper TLS (e.g. using a frontend proxy) to protect headscale-admin can successfully prevent HTML injection of the application in an active MITM attack. This is highly recommended.
- If you decide to host headscale-admin on a different subdomain than headscale, please make sure you allow your headscale-admin domain in the CORS headers of headscale to allow the requests.
- Password protection or SSO is not required to secure headscale-admin. All API key information is solely stored on your browser and is not found in cookies or session information, but you may want to obscure the fact that you have an admin utility in place just for increased anonymity. This does not prevent an attacker from using a compromised API key from interacting with your headscale API, so ensure that your keys stay protected.


### Building

headscale-admin was built using the [Skeleton](https://github.com/skeletonlabs/skeleton) framework on top of [SvelteKit](https://svelte.dev/tutorial/kit/introducing-sveltekit) + [TailwindCSS](https://tailwindcss.com/). It uses svelte/adapter-static to produce only static files when built. They can be hosted on nearly any server or environment as they are not contingent upon any specific runtime library.

#### Endpoint

**Note:** If you are building headscale-admin from source and want to host it on an endpoint other than the base of the domain, (e.g. `headscale.example.com/admin`) then you must set the `ENDPOINT` environment variable when building. Otherwise, it will default to expecting to be hosted on the root path "myheadscale.com/" and redirects and resource loading will not work correctly if you place them in a child folder. Once built, it is recommended to rename the `build` directory to the same name as your `$ENDPOINT` variable so the requests can follow the folder structure and not have to be stripped or rewritten by a front end proxy. The provided Dockerfile shows this in practice.

The default endpoint for the [headscale-admin Docker container](https://hub.docker.com/r/goodieshq/headscale-admin) is always `/admin` as this does not clash with any URL endpoints provided by the headscale API and can safely be hosted on the same subdomain.
Expand Down Expand Up @@ -70,6 +71,7 @@ You can create the production build by running:
```
npm run build
```

This will create a `build` directory that expects to be hosted at the provided `ENDPOINT` variable relative to the root domain.

#### Host Files
Expand All @@ -81,6 +83,7 @@ If you are hosting the static files locally, it is recommended that you replicat
The recommended way to deploy headscale-admin is by utilizing docker. Pre-built images are provided and always use the `/admin` endpoint by default. If you want to build an image for a different endpoint, you must use Docker build args.

Examples:

```
# Using default endpoint (/admin)
docker build . -t headscale-admin
Expand Down Expand Up @@ -111,7 +114,8 @@ docker run -p 8000:80 goodieshq/headscale-admin:latest
```

#### Docker Versioning
Due to the dynamic nature of the headscale API, starting with version `v0.24`, headscale-admin will no longer attempt to maintain compatibility with multiple version of headscale (previously there was a *"Legacy API"* option). Instead, headscale-admin versions will track the major versions of headscale itself. This means that the tag for `goodieshq/headscale-admin:0.24` of headscale-admin will be compatible with version `headscale/headscale:0.24` of headscale. Minor versions will not track headscale as there may be a need for bugfixes outside of headscale's release schedule.

Due to the dynamic nature of the headscale API, starting with version `v0.24`, headscale-admin will no longer attempt to maintain compatibility with multiple version of headscale (previously there was a _"Legacy API"_ option). Instead, headscale-admin versions will track the major versions of headscale itself. This means that the tag for `goodieshq/headscale-admin:0.24` of headscale-admin will be compatible with version `headscale/headscale:0.24` of headscale. Minor versions will not track headscale as there may be a need for bugfixes outside of headscale's release schedule.

The `goodieshq/headscale-admin:latest` tag will only be used for the latest stable release of headscale-admin that is compatible with a recent stable release of headscale. Because these are not guaranteed to track, it is recommended to explicitly tag the major version of headscale that you are using.

Expand All @@ -137,12 +141,12 @@ For larger deployments, it is recommended to use [PostgreSQL](https://www.postgr
- Assign the headscale service to both "proxy" and "backend" networks to allow it to be reachable over the internet and also to reach the database without exposing the database to the public proxy network.
- Setting the Postgres settings in your config.yaml file


### Traefik Configuration

The first thing you will need is a functional Traefik installation. I am using Traefik v3.3 which is the newest version as of January, 2025.
The first thing you will need is a functional Traefik installation. I am using Traefik v3.3 which is the newest version as of January, 2025.

#### Create a `proxy` network

Create a new, external network called `proxy`. Anything on this network, even if it is in a different `docker-compose.yml` configuration file, will reachable by Traefik and can therefore be reached over the Internet based on your Traefik labels and configuration.

```
Expand All @@ -152,6 +156,7 @@ docker network create proxy
Next, set up a `docker-compose.yml` configuration file in a directory called `traefik` somewhere safe (e.g. `/opt/traefik`) to keep it separate from other containers or software that you may deploy.

Contents of `traefik/docker-compose.yml`:

```yaml
services:
traefik:
Expand Down Expand Up @@ -183,7 +188,7 @@ services:
env_file:
- ./env/cloudflare.env
labels:
traefik.enable: "true"
traefik.enable: 'true'
traefik.docker.network: proxy
traefik.http.routers.traefik.tls.certresolver: myresolver
traefik.http.routers.traefik.rule: Host(`traefik.exampe.com`)
Expand All @@ -199,9 +204,11 @@ volumes:
```

#### Certificate Resolvers

Traefik using LetsEncrypt to automatically provision SSL certificates for your provisioned services. You may need to play with your `certresolvers` settings to get the right domain and SSL support working for you. This line in the YML file should contain the email address used for Cloudflare:

```yml
- --certificatesresolvers.myresolver.acme.email=username@email.com
- --certificatesresolvers.myresolver.acme.email=username@email.com
```

This example uses the Cloudflare API to perform DNS verification and requires a `./env/cloudflare.env` file (relative to `docker-compose.yml`) to contain something like:
Expand Down Expand Up @@ -239,8 +246,8 @@ services:
networks:
- proxy
labels:
traefik.enable: "true"
traefik.docker.network: "proxy"
traefik.enable: 'true'
traefik.docker.network: 'proxy'
# Headscale Service Configuration
traefik.http.services.headscale.loadbalancer.server.port: 8080
traefik.http.services.headscale.loadbalancer.server.scheme: http
Expand All @@ -249,9 +256,9 @@ services:
traefik.http.routers.headscale.tls.certresolver: myresolver
traefik.http.routers.headscale.service: headscale
# CORS Middleware Configuration
traefik.http.middlewares.headscale-cors.headers.accessControlAllowMethods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
traefik.http.middlewares.headscale-cors.headers.accessControlAllowHeaders: "Authorization,Content-Type"
traefik.http.middlewares.headscale-cors.headers.accessControlAllowOriginList: "https://headscale.example.com"
traefik.http.middlewares.headscale-cors.headers.accessControlAllowMethods: 'GET,POST,PUT,PATCH,DELETE,OPTIONS'
traefik.http.middlewares.headscale-cors.headers.accessControlAllowHeaders: 'Authorization,Content-Type'
traefik.http.middlewares.headscale-cors.headers.accessControlAllowOriginList: 'https://headscale.example.com'
traefik.http.middlewares.headscale-cors.headers.accessControlMaxAge: 100
traefik.http.middlewares.headscale-cors.headers.addVaryHeader: true
# Attach Middleware to Router
Expand All @@ -264,8 +271,8 @@ services:
networks:
- proxy
labels:
traefik.enable: "true"
traefik.docker.network: "proxy"
traefik.enable: 'true'
traefik.docker.network: 'proxy'
# Headscale Admin Service Configuration
traefik.http.services.headscale-admin.loadbalancer.server.port: 80
traefik.http.services.headscale-admin.loadbalancer.server.scheme: http
Expand Down Expand Up @@ -301,8 +308,6 @@ docker exec headscale headscale apikey create

A brief overview of the headscale environment with the number of users, nodes, preauth keys, and routes.



### Users Page

An overview of all headscale Users with a List or Tile layout.
Expand All @@ -315,7 +320,6 @@ An overview of all headscale Users with a List or Tile layout.

<img width="1000" alt="image" src="./img/HA-Users-Tile.png">


### Nodes Page

An overview of all headscale Nodes with a List or Tile layout.
Expand Down Expand Up @@ -348,41 +352,47 @@ The ACL builder provides a GUI for loading, creating, modifying, and applying [h

```yml
policy:
mode: "database"
mode: 'database'
```

Renaming elements of an ACL should properly affect downstream elements (e.g. renaming a group should change all instances of the group name within the policies).

#### Groups

Create, modify, and rename groups of users.

<img width="1000" alt="image" src="./img/HA-ACL-Groups.png">

#### Tag Owners

Assign users or groups as authorized owners of headscale node tags.

<img width="1000" alt="image" src="./img/HA-ACL-Groups.png">

#### Hosts

Create, modify, or rename hosts used in policies. Hosts can be individual IP's or networks, but must always be in CIDR format. Simply click on a host name or value to modify it after creation.

<img width="1000" alt="image" src="./img/HA-ACL-Hosts.png">

#### Policies
The policy entries allow you to create traffic restrictions based on the source and destination user, group, tag, host, or tag. These policies support ICMP, TCP, UDP, or Any protocol. Allowed values for ports are `*` or a comma-separated list of integers between 1-65535.

The policy entries allow you to create traffic restrictions based on the source and destination user, group, tag, host, or tag. These policies support ICMP, TCP, UDP, or Any protocol. Allowed values for ports are `*` or a comma-separated list of integers between 1-65535.

<img width="1000" alt="image" src="./img/HA-ACL-Policies.png">

<img width="1000" alt="image" src="./img/HA-ACL-Policies-Entry.png">

#### SSH

Create rules to permit SSH authentication using specific usernames based on the source user, group, or tag destined to a particular user or tag.

<img width="1000" alt="image" src="./img/HA-ACL-SSH.png">

<img width="1000" alt="image" src="./img/HA-ACL-SSH-Entry.png">

#### Config

Allows a user to save the ACL configuration to the headscale server or load a new configuration from an existing HuJSON file.

<img width="1000" alt="image" src="./img/HA-ACL-Config.png">
Expand All @@ -393,4 +403,4 @@ Allows a user to save the ACL configuration to the headscale server or load a ne

Store API URL and API Key information in the browser's LocalStorage. Set API refresh interval (how frequently users, preauth keys, nodes, and routes are updated) and toggle console debugging.

<img width="1000" alt="image" src="./img/HA-Settings.png">
<img width="1000" alt="image" src="./img/HA-Settings.png">
63 changes: 63 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import js from '@eslint/js';
import prettier from 'eslint-config-prettier';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';

export default [
{
ignores: [
'.DS_Store',
'build/**',
'.svelte-kit/**',
'package/**',
'node_modules/**',
'package-lock.json',
'pnpm-lock.yaml',
'yarn.lock',
],
},
js.configs.recommended,
...tsPlugin.configs['flat/recommended'],
...svelte.configs['flat/recommended'],
...svelte.configs['flat/prettier'],
prettier,
{
languageOptions: {
ecmaVersion: 2020,
sourceType: 'module',
globals: {
...globals.browser,
...globals.es2017,
...globals.node,
},
},
},
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
},
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: tsParser,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-expressions': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
'no-useless-escape': 'warn',
'svelte/no-navigation-without-resolve': 'warn',
'svelte/no-useless-children-snippet': 'warn',
'svelte/prefer-svelte-reactivity': 'warn',
'svelte/require-each-key': 'warn',
},
},
];
2 changes: 1 addition & 1 deletion example/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ services:
restart: unless-stopped

networks:
proxy:
proxy:
Loading