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:
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
'';
|
||||
}
|
||||
@@ -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=";
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user