How Apache Maka's Credential Vault Protects API Keys and Secrets Locally
Apache Maka stores API keys and secrets in a secured, bounded JSON credential vault that enforces strict size limits, atomic disk writes, and type-safe validation to prevent data leakage or corruption.
The credential vault is the central mechanism in Apache Maka for protecting sensitive credentials locally. Unlike simple configuration files, it implements defense-in-depth through schema validation, filesystem hardening, and careful API design that prevents accidental exposure. This article examines the complete technical implementation as found in the Apache Maka source code.
How the Credential Vault Works
Vault Document Structure and Limits
The vault persists to a single file named credential-vault.json with a strictly defined schema. In packages/storage/src/runtime-policy/credential-vault-document.ts, the CredentialVaultDocument interface enforces:
- Schema version locked to
SCHEMA_VERSION = 1 - Maximum entries capped at
MAX_VAULT_ENTRIES = 2048 - Document size limit of
VAULT_DOCUMENT_MAX_BYTES = 2 MiB - Per-secret limit of
MAX_SECRET_LENGTH = 64 KiB
Each entry contains a locator (hierarchical key), a generated credentialId, a revision number for optimistic concurrency, the encrypted secret, and an updatedAt timestamp. These limits prevent denial-of-service through excessive storage and ensure predictable memory usage.
Safe File Reading with Symlink Protection
Reading the vault follows a hardened path in packages/storage/src/runtime-policy/document-io.ts. The readBoundedJsonDocument function:
- Opens with
O_NOFOLLOW— refuses to follow symbolic links - Sets
O_NONBLOCK— prevents blocking on special files - Verifies the target is a regular file via
fstat - Enforces the 2 MiB size limit before parsing
- Validates UTF-8 encoding and JSON structure
Any violation throws RuntimePolicyStoreError, preventing malformed or malicious files from reaching the parser.
// From document-io.ts: readBoundedJsonDocument enforces these checks
// before JSON parsing begins. Errors are categorized and never leak
// secret material in exception messages.
Atomic Writes to Prevent Partial Secrets
The vault guarantees crash safety through atomic writes. When CredentialVaultDocumentOwner.set() commits changes:
// Simplified sequence from document-io.ts write path
const tmpPath = `${vaultPath}.${randomUUID()}.tmp`;
await fs.writeFile(tmpPath, serialized, { mode: 0o600 });
await fs.fsync(fd); // Ensure data reaches physical storage
await fs.rename(tmpPath, vaultPath); // Atomic visibility switch
This pattern ensures that:
- Partial writes never appear — readers see only the complete old or new version
- Temporary files use restrictive permissions (
0o600: owner read/write only) - fsync flushes buffers — secrets survive system crashes
Stale temporary files are cleaned at startup via cleanupRuntimePolicyDocumentTemps to prevent secret leakage in crash artifacts.
Validation Pipeline Before Persistence
Every write passes through assertDocumentSize and assertCredentialInputSecretLimit in credential-vault-document.ts. The document is serialized to check its byte length against the 2 MiB limit. Individual secrets are validated against the 64 KiB limit. This double-checking catches oversized data before it reaches the atomic write stage.
Immutable Snapshots and Controlled Secret Access
The CredentialVaultDocumentOwner class exposes a snapshot API that returns deeply frozen objects:
// From credential-vault-document.ts - snapshot creation
const vaultSnapshot = {
revision: document.revision,
entries: document.entries.map(entry => ({
credentialId: entry.credentialId,
locator: entry.locator,
revision: entry.revision,
updatedAt: entry.updatedAt,
// secret is EXCLUDED from snapshots
})),
};
The raw secret field never appears in snapshots. Code that needs the actual credential must explicitly call credentialMaterial(), making secret access auditable and intentional.
Working with the Credential Vault API
Setting and Updating Secrets
import { CredentialVaultDocumentOwner } from '@maka/storage/runtime-policy';
const vault = new CredentialVaultDocumentOwner();
// Store a new API key
await vault.set('/path/to/workspace', {
locator: {
scope: 'web_search',
provider: 'tavily',
kind: 'api_key'
},
secret: process.env.MAKA_TAVILY_API_KEY ?? '',
expected: null, // null = create new, number = optimistic concurrency check
});
The locator uses a namespaced format: connection:<id>:<kind>, web_search:<provider>:api_key, or network_proxy:password. The vault enforces uniqueness on both locators and generated credentialId values.
Reading Without Exposing Secrets
const snapshot = await vault.read('/path/to/workspace');
console.log('Available credentials:', snapshot.entries.map(e => ({
id: e.credentialId,
type: e.locator.scope,
lastModified: e.updatedAt,
})));
// Note: snapshot.entries[].secret does not exist
Deleting with Optimistic Concurrency
await vault.delete('/path/to/workspace', {
expected: {
locator: { scope: 'web_search', provider: 'tavily', kind: 'api_key' },
credentialId: 'c0d3cafe-1234-5678-9abc-def012345678',
revision: 3, // Must match current revision or CredentialStaleResult
},
});
The expected.revision requirement prevents lost updates in concurrent scenarios.
Key Implementation Files
| File | Responsibility |
|---|---|
packages/storage/src/runtime-policy/credential-vault-document.ts |
CredentialVaultDocumentOwner class, schema definitions, validation logic, and snapshot creation |
packages/storage/src/runtime-policy/document-io.ts |
Bounded JSON I/O, atomic writes with temp files, symlink protection, and startup cleanup |
packages/storage/src/__tests__/runtime-policy-stores.test.ts |
Test coverage for size limits, atomicity guarantees, and secret lifecycle |
packages/core/src/runtime-policy/credential-vault-codec.ts |
Serialization codec for runtime policy integration |
apps/desktop/src/main/.../credential-store |
Desktop application integration point per workspace |
Summary
- Bounded storage: Hard limits on document size (2 MiB) and individual secrets (64 KiB) prevent abuse
- Filesystem hardening:
O_NOFOLLOW, regular file verification, and0o600permissions block symlink attacks and unauthorized access - Atomic durability: Temp-file-plus-rename pattern with
fsyncensures crash-safe writes without partial secrets - Immutable snapshots: Deeply frozen read-only views exclude secret material by default
- Optimistic concurrency: Revision numbers prevent race conditions during concurrent updates
- Automatic cleanup: Startup removal of temp files eliminates stale secret leakage
Frequently Asked Questions
What happens if the credential vault file is corrupted or tampered with?
Any corruption — invalid JSON, UTF-8 encoding errors, schema violations, or size limit breaches — causes readBoundedJsonDocument to throw RuntimePolicyStoreError. The error messages intentionally exclude secret content. Administrators must restore from backup or reinitialize the vault.
Can multiple processes safely modify the credential vault concurrently?
While the atomic rename guarantees readers see complete writes, Apache Maka uses optimistic concurrency control rather than file locking. Each set or delete operation includes an expected revision; mismatches return CredentialStaleResult, allowing callers to retry with fresh state.
Why are secrets limited to 64 KiB?
The MAX_SECRET_LENGTH constant prevents accidental or malicious storage of large binary blobs that could exhaust disk space or memory. This accommodates all standard API keys, PEM certificates, and connection strings while rejecting inappropriate vault use.
Where does the credential vault store its files?
The vault resides at {workspaceRoot}/credential-vault.json. The CredentialVaultDocumentOwner methods accept the workspace path as their first argument, keeping vault location explicit and configurable per workspace rather than using hidden global directories.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →