fix(proxy): avoid exposing local auth token in daemon argv (#1230)

This commit is contained in:
Kai (Tam Nhu) Tran
2026-05-12 18:13:20 -04:00
committed by GitHub
parent 9c5d4976c5
commit a840793a9b
5 changed files with 160 additions and 19 deletions
+26 -3
View File
@@ -1,3 +1,4 @@
import * as fs from 'fs';
import { resolveOpenAICompatProfileConfig } from './profile-router';
import { OPENAI_COMPAT_PROXY_DEFAULT_PORT } from './proxy-daemon-paths';
import { startOpenAICompatProxyServer } from './server/proxy-server';
@@ -9,6 +10,7 @@ interface RuntimeOptions {
profileName: string;
settingsPath: string;
authToken: string;
authTokenFile: string;
insecure: boolean;
}
@@ -18,6 +20,7 @@ function parseArgs(argv: string[]): RuntimeOptions {
let profileName = '';
let settingsPath = '';
let authToken = '';
let authTokenFile = '';
let insecure = false;
for (let i = 0; i < argv.length; i++) {
@@ -42,16 +45,36 @@ function parseArgs(argv: string[]): RuntimeOptions {
authToken = argv[++i] || '';
continue;
}
if (arg === '--auth-token-file' && argv[i + 1]) {
authTokenFile = argv[++i] || '';
continue;
}
if (arg === '--insecure') {
insecure = true;
}
}
return { port, host, profileName, settingsPath, authToken, insecure };
return { port, host, profileName, settingsPath, authToken, authTokenFile, insecure };
}
function readAuthToken(options: RuntimeOptions): string {
if (options.authTokenFile) {
try {
const token = fs.readFileSync(options.authTokenFile, 'utf8').trim();
fs.unlinkSync(options.authTokenFile);
return token;
} catch (error) {
throw new Error(`Failed to read local proxy auth token file: ${(error as Error).message}`);
}
}
return options.authToken;
}
function startRuntime(options: RuntimeOptions): void {
if (!options.authToken.trim()) {
const authToken = readAuthToken(options);
if (!authToken.trim()) {
throw new Error('Missing local proxy auth token');
}
@@ -71,7 +94,7 @@ function startRuntime(options: RuntimeOptions): void {
profile,
host: options.host,
port: options.port,
authToken: options.authToken,
authToken,
insecure: options.insecure,
});
server.once('error', (error) => {
+46 -7
View File
@@ -1,3 +1,4 @@
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import {
@@ -20,7 +21,40 @@ export interface OpenAICompatProxySession {
}
function ensureProxyDir(): void {
fs.mkdirSync(getOpenAICompatProxyDir(), { recursive: true });
const proxyDir = getOpenAICompatProxyDir();
fs.mkdirSync(proxyDir, { recursive: true, mode: 0o700 });
fs.chmodSync(proxyDir, 0o700);
}
function encodeProfileFileKey(profileName: string): string {
return encodeURIComponent(profileName);
}
export function writeOpenAICompatProxyAuthTokenFile(
profileName: string,
authToken: string
): string {
ensureProxyDir();
const tokenPath = path.join(
getOpenAICompatProxyDir(),
`.${encodeProfileFileKey(profileName)}.${crypto.randomBytes(8).toString('hex')}.token`
);
const fd = fs.openSync(tokenPath, 'wx', 0o600);
try {
fs.writeFileSync(fd, authToken, 'utf8');
} finally {
fs.closeSync(fd);
}
fs.chmodSync(tokenPath, 0o600);
return tokenPath;
}
export function removeOpenAICompatProxyAuthTokenFile(tokenPath: string): void {
try {
fs.unlinkSync(tokenPath);
} catch {
// Best-effort cleanup.
}
}
function readPid(pidPath: string): number | null {
@@ -43,7 +77,11 @@ export function getLegacyOpenAICompatProxyPid(): number | null {
export function writeOpenAICompatProxyPid(profileName: string, pid: number): void {
ensureProxyDir();
fs.writeFileSync(getOpenAICompatProxyPidPath(profileName), String(pid), 'utf8');
fs.writeFileSync(getOpenAICompatProxyPidPath(profileName), String(pid), {
encoding: 'utf8',
mode: 0o600,
});
fs.chmodSync(getOpenAICompatProxyPidPath(profileName), 0o600);
}
export function removeOpenAICompatProxyPid(profileName: string): void {
@@ -80,11 +118,12 @@ export function readLegacyOpenAICompatProxySession(): OpenAICompatProxySession |
export function writeOpenAICompatProxySession(session: OpenAICompatProxySession): void {
ensureProxyDir();
fs.writeFileSync(
getOpenAICompatProxySessionPath(session.profileName),
JSON.stringify(session, null, 2) + '\n',
'utf8'
);
const sessionPath = getOpenAICompatProxySessionPath(session.profileName);
fs.writeFileSync(sessionPath, JSON.stringify(session, null, 2) + '\n', {
encoding: 'utf8',
mode: 0o600,
});
fs.chmodSync(sessionPath, 0o600);
}
export function removeOpenAICompatProxySession(profileName: string): void {
+13 -6
View File
@@ -19,10 +19,12 @@ import {
readOpenAICompatProxySession,
removeLegacyOpenAICompatProxyPid,
removeLegacyOpenAICompatProxySession,
removeOpenAICompatProxyAuthTokenFile,
removeOpenAICompatProxyPid,
removeOpenAICompatProxySession,
resolveOpenAICompatProxyEntrypointCandidates,
type OpenAICompatProxySession,
writeOpenAICompatProxyAuthTokenFile,
writeOpenAICompatProxyPid,
writeOpenAICompatProxySession,
} from './proxy-daemon-state';
@@ -82,7 +84,8 @@ function generateProxyAuthToken(): string {
async function withOpenAICompatProxyLock<T>(operation: () => Promise<T>): Promise<T> {
const proxyDir = getOpenAICompatProxyDir();
await fs.promises.mkdir(proxyDir, { recursive: true });
await fs.promises.mkdir(proxyDir, { recursive: true, mode: 0o700 });
await fs.promises.chmod(proxyDir, 0o700);
let release: (() => Promise<void>) | undefined;
try {
@@ -543,6 +546,7 @@ export async function startOpenAICompatProxy(
let timeout: NodeJS.Timeout | null = null;
let stderr = '';
const authToken = generateProxyAuthToken();
const authTokenFile = writeOpenAICompatProxyAuthTokenFile(profile.profileName, authToken);
const commitState = () => {
if (proc.pid) {
writeOpenAICompatProxyPid(profile.profileName, proc.pid);
@@ -563,9 +567,12 @@ export async function startOpenAICompatProxy(
if (resolved) return;
resolved = true;
if (timeout) clearTimeout(timeout);
if (!result.success && persistState) {
removeOpenAICompatProxyPid(profile.profileName);
removeOpenAICompatProxySession(profile.profileName);
if (!result.success) {
removeOpenAICompatProxyAuthTokenFile(authTokenFile);
if (persistState) {
removeOpenAICompatProxyPid(profile.profileName);
removeOpenAICompatProxySession(profile.profileName);
}
}
resolve(result);
};
@@ -582,8 +589,8 @@ export async function startOpenAICompatProxy(
profile.profileName,
'--settings-path',
profile.settingsPath,
'--auth-token',
authToken,
'--auth-token-file',
authTokenFile,
...(options.insecure ? ['--insecure'] : []),
'--ccs-openai-proxy-daemon',
],
@@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
@@ -71,6 +72,27 @@ async function getPortOutsideOpenAICompatAdaptiveRange(): Promise<number> {
throw new Error('No stale proxy fixture port found outside the adaptive range');
}
function readProcessCommandLine(pid: number): string | null {
if (process.platform === 'linux') {
try {
const commandLine = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8');
return commandLine.split('\u0000').filter(Boolean).join(' ') || null;
} catch {
// Fall back to ps below for platforms without readable procfs entries.
}
}
try {
return (
execFileSync('ps', ['-ww', '-p', String(pid), '-o', 'command='], {
encoding: 'utf8',
}).trim() || null
);
} catch {
return null;
}
}
describe('openai proxy daemon lifecycle', () => {
it('starts, reports status, serves health/models, and stops', async () => {
const port = await getPort();
@@ -101,18 +123,42 @@ describe('openai proxy daemon lifecycle', () => {
const started = await startOpenAICompatProxy(profile, { port });
expect(started.success).toBe(true);
expect(started.authToken).toBeTruthy();
const authToken = started.authToken;
if (!authToken) {
throw new Error('Expected proxy auth token');
}
const status = await getOpenAICompatProxyStatus();
expect(status.running).toBe(true);
expect(status.profileName).toBe('hf');
expect(status.authToken).toBe(started.authToken);
expect(status.authToken).toBe(authToken);
const sessionPath = getOpenAICompatProxySessionPath('hf');
const proxyDir = path.dirname(sessionPath);
if (process.platform !== 'win32') {
expect(fs.statSync(proxyDir).mode & 0o777).toBe(0o700);
expect(fs.statSync(sessionPath).mode & 0o777).toBe(0o600);
}
expect(fs.readdirSync(proxyDir).some((entry) => entry.endsWith('.token'))).toBe(false);
if (started.pid) {
const commandLine = readProcessCommandLine(started.pid);
if (!commandLine) {
console.warn(
`Skipping daemon argv assertion: could not read command line for PID ${started.pid}`
);
} else {
expect(commandLine).toContain('--auth-token-file');
expect(commandLine).not.toContain(authToken);
expect(commandLine).not.toMatch(/(^|\s)--auth-token(?:\s|=|$)/);
}
}
const health = await fetch(`http://127.0.0.1:${port}/health`);
expect(health.status).toBe(200);
const models = (await (
await fetch(`http://127.0.0.1:${port}/v1/models`, {
headers: { 'x-api-key': started.authToken! },
headers: { 'x-api-key': authToken },
})
).json()) as { data?: Array<{ id: string }> };
expect(models.data?.map((entry) => entry.id)).toEqual(['qwen3-coder']);
+27 -1
View File
@@ -3,11 +3,16 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
getOpenAICompatProxyDir,
getLegacyOpenAICompatProxySessionPath,
getOpenAICompatProxyPidPath,
getOpenAICompatProxySessionPath,
} from '../../../src/proxy/proxy-daemon-paths';
import { listOpenAICompatProxyProfileNames } from '../../../src/proxy/proxy-daemon-state';
import {
listOpenAICompatProxyProfileNames,
removeOpenAICompatProxyAuthTokenFile,
writeOpenAICompatProxyAuthTokenFile,
} from '../../../src/proxy/proxy-daemon-state';
let originalCcsHome: string | undefined;
let tempDir: string;
@@ -51,3 +56,24 @@ describe('listOpenAICompatProxyProfileNames', () => {
expect(listOpenAICompatProxyProfileNames()).toEqual(['ccg']);
});
});
describe('writeOpenAICompatProxyAuthTokenFile', () => {
it('writes a private token file under the CCS proxy dir and removes it', () => {
const authToken = 'proxy-token-regression-secret';
const tokenPath = writeOpenAICompatProxyAuthTokenFile('ccg', authToken);
const proxyDir = getOpenAICompatProxyDir();
expect(path.dirname(tokenPath)).toBe(proxyDir);
expect(tokenPath.startsWith(`${proxyDir}${path.sep}`)).toBe(true);
expect(fs.readFileSync(tokenPath, 'utf8')).toBe(authToken);
if (process.platform !== 'win32') {
expect(fs.statSync(proxyDir).mode & 0o777).toBe(0o700);
expect(fs.statSync(tokenPath).mode & 0o777).toBe(0o600);
}
removeOpenAICompatProxyAuthTokenFile(tokenPath);
expect(fs.existsSync(tokenPath)).toBe(false);
});
});