fix(supply-depot): generate Homebox API key pepper so it stops crash-looping (#1043)
Homebox >= 0.26 (ghcr.io/sysadminsmedia/homebox:0.26.2) panics at boot unless HBOX_AUTH_API_KEY_PEPPER is set to a >= 32-byte value. The seeder ships no Env block, so every clean install crash-loops immediately. Generate a per-install pepper, persist it in the KV store (apps.homebox.apiKeyPepper), and inject it as HBOX_AUTH_API_KEY_PEPPER at every container-create path. The pepper is generated once and reused: rotating it would invalidate every API key a user has issued from Homebox. Inject at all three create paths so no lifecycle action drops it: - _createContainer (install / force-reinstall) - the service update path (heals a pre-fix container on update) - recreateCustomAppContainer (the Edit / reconfigure rebuild — the in-app docs tell users to add HBOX_OPTIONS_ALLOW_REGISTRATION=false via Manage > Edit, which rebuilds Env from container_config alone and would otherwise drop the pepper and re-trigger the crash loop) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6a4f02dd46
commit
d6e94cf3a5
|
|
@ -21,6 +21,7 @@ import { SERVICE_NAMES } from '../../constants/service_names.js'
|
|||
import { exec } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { readFile, mkdir, copyFile, chown, chmod, access, writeFile } from 'node:fs/promises'
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import KVStore from '#models/kv_store'
|
||||
import { BROADCAST_CHANNELS } from '../../constants/broadcast.js'
|
||||
import { KIWIX_LIBRARY_CMD } from '../../constants/kiwix.js'
|
||||
|
|
@ -810,6 +811,13 @@ export class DockerService {
|
|||
}
|
||||
}
|
||||
|
||||
const appEnv: string[] = []
|
||||
if (service.service_name === SERVICE_NAMES.HOMEBOX) {
|
||||
// Homebox >= 0.26 panics at boot without a >= 32-byte pepper (#1043). Generate once,
|
||||
// persist, and reuse so updates/reinstalls don't invalidate issued API keys.
|
||||
appEnv.push(`HBOX_AUTH_API_KEY_PEPPER=${await this._resolveHomeboxPepper()}`)
|
||||
}
|
||||
|
||||
this._broadcast(
|
||||
service.service_name,
|
||||
'creating',
|
||||
|
|
@ -827,7 +835,7 @@ export class DockerService {
|
|||
HostConfig: gpuHostConfig,
|
||||
...(containerConfig?.WorkingDir && { WorkingDir: containerConfig.WorkingDir }),
|
||||
...(containerConfig?.ExposedPorts && { ExposedPorts: containerConfig.ExposedPorts }),
|
||||
Env: [...(containerConfig?.Env ?? []), ...ollamaEnv],
|
||||
Env: [...(containerConfig?.Env ?? []), ...ollamaEnv, ...appEnv],
|
||||
...(service.container_command ? { Cmd: service.container_command.split(' ') } : {}),
|
||||
// Ensure container is attached to the Nomad docker network in production
|
||||
...(process.env.NODE_ENV === 'production' && {
|
||||
|
|
@ -1465,6 +1473,26 @@ export class DockerService {
|
|||
*
|
||||
* Returns null when no override should be applied.
|
||||
*/
|
||||
/**
|
||||
* Resolve the Homebox API-key pepper, generating and persisting one on first use.
|
||||
*
|
||||
* Homebox >= 0.26 panics at boot unless HBOX_AUTH_API_KEY_PEPPER is set to a value of at
|
||||
* least 32 bytes (#1043). The pepper must be STABLE across updates and reinstalls:
|
||||
* rotating it invalidates every API key a user has issued from Homebox. So we generate it
|
||||
* once and persist it in the KV store, then reuse it for the life of the install.
|
||||
*/
|
||||
private async _resolveHomeboxPepper(): Promise<string> {
|
||||
const existing = await KVStore.getValue('apps.homebox.apiKeyPepper')
|
||||
if (typeof existing === 'string' && existing.length >= 32) {
|
||||
return existing
|
||||
}
|
||||
// 48 raw bytes -> 64 base64 chars, comfortably over Homebox's 32-byte floor.
|
||||
const pepper = randomBytes(48).toString('base64')
|
||||
await KVStore.setValue('apps.homebox.apiKeyPepper', pepper)
|
||||
logger.info('[DockerService] Generated and persisted Homebox API key pepper')
|
||||
return pepper
|
||||
}
|
||||
|
||||
private async _resolveAmdHsaOverride(): Promise<string | null> {
|
||||
const manualRaw = await KVStore.getValue('ai.amdHsaOverride')
|
||||
if (manualRaw !== null && manualRaw !== undefined && String(manualRaw).trim() !== '') {
|
||||
|
|
@ -1694,6 +1722,15 @@ export class DockerService {
|
|||
}
|
||||
}
|
||||
|
||||
// Heal a Homebox container that predates the #1043 fix: inject the persisted pepper on
|
||||
// update if it's missing, so an update (not just a force-reinstall) fixes the crash loop.
|
||||
if (
|
||||
serviceName === SERVICE_NAMES.HOMEBOX &&
|
||||
!finalEnv.some((e: string) => e.startsWith('HBOX_AUTH_API_KEY_PEPPER='))
|
||||
) {
|
||||
finalEnv = [...finalEnv, `HBOX_AUTH_API_KEY_PEPPER=${await this._resolveHomeboxPepper()}`]
|
||||
}
|
||||
|
||||
const newContainerConfig: any = {
|
||||
Image: runtimeImage,
|
||||
name: serviceName,
|
||||
|
|
@ -2141,6 +2178,17 @@ export class DockerService {
|
|||
await this.pullImage(service.container_image)
|
||||
}
|
||||
|
||||
// Runtime-injected secrets (e.g. Homebox's API-key pepper, #1043) live in the KV store, not
|
||||
// in container_config. This path rebuilds Env from container_config alone, so re-inject them
|
||||
// here — otherwise an Edit or in-place recreate drops the pepper and Homebox crash-loops again.
|
||||
let recreateEnv: string[] = containerConfig?.Env ?? []
|
||||
if (
|
||||
serviceName === SERVICE_NAMES.HOMEBOX &&
|
||||
!recreateEnv.some((e: string) => e.startsWith('HBOX_AUTH_API_KEY_PEPPER='))
|
||||
) {
|
||||
recreateEnv = [...recreateEnv, `HBOX_AUTH_API_KEY_PEPPER=${await this._resolveHomeboxPepper()}`]
|
||||
}
|
||||
|
||||
const newContainer = await this.docker.createContainer({
|
||||
Image: service.container_image,
|
||||
name: serviceName,
|
||||
|
|
@ -2152,7 +2200,7 @@ export class DockerService {
|
|||
...(containerConfig?.User && { User: containerConfig.User }),
|
||||
HostConfig: containerConfig?.HostConfig ?? {},
|
||||
...(containerConfig?.ExposedPorts && { ExposedPorts: containerConfig.ExposedPorts }),
|
||||
...(containerConfig?.Env && { Env: containerConfig.Env }),
|
||||
...(recreateEnv.length ? { Env: recreateEnv } : {}),
|
||||
...(service.container_command ? { Cmd: service.container_command.split(' ') } : {}),
|
||||
...(process.env.NODE_ENV === 'production' && {
|
||||
NetworkingConfig: { EndpointsConfig: { [DockerService.NOMAD_NETWORK]: {} } },
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export const KV_STORE_SCHEMA = {
|
|||
'ai.amdHsaOverride': 'string',
|
||||
'ai.autoFixGpuPassthrough': 'boolean',
|
||||
'gpu.autoRemediatedAt': 'string',
|
||||
'apps.homebox.apiKeyPepper': 'string',
|
||||
} as const
|
||||
|
||||
type KVTagToType<T extends string> = T extends 'boolean' ? boolean : string
|
||||
|
|
|
|||
Loading…
Reference in New Issue