Protect private bank configuration from drift reconciliation (Refs #152)

This commit is contained in:
2026-10-12 23:32:20 -04:00
parent 4e3df81786
commit d9fa15cdf6
23 changed files with 597 additions and 28 deletions
+32 -2
View File
@@ -1,3 +1,9 @@
import {
assertPublicFilePayload,
BANK_CONFIG_FILE,
PrivateFileOperationError,
isPrivateConfigPath
} from '../fleet/private-config.js';
import { packFileJson, unpackFileJson, referenceBlobs } from '../db/file-blobs.js';
import { pruneHistory } from '../retention.js';
import { recordAgentReceipt } from '../provisioning/agent-events.js';
@@ -1248,7 +1254,9 @@ function enqueueHostOperation(
)
throw new Error('Host is disabled. Enable it before requesting operations.');
const requestHash = createHash('sha256').update(payload).digest('hex');
const packed = packFileJson(JSON.parse(payload.toString()));
const parsedPayload = JSON.parse(payload.toString());
assertPublicFilePayload(parsedPayload);
const packed = packFileJson(parsedPayload);
payload = JSON.stringify(packed.value);
const id = options.id ?? randomUUID();
const timestamp = now();
@@ -1405,6 +1413,15 @@ export function pendingHostOperationRows(
)
: new Set<string>();
const admitted = rows.filter((operation) => {
// Withhold unsafe legacy commands without breaking the entire delivery stream.
if (operation.kind.startsWith('files.') || operation.kind === 'logs.read') {
try {
getHostOperationPayload(hostId, operation.id);
} catch (error) {
if (error instanceof PrivateFileOperationError) return false;
throw error;
}
}
if (
!libraryKinds.includes(operation.kind) ||
preparing.has(hostId) ||
@@ -1431,6 +1448,9 @@ export function getHostOperationPayload(hostId: string, id: string): Buffer {
)
.get(id, hostId) as { payload: Buffer } | undefined;
if (!row) throw new Error('Host operation was not found for this host.');
// Block old private-file commands before delivery to agents predating the guard.
if (row.payload.includes(BANK_CONFIG_FILE))
assertPublicFilePayload(JSON.parse(row.payload.toString()));
if (
!row.payload.includes('"contentSha256"') ||
getHost(hostId)?.inventory.capabilities?.includes('files.blobs')
@@ -1468,8 +1488,18 @@ export function reportHostOperationStatus(
if (result !== null && Buffer.byteLength(result) > 128 * 1024 * 1024)
throw new Error('Operation result exceeds the limit.');
if (result !== null) {
const target =
operation.kind === 'files.read'
? (database()
.prepare('SELECT target_path FROM ec_host_operations WHERE id=?')
.get(id) as { target_path: string | null })
: null;
const packed = packFileJson(
operation.kind === 'logs.read' ? logResultMetadata(input.result) : input.result
target?.target_path && isPrivateConfigPath(target.target_path)
? { redacted: true }
: operation.kind === 'logs.read'
? logResultMetadata(input.result)
: input.result
);
result = JSON.stringify(packed.value);
referenceBlobs('operation-result', id, packed.hashes);