feat(ql3): harden cluster secret projection checks

This commit is contained in:
whyour
2026-08-13 11:45:42 +08:00
parent 8ba27dc148
commit 5e70b89f55
7 changed files with 321 additions and 59 deletions
+5
View File
@@ -250,6 +250,11 @@
"require": "./dist/plugin-package/management/pluginPackageManagement.js",
"default": "./dist/plugin-package/management/pluginPackageManagement.js"
},
"./plugin-package-secret-existence-inspector": {
"types": "./dist/plugin-package/secret-binding/projectedSecretExistenceInspector.d.ts",
"require": "./dist/plugin-package/secret-binding/projectedSecretExistenceInspector.js",
"default": "./dist/plugin-package/secret-binding/projectedSecretExistenceInspector.js"
},
"./plugin-package-lifecycle-management": {
"types": "./dist/plugin-package/lifecycle/pluginPackageLifecycleManagement.d.ts",
"require": "./dist/plugin-package/lifecycle/pluginPackageLifecycleManagement.js",
@@ -0,0 +1,138 @@
import { lstat, realpath, stat } from 'node:fs/promises';
import { isAbsolute, join, normalize, parse, relative } from 'node:path';
import { secretProjectionFileName } from '@qinglong/runtime-core/secret-projection';
import { parseSecretRef } from '@qinglong/runtime-core/secret-reference';
const MAX_SECRET_ROOT_BYTES = 4096;
export interface PluginPackageSecretExistenceInspector {
assertExists(secretRefs: readonly string[]): Promise<void>;
}
export interface ProjectedPluginPackageSecretExistenceInspectorOptions {
readonly rootDirectory: string;
}
export class ProjectedPluginPackageSecretExistenceError extends Error {
readonly code = 'QL3_PROJECTED_PLUGIN_PACKAGE_SECRET_UNAVAILABLE';
constructor(
readonly reason:
| 'invalid_configuration'
| 'root_unavailable'
| 'reference_unavailable',
options?: ErrorOptions,
) {
super(`Projected Plugin Package Secret failed: ${reason}`, options);
this.name = 'ProjectedPluginPackageSecretExistenceError';
}
}
function rootDirectory(value: string): string {
if (
typeof value !== 'string' ||
!isAbsolute(value) ||
parse(value).root === value ||
normalize(value) !== value ||
value.includes('\0') ||
Buffer.byteLength(value, 'utf8') > MAX_SECRET_ROOT_BYTES
) {
throw new ProjectedPluginPackageSecretExistenceError(
'invalid_configuration',
);
}
return value;
}
function remainsBelow(root: string, candidate: string): boolean {
const suffix = relative(root, candidate);
return (
suffix.length > 0 &&
!isAbsolute(suffix) &&
suffix !== '..' &&
!suffix.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)
);
}
async function resolvedRoot(path: string): Promise<string> {
try {
const configured = await lstat(path);
if (!configured.isDirectory() || configured.isSymbolicLink()) {
throw new Error('root is not a direct directory');
}
return await realpath(path);
} catch (error) {
throw new ProjectedPluginPackageSecretExistenceError('root_unavailable', {
cause: error,
});
}
}
async function assertProjectedReference(
root: string,
secretRef: string,
): Promise<void> {
try {
const reference = parseSecretRef(secretRef);
if (reference.version === undefined) {
throw new Error('projected Secret reference is not versioned');
}
const candidate = join(root, secretProjectionFileName(secretRef));
const target = await realpath(candidate);
if (!remainsBelow(root, target)) {
throw new Error('projected Secret escaped its root');
}
const metadata = await stat(target);
if (
!metadata.isFile() ||
metadata.nlink !== 1 ||
metadata.size < 0 ||
(metadata.mode & 0o111) !== 0 ||
(await realpath(candidate)) !== target
) {
throw new Error('projected Secret metadata is unsafe');
}
} catch (error) {
throw new ProjectedPluginPackageSecretExistenceError(
'reference_unavailable',
{ cause: error },
);
}
}
/**
* Metadata-only existence proof for the short-lived Package executor. It never
* opens or reads projected Secret material and retains no cache or watcher.
*/
export class ProjectedPluginPackageSecretExistenceInspector
implements PluginPackageSecretExistenceInspector
{
private readonly rootDirectory: string;
constructor(options: ProjectedPluginPackageSecretExistenceInspectorOptions) {
if (!options || typeof options !== 'object' || Array.isArray(options)) {
throw new ProjectedPluginPackageSecretExistenceError(
'invalid_configuration',
);
}
this.rootDirectory = rootDirectory(options.rootDirectory);
}
async assertExists(secretRefs: readonly string[]): Promise<void> {
if (
!Array.isArray(secretRefs) ||
secretRefs.length < 1 ||
secretRefs.length > 64 ||
new Set(secretRefs).size !== secretRefs.length
) {
throw new ProjectedPluginPackageSecretExistenceError(
'reference_unavailable',
);
}
const root = await resolvedRoot(this.rootDirectory);
for (const secretRef of secretRefs) {
await assertProjectedReference(root, secretRef);
}
}
}
@@ -0,0 +1,96 @@
'use strict';
const assert = require('node:assert/strict');
const {
chmod,
mkdir,
mkdtemp,
rm,
symlink,
writeFile,
} = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
const {
secretProjectionFileName,
} = require('@qinglong/runtime-core/secret-projection');
const {
ProjectedPluginPackageSecretExistenceError,
ProjectedPluginPackageSecretExistenceInspector,
} = require('@qinglong/cluster-admin/plugin-package-secret-existence-inspector');
const SECRET_REF = createSecretRef({
projectId: 'project-1',
name: 'api-token',
version: 3,
});
test('proves an exact projected Secret without reading its bytes', async (t) => {
const root = await mkdtemp(path.join(os.tmpdir(), 'ql3-secret-inspect-'));
t.after(() => rm(root, { recursive: true, force: true }));
await chmod(root, 0o700);
const file = path.join(root, secretProjectionFileName(SECRET_REF));
await writeFile(file, 'unreadable-to-executor', { mode: 0o000 });
const inspector = new ProjectedPluginPackageSecretExistenceInspector({
rootDirectory: root,
});
await inspector.assertExists([SECRET_REF]);
await assert.rejects(
inspector.assertExists([
createSecretRef({
projectId: 'project-1',
name: 'missing',
version: 1,
}),
]),
(error) =>
error instanceof ProjectedPluginPackageSecretExistenceError &&
error.reason === 'reference_unavailable',
);
});
test('accepts an in-root projection symlink and rejects an escape', async (t) => {
const root = await mkdtemp(path.join(os.tmpdir(), 'ql3-secret-project-'));
const outside = await mkdtemp(path.join(os.tmpdir(), 'ql3-secret-outside-'));
t.after(async () => {
await rm(root, { recursive: true, force: true });
await rm(outside, { recursive: true, force: true });
});
await chmod(root, 0o700);
await chmod(outside, 0o700);
const name = secretProjectionFileName(SECRET_REF);
const generation = path.join(root, '..data-v1');
await mkdir(generation, { mode: 0o700 });
await writeFile(path.join(generation, name), 'value', { mode: 0o000 });
await symlink(path.join('..data-v1', name), path.join(root, name));
const inspector = new ProjectedPluginPackageSecretExistenceInspector({
rootDirectory: root,
});
await inspector.assertExists([SECRET_REF]);
await rm(path.join(root, name));
await writeFile(path.join(outside, name), 'value', { mode: 0o000 });
await symlink(path.join(outside, name), path.join(root, name));
await assert.rejects(inspector.assertExists([SECRET_REF]));
});
test('rejects duplicate, unversioned and noncanonical references', async (t) => {
const root = await mkdtemp(path.join(os.tmpdir(), 'ql3-secret-invalid-'));
t.after(() => rm(root, { recursive: true, force: true }));
await chmod(root, 0o700);
const inspector = new ProjectedPluginPackageSecretExistenceInspector({
rootDirectory: root,
});
await assert.rejects(inspector.assertExists([SECRET_REF, SECRET_REF]));
await assert.rejects(
inspector.assertExists([
createSecretRef({ projectId: 'project-1', name: 'unversioned' }),
]),
);
await assert.rejects(inspector.assertExists(['not-a-secret-ref']));
});