mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): add opt-in security administration job
This commit is contained in:
+431
@@ -0,0 +1,431 @@
|
||||
import {
|
||||
closeSync,
|
||||
constants,
|
||||
fstatSync,
|
||||
fsyncSync,
|
||||
linkSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
openSync,
|
||||
readSync,
|
||||
realpathSync,
|
||||
rmdirSync,
|
||||
unlinkSync,
|
||||
writeSync,
|
||||
} from 'node:fs';
|
||||
import {
|
||||
dirname,
|
||||
isAbsolute,
|
||||
join,
|
||||
normalize,
|
||||
parse,
|
||||
relative,
|
||||
sep,
|
||||
} from 'node:path';
|
||||
|
||||
const INPUTS = Object.freeze([
|
||||
Object.freeze({ name: 'command.json', maximumBytes: 64 * 1024 }),
|
||||
Object.freeze({ name: 'assertion.jwt', maximumBytes: 16 * 1024 }),
|
||||
Object.freeze({ name: 'keyset.json', maximumBytes: 256 * 1024 }),
|
||||
Object.freeze({ name: 'pepper', maximumBytes: 256 }),
|
||||
]);
|
||||
|
||||
export interface ClusterAdministrationKubernetesInputStagePaths {
|
||||
readonly sourceDirectory: string;
|
||||
readonly targetDirectory: string;
|
||||
readonly deliveryDirectory?: string;
|
||||
}
|
||||
|
||||
export interface ClusterAdministrationKubernetesInputStageResult {
|
||||
readonly schemaVersion: 1;
|
||||
readonly component: 'qinglong3-security-administration-kubernetes-input-stage';
|
||||
readonly stagedFileCount: 4;
|
||||
readonly deliveryDirectoryPrepared: boolean;
|
||||
}
|
||||
|
||||
export class ClusterAdministrationKubernetesInputStageError extends TypeError {
|
||||
readonly code = 'QL3_CLUSTER_ADMINISTRATION_KUBERNETES_INPUT_STAGE_INVALID';
|
||||
|
||||
constructor(message: string, readonly cause?: unknown) {
|
||||
super(
|
||||
`Cluster administration Kubernetes input stage is invalid: ${message}`,
|
||||
);
|
||||
this.name = 'ClusterAdministrationKubernetesInputStageError';
|
||||
}
|
||||
}
|
||||
|
||||
function exactObject(
|
||||
value: unknown,
|
||||
): asserts value is ClusterAdministrationKubernetesInputStagePaths {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
'paths must be an object',
|
||||
);
|
||||
}
|
||||
const actual = Object.keys(value).sort();
|
||||
const expected = [
|
||||
'sourceDirectory',
|
||||
'targetDirectory',
|
||||
...('deliveryDirectory' in value ? ['deliveryDirectory'] : []),
|
||||
].sort();
|
||||
if (
|
||||
actual.length !== expected.length ||
|
||||
actual.some((key, index) => key !== expected[index])
|
||||
) {
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
'paths shape is invalid',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function directoryPath(value: unknown, label: string): string {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
!isAbsolute(value) ||
|
||||
normalize(value) !== value ||
|
||||
parse(value).root === value ||
|
||||
value.includes('\0') ||
|
||||
Buffer.byteLength(value, 'utf8') > 4_096
|
||||
) {
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
`${label} must be a normalized absolute non-root path`,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function sameFileState(
|
||||
left: Readonly<{
|
||||
dev: number;
|
||||
ino: number;
|
||||
size: number;
|
||||
mtimeMs: number;
|
||||
ctimeMs: number;
|
||||
}>,
|
||||
right: Readonly<{
|
||||
dev: number;
|
||||
ino: number;
|
||||
size: number;
|
||||
mtimeMs: number;
|
||||
ctimeMs: number;
|
||||
}>,
|
||||
): boolean {
|
||||
return (
|
||||
left.dev === right.dev &&
|
||||
left.ino === right.ino &&
|
||||
left.size === right.size &&
|
||||
left.mtimeMs === right.mtimeMs &&
|
||||
left.ctimeMs === right.ctimeMs
|
||||
);
|
||||
}
|
||||
|
||||
function verifySourceDirectory(sourceDirectory: string): string {
|
||||
const status = lstatSync(sourceDirectory, { throwIfNoEntry: false });
|
||||
if (
|
||||
status === undefined ||
|
||||
!status.isDirectory() ||
|
||||
status.isSymbolicLink() ||
|
||||
(status.mode & 0o002) !== 0
|
||||
) {
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
'projected source directory authority is invalid',
|
||||
);
|
||||
}
|
||||
try {
|
||||
return realpathSync(sourceDirectory);
|
||||
} catch (error) {
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
'projected source directory cannot be resolved',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function confinedSourceFile(
|
||||
sourceDirectory: string,
|
||||
sourceRealDirectory: string,
|
||||
name: string,
|
||||
): string {
|
||||
const candidate = join(sourceDirectory, name);
|
||||
let resolved: string;
|
||||
try {
|
||||
resolved = realpathSync(candidate);
|
||||
} catch (error) {
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
'projected input cannot be resolved',
|
||||
error,
|
||||
);
|
||||
}
|
||||
const pathFromSource = relative(sourceRealDirectory, resolved);
|
||||
if (
|
||||
pathFromSource === '' ||
|
||||
pathFromSource === '..' ||
|
||||
pathFromSource.startsWith(`..${sep}`) ||
|
||||
isAbsolute(pathFromSource)
|
||||
) {
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
'projected input escapes its source directory',
|
||||
);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function readStableSourceFile(filePath: string, maximumBytes: number): Buffer {
|
||||
let descriptor: number | undefined;
|
||||
let bytes: Buffer | undefined;
|
||||
try {
|
||||
descriptor = openSync(
|
||||
filePath,
|
||||
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0),
|
||||
);
|
||||
const before = fstatSync(descriptor);
|
||||
if (
|
||||
!before.isFile() ||
|
||||
before.size < 1 ||
|
||||
before.size > maximumBytes ||
|
||||
(before.mode & 0o027) !== 0
|
||||
) {
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
'projected input file authority is invalid',
|
||||
);
|
||||
}
|
||||
bytes = Buffer.alloc(before.size + 1);
|
||||
let offset = 0;
|
||||
while (offset < bytes.length) {
|
||||
const count = readSync(
|
||||
descriptor,
|
||||
bytes,
|
||||
offset,
|
||||
bytes.length - offset,
|
||||
offset,
|
||||
);
|
||||
if (count === 0) break;
|
||||
offset += count;
|
||||
}
|
||||
const after = fstatSync(descriptor);
|
||||
if (offset !== before.size || !sameFileState(before, after)) {
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
'projected input changed while being read',
|
||||
);
|
||||
}
|
||||
return bytes.subarray(0, offset);
|
||||
} catch (error) {
|
||||
bytes?.fill(0);
|
||||
if (error instanceof ClusterAdministrationKubernetesInputStageError) {
|
||||
throw error;
|
||||
}
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
'projected input cannot be read',
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
if (descriptor !== undefined) closeSync(descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
function verifyPrivateDirectory(directory: string, label: string): void {
|
||||
const status = lstatSync(directory, { throwIfNoEntry: false });
|
||||
const effectiveUser = process.geteuid?.();
|
||||
if (
|
||||
status === undefined ||
|
||||
!status.isDirectory() ||
|
||||
status.isSymbolicLink() ||
|
||||
(status.mode & 0o777) !== 0o700 ||
|
||||
(effectiveUser !== undefined && status.uid !== effectiveUser)
|
||||
) {
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
`${label} authority is invalid`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function verifyWritableParent(parent: string, label: string): void {
|
||||
const status = lstatSync(parent, { throwIfNoEntry: false });
|
||||
if (
|
||||
status === undefined ||
|
||||
!status.isDirectory() ||
|
||||
status.isSymbolicLink() ||
|
||||
(status.mode & 0o002) !== 0
|
||||
) {
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
`${label} parent authority is invalid`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function syncDirectory(directory: string): void {
|
||||
const descriptor = openSync(
|
||||
directory,
|
||||
constants.O_RDONLY | (constants.O_DIRECTORY ?? 0),
|
||||
);
|
||||
try {
|
||||
fsyncSync(descriptor);
|
||||
} finally {
|
||||
closeSync(descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
function createPrivateDirectory(directory: string, label: string): void {
|
||||
const parent = dirname(directory);
|
||||
verifyWritableParent(parent, label);
|
||||
try {
|
||||
mkdirSync(directory, { mode: 0o700 });
|
||||
} catch (error) {
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
`${label} cannot be created`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
try {
|
||||
verifyPrivateDirectory(directory, label);
|
||||
syncDirectory(parent);
|
||||
} catch (error) {
|
||||
try {
|
||||
rmdirSync(directory);
|
||||
} catch {
|
||||
// Preserve the original authority failure.
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function prepareDeliveryDirectory(directory: string): void {
|
||||
const existing = lstatSync(directory, { throwIfNoEntry: false });
|
||||
if (existing === undefined) {
|
||||
createPrivateDirectory(directory, 'delivery directory');
|
||||
return;
|
||||
}
|
||||
verifyPrivateDirectory(directory, 'delivery directory');
|
||||
}
|
||||
|
||||
function publishPrivateFile(filePath: string, bytes: Buffer): void {
|
||||
const temporary = `${filePath}.stage`;
|
||||
let descriptor: number | undefined;
|
||||
try {
|
||||
descriptor = openSync(
|
||||
temporary,
|
||||
constants.O_WRONLY |
|
||||
constants.O_CREAT |
|
||||
constants.O_EXCL |
|
||||
(constants.O_NOFOLLOW ?? 0),
|
||||
0o600,
|
||||
);
|
||||
let offset = 0;
|
||||
while (offset < bytes.length) {
|
||||
offset += writeSync(
|
||||
descriptor,
|
||||
bytes,
|
||||
offset,
|
||||
bytes.length - offset,
|
||||
offset,
|
||||
);
|
||||
}
|
||||
fsyncSync(descriptor);
|
||||
const status = fstatSync(descriptor);
|
||||
if (
|
||||
!status.isFile() ||
|
||||
status.size !== bytes.length ||
|
||||
(status.mode & 0o077) !== 0
|
||||
) {
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
'private staged input file authority is invalid',
|
||||
);
|
||||
}
|
||||
closeSync(descriptor);
|
||||
descriptor = undefined;
|
||||
linkSync(temporary, filePath);
|
||||
unlinkSync(temporary);
|
||||
} catch (error) {
|
||||
if (descriptor !== undefined) closeSync(descriptor);
|
||||
try {
|
||||
unlinkSync(temporary);
|
||||
} catch {
|
||||
// Preserve the original publication failure.
|
||||
}
|
||||
if (error instanceof ClusterAdministrationKubernetesInputStageError) {
|
||||
throw error;
|
||||
}
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
'private staged input cannot be published',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function stageClusterAdministrationKubernetesInputs(
|
||||
pathsValue: ClusterAdministrationKubernetesInputStagePaths,
|
||||
): Readonly<ClusterAdministrationKubernetesInputStageResult> {
|
||||
exactObject(pathsValue);
|
||||
const sourceDirectory = directoryPath(
|
||||
pathsValue.sourceDirectory,
|
||||
'sourceDirectory',
|
||||
);
|
||||
const targetDirectory = directoryPath(
|
||||
pathsValue.targetDirectory,
|
||||
'targetDirectory',
|
||||
);
|
||||
const deliveryDirectory =
|
||||
pathsValue.deliveryDirectory === undefined
|
||||
? undefined
|
||||
: directoryPath(pathsValue.deliveryDirectory, 'deliveryDirectory');
|
||||
if (
|
||||
sourceDirectory === targetDirectory ||
|
||||
sourceDirectory === deliveryDirectory ||
|
||||
targetDirectory === deliveryDirectory
|
||||
) {
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
'source, target and delivery directories must be distinct',
|
||||
);
|
||||
}
|
||||
|
||||
const sourceRealDirectory = verifySourceDirectory(sourceDirectory);
|
||||
if (lstatSync(targetDirectory, { throwIfNoEntry: false }) !== undefined) {
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
'target directory must not already exist',
|
||||
);
|
||||
}
|
||||
createPrivateDirectory(targetDirectory, 'target directory');
|
||||
const published: string[] = [];
|
||||
try {
|
||||
for (const input of INPUTS) {
|
||||
const sourceFile = confinedSourceFile(
|
||||
sourceDirectory,
|
||||
sourceRealDirectory,
|
||||
input.name,
|
||||
);
|
||||
const bytes = readStableSourceFile(sourceFile, input.maximumBytes);
|
||||
const targetFile = join(targetDirectory, input.name);
|
||||
try {
|
||||
publishPrivateFile(targetFile, bytes);
|
||||
published.push(targetFile);
|
||||
} finally {
|
||||
bytes.fill(0);
|
||||
}
|
||||
}
|
||||
syncDirectory(targetDirectory);
|
||||
if (deliveryDirectory !== undefined) {
|
||||
prepareDeliveryDirectory(deliveryDirectory);
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
component: 'qinglong3-security-administration-kubernetes-input-stage',
|
||||
stagedFileCount: 4,
|
||||
deliveryDirectoryPrepared: deliveryDirectory !== undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
for (const targetFile of published.reverse()) {
|
||||
try {
|
||||
unlinkSync(targetFile);
|
||||
} catch {
|
||||
// The failed stage remains fail-closed and the Pod never starts main.
|
||||
}
|
||||
}
|
||||
try {
|
||||
syncDirectory(targetDirectory);
|
||||
rmdirSync(targetDirectory);
|
||||
} catch {
|
||||
// Preserve the original staging failure.
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {
|
||||
ClusterAdministrationKubernetesInputStageError,
|
||||
stageClusterAdministrationKubernetesInputs,
|
||||
} from './clusterAdministrationKubernetesInputStage';
|
||||
|
||||
const USAGE =
|
||||
'Usage: ql3-security-admin-kubernetes-stage --source=/absolute/projected-input --target=/absolute/private-input [--delivery-directory=/absolute/private-delivery]';
|
||||
|
||||
function argumentsFrom(argv: readonly string[]) {
|
||||
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
|
||||
return Object.freeze({ kind: 'help' as const });
|
||||
}
|
||||
const values = new Map<string, string>();
|
||||
for (const argument of argv) {
|
||||
const match = /^--(source|target|delivery-directory)=(\/.+)$/.exec(
|
||||
argument,
|
||||
);
|
||||
if (!match || values.has(match[1]!)) {
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
'CLI arguments are invalid',
|
||||
);
|
||||
}
|
||||
values.set(match[1]!, match[2]!);
|
||||
}
|
||||
if (!values.has('source') || !values.has('target')) {
|
||||
throw new ClusterAdministrationKubernetesInputStageError(
|
||||
'CLI arguments are invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
kind: 'run' as const,
|
||||
paths: Object.freeze({
|
||||
sourceDirectory: values.get('source')!,
|
||||
targetDirectory: values.get('target')!,
|
||||
...(values.has('delivery-directory')
|
||||
? { deliveryDirectory: values.get('delivery-directory')! }
|
||||
: {}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function failure(error: unknown): Readonly<Record<string, unknown>> {
|
||||
const candidate = error as {
|
||||
readonly name?: unknown;
|
||||
readonly code?: unknown;
|
||||
};
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
component: 'qinglong3-security-administration-kubernetes-input-stage',
|
||||
event: 'stage_failed',
|
||||
name:
|
||||
typeof candidate?.name === 'string' && candidate.name.length <= 128
|
||||
? candidate.name
|
||||
: 'Error',
|
||||
...(typeof candidate?.code === 'string' && candidate.code.length <= 128
|
||||
? { code: candidate.code }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
function main(argv: readonly string[]): void {
|
||||
try {
|
||||
const parsed = argumentsFrom(argv);
|
||||
if (parsed.kind === 'help') {
|
||||
process.stdout.write(`${USAGE}\n`);
|
||||
return;
|
||||
}
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
stageClusterAdministrationKubernetesInputs(parsed.paths),
|
||||
)}\n`,
|
||||
);
|
||||
} catch (error) {
|
||||
process.stderr.write(`${JSON.stringify(failure(error))}\n`);
|
||||
process.exitCode =
|
||||
error instanceof ClusterAdministrationKubernetesInputStageError ? 64 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main(process.argv.slice(2));
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
chmodSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} = require('node:fs');
|
||||
const { tmpdir } = require('node:os');
|
||||
const { join, resolve } = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { afterEach, test } = require('node:test');
|
||||
|
||||
const {
|
||||
ClusterAdministrationKubernetesInputStageError,
|
||||
stageClusterAdministrationKubernetesInputs,
|
||||
} = require('../dist/security-administration/clusterAdministrationKubernetesInputStage.js');
|
||||
|
||||
const roots = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) {
|
||||
rmSync(root, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
function projectedInput() {
|
||||
const root = mkdtempSync(join(tmpdir(), 'ql3-security-admin-stage-'));
|
||||
roots.push(root);
|
||||
const sourceDirectory = join(root, 'projected');
|
||||
const versionDirectory = join(sourceDirectory, '..2026_08_25_00_00_00');
|
||||
mkdirSync(versionDirectory, { mode: 0o700, recursive: true });
|
||||
const inputs = {
|
||||
'command.json': '{"schemaVersion":1,"operation":"audit.list"}\n',
|
||||
'assertion.jwt': 'signed.assertion.value',
|
||||
'keyset.json': '{"keys":[]}',
|
||||
pepper: 'A'.repeat(43),
|
||||
};
|
||||
for (const [name, value] of Object.entries(inputs)) {
|
||||
const versionFile = join(versionDirectory, name);
|
||||
writeFileSync(versionFile, value, { mode: 0o440 });
|
||||
symlinkSync(join('..data', name), join(sourceDirectory, name));
|
||||
}
|
||||
symlinkSync('..2026_08_25_00_00_00', join(sourceDirectory, '..data'));
|
||||
return {
|
||||
root,
|
||||
sourceDirectory,
|
||||
targetDirectory: join(root, 'private-input'),
|
||||
deliveryDirectory: join(root, 'private-delivery'),
|
||||
inputs,
|
||||
};
|
||||
}
|
||||
|
||||
function mode(filePath) {
|
||||
return lstatSync(filePath).mode & 0o777;
|
||||
}
|
||||
|
||||
test('copies a Kubernetes projected Secret into a private immutable input boundary', () => {
|
||||
const fixture = projectedInput();
|
||||
|
||||
const result = stageClusterAdministrationKubernetesInputs({
|
||||
sourceDirectory: fixture.sourceDirectory,
|
||||
targetDirectory: fixture.targetDirectory,
|
||||
});
|
||||
|
||||
assert.deepEqual(result, {
|
||||
schemaVersion: 1,
|
||||
component: 'qinglong3-security-administration-kubernetes-input-stage',
|
||||
stagedFileCount: 4,
|
||||
deliveryDirectoryPrepared: false,
|
||||
});
|
||||
assert.equal(mode(fixture.targetDirectory), 0o700);
|
||||
for (const [name, value] of Object.entries(fixture.inputs)) {
|
||||
const target = join(fixture.targetDirectory, name);
|
||||
assert.equal(mode(target), 0o600);
|
||||
assert.equal(readFileSync(target, 'utf8'), value);
|
||||
assert.equal(lstatSync(target).isSymbolicLink(), false);
|
||||
}
|
||||
assert.equal(
|
||||
JSON.stringify(result).includes('signed.assertion.value'),
|
||||
false,
|
||||
);
|
||||
assert.equal(JSON.stringify(result).includes('A'.repeat(43)), false);
|
||||
});
|
||||
|
||||
test('prepares a private persistent delivery directory without weakening it', () => {
|
||||
const fixture = projectedInput();
|
||||
|
||||
const first = stageClusterAdministrationKubernetesInputs({
|
||||
sourceDirectory: fixture.sourceDirectory,
|
||||
targetDirectory: fixture.targetDirectory,
|
||||
deliveryDirectory: fixture.deliveryDirectory,
|
||||
});
|
||||
|
||||
assert.equal(first.deliveryDirectoryPrepared, true);
|
||||
assert.equal(mode(fixture.deliveryDirectory), 0o700);
|
||||
|
||||
const secondTarget = join(fixture.root, 'second-private-input');
|
||||
const second = stageClusterAdministrationKubernetesInputs({
|
||||
sourceDirectory: fixture.sourceDirectory,
|
||||
targetDirectory: secondTarget,
|
||||
deliveryDirectory: fixture.deliveryDirectory,
|
||||
});
|
||||
assert.equal(second.deliveryDirectoryPrepared, true);
|
||||
assert.equal(mode(fixture.deliveryDirectory), 0o700);
|
||||
});
|
||||
|
||||
test('rejects a projected input symlink that escapes the Secret authority', () => {
|
||||
const fixture = projectedInput();
|
||||
const external = join(fixture.root, 'external-command.json');
|
||||
writeFileSync(external, 'outside', { mode: 0o400 });
|
||||
unlinkSync(join(fixture.sourceDirectory, 'command.json'));
|
||||
symlinkSync(external, join(fixture.sourceDirectory, 'command.json'));
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
stageClusterAdministrationKubernetesInputs({
|
||||
sourceDirectory: fixture.sourceDirectory,
|
||||
targetDirectory: fixture.targetDirectory,
|
||||
}),
|
||||
(error) =>
|
||||
error instanceof ClusterAdministrationKubernetesInputStageError &&
|
||||
/escapes/.test(error.message),
|
||||
);
|
||||
assert.throws(() => lstatSync(fixture.targetDirectory));
|
||||
});
|
||||
|
||||
test('rejects source material readable by every local process', () => {
|
||||
const fixture = projectedInput();
|
||||
chmodSync(resolve(fixture.sourceDirectory, '..data', 'assertion.jwt'), 0o444);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
stageClusterAdministrationKubernetesInputs({
|
||||
sourceDirectory: fixture.sourceDirectory,
|
||||
targetDirectory: fixture.targetDirectory,
|
||||
}),
|
||||
/file authority is invalid/,
|
||||
);
|
||||
assert.throws(() => lstatSync(fixture.targetDirectory));
|
||||
});
|
||||
|
||||
test('never replaces an existing private input directory', () => {
|
||||
const fixture = projectedInput();
|
||||
mkdirSync(fixture.targetDirectory, { mode: 0o700 });
|
||||
const sentinel = join(fixture.targetDirectory, 'sentinel');
|
||||
writeFileSync(sentinel, 'preserve', { mode: 0o600 });
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
stageClusterAdministrationKubernetesInputs({
|
||||
sourceDirectory: fixture.sourceDirectory,
|
||||
targetDirectory: fixture.targetDirectory,
|
||||
}),
|
||||
/must not already exist/,
|
||||
);
|
||||
assert.equal(readFileSync(sentinel, 'utf8'), 'preserve');
|
||||
});
|
||||
|
||||
test('CLI emits only bounded content-free failures', () => {
|
||||
const cli = join(
|
||||
__dirname,
|
||||
'../dist/security-administration/clusterAdministrationKubernetesInputStageCli.js',
|
||||
);
|
||||
const sensitive = 'ql3c_private-token-material';
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[cli, `--source=/${sensitive}`, '--target=relative'],
|
||||
{ encoding: 'utf8' },
|
||||
);
|
||||
|
||||
assert.equal(result.status, 64);
|
||||
assert.equal(result.stdout, '');
|
||||
assert.equal(result.stderr.includes(sensitive), false);
|
||||
assert.deepEqual(JSON.parse(result.stderr), {
|
||||
schemaVersion: 1,
|
||||
component: 'qinglong3-security-administration-kubernetes-input-stage',
|
||||
event: 'stage_failed',
|
||||
name: 'ClusterAdministrationKubernetesInputStageError',
|
||||
code: 'QL3_CLUSTER_ADMINISTRATION_KUBERNETES_INPUT_STAGE_INVALID',
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user