Files
2026-06-22 16:40:29 -04:00

462 lines
18 KiB
Nix

{
lib,
pkgs,
...
}: let
inherit
(lib)
mkEnableOption
mkOption
mkPackageOption
types
;
in {
options.services.headplane = {
enable = mkEnableOption "headplane";
package = mkPackageOption pkgs "headplane" {};
debug = mkOption {
type = types.bool;
default = false;
description = "Enable debug logging";
};
settings = mkOption {
description = ''
Headplane configuration options. Generates a YAML config file.
See: https://github.com/tale/headplane/blob/main/config.example.yaml
'';
type = types.submodule {
options = {
server = mkOption {
type = types.submodule {
options = {
host = mkOption {
type = types.str;
default = "127.0.0.1";
description = "The host address to bind to.";
example = "0.0.0.0";
};
port = mkOption {
type = types.port;
default = 3000;
description = "The port to listen on.";
};
base_url = mkOption {
type = types.str;
default = "http://localhost:3000";
description = "The base URL for Headplane, not including the dashboard prefix (/admin) portion";
example = "http://localhost:3000";
};
cookie_secret_path = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to a file containing the cookie secret.
The secret must be exactly 32 characters long.
'';
example = "config.sops.secrets.headplane_cookie.path";
};
cookie_secure = mkOption {
type = types.bool;
default = true;
description = ''
Should the cookies only work over HTTPS?
Set to false if running via HTTP without a proxy.
Recommended to be true in production.
'';
};
cookie_max_age = mkOption {
type = types.int;
default = 86400;
description = "The maximum age of the session cookie in seconds.";
example = "3600";
};
cookie_domain = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
This is not required, but if you want to restrict the
cookie to a specific domain, set it here. Otherwise leave
it commented out. This may not work as expected if not
using a reverse proxy.
'';
example = "headscale.example.com";
};
proxy_auth = mkOption {
type = types.submodule {
options = {
enabled = mkOption {
type = types.bool;
default = false;
description = "Whether to trust reverse proxy authentication for allowed client CIDRs.";
};
user_header = mkOption {
type = types.str;
default = "Remote-User";
description = "Header containing the stable authenticated proxy user identity.";
};
ip_header = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optional header containing the original client IP, such as X-Forwarded-For or X-Real-IP.";
};
trusted_proxy_cidrs = mkOption {
type = types.listOf types.str;
default = [
"127.0.0.1/32"
"::1/128"
];
description = ''
Direct proxy CIDR ranges trusted to supply ip_header.
Only used when ip_header is set.
'';
example = ["127.0.0.1/32"];
};
email_header = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optional header containing the authenticated user's email address.";
};
name_header = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optional header containing the authenticated user's display name.";
};
picture_header = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optional header containing the authenticated user's profile picture URL.";
};
allowed_cidrs = mkOption {
type = types.listOf types.str;
default = [
"127.0.0.1/32"
"::1/128"
];
description = ''
Direct client CIDR ranges allowed to bypass Headplane's login flow.
These should be the addresses your trusted reverse proxy uses to connect
to Headplane. Requires headscale.api_key_path.
'';
example = ["10.0.0.0/24"];
};
};
};
default = {};
description = "Proxy authentication configuration.";
};
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 = mkOption {
type = types.nullOr types.str;
default = null;
description = "Secret is optional and allows access to certain debug endpoints";
example = "config.sops.secrets.info_secret.path";
};
};
};
default = {};
description = "Server configuration for Headplane web application.";
};
headscale = mkOption {
type = types.submodule {
options = {
url = mkOption {
type = types.str;
default = "http://127.0.0.1:8080";
description = ''
The URL to your Headscale instance.
All API requests are routed through this URL.
THIS IS NOT the gRPC endpoint, but the HTTP endpoint.
IMPORTANT: If you are using TLS this MUST be set to `https://`.
'';
example = "https://headscale.example.com";
};
tls_cert_path = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to a file containing the TLS certificate.
'';
example = "config.sops.secrets.tls_cert.path";
};
public_url = mkOption {
type = types.nullOr types.str;
default = null;
description = "Public URL if differrent. This affects certain parts of the web UI.";
example = "https://headscale.example.com";
};
config_path = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to the Headscale configuration file.
This is optional, but HIGHLY recommended for the best experience.
If this is read only, Headplane will show your configuration settings
in the Web UI, but they cannot be changed.
'';
example = "/etc/headscale/config.yaml";
};
config_strict = mkOption {
type = types.bool;
default = true;
description = ''
Deprecated. Headplane no longer validates the complete
Headscale configuration and this option has no effect.
'';
};
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 and the Headplane agent.
'';
example = "config.sops.secrets.headscale_api_key.path";
};
dns_records_path = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
If you are using `dns.extra_records_path` in your Headscale configuration, you need to set this to the path for Headplane to be able to read the DNS records.
Ensure that the file is both readable and writable by the Headplane process.
When using this, Headplane will no longer need to automatically restart Headscale for DNS record changes.
'';
example = "/var/lib/headplane/extra_records.json";
};
};
};
default = {};
description = "Headscale specific settings for Headplane integration.";
};
integration = mkOption {
type = types.submodule {
options = {
agent = mkOption {
type = types.submodule {
options = {
enabled = mkOption {
type = types.bool;
default = false;
description = ''
The Headplane agent periodically syncs node information (version, OS, etc.)
from your Tailnet. It auto-generates ephemeral pre-auth keys using
headscale.api_key, so no manual key configuration is needed.
Requires Headscale 0.28 or newer.
'';
};
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.int;
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.
'';
};
executable_path = mkOption {
type = types.path;
default = "/usr/libexec/headplane/agent";
description = ''
Path to the Headplane agent binary.
The default is correct if using the NixOS module package.
'';
};
work_dir = mkOption {
type = types.path;
default = "/var/lib/headplane/agent";
description = ''
Do not change this unless you are running a custom deployment.
The work_dir represents where the agent will store its data to be able to automatically reauthenticate with your Tailnet.
It needs to be writable by the user running the Headplane process.
'';
};
package = mkPackageOption pkgs "headplane-agent" {};
};
};
default = {};
description = "Agent configuration for the Headplane agent.";
};
proc = mkOption {
type = types.submodule {
options = {
enabled = mkOption {
type = types.bool;
default = true;
description = ''
Enable "Native" integration that works when Headscale and
Headplane are running outside of a container. There is no additional
configuration, but you need to ensure that the Headplane process
can terminate the Headscale process.
'';
};
};
};
default = {};
description = "Native process integration settings.";
};
};
};
default = {};
description = "Integration configurations for Headplane to interact with Headscale.";
};
oidc = mkOption {
type = types.submodule {
options = {
issuer = mkOption {
type = types.str;
default = "";
description = "URL to OpenID issuer.";
example = "https://provider.example.com/issuer-url";
};
authorization_endpoint = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optionally override authorization_endpoint";
example = "https://provider.example.com/authorization_endpoint";
};
token_endpoint = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optionally override token_endpoint";
example = "https://provider.example.com/token_endpoint";
};
userinfo_endpoint = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optionally override userinfo_endpoint";
example = "https://provider.example.com/userinfo_endpoint";
};
client_id = mkOption {
type = types.str;
default = "";
description = "The client ID for the OIDC client.";
example = "your-client-id";
};
client_secret_path = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to a file containing the OIDC client secret.
'';
example = "config.sops.secrets.oidc_client_secret.path";
};
disable_api_key_login = mkOption {
type = types.bool;
default = false;
description = "Whether to disable API key login.";
};
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;
default = null;
description = ''
DEPRECATED: Use headscale.api_key_path instead.
Path to a file containing the Headscale API key.
'';
example = "config.sops.secrets.headscale_api_key.path";
};
use_pkce = mkOption {
type = types.bool;
default = false;
description = "Whether to use PKCE when authenticating users.";
};
default_role = mkOption {
type = types.enum [
"admin"
"network_admin"
"it_admin"
"auditor"
"viewer"
"member"
];
default = "member";
description = ''
Role assigned to newly created OIDC users after the first owner is bootstrapped.
The owner role is reserved for the first-login bootstrap.
'';
};
role_claim = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Optional OIDC claim containing the Headplane role to assign to newly created users.
A valid role claim takes precedence over default_role.
'';
example = "headplane_role";
};
};
};
default = {};
description = "OIDC Configuration for authentication.";
};
};
};
default = {};
};
};
}