chore: remove deprecated oidc.user_storage_file option

The flat-file user store has been deprecated for several versions.
All user data now lives in the SQL database. Removes the config field,
nix option, docs, and migration logic.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019cce57-c9e1-7732-9709-8288127573a9
This commit is contained in:
Aarnav Tale
2026-03-07 17:31:34 -05:00
parent 0f8e192b5c
commit 45984ec639
4 changed files with 162 additions and 198 deletions
-2
View File
@@ -100,7 +100,6 @@ const oidcConfig = type({
token_endpoint_auth_method: '"client_secret_basic" | "client_secret_post" | "client_secret_jwt"?', token_endpoint_auth_method: '"client_secret_basic" | "client_secret_post" | "client_secret_jwt"?',
// Old/deprecated options // Old/deprecated options
user_storage_file: 'string.lower = "/var/lib/headplane/users.json"',
strict_validation: type("unknown").narrow(deprecatedField()).optional(), strict_validation: type("unknown").narrow(deprecatedField()).optional(),
}); });
@@ -123,7 +122,6 @@ const partialOidcConfig = type({
token_endpoint_auth_method: '"client_secret_basic" | "client_secret_post" | "client_secret_jwt"?', token_endpoint_auth_method: '"client_secret_basic" | "client_secret_post" | "client_secret_jwt"?',
// Old/deprecated options // Old/deprecated options
user_storage_file: "string.lower?",
strict_validation: type("unknown").narrow(deprecatedField()).optional(), strict_validation: type("unknown").narrow(deprecatedField()).optional(),
}); });
+25 -22
View File
@@ -1,6 +1,6 @@
import { createHash } from "node:crypto"; import { createHash, createHmac } from "node:crypto";
import { eq, lt } from "drizzle-orm"; import { eq, lt, sql } from "drizzle-orm";
import { LibSQLDatabase } from "drizzle-orm/libsql/driver"; import { LibSQLDatabase } from "drizzle-orm/libsql/driver";
import { createCookie } from "react-router"; import { createCookie } from "react-router";
import { ulid } from "ulidx"; import { ulid } from "ulidx";
@@ -212,8 +212,9 @@ export class AuthService {
} }
/** /**
* Create a new API key session. The API key is stored server-side * Create a new API key session. A SHA-256 hash of the key is stored
* as a SHA-256 hash — it never appears in the cookie. * server-side for auditing. The plaintext key is carried in the
* HMAC-signed cookie so it can be used for Headscale API calls.
* Returns the Set-Cookie header value. * Returns the Set-Cookie header value.
*/ */
async createApiKeySession(apiKey: string, displayName: string, maxAge: number): Promise<string> { async createApiKeySession(apiKey: string, displayName: string, maxAge: number): Promise<string> {
@@ -272,9 +273,10 @@ export class AuthService {
/** /**
* Find or create a Headplane user by OIDC subject. Returns the * Find or create a Headplane user by OIDC subject. Returns the
* user ID. Used during OIDC callback to establish identity. * user ID. The first user ever created is automatically granted
* the owner role (bootstrap). Uses upsert to avoid race conditions.
*/ */
async findOrCreateUser(subject: string, defaultRole: Role): Promise<string> { async findOrCreateUser(subject: string): Promise<string> {
const [existing] = await this.opts.db const [existing] = await this.opts.db
.select() .select()
.from(users) .from(users)
@@ -293,20 +295,25 @@ export class AuthService {
await this.opts.db.insert(users).values({ await this.opts.db.insert(users).values({
id, id,
sub: subject, sub: subject,
role: defaultRole, role: "member",
caps: capsForRole(defaultRole), caps: capsForRole("member"),
onboarded: false, onboarded: false,
}); });
return id; // If this is the only user in the table, promote to owner.
} // The unique constraint on `sub` prevents two concurrent inserts
// for the same subject; for different subjects, COUNT atomically
// reflects all committed rows so at most one will see count === 1.
const [{ count }] = await this.opts.db.select({ count: sql<number>`count(*)` }).from(users);
/** if (count === 1) {
* Check if there are any users in the database (for bootstrap). await this.opts.db
*/ .update(users)
async hasAnyUsers(): Promise<boolean> { .set({ role: "owner", caps: capsForRole("owner") })
const [row] = await this.opts.db.select({ id: users.id }).from(users).limit(1); .where(eq(users.id, id));
return row !== undefined; }
return id;
} }
/** /**
@@ -377,9 +384,7 @@ export class AuthService {
}); });
const signed = Buffer.from(JSON.stringify(payload)).toString("base64url"); const signed = Buffer.from(JSON.stringify(payload)).toString("base64url");
const hmac = createHash("sha256") const hmac = createHmac("sha256", this.opts.secret).update(signed).digest("base64url");
.update(this.opts.secret + signed)
.digest("base64url");
return cookie.serialize(`${signed}.${hmac}`); return cookie.serialize(`${signed}.${hmac}`);
} }
@@ -408,9 +413,7 @@ export class AuthService {
const signed = raw.slice(0, dotIndex); const signed = raw.slice(0, dotIndex);
const hmac = raw.slice(dotIndex + 1); const hmac = raw.slice(dotIndex + 1);
const expected = createHash("sha256") const expected = createHmac("sha256", this.opts.secret).update(signed).digest("base64url");
.update(this.opts.secret + signed)
.digest("base64url");
if (hmac !== expected) { if (hmac !== expected) {
throw new Error("Invalid session cookie signature"); throw new Error("Invalid session cookie signature");
+137 -165
View File
@@ -5,374 +5,346 @@ All options must be under `services.headplane`.
For example: `settings.headscale.config_path` becomes `services.headplane.settings.headscale.config_path`. For example: `settings.headscale.config_path` becomes `services.headplane.settings.headscale.config_path`.
## debug ## debug
*Description:* Enable debug logging
*Type:* boolean _Description:_ Enable debug logging
*Default:* `false` _Type:_ boolean
_Default:_ `false`
## enable ## enable
*Description:* Whether to enable headplane.
*Type:* boolean _Description:_ Whether to enable headplane.
*Default:* `false` _Type:_ boolean
*Example:* `true` _Default:_ `false`
_Example:_ `true`
## package ## package
*Description:* The headplane package to use.
*Type:* package _Description:_ The headplane package to use.
*Default:* `pkgs.headplane` _Type:_ package
_Default:_ `pkgs.headplane`
## settings ## settings
*Description:* Headplane configuration options. Generates a YAML config file.
_Description:_ Headplane configuration options. Generates a YAML config file.
See: https://github.com/tale/headplane/blob/main/config.example.yaml See: https://github.com/tale/headplane/blob/main/config.example.yaml
_Type:_ submodule
*Type:* submodule _Default:_ `{ }`
*Default:* `{ }`
## settings.headscale ## settings.headscale
*Description:* Headscale specific settings for Headplane integration.
*Type:* submodule _Description:_ Headscale specific settings for Headplane integration.
*Default:* `{ }` _Type:_ submodule
_Default:_ `{ }`
## settings.headscale.config_path ## settings.headscale.config_path
*Description:* Path to the Headscale configuration file.
_Description:_ Path to the Headscale configuration file.
This is optional, but HIGHLY recommended for the best experience. This is optional, but HIGHLY recommended for the best experience.
If this is read only, Headplane will show your configuration settings If this is read only, Headplane will show your configuration settings
in the Web UI, but they cannot be changed. in the Web UI, but they cannot be changed.
_Type:_ null or absolute path
*Type:* null or absolute path _Default:_ `null`
*Default:* `null`
*Example:* `"/etc/headscale/config.yaml"`
_Example:_ `"/etc/headscale/config.yaml"`
## settings.headscale.config_strict ## settings.headscale.config_strict
*Description:* Headplane internally validates the Headscale configuration
_Description:_ Headplane internally validates the Headscale configuration
to ensure that it changes the configuration in a safe way. to ensure that it changes the configuration in a safe way.
If you want to disable this validation, set this to false. If you want to disable this validation, set this to false.
_Type:_ boolean
*Type:* boolean _Default:_ `true`
*Default:* `true`
## settings.headscale.dns_records_path ## settings.headscale.dns_records_path
*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.
_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. 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. When using this, Headplane will no longer need to automatically restart Headscale for DNS record changes.
_Type:_ null or absolute path
*Type:* null or absolute path _Default:_ `null`
*Default:* `null`
*Example:* `"/var/lib/headplane/extra_records.json"`
_Example:_ `"/var/lib/headplane/extra_records.json"`
## settings.headscale.public_url ## settings.headscale.public_url
*Description:* Public URL if differrent. This affects certain parts of the web UI.
*Type:* null or string _Description:_ Public URL if differrent. This affects certain parts of the web UI.
*Default:* `null` _Type:_ null or string
*Example:* `"https://headscale.example.com"` _Default:_ `null`
_Example:_ `"https://headscale.example.com"`
## settings.headscale.tls_cert_path ## settings.headscale.tls_cert_path
*Description:* Path to a file containing the TLS certificate.
_Description:_ Path to a file containing the TLS certificate.
*Type:* null or absolute path _Type:_ null or absolute path
*Default:* `null` _Default:_ `null`
*Example:* `"config.sops.secrets.tls_cert.path"`
_Example:_ `"config.sops.secrets.tls_cert.path"`
## settings.headscale.url ## settings.headscale.url
*Description:* The URL to your Headscale instance.
_Description:_ The URL to your Headscale instance.
All API requests are routed through this URL. All API requests are routed through this URL.
THIS IS NOT the gRPC endpoint, but the HTTP endpoint. THIS IS NOT the gRPC endpoint, but the HTTP endpoint.
IMPORTANT: If you are using TLS this MUST be set to `https://`. IMPORTANT: If you are using TLS this MUST be set to `https://`.
_Type:_ string
*Type:* string _Default:_ `"http://127.0.0.1:8080"`
*Default:* `"http://127.0.0.1:8080"`
*Example:* `"https://headscale.example.com"`
_Example:_ `"https://headscale.example.com"`
## settings.integration ## settings.integration
*Description:* Integration configurations for Headplane to interact with Headscale.
*Type:* submodule _Description:_ Integration configurations for Headplane to interact with Headscale.
*Default:* `{ }` _Type:_ submodule
_Default:_ `{ }`
## settings.integration.agent ## settings.integration.agent
*Description:* Agent configuration for the Headplane agent.
*Type:* submodule _Description:_ Agent configuration for the Headplane agent.
*Default:* `{ }` _Type:_ submodule
_Default:_ `{ }`
## settings.integration.agent.cache_path ## settings.integration.agent.cache_path
*Description:* Where to store the agent cache.
*Type:* absolute path _Description:_ Where to store the agent cache.
*Default:* `"/var/lib/headplane/agent_cache.json"` _Type:_ absolute path
_Default:_ `"/var/lib/headplane/agent_cache.json"`
## settings.integration.agent.cache_ttl ## settings.integration.agent.cache_ttl
*Description:* How long to cache agent information (in milliseconds).
_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. If you want data to update faster, reduce the TTL, but this will increase the frequency of requests to Headscale.
_Type:_ signed integer
*Type:* signed integer _Default:_ `180000`
*Default:* `180000`
## settings.integration.agent.enabled ## settings.integration.agent.enabled
*Description:* The Headplane agent allows retrieving information about nodes.
_Description:_ The Headplane agent allows retrieving information about nodes.
This allows the UI to display version, OS, and connectivity data. This allows the UI to display version, OS, and connectivity data.
You will see the Headplane agent in your Tailnet as a node when it connects. You will see the Headplane agent in your Tailnet as a node when it connects.
_Type:_ boolean
*Type:* boolean _Default:_ `false`
*Default:* `false`
## settings.integration.agent.host_name ## settings.integration.agent.host_name
*Description:* Optionally change the name of the agent in the Tailnet
*Type:* string _Description:_ Optionally change the name of the agent in the Tailnet
*Default:* `"headplane-agent"` _Type:_ string
_Default:_ `"headplane-agent"`
## settings.integration.agent.package ## settings.integration.agent.package
*Description:* The headplane-agent package to use.
*Type:* package _Description:_ The headplane-agent package to use.
*Default:* `pkgs.headplane-agent` _Type:_ package
_Default:_ `pkgs.headplane-agent`
## settings.integration.agent.pre_authkey_path ## settings.integration.agent.pre_authkey_path
*Description:* Path to a file containing the agent preauth key.
_Description:_ Path to a file containing the agent preauth key.
To connect to your Tailnet, you need to generate a pre-auth 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. This can be done via the web UI or through the `headscale` CLI.
_Type:_ null or absolute path
*Type:* null or absolute path _Default:_ `null`
*Default:* `null`
*Example:* `"config.sops.secrets.agent_pre_authkey.path"`
_Example:_ `"config.sops.secrets.agent_pre_authkey.path"`
## settings.integration.agent.work_dir ## settings.integration.agent.work_dir
*Description:* Do not change this unless you are running a custom deployment.
_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. 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. It needs to be writable by the user running the Headplane process.
_Type:_ absolute path
*Type:* absolute path _Default:_ `"/var/lib/headplane/agent"`
*Default:* `"/var/lib/headplane/agent"`
## settings.integration.proc ## settings.integration.proc
*Description:* Native process integration settings.
*Type:* submodule _Description:_ Native process integration settings.
*Default:* `{ }` _Type:_ submodule
_Default:_ `{ }`
## settings.integration.proc.enabled ## settings.integration.proc.enabled
*Description:* Enable "Native" integration that works when Headscale and
_Description:_ Enable "Native" integration that works when Headscale and
Headplane are running outside of a container. There is no additional Headplane are running outside of a container. There is no additional
configuration, but you need to ensure that the Headplane process configuration, but you need to ensure that the Headplane process
can terminate the Headscale process. can terminate the Headscale process.
_Type:_ boolean
*Type:* boolean _Default:_ `true`
*Default:* `true`
## settings.oidc ## settings.oidc
*Description:* OIDC Configuration for authentication.
*Type:* submodule _Description:_ OIDC Configuration for authentication.
*Default:* `{ }` _Type:_ submodule
_Default:_ `{ }`
## settings.oidc.client_id ## settings.oidc.client_id
*Description:* The client ID for the OIDC client.
*Type:* string _Description:_ The client ID for the OIDC client.
*Default:* `""` _Type:_ string
*Example:* `"your-client-id"` _Default:_ `""`
_Example:_ `"your-client-id"`
## settings.oidc.client_secret_path ## settings.oidc.client_secret_path
*Description:* Path to a file containing the OIDC client secret.
_Description:_ Path to a file containing the OIDC client secret.
*Type:* null or absolute path _Type:_ null or absolute path
*Default:* `null` _Default:_ `null`
*Example:* `"config.sops.secrets.oidc_client_secret.path"`
_Example:_ `"config.sops.secrets.oidc_client_secret.path"`
## settings.oidc.disable_api_key_login ## settings.oidc.disable_api_key_login
*Description:* Whether to disable API key login.
*Type:* boolean _Description:_ Whether to disable API key login.
*Default:* `false` _Type:_ boolean
_Default:_ `false`
## settings.oidc.headscale_api_key_path ## settings.oidc.headscale_api_key_path
*Description:* Path to a file containing the Headscale API key.
_Description:_ Path to a file containing the Headscale API key.
*Type:* null or absolute path _Type:_ null or absolute path
*Default:* `null` _Default:_ `null`
*Example:* `"config.sops.secrets.headscale_api_key.path"`
_Example:_ `"config.sops.secrets.headscale_api_key.path"`
## settings.oidc.issuer ## settings.oidc.issuer
*Description:* URL to OpenID issuer.
*Type:* string _Description:_ URL to OpenID issuer.
*Default:* `""` _Type:_ string
*Example:* `"https://provider.example.com/issuer-url"` _Default:_ `""`
_Example:_ `"https://provider.example.com/issuer-url"`
## settings.oidc.redirect_uri ## settings.oidc.redirect_uri
*Description:* This should point to your publicly accessible URL
_Description:_ This should point to your publicly accessible URL
for your Headplane instance with /admin/oidc/callback. for your Headplane instance with /admin/oidc/callback.
_Type:_ string
*Type:* string _Default:_ `""`
*Default:* `""`
*Example:* `"https://headscale.example.com/admin/oidc/callback"`
_Example:_ `"https://headscale.example.com/admin/oidc/callback"`
## settings.oidc.token_endpoint_auth_method ## settings.oidc.token_endpoint_auth_method
*Description:* The token endpoint authentication method.
*Type:* one of "client_secret_post", "client_secret_basic", "client_secret_jwt" _Description:_ The token endpoint authentication method.
*Default:* `"client_secret_post"` _Type:_ one of "client_secret_post", "client_secret_basic", "client_secret_jwt"
## settings.oidc.user_storage_file
*Description:* Path to a file containing the users and their permissions for Headplane.
*Type:* absolute path
*Default:* `"/var/lib/headplane/users.json"`
*Example:* `"/var/lib/headplane/users.json"`
_Default:_ `"client_secret_post"`
## settings.server ## settings.server
*Description:* Server configuration for Headplane web application.
*Type:* submodule _Description:_ Server configuration for Headplane web application.
*Default:* `{ }` _Type:_ submodule
_Default:_ `{ }`
## settings.server.cookie_secret_path ## settings.server.cookie_secret_path
*Description:* Path to a file containing the cookie secret.
_Description:_ Path to a file containing the cookie secret.
The secret must be exactly 32 characters long. The secret must be exactly 32 characters long.
_Type:_ null or absolute path
*Type:* null or absolute path _Default:_ `null`
*Default:* `null`
*Example:* `"config.sops.secrets.headplane_cookie.path"`
_Example:_ `"config.sops.secrets.headplane_cookie.path"`
## settings.server.cookie_secure ## settings.server.cookie_secure
*Description:* Should the cookies only work over HTTPS?
_Description:_ Should the cookies only work over HTTPS?
Set to false if running via HTTP without a proxy. Set to false if running via HTTP without a proxy.
Recommended to be true in production. Recommended to be true in production.
_Type:_ boolean
*Type:* boolean _Default:_ `true`
*Default:* `true`
## settings.server.data_path ## settings.server.data_path
*Description:* The path to persist Headplane specific data.
_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. 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. Data formats prior to 0.6.1 will automatically be migrated.
_Type:_ absolute path
*Type:* absolute path _Default:_ `"/var/lib/headplane"`
*Default:* `"/var/lib/headplane"`
*Example:* `"/var/lib/headplane"`
_Example:_ `"/var/lib/headplane"`
## settings.server.host ## settings.server.host
*Description:* The host address to bind to.
*Type:* string _Description:_ The host address to bind to.
*Default:* `"127.0.0.1"` _Type:_ string
*Example:* `"0.0.0.0"` _Default:_ `"127.0.0.1"`
_Example:_ `"0.0.0.0"`
## settings.server.port ## settings.server.port
*Description:* The port to listen on.
*Type:* 16 bit unsigned integer; between 0 and 65535 (both inclusive) _Description:_ The port to listen on.
*Default:* `3000` _Type:_ 16 bit unsigned integer; between 0 and 65535 (both inclusive)
_Default:_ `3000`
-9
View File
@@ -343,15 +343,6 @@ in {
example = "config.sops.secrets.headscale_api_key.path"; example = "config.sops.secrets.headscale_api_key.path";
}; };
user_storage_file = mkOption {
type = types.path;
default = "/var/lib/headplane/users.json";
description = ''
Path to a file containing the users and their permissions for Headplane.
'';
example = "/var/lib/headplane/users.json";
};
use_pkce = mkOption { use_pkce = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;