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.
424 lines
15 KiB
JavaScript
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 */
|
|
}
|
|
}
|