Sidecars read the vault's key and restart when it is remade; a stale WebUI attempt bans for a minute
The jackett, sonarr, radarr and lidarr runtimes discovered the key from the software's config at start, so after a rotation they held the old one. They now read the vault's file first and restart when it is remade. qBittorrent's init sets WebUI\BanDuration=60 so a consumer's step still carrying the previous value no longer locks the others out for an hour.
This commit is contained in:
@@ -32,6 +32,12 @@ function meshConfig(file?: string): Record<string, string> {
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
/** The vault's copy of the key, where the runtime mounts it (ADR 0158): read fresh, trimmed, or null. */
|
||||
function keyFile(file?: string): string | null {
|
||||
if (!file) return null;
|
||||
try { const v = readFileSync(file, "utf8").trim(); return v.length ? v : null; } catch { return null; }
|
||||
}
|
||||
|
||||
export class JackettClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
@@ -54,7 +60,7 @@ export class JackettClient {
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): JackettClient {
|
||||
const cfg = meshConfig(env.MESH_JACKETT_CONFIG_FILE);
|
||||
const url = cfg.url ?? env.MESH_JACKETT_URL;
|
||||
const apiKey = cfg.apiKey ?? env.MESH_JACKETT_API_KEY
|
||||
const apiKey = cfg.apiKey ?? keyFile(env.MESH_JACKETT_API_KEY_FILE) ?? env.MESH_JACKETT_API_KEY
|
||||
?? JackettClient.detectApiKey(env.MESH_JACKETT_CONFIG_DIR ?? "/config");
|
||||
if (!url) throw new Error("no Jackett URL — set MESH_JACKETT_URL");
|
||||
if (!apiKey) throw new Error("no Jackett API key — set MESH_JACKETT_API_KEY or make the config dir readable");
|
||||
|
||||
@@ -108,16 +108,19 @@
|
||||
"volumes": [
|
||||
"${dir:mesh-state}/broker:/run/secrets/broker:ro",
|
||||
"${dir:state}/config.json:/run/config/config.json:ro",
|
||||
"${dir:config}:/var/lib/jackett/config:ro"
|
||||
"${dir:config}:/var/lib/jackett/config:ro",
|
||||
"${dir:mesh-state}/api-key:/run/secrets/api-key:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_JACKETT_URL": "http://127.0.0.1:${port:9117}",
|
||||
"MESH_JACKETT_CONFIG_FILE": "/run/config/config.json",
|
||||
"MESH_JACKETT_CONFIG_DIR": "/var/lib/jackett/config"
|
||||
"MESH_JACKETT_CONFIG_DIR": "/var/lib/jackett/config",
|
||||
"MESH_JACKETT_API_KEY_FILE": "/run/secrets/api-key"
|
||||
},
|
||||
"restart-on": [
|
||||
"runtime-config"
|
||||
"runtime-config",
|
||||
"needs-api-key"
|
||||
],
|
||||
"artifact": "runtime"
|
||||
}
|
||||
|
||||
@@ -33,6 +33,12 @@ export interface LidarrContentItem {
|
||||
monitored: boolean;
|
||||
}
|
||||
|
||||
/** The vault's copy of the key, where the runtime mounts it (ADR 0158): read fresh, trimmed, or null. */
|
||||
function keyFile(file?: string): string | null {
|
||||
if (!file) return null;
|
||||
try { const v = readFileSync(file, "utf8").trim(); return v.length ? v : null; } catch { return null; }
|
||||
}
|
||||
|
||||
export class LidarrClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
@@ -54,7 +60,7 @@ export class LidarrClient {
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): LidarrClient {
|
||||
const url = env.MESH_LIDARR_URL ?? `http://127.0.0.1:${env.MESH_LIDARR_PORT ?? "8686"}`;
|
||||
const configDir = env.MESH_LIDARR_CONFIG_DIR ?? "/config";
|
||||
const apiKey = env.MESH_LIDARR_API_KEY ?? LidarrClient.detectApiKey(configDir);
|
||||
const apiKey = keyFile(env.MESH_LIDARR_API_KEY_FILE) ?? env.MESH_LIDARR_API_KEY ?? LidarrClient.detectApiKey(configDir);
|
||||
if (!apiKey) {
|
||||
throw new Error("Lidarr not configured — set MESH_LIDARR_API_KEY or make the config dir readable");
|
||||
}
|
||||
|
||||
@@ -131,14 +131,19 @@
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"${dir:mesh-state}/broker:/run/secrets/broker:ro",
|
||||
"${dir:config}:/var/lib/lidarr/config:ro"
|
||||
"${dir:config}:/var/lib/lidarr/config:ro",
|
||||
"${dir:mesh-state}/api-key:/run/secrets/api-key:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_LIDARR_URL": "http://127.0.0.1:${port:8686}",
|
||||
"MESH_LIDARR_CONFIG_DIR": "/var/lib/lidarr/config"
|
||||
"MESH_LIDARR_CONFIG_DIR": "/var/lib/lidarr/config",
|
||||
"MESH_LIDARR_API_KEY_FILE": "/run/secrets/api-key"
|
||||
},
|
||||
"artifact": "runtime"
|
||||
"artifact": "runtime",
|
||||
"restart-on": [
|
||||
"needs-api-key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "downloads-config",
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
"type": "file",
|
||||
"path": "${dir:mesh-init}/10-mesh-webui-login.sh",
|
||||
"mode": "0755",
|
||||
"content": "#!/bin/bash\n# Written by the mesh (ADR 0158): this module's one credential is the vault's, shared with every\n# consumer; the file is applied here, before the service starts, on every start.\nset -euo pipefail\nCONF=/config/qBittorrent/qBittorrent.conf\nUSER='${setting:username}'\nPASS=$(cat /run/secrets/password)\nmkdir -p /config/qBittorrent\nHASH=$(python3 - \"$PASS\" <<'PY'\nimport sys, os, base64, hashlib\npw = sys.argv[1].encode(); salt = os.urandom(16)\ndk = hashlib.pbkdf2_hmac(\"sha512\", pw, salt, 100000, dklen=64)\nprint(\"@ByteArray(\" + base64.b64encode(salt).decode() + \":\" + base64.b64encode(dk).decode() + \")\")\nPY\n)\ntouch \"$CONF\"\ngrep -q '^\\[Preferences\\]' \"$CONF\" || printf '\\n[Preferences]\\n' >> \"$CONF\"\nfor kv in \"WebUI\\\\Username=$USER\" \"WebUI\\\\Password_PBKDF2=\\\"$HASH\\\"\"; do\n key=${kv%%=*}\n if grep -q \"^${key//\\\\/\\\\\\\\}=\" \"$CONF\"; then\n awk -v kv=\"$kv\" -v key=\"$key\" 'BEGIN{FS=OFS=\"=\"} $1==key {print kv; next} {print}' \"$CONF\" > \"$CONF.mesh\" && mv \"$CONF.mesh\" \"$CONF\"\n else\n awk -v kv=\"$kv\" '{print} /^\\[Preferences\\]/ && !done {print kv; done=1}' \"$CONF\" > \"$CONF.mesh\" && mv \"$CONF.mesh\" \"$CONF\"\n fi\ndone\necho \"[mesh] qBittorrent WebUI login set for $USER from the vault's credential\"\n"
|
||||
"content": "#!/bin/bash\n# Written by the mesh (ADR 0158): this module's one credential is the vault's, shared with every\n# consumer; the file is applied here, before the service starts, on every start.\nset -euo pipefail\nCONF=/config/qBittorrent/qBittorrent.conf\nUSER='${setting:username}'\nPASS=$(cat /run/secrets/password)\nmkdir -p /config/qBittorrent\nHASH=$(python3 - \"$PASS\" <<'PY'\nimport sys, os, base64, hashlib\npw = sys.argv[1].encode(); salt = os.urandom(16)\ndk = hashlib.pbkdf2_hmac(\"sha512\", pw, salt, 100000, dklen=64)\nprint(\"@ByteArray(\" + base64.b64encode(salt).decode() + \":\" + base64.b64encode(dk).decode() + \")\")\nPY\n)\ntouch \"$CONF\"\ngrep -q '^\\[Preferences\\]' \"$CONF\" || printf '\\n[Preferences]\\n' >> \"$CONF\"\nfor kv in \"WebUI\\\\Username=$USER\" \"WebUI\\\\Password_PBKDF2=\\\"$HASH\\\"\" \"WebUI\\\\BanDuration=60\"; do\n key=${kv%%=*}\n if grep -q \"^${key//\\\\/\\\\\\\\}=\" \"$CONF\"; then\n awk -v kv=\"$kv\" -v key=\"$key\" 'BEGIN{FS=OFS=\"=\"} $1==key {print kv; next} {print}' \"$CONF\" > \"$CONF.mesh\" && mv \"$CONF.mesh\" \"$CONF\"\n else\n awk -v kv=\"$kv\" '{print} /^\\[Preferences\\]/ && !done {print kv; done=1}' \"$CONF\" > \"$CONF.mesh\" && mv \"$CONF.mesh\" \"$CONF\"\n fi\ndone\necho \"[mesh] qBittorrent WebUI login set for $USER from the vault's credential\"\n"
|
||||
},
|
||||
{
|
||||
"id": "server",
|
||||
|
||||
@@ -34,6 +34,12 @@ export interface RadarrContentItem {
|
||||
monitored: boolean;
|
||||
}
|
||||
|
||||
/** The vault's copy of the key, where the runtime mounts it (ADR 0158): read fresh, trimmed, or null. */
|
||||
function keyFile(file?: string): string | null {
|
||||
if (!file) return null;
|
||||
try { const v = readFileSync(file, "utf8").trim(); return v.length ? v : null; } catch { return null; }
|
||||
}
|
||||
|
||||
export class RadarrClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
@@ -55,7 +61,7 @@ export class RadarrClient {
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): RadarrClient {
|
||||
const url = env.MESH_RADARR_URL ?? `http://127.0.0.1:${env.MESH_RADARR_PORT ?? "7878"}`;
|
||||
const configDir = env.MESH_RADARR_CONFIG_DIR ?? "/config";
|
||||
const apiKey = env.MESH_RADARR_API_KEY ?? RadarrClient.detectApiKey(configDir);
|
||||
const apiKey = keyFile(env.MESH_RADARR_API_KEY_FILE) ?? env.MESH_RADARR_API_KEY ?? RadarrClient.detectApiKey(configDir);
|
||||
if (!apiKey) {
|
||||
throw new Error("Radarr not configured — set MESH_RADARR_API_KEY or make the config dir readable");
|
||||
}
|
||||
|
||||
@@ -125,14 +125,19 @@
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"${dir:mesh-state}/broker:/run/secrets/broker:ro",
|
||||
"${dir:config}:/var/lib/radarr/config:ro"
|
||||
"${dir:config}:/var/lib/radarr/config:ro",
|
||||
"${dir:mesh-state}/api-key:/run/secrets/api-key:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_RADARR_URL": "http://127.0.0.1:${port:7878}",
|
||||
"MESH_RADARR_CONFIG_DIR": "/var/lib/radarr/config"
|
||||
"MESH_RADARR_CONFIG_DIR": "/var/lib/radarr/config",
|
||||
"MESH_RADARR_API_KEY_FILE": "/run/secrets/api-key"
|
||||
},
|
||||
"artifact": "runtime"
|
||||
"artifact": "runtime",
|
||||
"restart-on": [
|
||||
"needs-api-key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "downloads-config",
|
||||
|
||||
@@ -34,6 +34,12 @@ export interface SonarrContentItem {
|
||||
monitored: boolean;
|
||||
}
|
||||
|
||||
/** The vault's copy of the key, where the runtime mounts it (ADR 0158): read fresh, trimmed, or null. */
|
||||
function keyFile(file?: string): string | null {
|
||||
if (!file) return null;
|
||||
try { const v = readFileSync(file, "utf8").trim(); return v.length ? v : null; } catch { return null; }
|
||||
}
|
||||
|
||||
export class SonarrClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
@@ -55,7 +61,7 @@ export class SonarrClient {
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): SonarrClient {
|
||||
const url = env.MESH_SONARR_URL ?? `http://127.0.0.1:${env.MESH_SONARR_PORT ?? "8989"}`;
|
||||
const configDir = env.MESH_SONARR_CONFIG_DIR ?? "/config";
|
||||
const apiKey = env.MESH_SONARR_API_KEY ?? SonarrClient.detectApiKey(configDir);
|
||||
const apiKey = keyFile(env.MESH_SONARR_API_KEY_FILE) ?? env.MESH_SONARR_API_KEY ?? SonarrClient.detectApiKey(configDir);
|
||||
if (!apiKey) {
|
||||
throw new Error("Sonarr not configured — set MESH_SONARR_API_KEY or make the config dir readable");
|
||||
}
|
||||
|
||||
@@ -131,14 +131,19 @@
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"${dir:mesh-state}/broker:/run/secrets/broker:ro",
|
||||
"${dir:config}:/var/lib/sonarr/config:ro"
|
||||
"${dir:config}:/var/lib/sonarr/config:ro",
|
||||
"${dir:mesh-state}/api-key:/run/secrets/api-key:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_SONARR_URL": "http://127.0.0.1:${port:8989}",
|
||||
"MESH_SONARR_CONFIG_DIR": "/var/lib/sonarr/config"
|
||||
"MESH_SONARR_CONFIG_DIR": "/var/lib/sonarr/config",
|
||||
"MESH_SONARR_API_KEY_FILE": "/run/secrets/api-key"
|
||||
},
|
||||
"artifact": "runtime"
|
||||
"artifact": "runtime",
|
||||
"restart-on": [
|
||||
"needs-api-key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "downloads-config",
|
||||
|
||||
Reference in New Issue
Block a user