headplane: 0.6.1 -> 0.6.2

Atomic bump of headplane and headplane-agent to 0.6.2.

Reworks the NixOS module to track upstream config changes:
- Adds new 0.6.2 options for server (base_url, cookie_max_age, cookie_domain) and OIDC (PKCE, end-session, subject claims).
- Migrates secret options to use _path variants for secure file loading.
- Uses mkRemovedOptionModule for truly removed options (redirect_uri, user_storage_file, strict_validation, cache_ttl).
- Hardens the NixOS test to verify rendered config and session cookie issuance.
- Exposes headplane-agent and tests under headplane.passthru.
This commit is contained in:
Erik Parawell
2026-05-01 00:56:40 -07:00
parent 0a97f2edde
commit 47db95888a
6 changed files with 247 additions and 70 deletions
@@ -245,6 +245,13 @@ of pulling the upstream container image from Docker Hub. If you want the old beh
- Ethercalc and its associated module have been removed, as the package is unmaintained and cannot be installed from source with npm now.
- `services.headplane` has been updated to 0.6.2, which introduces several breaking changes to the configuration schema:
- `services.headplane.settings.oidc.redirect_uri` has been removed. Use `services.headplane.settings.server.base_url` instead; the OIDC redirect URI is now automatically derived from it. Ensure `base_url` is the bare host URL without the `/admin` suffix.
- `services.headplane.settings.oidc.user_storage_file` has been removed. Users are now stored in an internal SQL database.
- `services.headplane.settings.oidc.strict_validation` has been removed.
- `services.headplane.settings.oidc.token_endpoint_auth_method` now defaults to `null` (auto-detection), which typically falls back to `client_secret_basic`. Previous versions defaulted to `client_secret_post`.
- `services.headplane.settings.integration.agent.cache_ttl` has been removed as it had no effect in 0.6.2.
- `services.immich` no longer supports pgvecto.rs since the package has been removed from nixpkgs.
As a result, options `services.immich.database.enableVectors` and `services.immich.database.enableVectorchord` have been removed, and VectorChord is now always used.
If you have not completed the migration yet, ensure you completely remove the extension from your database before upgrading by following the [migration guide](https://github.com/NixOS/nixpkgs/blob/nixos-25.11/nixos/modules/services/web-apps/immich.md#migrating-from-pgvecto-rs-to-vectorchord-pre-2511-installations-module-services-immich-vectorchord-migration).
+170 -65
View File
@@ -33,6 +33,37 @@ let
settingsFile = settingsFormat.generate "headplane-config.yaml" (filterSettings settings);
in
{
imports = [
(lib.mkRemovedOptionModule
[
"services.headplane"
"settings"
"oidc"
"redirect_uri"
]
"Use services.headplane.settings.server.base_url instead; the OIDC redirect URI is now derived from it. Do not include the /admin suffix."
)
(lib.mkRemovedOptionModule [
"services.headplane"
"settings"
"oidc"
"user_storage_file"
] "Users are now stored in the internal database.")
(lib.mkRemovedOptionModule [
"services.headplane"
"settings"
"oidc"
"strict_validation"
] "This option has no effect in Headplane 0.6.2 and later.")
(lib.mkRemovedOptionModule [
"services.headplane"
"settings"
"integration"
"agent"
"cache_ttl"
] "This option has no effect in Headplane 0.6.2 and later.")
];
options.services.headplane = {
enable = mkEnableOption "Headplane";
package = mkPackageOption pkgs "headplane" { };
@@ -64,6 +95,16 @@ in
description = "The port to listen on.";
};
base_url = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
The base URL for Headplane. Used for OIDC redirect callback URL
detection. Should not include the dashboard prefix (/admin).
'';
example = "https://headplane.example.com";
};
cookie_secret_path = mkOption {
type = types.nullOr types.path;
default = null;
@@ -84,16 +125,43 @@ in
'';
};
cookie_max_age = mkOption {
type = types.ints.positive;
default = 86400;
description = "The maximum age of the session cookie in seconds.";
};
cookie_domain = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Restrict the cookie to a specific domain.
This may not work as expected if not using a reverse proxy.
'';
example = "example.com";
};
data_path = mkOption {
type = types.path;
default = "/var/lib/headplane";
description = ''
The path to persist Headplane specific data.
All data going forward is stored in this directory, including the internal database and any cache related files.
Data formats prior to 0.6.1 will automatically be migrated.
'';
example = "/var/lib/headplane";
};
info_secret_path = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to a file containing the info secret.
Allows access to certain debug endpoints that may expose
sensitive information about your Headplane instance.
If not set, these endpoints will be disabled.
'';
example = lib.literalExpression "config.sops.secrets.headplane_info_secret.path";
};
};
};
default = { };
@@ -185,6 +253,15 @@ in
'';
};
pre_authkey_path = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to a file containing a Headscale pre-auth key for the agent.
'';
example = lib.literalExpression "config.sops.secrets.headplane_pre_authkey.path";
};
executable_path = mkOption {
type = types.path;
readOnly = true;
@@ -195,36 +272,16 @@ in
'';
};
pre_authkey_path = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to a file containing the agent preauth key.
To connect to your Tailnet, you need to generate a pre-auth key.
This can be done via the web UI or through the `headscale` CLI.
'';
example = lib.literalExpression "config.sops.secrets.agent_pre_authkey.path";
};
host_name = mkOption {
type = types.str;
default = "headplane-agent";
description = "Optionally change the name of the agent in the Tailnet";
};
cache_ttl = mkOption {
type = types.ints.positive;
default = 180000;
description = ''
How long to cache agent information (in milliseconds).
If you want data to update faster, reduce the TTL, but this will increase the frequency of requests to Headscale.
'';
description = "Optionally change the name of the agent in the Tailnet.";
};
cache_path = mkOption {
type = types.path;
type = types.str;
default = "/var/lib/headplane/agent_cache.json";
description = "Where to store the agent cache.";
description = "The path to store the agent's cache.";
};
work_dir = mkOption {
@@ -271,6 +328,15 @@ in
type = types.nullOr (
types.submodule {
options = {
enabled = mkOption {
type = types.bool;
default = true;
description = ''
Explicitly control OIDC availability.
Set to false to define OIDC config without enabling it.
'';
};
issuer = mkOption {
type = types.str;
description = "URL to OpenID issuer.";
@@ -292,6 +358,16 @@ in
example = lib.literalExpression "config.sops.secrets.oidc_client_secret.path";
};
headscale_api_key_path = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to a file containing the Headscale API key.
Required for OIDC authentication.
'';
example = lib.literalExpression "config.sops.secrets.headscale_api_key.path";
};
disable_api_key_login = mkOption {
type = types.bool;
default = false;
@@ -299,42 +375,81 @@ in
};
token_endpoint_auth_method = mkOption {
type = types.enum [
"client_secret_post"
"client_secret_basic"
"client_secret_jwt"
];
default = "client_secret_post";
description = "The token endpoint authentication method.";
};
headscale_api_key_path = mkOption {
type = types.nullOr types.path;
type = types.nullOr (
types.enum [
"client_secret_post"
"client_secret_basic"
"client_secret_jwt"
]
);
default = null;
description = ''
Path to a file containing the Headscale API key.
Required when `services.headplane.settings.oidc` is set.
The token endpoint authentication method.
If not set, Headplane will auto-detect the best method
and fall back to client_secret_basic.
'';
example = lib.literalExpression "config.sops.secrets.headscale_api_key.path";
};
redirect_uri = mkOption {
use_pkce = mkOption {
type = types.bool;
default = false;
description = ''
Whether to use PKCE when authenticating users.
Your OIDC provider must support PKCE and it must be enabled on the client.
'';
};
use_end_session = mkOption {
type = types.bool;
default = false;
description = ''
Enable RP-initiated logout. When true, /logout redirects the user
to the IdP's end_session_endpoint. The post_logout_redirect_uri
MUST be pre-registered in your OIDC client configuration.
'';
};
end_session_endpoint = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
This should point to your publicly accessible URL
for your Headplane instance with /admin/oidc/callback.
Override the auto-discovered end_session_endpoint, or supply
one if your provider does not advertise it via discovery.
'';
example = "https://headscale.example.com/admin/oidc/callback";
example = "https://provider.example.com/logout";
};
user_storage_file = mkOption {
type = types.path;
default = "/var/lib/headplane/users.json";
post_logout_redirect_uri = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Path to a file containing the users and their permissions for Headplane.
Where the identity provider should redirect after RP-initiated logout.
If unset, Headplane defaults to its own login page.
'';
example = "https://headplane.example.com/admin/login?s=logout";
};
subject_claims = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = ''
Fallback claims to use when your provider does not return a standard
OIDC `sub` claim. Headplane always checks `sub` first, then each
claim here in order.
'';
example = [
"open_id"
"email"
];
};
allow_weak_rsa_keys = mkOption {
type = types.bool;
default = false;
description = ''
Allow ID token verification with legacy RSA keys smaller than 2048 bits.
Only use as a temporary compatibility workaround.
'';
example = "/var/lib/headplane/users.json";
};
profile_picture_source = mkOption {
@@ -346,12 +461,6 @@ in
description = "Source for user profile pictures.";
};
strict_validation = mkOption {
type = types.bool;
default = true;
description = "Enable strict validation of OIDC configuration.";
};
scope = mkOption {
type = types.str;
default = "openid email profile";
@@ -422,23 +531,19 @@ in
via systemd `LoadCredential` or sops-nix.
'';
}
{
assertion =
cfg.settings.integration.agent == null
|| !cfg.settings.integration.agent.enabled
|| cfg.settings.integration.agent.pre_authkey_path != null;
message = ''
services.headplane.settings.integration.agent.pre_authkey_path must be set
when services.headplane.settings.integration.agent.enabled is true.
'';
}
{
assertion = cfg.settings.oidc == null || cfg.settings.oidc.headscale_api_key_path != null;
message = ''
services.headplane.settings.oidc.headscale_api_key_path must be set
when services.headplane.settings.oidc is non-null. Headplane's OIDC
flow requires a Headscale API key to mint sessions; upstream config
validation rejects an OIDC block without it.
flow requires a Headscale API key to mint sessions.
'';
}
{
assertion = agentSettings == null || !agentSettings.enabled || agentSettings.pre_authkey_path != null;
message = ''
services.headplane.settings.integration.agent.pre_authkey_path must be set
when the agent is enabled.
'';
}
];
+1
View File
@@ -724,6 +724,7 @@ in
imports = [ ./hbase.nix ];
_module.args.getPackage = pkgs: pkgs.hbase_2_5;
};
headplane = runTest ./headplane.nix;
headscale = runTest ./headscale.nix;
healthchecks = runTest ./web-apps/healthchecks.nix;
hedgedoc = runTest ./hedgedoc.nix;
+57
View File
@@ -0,0 +1,57 @@
{ lib, pkgs, ... }:
let
cookieSecret = pkgs.writeText "cookie-secret" "01234567890123456789012345678901";
in
{
name = "headplane";
meta.maintainers = with lib.maintainers; [
igor-ramazanov
stealthbadger747
];
nodes.machine = {
services.headscale = {
enable = true;
settings = {
server_url = "http://127.0.0.1";
ip_prefixes = [ "100.64.0.0/10" ];
dns = {
base_domain = "tailnet";
override_local_dns = false;
};
};
};
services.headplane = {
enable = true;
settings.server = {
cookie_secret_path = cookieSecret;
cookie_secure = false;
# Exercise the new 0.6.2 server-level options so the test fails
# if their option-name -> YAML-key mapping ever regresses.
base_url = "http://127.0.0.1:3000";
cookie_max_age = 3600;
cookie_domain = "127.0.0.1";
};
};
};
testScript = ''
machine.wait_for_unit("headscale.service")
machine.wait_for_unit("headplane.service")
machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/admin/login")
with subtest("rendered config contains new 0.6.2 options"):
config = machine.succeed("cat /etc/headplane/config.yaml")
for key in ["base_url:", "cookie_max_age:", "cookie_domain:"]:
assert key in config, f"{key} missing from rendered config"
with subtest("removed options are not rendered"):
config = machine.succeed("cat /etc/headplane/config.yaml")
for key in ["redirect_uri:", "user_storage_file:", "strict_validation:", "cache_ttl:"]:
assert key not in config, f"removed option {key} still rendered"
with subtest("session cookie is set on login page"):
machine.succeed("curl -sf -c /tmp/cj http://127.0.0.1:3000/admin/login")
machine.succeed("test -s /tmp/cj")
'';
}
+2 -2
View File
@@ -7,13 +7,13 @@ buildGoModule (finalAttrs: {
pname = "headplane-agent";
__structuredAttrs = true;
# Note, if you are upgrading this, you should upgrade headplane at the same time
version = "0.6.1";
version = "0.6.2";
src = fetchFromGitHub {
owner = "tale";
repo = "headplane";
tag = "v${finalAttrs.version}";
hash = "sha256-hsrnmEwKXJlPjV4aIfmS6GAE414ArVRGoPPpZGmV0x4=";
hash = "sha256-2C/Pn2M2aHADtoljSFg9hz6xOaZp6IRI77jjy+LDAgw=";
};
vendorHash = "sha256-MvrqKMD+A+qBZmzQv+T9920U5uJop+pjfJpZdm2ZqEA=";
+10 -3
View File
@@ -3,8 +3,10 @@
fetchFromGitHub,
fetchPnpmDeps,
git,
headplane-agent,
lib,
makeWrapper,
nixosTests,
nodejs_22,
pnpm_10,
pnpmConfigHook,
@@ -13,13 +15,13 @@
let
pname = "headplane";
# Note, if you are upgrading this, you should upgrade headplane-agent at the same time
version = "0.6.1";
pnpmDepsHash = "sha256-AYfEL3HSRg87I+Y0fkLthFSDWgHTg5u0DBpzn6KBn1Q=";
version = "0.6.2";
pnpmDepsHash = "sha256-CsmffCo9Se/4oiOqbcuhjPMuGmR2GL+YfcyWgzBTAh8=";
src = fetchFromGitHub {
owner = "tale";
repo = "headplane";
tag = "v${version}";
hash = "sha256-hsrnmEwKXJlPjV4aIfmS6GAE414ArVRGoPPpZGmV0x4=";
hash = "sha256-2C/Pn2M2aHADtoljSFg9hz6xOaZp6IRI77jjy+LDAgw=";
};
headplaneSshWasm = buildGoModule {
@@ -104,6 +106,11 @@ stdenv.mkDerivation (finalAttrs: {
runHook postInstall
'';
passthru = {
agent = headplane-agent;
tests = { inherit (nixosTests) headplane; };
};
meta = {
description = "Feature-complete Web UI for Headscale";
homepage = "https://github.com/tale/headplane";