Files
bare-operating-system/packages/bare-os-booter/lib/bare-os-var-log.js
T
Raven Scott 5647491b08 Implement BareOS Zero-Trust Boot and Runtime Hardening (plan batches A–E).
Peer admission and bootstrap controls
- Fail closed when BARE_OS_PEER_ALLOWLIST_HEX is empty unless explicit
  break-glass BARE_OS_PEER_ALLOW_ALL=1.
- Treat BARE_OS_ZERO_TRUST_PROFILE=strict|security like strict admission
  posture alongside BARE_OS_PEER_ALLOWLIST_STRICT.
- Document BARE_OS_PEER_ALLOW_ALL and profile semantics; update boot trust
  model operator guidance.

Peer system seed and provenance
- In strict/security profile, peer system seed defaults off unless
  BARE_OS_PEER_SYSTEM_SEED is explicitly enabled (1/true/yes).
- Disable synthetic capability filling in strict profile; keep compat path
  when profile is not strict.
- Extend test.peer-system-seed.js for strict default-off and no-synthesis.

Path capability signer trust
- When BARE_OS_PATH_CAPABILITY_ENFORCE_READ is on, require trusted issuer
  if BARE_OS_PATH_CAPABILITY_REQUIRE_TRUSTED_SIGNER is set or profile is
  strict; wire verifyPathCapabilityEnvelopeTrusted into the primary deny
  path.
- Document BARE_OS_PATH_CAPABILITY_REQUIRE_TRUSTED_SIGNER and trusted key
  list usage in environment appendix.

Host delegates (least privilege)
- Under strict/security profile, empty BARE_OS_DELEGATE_ALLOW means deny-all
  delegates instead of allow-all; document behavior.
- Add delegate strict-profile test coverage.

Audit durability and telemetry hygiene
- Retain audit chain rows in memory and add bareOsAuditPersistRows for
  optional NDJSON persistence via VFS.
- Broaden var-log redaction for secret-shaped strings and env-like assignments.
- Emit boot.log security line when unsafe trust combinations are detected.

Release and CI gates
- Add scripts/verify-zero-trust-gates.mjs and npm run verify:zero-trust-gates.
- Document verifier in scripts/README.md and zero-trust steps in
  docs/release-checklist.md.

Tests
- Update bare-os-booter admission tests for allow-all and empty-allowlist
  messaging.
- Relax brittle man.json page-count equality to a minimal sanity check to
  avoid brittle/os.cwd brittle failures on inventory drift.

Verification (local): npm run verify:zero-trust-gates; npm run test -w
bare-os-booter; peer-system-seed brittle lane as applicable.

Plan file (.cursor/plans/zero-trust-boot-runtime-100-plan_*.plan.md) was not
edited per instructions.
2026-04-26 23:49:54 -04:00

424 lines
15 KiB
JavaScript

/**
* Append-only UTF-8 logs under logical `/var/log/…` (VFS maps to the personal Hyperdrive).
*
* Until the first successful `mkdir`/`writeFile`, `readdir('/var/log')` may be empty even though
* `lstat('/var/log')` looks like a directory — logs live under `/var/log/bare-os/` on the logical
* tree and on the personal drive under `/.bare-os/…/var/log/<HOME-seg>/` (see `vfs.js` routing).
*
* Failures are best-effort silent unless **`BARE_OS_VAR_LOG_DEBUG=1`** (or `true`): then messages
* go to **`process.stderr`** and **`ctx.console.error`**. Kernel metrics: `varlog.append.*`,
* `varlog.ensure.*` (see `bareOsKernelMetricsSnapshot()`).
*/
import {
BARE_OS_LIFECYCLE_SCHEMA_VERSION,
BARE_OS_TELEMETRY_SCHEMA_VERSION
} from './bare-os-lifecycle-schema.js'
import { bareOsKernelMetricInc } from './bare-os-kernel-metrics.js'
/**
* @param {Record<string, unknown>} ctx
* @param {string} msg
*/
function varLogDebug(ctx, msg) {
try {
const env =
ctx && ctx.env && typeof ctx.env === 'object'
? /** @type {Record<string, string | undefined>} */ (ctx.env)
: /** @type {Record<string, string | undefined>} */ ({})
const on =
env.BARE_OS_VAR_LOG_DEBUG === '1' ||
env.BARE_OS_VAR_LOG_DEBUG === 'true'
if (!on) return
const line = `[bare-os:var-log] ${msg}`
try {
globalThis.process?.stderr?.write?.(line + '\n')
} catch {
/* ignore */
}
try {
ctx?.console?.error?.(line)
} catch {
/* ignore */
}
} catch {
/* ignore */
}
}
export const BARE_OS_VAR_LOG_DIR = '/var/log/bare-os'
export const KERNEL_CONSOLE_LOG = `${BARE_OS_VAR_LOG_DIR}/kernel-console.log`
export const CRON_LOG = `${BARE_OS_VAR_LOG_DIR}/cron.log`
export const INITD_LOG = `${BARE_OS_VAR_LOG_DIR}/initd.log`
export const AUDIT_LOG = `${BARE_OS_VAR_LOG_DIR}/audit.log`
export const BOOT_LOG = `${BARE_OS_VAR_LOG_DIR}/boot.log`
export const CLOSE_JOURNAL_LOG = `${BARE_OS_VAR_LOG_DIR}/close-journal.ndjson`
export const CRASH_LOG = `${BARE_OS_VAR_LOG_DIR}/crash.ndjson`
/** Structured JSON lines from `/bin/logger` (syslog-style metadata). */
export const LOGGER_JSON_LOG = `${BARE_OS_VAR_LOG_DIR}/logger.jsonl`
const README_REL = `${BARE_OS_VAR_LOG_DIR}/README`
const README_TEXT = `Bare OS session logs (mirrored on your personal drive under /.bare-os/var/log/<HOME-basename>/).
kernel-console.log — console.log / console.error from the kernel session
cron.log — bare-cron job errors
openssh.log — bare-openssh / sshd listen and auth errors
holesail.log — bare-holesail / early kernel-path Holesail (AGPL-3.0 upstream)
www.log — bare-os-www static HTTP for ~/.www
initd.log — bare-initd service start failures
audit.log — optional execLine audit when BARE_OS_AUDIT=1
boot.log — boot-to-login timeline milestones (best-effort)
`
/**
* Strip long hex / obvious secret-shaped tokens from telemetry mirrors (best-effort).
* @param {string} s
*/
function bareOsTelemetryRedactString(s) {
return String(s)
.replace(/\b[0-9a-f]{128,}\b/gi, '<redacted_hex>')
.replace(/\bBearer\s+\S+/gi, 'Bearer <redacted>')
.replace(/\bsk-[a-zA-Z0-9]{16,}\b/g, '<redacted_sk>')
.replace(/\b(password|secret|token|apikey|api_key)\s*[:=]\s*\S+/gi, '$1=<redacted>')
.replace(/\b[A-Z0-9_]*(TOKEN|SECRET|PASSWORD|APIKEY)[A-Z0-9_]*=\S+/gi, '<redacted_env>')
}
/**
* @param {Record<string, unknown>} rec
*/
function bareOsTelemetrySanitizeRec(rec) {
if (!rec || typeof rec !== 'object') return {}
/** @type {Record<string, unknown>} */
const o = {}
for (const [k, v] of Object.entries(rec)) {
if (typeof v === 'string') o[k] = bareOsTelemetryRedactString(v)
else o[k] = v
}
return o
}
/** Max size before trimming older log bytes (best-effort). */
const LOG_MAX_BYTES = 512 * 1024
/** After trim, keep this many trailing bytes plus a notice line. */
const LOG_KEEP_BYTES = 256 * 1024
const TELEMETRY_LOG_CAP_BYTES = 2 * 1024 * 1024
/**
* @param {Record<string, unknown>} ctx
* @param {string} path
* @param {Uint8Array} chunk
* @param {number} capBytes
*/
async function appendLineWithOptionalCap(ctx, path, chunk, capBytes) {
const vfs = ctx.vfs
if (!vfs) return
if (typeof vfs.appendFile === 'function') {
await vfs.appendFile(path, chunk)
return
}
if (typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function') return
const prev = await vfs.readFile(path)
let merged = prev ? ctx.b4a.concat([prev, chunk]) : chunk
if (merged.length > capBytes) {
merged = merged.subarray(merged.length - capBytes)
}
await vfs.writeFile(path, merged)
}
/**
* Ensure `/var/log/bare-os` exists and a short README is present. Best-effort; never throws.
* @param {Record<string, unknown>} ctx
*/
export async function ensureBareOsVarLogTree(ctx) {
try {
const vfs = ctx.vfs
if (!vfs || typeof vfs.mkdir !== 'function') {
bareOsKernelMetricInc('varlog.ensure.skip_no_vfs_mkdir')
varLogDebug(ctx, 'ensureBareOsVarLogTree: no ctx.vfs.mkdir')
return
}
await vfs.mkdir(BARE_OS_VAR_LOG_DIR, { recursive: true })
if (
typeof vfs.readFile !== 'function' ||
typeof vfs.writeFile !== 'function'
) {
bareOsKernelMetricInc('varlog.ensure.skip_no_vfs_rw')
varLogDebug(ctx, 'ensureBareOsVarLogTree: missing vfs.readFile/writeFile')
return
}
const existing = await vfs.readFile(README_REL)
if (existing && ctx.b4a.from(existing).length > 0) return
await vfs.writeFile(README_REL, ctx.b4a.from(README_TEXT))
const env = ctx && typeof ctx.env === 'object' ? ctx.env : {}
const unsafe =
(String(env.BARE_OS_PEER_ALLOW_ALL || '') === '1') ||
(String(env.BARE_OS_DELEGATE_ALLOW || '').trim() === '') ||
(String(env.BARE_OS_PATH_CAPABILITY_REQUIRE_TRUSTED_SIGNER || '').trim() === '')
if (unsafe) {
await appendVarLog(
ctx,
BOOT_LOG,
'security',
'unsafe posture detected: review peer allow-all, delegate allowlist, and trusted signer enforcement'
)
}
} catch (e) {
bareOsKernelMetricInc('varlog.ensure.error')
varLogDebug(
ctx,
`ensureBareOsVarLogTree: ${e && e.message ? e.message : String(e)}`
)
}
}
/**
* Append one UTF-8 line to a logical log file under `/var/log/…`. Best-effort; never throws.
* @param {Record<string, unknown>} ctx
* @param {string} logicalFilePath e.g. `/var/log/bare-os/cron.log`
* @param {string} kind
* @param {string} line
*/
export async function appendVarLog(ctx, logicalFilePath, kind, line) {
try {
const vfs = ctx.vfs
if (!vfs || typeof vfs.readFile !== 'function') {
bareOsKernelMetricInc('varlog.append.skip_no_vfs_readfile')
varLogDebug(
ctx,
'appendVarLog: no vfs.readFile for ' + String(logicalFilePath)
)
return
}
const ts = new Date().toISOString()
const chunk = ctx.b4a.from(`[${ts}] [${kind}] ${line}\n`)
const fastAppend =
(ctx.env?.BARE_OS_VAR_LOG_FAST_APPEND === '1' ||
ctx.env?.BARE_OS_VAR_LOG_FAST_APPEND === 'true') &&
typeof vfs.appendFile === 'function'
if (fastAppend) {
await appendLineWithOptionalCap(ctx, logicalFilePath, chunk, LOG_MAX_BYTES)
await mirrorBareOsTelemetryNdjson(ctx, {
type: 'varLog',
path: logicalFilePath,
kind,
line: String(line).slice(0, 4000)
})
await mirrorBareOsOtelJsonl(ctx, 'varLog', {
path: logicalFilePath,
kind,
line: String(line).slice(0, 4000)
})
return
}
const prev = await vfs.readFile(logicalFilePath)
let merged = prev ? ctx.b4a.concat([prev, chunk]) : chunk
if (merged.length > LOG_MAX_BYTES) {
const start = Math.max(0, merged.length - LOG_KEEP_BYTES)
const tail = merged.subarray(start)
const notice = ctx.b4a.from(
`[${ts}] [bare-os] log truncated (kept last ${LOG_KEEP_BYTES} bytes): ${logicalFilePath}\n`
)
merged = ctx.b4a.concat([notice, tail])
}
await vfs.writeFile(logicalFilePath, merged)
await mirrorBareOsTelemetryNdjson(ctx, {
type: 'varLog',
path: logicalFilePath,
kind,
line: String(line).slice(0, 4000)
})
await mirrorBareOsOtelJsonl(ctx, 'varLog', {
path: logicalFilePath,
kind,
line: String(line).slice(0, 4000)
})
} catch (e) {
bareOsKernelMetricInc('varlog.append.error')
varLogDebug(
ctx,
`appendVarLog(${String(logicalFilePath)}): ${
e && e.message ? e.message : String(e)
}`
)
}
}
/**
* @param {Record<string, unknown>} ctx
* @param {{ resource: string, phase: string, ok: boolean, durationMs?: number, retries?: number, error?: string }} row
*/
export async function appendBareOsCloseJournal(ctx, row) {
await appendVarLog(
ctx,
CLOSE_JOURNAL_LOG,
'close',
JSON.stringify({
schema: 1,
atMs: Date.now(),
resource: String(row.resource || '').slice(0, 128),
phase: String(row.phase || '').slice(0, 64),
ok: row.ok === true,
durationMs:
typeof row.durationMs === 'number' && Number.isFinite(row.durationMs)
? Math.max(0, Math.floor(row.durationMs))
: undefined,
retries:
typeof row.retries === 'number' && Number.isFinite(row.retries)
? Math.max(0, Math.floor(row.retries))
: undefined,
error: row.error ? String(row.error).slice(0, 512) : undefined
})
)
}
/**
* @param {Record<string, unknown>} ctx
* @param {Record<string, unknown>} crash
*/
export async function appendBareOsCrashEvent(ctx, crash) {
const safe = bareOsTelemetrySanitizeRec(crash || {})
await appendVarLog(
ctx,
CRASH_LOG,
'crash',
JSON.stringify({
schema: 1,
atMs: Date.now(),
...safe
})
)
}
/**
* @param {Record<string, unknown>} ctx
* @param {Record<string, unknown>} rec
*/
/** @internal Mirrored telemetry sink; exported for structured host events (e.g. swarm chat). */
export async function mirrorBareOsTelemetryNdjson(ctx, rec) {
try {
const env = /** @type {Record<string, string>} */ (
ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
)
const raw = env.BARE_OS_TELEMETRY_NDJSON
if (raw == null || raw === '' || raw === '0' || raw === 'false') return
const dest = String(raw).trim()
if (!dest.startsWith('/') && !dest.startsWith('~/')) return
const vfs = ctx.vfs
if (!vfs) return
const lineage = String(env.BARE_OS_TELEMETRY_SESSION_LINEAGE_ID || '').trim()
const bootAttemptId = String(env.BARE_OS_BOOT_ATTEMPT_ID || '').trim()
const bareModuleCryptoStagingProbeId = String(env.BARE_OS_PROBE_ID_BARE_MODULE_CRYPTO_STAGING || '').trim()
const pearInspectLoggerTlsProbeId = String(env.BARE_OS_PROBE_ID_PEAR_INSPECT_LOGGER_TLS || '').trim()
const hypercorePackHrpcLifecycleProbeId = String(env.BARE_OS_PROBE_ID_HYPERCORE_PACK_HRPC_LIFECYCLE || '').trim()
const traceId = String(env.BARE_OS_TRACE_ID || '').trim()
const safeRec = bareOsTelemetrySanitizeRec(
/** @type {Record<string, unknown>} */ (rec)
)
const line =
JSON.stringify({
telemetrySchemaVersion: BARE_OS_TELEMETRY_SCHEMA_VERSION,
lifecycleSchemaVersion: BARE_OS_LIFECYCLE_SCHEMA_VERSION,
ts: Date.now(),
traceId: traceId ? traceId.slice(0, 128) : undefined,
bootAttemptId: bootAttemptId ? bootAttemptId.slice(0, 128) : undefined,
bareModuleCryptoStagingProbeId: bareModuleCryptoStagingProbeId ? bareModuleCryptoStagingProbeId.slice(0, 128) : undefined,
pearInspectLoggerTlsProbeId: pearInspectLoggerTlsProbeId ? pearInspectLoggerTlsProbeId.slice(0, 128) : undefined,
hypercorePackHrpcLifecycleProbeId: hypercorePackHrpcLifecycleProbeId ? hypercorePackHrpcLifecycleProbeId.slice(0, 128) : undefined,
bareRpcProbeClass: String(
env.BARE_OS_BARE_RPC_PROBE_CLASS || ''
)
.trim()
.slice(0, 64) || undefined,
bareModuleProbeClass: String(
env.BARE_OS_BARE_MODULE_PROBE_CLASS || ''
).trim().slice(0, 64) || undefined,
sessionLineageId: lineage ? lineage.slice(0, 128) : undefined,
hdmsPairingBackoffCount: (() => {
const n = Number.parseInt(
env.BARE_OS_HDMS_PAIRING_BACKOFF_COUNT || '',
10
)
return Number.isFinite(n) ? Math.min(1e6, n) : undefined
})(),
...safeRec
}) + '\n'
const chunk = ctx.b4a.from(line)
await appendLineWithOptionalCap(ctx, dest, chunk, TELEMETRY_LOG_CAP_BYTES)
} catch {
/* ignore */
}
}
/**
* Optional OpenTelemetry-inspired JSON line export (`BARE_OS_TELEMETRY_OTEL_JSONL` logical path).
* @param {Record<string, unknown>} ctx
* @param {string} eventName
* @param {Record<string, unknown>} attrs
*/
async function mirrorBareOsOtelJsonl(ctx, eventName, attrs) {
try {
const env = /** @type {Record<string, string>} */ (
ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
)
const raw = env.BARE_OS_TELEMETRY_OTEL_JSONL
if (raw == null || raw === '' || raw === '0' || raw === 'false') return
const dest = String(raw).trim()
if (!dest.startsWith('/') && !dest.startsWith('~/')) return
const vfs = ctx.vfs
if (!vfs) return
const safeAttrs = bareOsTelemetrySanitizeRec(
/** @type {Record<string, unknown>} */ (attrs)
)
const line =
JSON.stringify({
otlSchemaVersion: 8,
resourceLogs: [
{
resource: {
attributes: [
{ key: 'service.name', value: { stringValue: 'bare-os' } },
{
key: 'telemetry.sdk.language',
value: { stringValue: 'javascript' }
}
]
},
scopeLogs: [
{
scope: { name: 'bare-os-booter', version: '1.0.0' },
logRecords: [
{
timeUnixNano: String(Date.now() * 1e6),
severityNumber: 9,
severityText: 'INFO',
body: { stringValue: eventName },
events: [],
links: [],
exemplars: [],
attributes: Object.entries(safeAttrs).map(([k, v]) => ({
key: k,
value:
typeof v === 'string'
? { stringValue: v }
: { stringValue: JSON.stringify(v) }
}))
}
]
}
]
}
]
}) + '\n'
const chunk = ctx.b4a.from(line)
await appendLineWithOptionalCap(ctx, dest, chunk, TELEMETRY_LOG_CAP_BYTES)
} catch {
/* ignore */
}
}