mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(cli): generate per-install secrets instead of using fixed values (#6433)
* fix(cli): generate per-install secrets instead of using fixed values The launcher passed the same built-in BETTER_AUTH_SECRET and ENCRYPTION_KEY to every install. Generate them once per install, persist them 0600 at ~/.simstudio/secrets.env, and reuse them on later runs so data already in the Postgres volume stays readable. Also passes INTERNAL_API_SECRET, which the realtime container requires and never received. * fix(cli): reassert owner-only permissions on the secrets file writeFileSync's `mode` applies only when it creates the file, and the write is skipped entirely when the stored values are already valid — so a secrets file left with permissive permissions kept them. chmod it on every run. * fix(cli): write the secrets file atomically Regenerating any one key rewrites all of them, and a plain write truncates first — a crash mid-rewrite would strand a still-valid ENCRYPTION_KEY and orphan the data it protects. Write to a temp file and rename into place.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execSync, spawn } from 'child_process'
|
||||
import { existsSync, mkdirSync } from 'fs'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs'
|
||||
import { homedir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { createInterface } from 'readline'
|
||||
@@ -15,6 +16,55 @@ const REALTIME_CONTAINER = 'simstudio-realtime'
|
||||
const APP_CONTAINER = 'simstudio-app'
|
||||
const DEFAULT_PORT = '3000'
|
||||
|
||||
const SECRET_KEYS = ['BETTER_AUTH_SECRET', 'ENCRYPTION_KEY', 'INTERNAL_API_SECRET'] as const
|
||||
|
||||
const AES_KEY_PATTERN = /^[0-9a-f]{64}$/i
|
||||
|
||||
/**
|
||||
* Per-install secrets, generated on first run and reused afterwards.
|
||||
*
|
||||
* They have to persist: `ENCRYPTION_KEY` decrypts credentials already stored in the
|
||||
* Postgres volume under `~/.simstudio/data`, so minting a fresh one each launch would
|
||||
* leave that data permanently unreadable. Any value that is not a 32-byte hex key is
|
||||
* replaced.
|
||||
*
|
||||
* Regenerating one key rewrites the whole file, so the write goes to a temp file and is
|
||||
* renamed into place: a plain write truncates first, and a crash mid-write would strand a
|
||||
* still-valid `ENCRYPTION_KEY` and orphan the data it protects.
|
||||
*
|
||||
* Permissions are reasserted on every run: `writeFileSync`'s `mode` applies only when it
|
||||
* creates the file, so a file left by an earlier run — or one the user created — would
|
||||
* otherwise keep whatever mode it already had and stay readable by other local accounts.
|
||||
*/
|
||||
function resolveSecrets(): Record<string, string> {
|
||||
const configDir = join(homedir(), '.simstudio')
|
||||
const secretsPath = join(configDir, 'secrets.env')
|
||||
const secrets: Record<string, string> = {}
|
||||
|
||||
if (existsSync(secretsPath)) {
|
||||
for (const line of readFileSync(secretsPath, 'utf8').split('\n')) {
|
||||
const separator = line.indexOf('=')
|
||||
if (separator > 0) secrets[line.slice(0, separator).trim()] = line.slice(separator + 1).trim()
|
||||
}
|
||||
}
|
||||
|
||||
const missing = SECRET_KEYS.filter((key) => !AES_KEY_PATTERN.test(secrets[key] ?? ''))
|
||||
for (const key of missing) secrets[key] = randomBytes(32).toString('hex')
|
||||
|
||||
if (missing.length > 0) {
|
||||
mkdirSync(configDir, { recursive: true })
|
||||
const contents = SECRET_KEYS.map((key) => `${key}=${secrets[key]}`).join('\n')
|
||||
const pending = `${secretsPath}.tmp`
|
||||
writeFileSync(pending, `${contents}\n`, { mode: 0o600 })
|
||||
renameSync(pending, secretsPath)
|
||||
console.log(chalk.gray(`🔑 Generated local secrets in ${secretsPath}`))
|
||||
}
|
||||
|
||||
chmodSync(secretsPath, 0o600)
|
||||
|
||||
return secrets
|
||||
}
|
||||
|
||||
const program = new Command()
|
||||
|
||||
program.name('simstudio').description('Run Sim using Docker').version('0.1.0')
|
||||
@@ -196,6 +246,8 @@ async function main() {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const secrets = resolveSecrets()
|
||||
|
||||
// Start the realtime server
|
||||
console.log(chalk.blue('🔄 Starting Realtime Server...'))
|
||||
const realtimeSuccess = await runCommand([
|
||||
@@ -215,7 +267,9 @@ async function main() {
|
||||
'-e',
|
||||
`NEXT_PUBLIC_APP_URL=http://localhost:${port}`,
|
||||
'-e',
|
||||
'BETTER_AUTH_SECRET=your_auth_secret_here',
|
||||
`BETTER_AUTH_SECRET=${secrets.BETTER_AUTH_SECRET}`,
|
||||
'-e',
|
||||
`INTERNAL_API_SECRET=${secrets.INTERNAL_API_SECRET}`,
|
||||
'ghcr.io/simstudioai/realtime:latest',
|
||||
])
|
||||
|
||||
@@ -243,9 +297,11 @@ async function main() {
|
||||
'-e',
|
||||
`NEXT_PUBLIC_APP_URL=http://localhost:${port}`,
|
||||
'-e',
|
||||
'BETTER_AUTH_SECRET=your_auth_secret_here',
|
||||
`BETTER_AUTH_SECRET=${secrets.BETTER_AUTH_SECRET}`,
|
||||
'-e',
|
||||
'ENCRYPTION_KEY=your_encryption_key_here',
|
||||
`ENCRYPTION_KEY=${secrets.ENCRYPTION_KEY}`,
|
||||
'-e',
|
||||
`INTERNAL_API_SECRET=${secrets.INTERNAL_API_SECRET}`,
|
||||
'ghcr.io/simstudioai/simstudio:latest',
|
||||
])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user