security: pass secrets via SDK env option and delete temp file (#213)
Pass secrets to the SDK via the `env` query option instead of setting process.env, so Bash subprocesses never inherit API keys. Delete /tmp/input.json immediately after reading to remove secrets from disk. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,7 +51,7 @@ RUN npm run build
|
|||||||
RUN mkdir -p /workspace/group /workspace/global /workspace/extra /workspace/ipc/messages /workspace/ipc/tasks /workspace/ipc/input
|
RUN mkdir -p /workspace/group /workspace/global /workspace/extra /workspace/ipc/messages /workspace/ipc/tasks /workspace/ipc/input
|
||||||
|
|
||||||
# Create entrypoint script
|
# Create entrypoint script
|
||||||
# Secrets are passed via stdin JSON and set in Node.js — no env files or temp files on disk
|
# Secrets are passed via stdin JSON — temp file is deleted immediately after Node reads it
|
||||||
# Follow-up messages arrive via IPC files in /workspace/ipc/input/
|
# Follow-up messages arrive via IPC files in /workspace/ipc/input/
|
||||||
RUN printf '#!/bin/bash\nset -e\ncd /app && npx tsc --outDir /tmp/dist 2>&1 >&2\nln -s /app/node_modules /tmp/dist/node_modules\nchmod -R a-w /tmp/dist\ncat > /tmp/input.json\nnode /tmp/dist/index.js < /tmp/input.json\n' > /app/entrypoint.sh && chmod +x /app/entrypoint.sh
|
RUN printf '#!/bin/bash\nset -e\ncd /app && npx tsc --outDir /tmp/dist 2>&1 >&2\nln -s /app/node_modules /tmp/dist/node_modules\nchmod -R a-w /tmp/dist\ncat > /tmp/input.json\nnode /tmp/dist/index.js < /tmp/input.json\n' > /app/entrypoint.sh && chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
|
|||||||
@@ -358,6 +358,7 @@ async function runQuery(
|
|||||||
sessionId: string | undefined,
|
sessionId: string | undefined,
|
||||||
mcpServerPath: string,
|
mcpServerPath: string,
|
||||||
containerInput: ContainerInput,
|
containerInput: ContainerInput,
|
||||||
|
sdkEnv: Record<string, string | undefined>,
|
||||||
resumeAt?: string,
|
resumeAt?: string,
|
||||||
): Promise<{ newSessionId?: string; lastAssistantUuid?: string; closedDuringQuery: boolean }> {
|
): Promise<{ newSessionId?: string; lastAssistantUuid?: string; closedDuringQuery: boolean }> {
|
||||||
const stream = new MessageStream();
|
const stream = new MessageStream();
|
||||||
@@ -432,6 +433,7 @@ async function runQuery(
|
|||||||
'NotebookEdit',
|
'NotebookEdit',
|
||||||
'mcp__nanoclaw__*'
|
'mcp__nanoclaw__*'
|
||||||
],
|
],
|
||||||
|
env: sdkEnv,
|
||||||
permissionMode: 'bypassPermissions',
|
permissionMode: 'bypassPermissions',
|
||||||
allowDangerouslySkipPermissions: true,
|
allowDangerouslySkipPermissions: true,
|
||||||
settingSources: ['project', 'user'],
|
settingSources: ['project', 'user'],
|
||||||
@@ -493,6 +495,8 @@ async function main(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const stdinData = await readStdin();
|
const stdinData = await readStdin();
|
||||||
containerInput = JSON.parse(stdinData);
|
containerInput = JSON.parse(stdinData);
|
||||||
|
// Delete the temp file the entrypoint wrote — it contains secrets
|
||||||
|
try { fs.unlinkSync('/tmp/input.json'); } catch { /* may not exist */ }
|
||||||
log(`Received input for group: ${containerInput.groupFolder}`);
|
log(`Received input for group: ${containerInput.groupFolder}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
writeOutput({
|
writeOutput({
|
||||||
@@ -503,10 +507,11 @@ async function main(): Promise<void> {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set secrets as env vars for the SDK (needed for API auth).
|
// Build SDK env: merge secrets into process.env for the SDK only.
|
||||||
// The PreToolUse hook strips these from every Bash subprocess.
|
// Secrets never touch process.env itself, so Bash subprocesses can't see them.
|
||||||
|
const sdkEnv: Record<string, string | undefined> = { ...process.env };
|
||||||
for (const [key, value] of Object.entries(containerInput.secrets || {})) {
|
for (const [key, value] of Object.entries(containerInput.secrets || {})) {
|
||||||
process.env[key] = value;
|
sdkEnv[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
@@ -535,7 +540,7 @@ async function main(): Promise<void> {
|
|||||||
while (true) {
|
while (true) {
|
||||||
log(`Starting query (session: ${sessionId || 'new'}, resumeAt: ${resumeAt || 'latest'})...`);
|
log(`Starting query (session: ${sessionId || 'new'}, resumeAt: ${resumeAt || 'latest'})...`);
|
||||||
|
|
||||||
const queryResult = await runQuery(prompt, sessionId, mcpServerPath, containerInput, resumeAt);
|
const queryResult = await runQuery(prompt, sessionId, mcpServerPath, containerInput, sdkEnv, resumeAt);
|
||||||
if (queryResult.newSessionId) {
|
if (queryResult.newSessionId) {
|
||||||
sessionId = queryResult.newSessionId;
|
sessionId = queryResult.newSessionId;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user