feat(ql3): add short-lived security administration command

This commit is contained in:
whyour
2026-08-25 02:09:06 +08:00
parent 9c736f0943
commit 7b3d49acda
17 changed files with 1756 additions and 17 deletions
+6
View File
@@ -75,6 +75,11 @@
"require": "./dist/security-administration/clusterAdministration.js",
"default": "./dist/security-administration/clusterAdministration.js"
},
"./administration-command": {
"types": "./dist/security-administration/clusterAdministrationCommand.d.ts",
"require": "./dist/security-administration/clusterAdministrationCommand.js",
"default": "./dist/security-administration/clusterAdministrationCommand.js"
},
"./automation-management": {
"types": "./dist/automation-management/automationManagement.d.ts",
"require": "./dist/automation-management/automationManagement.js",
@@ -414,6 +419,7 @@
},
"bin": {
"ql3-cluster-admin": "dist/product-cli/cli.js",
"ql3-security-admin": "dist/security-administration/clusterAdministrationCli.js",
"ql3-copilot-client": "dist/copilot-client/cli.js",
"ql3-copilot-mcp": "dist/copilot-mcp/cli.js",
"ql3-copilot-console": "dist/copilot-console/cli.js",
@@ -85,6 +85,12 @@ export const CLUSTER_RUN_MANAGEMENT_IDENTITY_ASSERTION_PROFILE = Object.freeze({
purpose: 'run-management',
});
export const CLUSTER_SECURITY_ADMINISTRATION_IDENTITY_ASSERTION_PROFILE =
Object.freeze({
type: 'ql3-security-administration+jwt',
purpose: 'security-administration',
});
export interface ClusterPluginPackageIdentityAssertionVerifierOptions {
readonly issuer: string;
readonly audience: string;
@@ -10,6 +10,7 @@ import {
CLUSTER_APPROVAL_MANAGEMENT_IDENTITY_ASSERTION_PROFILE,
CLUSTER_MODEL_PROVIDER_CREDENTIAL_MANAGEMENT_IDENTITY_ASSERTION_PROFILE,
CLUSTER_RUN_MANAGEMENT_IDENTITY_ASSERTION_PROFILE,
CLUSTER_SECURITY_ADMINISTRATION_IDENTITY_ASSERTION_PROFILE,
createClusterPluginPackageIdentityAssertionVerifier,
type ClusterManagementIdentityAssertionProfile,
type ClusterPluginPackageIdentityAssertionAuthentication,
@@ -517,3 +518,13 @@ export function createClusterRunIdentityKeysetFile(
assertionProfile: CLUSTER_RUN_MANAGEMENT_IDENTITY_ASSERTION_PROFILE,
});
}
export function createClusterSecurityAdministrationIdentityKeysetFile(
options: ClusterWorkerCredentialIdentityKeysetFileOptions,
): Readonly<ClusterPluginPackageIdentityKeysetFile> {
return createClusterPluginPackageIdentityKeysetFile({
...options,
assertionProfile:
CLUSTER_SECURITY_ADMINISTRATION_IDENTITY_ASSERTION_PROFILE,
});
}
@@ -83,6 +83,12 @@ export const QINGLONG3_CLUSTER_PRODUCT_COMMANDS: readonly QingLong3ClusterProduc
target: 'worker-credential/workerCredentialManagementClientCli.js',
description: 'manage Worker credentials through the authenticated API',
}),
Object.freeze({
name: 'security',
binary: 'ql3-security-admin',
target: 'security-administration/clusterAdministrationCli.js',
description: 'administer identities, API credentials and audit records',
}),
Object.freeze({
name: 'approval',
binary: 'ql3-approval-client',
@@ -0,0 +1,87 @@
#!/usr/bin/env node
import {
ClusterAdministrationCommandError,
createClusterAdministrationCommandRunner,
} from './clusterAdministrationCommand';
const USAGE =
'Usage: ql3-security-admin --command=/absolute/command.json --assertion=/absolute/assertion.jwt --keyset=/absolute/keyset.json --pepper=/absolute/pepper [--delivery=/absolute/token.json]';
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 = /^--(command|assertion|keyset|pepper|delivery)=(\/.+)$/.exec(
argument,
);
if (!match || values.has(match[1]!)) {
throw new ClusterAdministrationCommandError('CLI arguments are invalid');
}
values.set(match[1]!, match[2]!);
}
if (
!values.has('command') ||
!values.has('assertion') ||
!values.has('keyset') ||
!values.has('pepper')
) {
throw new ClusterAdministrationCommandError('CLI arguments are invalid');
}
return Object.freeze({
kind: 'run' as const,
paths: Object.freeze({
commandFile: values.get('command')!,
assertionFile: values.get('assertion')!,
keysetFile: values.get('keyset')!,
pepperFile: values.get('pepper')!,
...(values.has('delivery')
? { deliveryFile: values.get('delivery')! }
: {}),
}),
});
}
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',
event: 'command_failed',
name:
typeof candidate?.name === 'string' && candidate.name.length <= 128
? candidate.name
: 'Error',
...(typeof candidate?.code === 'string' && candidate.code.length <= 128
? { code: candidate.code }
: {}),
});
}
async function main(argv: readonly string[]): Promise<void> {
try {
const parsed = argumentsFrom(argv);
if (parsed.kind === 'help') {
process.stdout.write(`${USAGE}\n`);
return;
}
const result = await createClusterAdministrationCommandRunner().run(
parsed.paths,
process.env,
);
process.stdout.write(`${JSON.stringify(result)}\n`);
} catch (error) {
process.stderr.write(`${JSON.stringify(failure(error))}\n`);
process.exitCode =
error instanceof ClusterAdministrationCommandError ? 64 : 1;
}
}
if (require.main === module) {
void main(process.argv.slice(2));
}
@@ -0,0 +1,468 @@
import { createHash } from 'node:crypto';
import { basename } from 'node:path';
import { assertApiCredentialPepper } from '@qinglong/runtime-core/api-credential-token';
import { normalizeIdentityAdministrationSubject } from '@qinglong/runtime-core/identity-administration';
import {
normalizeSecurityAuditQuery,
type SecurityAuditQuery,
type SecurityAuditQueryPage,
type SecurityAuditQueryRepository,
} from '@qinglong/runtime-core/security-audit-query';
import type {
SecurityPrincipal,
SecuritySubject,
} from '@qinglong/runtime-core/security';
import { type ClusterAdministrationService } from './clusterAdministration';
import {
CLUSTER_ADMINISTRATION_COMMAND_RUNTIME_DEPENDENCIES,
ClusterAdministrationCommandError,
clusterAdministrationCommandFileBeforeAdmission,
normalizeClusterAdministrationCommandPaths,
publishClusterAdministrationCredentialDelivery,
} from './clusterAdministrationCommandRuntime';
export {
ClusterAdministrationCommandError,
publishClusterAdministrationCredentialDelivery,
};
const UUID_V4_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const MAX_VERSION = 2_147_483_646;
const MAX_COMMAND_BYTES = 64 * 1024;
const MAX_ASSERTION_BYTES = 16 * 1024;
const MAX_PEPPER_BYTES = 256;
export type ClusterAdministrationCommandOperation =
| 'identity.register'
| 'identity.enable'
| 'identity.disable'
| 'credential.issue'
| 'credential.rotate'
| 'credential.revoke'
| 'audit.list';
interface BaseMutationRequest {
readonly mutationId: string;
readonly requestId: string;
readonly expectedCurrentVersion: number;
readonly subject: SecuritySubject;
}
interface IdentityCommand {
readonly schemaVersion: 1;
readonly operation:
| 'identity.register'
| 'identity.enable'
| 'identity.disable';
readonly request: BaseMutationRequest;
}
interface CredentialCommand {
readonly schemaVersion: 1;
readonly operation:
| 'credential.issue'
| 'credential.rotate'
| 'credential.revoke';
readonly request: BaseMutationRequest & {
readonly credentialId: string;
readonly notBeforeAtMs?: number;
readonly expiresAtMs?: number;
};
}
interface AuditCommand {
readonly schemaVersion: 1;
readonly operation: 'audit.list';
readonly request: SecurityAuditQuery;
}
export type ClusterAdministrationCommand =
| IdentityCommand
| CredentialCommand
| AuditCommand;
export interface ClusterAdministrationCommandPaths {
readonly commandFile: string;
readonly assertionFile: string;
readonly keysetFile: string;
readonly pepperFile: string;
readonly deliveryFile?: string;
}
export type ClusterAdministrationCommandResult =
| Readonly<{
schemaVersion: 1;
operation: IdentityCommand['operation'];
status: 'inserted' | 'existing';
subject: Readonly<SecuritySubject>;
version: number;
identityStatus: 'active' | 'disabled';
}>
| Readonly<{
schemaVersion: 1;
operation: CredentialCommand['operation'];
status: 'inserted' | 'existing';
subject: Readonly<SecuritySubject>;
credentialId: string;
version: number;
state: 'active' | 'revoked';
delivery?: Readonly<{ fileName: string; digest: string }>;
}>
| Readonly<{
schemaVersion: 1;
operation: 'audit.list';
page: Readonly<SecurityAuditQueryPage>;
}>;
export interface ClusterAdministrationCommandAuthority {
readonly administration: ClusterAdministrationService;
readonly audit: SecurityAuditQueryRepository;
close(): Promise<void>;
}
export interface ClusterAdministrationCommandDependencies {
readonly openAuthority: (
environment: Readonly<Record<string, string | undefined>>,
pepper: string,
) => Promise<Readonly<ClusterAdministrationCommandAuthority>>;
readonly authenticate: (
keysetFile: string,
assertion: string,
) => Promise<Readonly<SecurityPrincipal>>;
readonly readFile: (
filePath: string,
maximumBytes: number,
privateMaterial: boolean,
) => Buffer;
readonly publishDelivery: (filePath: string, bytes: Buffer) => void;
}
function exactObject(
value: unknown,
expectedKeys: readonly string[],
label: string,
): asserts value is Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new ClusterAdministrationCommandError(`${label} must be an object`);
}
const actual = Object.keys(value).sort();
const expected = [...expectedKeys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
throw new ClusterAdministrationCommandError(`${label} shape is invalid`);
}
}
function strictUtf8(bytes: Buffer, label: string): string {
try {
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
} catch (error) {
throw new ClusterAdministrationCommandError(
`${label} must be strict UTF-8`,
error,
);
}
}
function normalizeMutationRequest(
value: unknown,
activeCredential: boolean,
credential: boolean,
): BaseMutationRequest & {
readonly credentialId?: string;
readonly notBeforeAtMs?: number;
readonly expiresAtMs?: number;
} {
exactObject(
value,
[
'expectedCurrentVersion',
'mutationId',
'requestId',
'subject',
...(credential ? ['credentialId'] : []),
...(activeCredential ? ['notBeforeAtMs', 'expiresAtMs'] : []),
],
'request',
);
let subject: Readonly<SecuritySubject>;
try {
subject = normalizeIdentityAdministrationSubject(
value.subject as SecuritySubject,
);
} catch (error) {
throw new ClusterAdministrationCommandError('subject is invalid', error);
}
if (
typeof value.mutationId !== 'string' ||
!UUID_V4_PATTERN.test(value.mutationId) ||
typeof value.requestId !== 'string' ||
!REQUEST_ID_PATTERN.test(value.requestId) ||
!Number.isSafeInteger(value.expectedCurrentVersion) ||
(value.expectedCurrentVersion as number) < 0 ||
(value.expectedCurrentVersion as number) > MAX_VERSION ||
(credential &&
(typeof value.credentialId !== 'string' ||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value.credentialId))) ||
(activeCredential &&
(!Number.isSafeInteger(value.notBeforeAtMs) ||
(value.notBeforeAtMs as number) < 0 ||
!Number.isSafeInteger(value.expiresAtMs) ||
(value.expiresAtMs as number) <= (value.notBeforeAtMs as number)))
) {
throw new ClusterAdministrationCommandError('mutation request is invalid');
}
return Object.freeze({
mutationId: value.mutationId,
requestId: value.requestId,
expectedCurrentVersion: value.expectedCurrentVersion as number,
subject,
...(credential ? { credentialId: value.credentialId as string } : {}),
...(activeCredential
? {
notBeforeAtMs: value.notBeforeAtMs as number,
expiresAtMs: value.expiresAtMs as number,
}
: {}),
});
}
export function normalizeClusterAdministrationCommand(
value: unknown,
): Readonly<ClusterAdministrationCommand> {
exactObject(value, ['operation', 'request', 'schemaVersion'], 'command');
const operations: readonly ClusterAdministrationCommandOperation[] = [
'identity.register',
'identity.enable',
'identity.disable',
'credential.issue',
'credential.rotate',
'credential.revoke',
'audit.list',
];
if (
value.schemaVersion !== 1 ||
typeof value.operation !== 'string' ||
!operations.includes(
value.operation as ClusterAdministrationCommandOperation,
)
) {
throw new ClusterAdministrationCommandError(
'command version or operation is invalid',
);
}
const operation = value.operation as ClusterAdministrationCommandOperation;
if (operation === 'audit.list') {
let request: Readonly<SecurityAuditQuery>;
try {
request = normalizeSecurityAuditQuery(
value.request as SecurityAuditQuery,
);
} catch (error) {
throw new ClusterAdministrationCommandError(
'audit query is invalid',
error,
);
}
return Object.freeze({ schemaVersion: 1, operation, request });
}
const credential = operation.startsWith('credential.');
const activeCredential =
operation === 'credential.issue' || operation === 'credential.rotate';
return Object.freeze({
schemaVersion: 1,
operation,
request: normalizeMutationRequest(
value.request,
activeCredential,
credential,
),
} as ClusterAdministrationCommand);
}
function parseCommand(bytes: Buffer): Readonly<ClusterAdministrationCommand> {
let value: unknown;
try {
value = JSON.parse(strictUtf8(bytes, 'command file'));
} catch (error) {
if (error instanceof ClusterAdministrationCommandError) throw error;
throw new ClusterAdministrationCommandError(
'command file must contain JSON',
error,
);
}
return normalizeClusterAdministrationCommand(value);
}
function credentialDelivery(
command: CredentialCommand,
token: string,
result: Awaited<ReturnType<ClusterAdministrationService['issueCredential']>>,
): Buffer {
return Buffer.from(
`${JSON.stringify({
schemaVersion: 1,
kind: 'qinglong3-cluster-api-credential-delivery',
operation: command.operation,
mutationId: command.request.mutationId,
requestId: command.request.requestId,
credentialId: result.credential.credentialId,
subject: result.credential.subject,
version: result.credential.version,
token,
notBeforeAtMs: result.credential.notBeforeAtMs,
expiresAtMs: result.credential.expiresAtMs,
})}\n`,
'utf8',
);
}
export function createClusterAdministrationCommandRunner(
dependencies: ClusterAdministrationCommandDependencies = CLUSTER_ADMINISTRATION_COMMAND_RUNTIME_DEPENDENCIES,
): Readonly<{
run(
paths: ClusterAdministrationCommandPaths,
environment: Readonly<Record<string, string | undefined>>,
): Promise<Readonly<ClusterAdministrationCommandResult>>;
}> {
exactObject(
dependencies,
['authenticate', 'openAuthority', 'publishDelivery', 'readFile'],
'dependencies',
);
if (
typeof dependencies.openAuthority !== 'function' ||
typeof dependencies.authenticate !== 'function' ||
typeof dependencies.readFile !== 'function' ||
typeof dependencies.publishDelivery !== 'function'
) {
throw new ClusterAdministrationCommandError('dependencies are invalid');
}
return Object.freeze({
async run(pathsValue, environment) {
const commandFile =
clusterAdministrationCommandFileBeforeAdmission(pathsValue);
const commandBytes = dependencies.readFile(
commandFile,
MAX_COMMAND_BYTES,
true,
);
let command: Readonly<ClusterAdministrationCommand>;
try {
command = parseCommand(commandBytes);
} finally {
commandBytes.fill(0);
}
const requiresDelivery =
command.operation === 'credential.issue' ||
command.operation === 'credential.rotate';
const paths = normalizeClusterAdministrationCommandPaths(
pathsValue,
requiresDelivery,
);
const assertionBytes = dependencies.readFile(
paths.assertionFile,
MAX_ASSERTION_BYTES,
true,
);
const pepperBytes = dependencies.readFile(
paths.pepperFile,
MAX_PEPPER_BYTES,
true,
);
let authority:
| Readonly<ClusterAdministrationCommandAuthority>
| undefined;
try {
const assertion = strictUtf8(assertionBytes, 'assertion file').trim();
const pepper = strictUtf8(pepperBytes, 'pepper file').trim();
assertApiCredentialPepper(pepper);
const principal = await dependencies.authenticate(
paths.keysetFile,
assertion,
);
authority = await dependencies.openAuthority(environment, pepper);
if (command.operation === 'audit.list') {
// Successful verification is the short-lived admin admission. Audit
// queries remain read-only and use the repository's bounded contract.
void principal;
return Object.freeze({
schemaVersion: 1 as const,
operation: command.operation,
page: await authority.audit.list(command.request),
});
}
if (command.operation.startsWith('identity.')) {
const identityCommand = command as Readonly<IdentityCommand>;
const operation = identityCommand.operation.slice(
'identity.'.length,
) as 'register' | 'enable' | 'disable';
const result = await authority.administration[
operation === 'register'
? 'registerIdentity'
: operation === 'enable'
? 'enableIdentity'
: 'disableIdentity'
]({ ...identityCommand.request, principal });
return Object.freeze({
schemaVersion: 1 as const,
operation: identityCommand.operation,
status: result.status,
subject: result.identity.subject,
version: result.identity.version,
identityStatus: result.identity.status,
});
}
const credentialCommand = command as Readonly<CredentialCommand>;
const method =
credentialCommand.operation === 'credential.issue'
? 'issueCredential'
: credentialCommand.operation === 'credential.rotate'
? 'rotateCredential'
: 'revokeCredential';
const result = await authority.administration[method]({
...credentialCommand.request,
principal,
} as never);
let delivery:
| Readonly<{ fileName: string; digest: string }>
| undefined;
if (typeof result.token === 'string') {
const bytes = credentialDelivery(
credentialCommand,
result.token,
result,
);
try {
dependencies.publishDelivery(paths.deliveryFile!, bytes);
delivery = Object.freeze({
fileName: basename(paths.deliveryFile!),
digest: createHash('sha256').update(bytes).digest('hex'),
});
} finally {
bytes.fill(0);
}
}
return Object.freeze({
schemaVersion: 1 as const,
operation: credentialCommand.operation,
status: result.status,
subject: result.credential.subject,
credentialId: result.credential.credentialId,
version: result.credential.version,
state: result.credential.state,
...(delivery === undefined ? {} : { delivery }),
});
} finally {
assertionBytes.fill(0);
pepperBytes.fill(0);
await authority?.close();
}
},
});
}
@@ -0,0 +1,467 @@
import { randomBytes as nodeRandomBytes } from 'node:crypto';
import {
closeSync,
constants,
fstatSync,
fsyncSync,
linkSync,
lstatSync,
openSync,
readSync,
unlinkSync,
writeSync,
} from 'node:fs';
import {
basename,
dirname,
isAbsolute,
normalize,
parse,
resolve,
} from 'node:path';
import { assertApiCredentialPepper } from '@qinglong/runtime-core/api-credential-token';
import {
PostgresApiCredentialAdministrationRepository,
PostgresIdentityAdministrationRepository,
PostgresSecurityAuditQueryRepository,
assertPostgresAdminSchemaReady,
createPostgresDatabaseOpener,
isPostgresTlsDnsServername,
loadPostgresCertificateAuthorityFile,
loadPostgresConnectionEnvironment,
type QingLongPostgresDatabaseResource,
} from '@qinglong/cluster-postgres/admin';
import { createClusterSecurityAdministrationIdentityKeysetFile } from '../management-support/pluginPackageIdentityKeyset';
import { createClusterAdministrationService } from './clusterAdministration';
import type {
ClusterAdministrationCommandAuthority,
ClusterAdministrationCommandDependencies,
ClusterAdministrationCommandPaths,
} from './clusterAdministrationCommand';
const MAX_DELIVERY_BYTES = 32 * 1024;
export class ClusterAdministrationCommandError extends TypeError {
readonly code = 'QL3_CLUSTER_ADMINISTRATION_COMMAND_INVALID';
constructor(message: string, readonly cause?: unknown) {
super(`Cluster administration command is invalid: ${message}`);
this.name = 'ClusterAdministrationCommandError';
}
}
export function boundedClusterAdministrationFile(
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 ClusterAdministrationCommandError(
`${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 readStableFile(
candidatePath: string,
maximumBytes: number,
privateMaterial: boolean,
): Buffer {
const filePath = boundedClusterAdministrationFile(
candidatePath,
'input file',
);
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 & (privateMaterial ? 0o077 : 0o022)) !== 0
) {
throw new ClusterAdministrationCommandError(
'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 ClusterAdministrationCommandError(
'input file changed while being read',
);
}
return bytes.subarray(0, offset);
} catch (error) {
bytes?.fill(0);
if (error instanceof ClusterAdministrationCommandError) throw error;
throw new ClusterAdministrationCommandError(
'input file cannot be read',
error,
);
} finally {
if (descriptor !== undefined) closeSync(descriptor);
}
}
function booleanEnvironment(
environment: Readonly<Record<string, string | undefined>>,
name: string,
): boolean {
const value = environment[name];
if (value === 'true') return true;
if (value === undefined || value === '' || value === 'false') return false;
throw new ClusterAdministrationCommandError(`${name} must be true or false`);
}
function defaultDatabaseOpener(
environment: Readonly<Record<string, string | undefined>>,
): () => Promise<QingLongPostgresDatabaseResource> {
let connection;
try {
connection = loadPostgresConnectionEnvironment(environment, {
connectionString: 'QL3_POSTGRES_ADMIN_URL',
host: 'QL3_POSTGRES_ADMIN_HOST',
port: 'QL3_POSTGRES_ADMIN_PORT',
database: 'QL3_POSTGRES_ADMIN_DATABASE',
user: 'QL3_POSTGRES_ADMIN_USER',
password: 'QL3_POSTGRES_ADMIN_PASSWORD',
});
} catch (error) {
throw new ClusterAdministrationCommandError(
'PostgreSQL admin connection is invalid',
error,
);
}
const mode = environment.QL3_POSTGRES_ADMIN_TLS_MODE ?? 'verify-full';
if (mode !== 'verify-full' && mode !== 'disable') {
throw new ClusterAdministrationCommandError(
'QL3_POSTGRES_ADMIN_TLS_MODE must be verify-full or disable',
);
}
if (
mode === 'disable' &&
!booleanEnvironment(environment, 'QL3_POSTGRES_ADMIN_ALLOW_INSECURE')
) {
throw new ClusterAdministrationCommandError(
'disabling PostgreSQL admin TLS requires explicit opt-in',
);
}
const servername = environment.QL3_POSTGRES_ADMIN_TLS_SERVERNAME;
if (mode === 'verify-full' && !isPostgresTlsDnsServername(servername)) {
throw new ClusterAdministrationCommandError(
'QL3_POSTGRES_ADMIN_TLS_SERVERNAME must be an explicit DNS name',
);
}
const caFile = environment.QL3_POSTGRES_ADMIN_TLS_CA_FILE;
if (mode === 'disable' && caFile) {
throw new ClusterAdministrationCommandError(
'PostgreSQL admin CA cannot be used when TLS is disabled',
);
}
let ca: string | undefined;
if (caFile) {
try {
ca = loadPostgresCertificateAuthorityFile(
boundedClusterAdministrationFile(caFile, 'PostgreSQL admin CA file'),
);
} catch (error) {
throw new ClusterAdministrationCommandError(
'PostgreSQL admin CA file is invalid',
error,
);
}
}
return createPostgresDatabaseOpener({
role: 'admin',
connection: Object.freeze({
...connection,
tls:
mode === 'disable'
? Object.freeze({ mode: 'disable' as const })
: Object.freeze({
mode: 'verify-full' as const,
servername: servername!,
...(ca === undefined ? {} : { ca }),
}),
}),
pool: Object.freeze({
applicationName: 'qinglong3-security-admin',
maxConnections: 1,
connectionTimeoutMs: 5_000,
idleTimeoutMs: 1_000,
maxLifetimeSeconds: 60,
}),
onPoolError() {},
});
}
async function openDefaultAuthority(
environment: Readonly<Record<string, string | undefined>>,
pepper: string,
): Promise<Readonly<ClusterAdministrationCommandAuthority>> {
assertApiCredentialPepper(pepper);
const database = await defaultDatabaseOpener(environment)();
let closePromise: Promise<void> | undefined;
const close = (): Promise<void> => {
closePromise ??= database.close();
return closePromise;
};
try {
await assertPostgresAdminSchemaReady(database.pool);
return Object.freeze({
administration: createClusterAdministrationService(
new PostgresIdentityAdministrationRepository(database.pool),
new PostgresApiCredentialAdministrationRepository(database.pool),
pepper,
),
audit: new PostgresSecurityAuditQueryRepository(database.pool),
close,
});
} catch (error) {
await close().catch(() => undefined);
throw error;
}
}
async function authenticateDefault(keysetFile: string, assertion: string) {
const identities = createClusterSecurityAdministrationIdentityKeysetFile({
filePath: boundedClusterAdministrationFile(
keysetFile,
'identity keyset file',
),
});
return identities.bind(assertion).authenticate();
}
export function publishClusterAdministrationCredentialDelivery(
filePathValue: string,
bytes: Buffer,
): void {
const filePath = boundedClusterAdministrationFile(
filePathValue,
'delivery file',
);
if (bytes.length < 1 || bytes.length > MAX_DELIVERY_BYTES) {
throw new ClusterAdministrationCommandError(
'credential delivery is oversized',
);
}
const parent = dirname(filePath);
const parentStatus = lstatSync(parent, { throwIfNoEntry: false });
if (
parentStatus === undefined ||
!parentStatus.isDirectory() ||
parentStatus.isSymbolicLink() ||
(parentStatus.mode & 0o077) !== 0
) {
throw new ClusterAdministrationCommandError(
'credential delivery directory authority is invalid',
);
}
const temporary = resolve(
parent,
`.${basename(filePath)}.${process.pid}.${nodeRandomBytes(12).toString(
'hex',
)}.tmp`,
);
let descriptor: number | undefined;
let linked = false;
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 ClusterAdministrationCommandError(
'credential delivery file authority is invalid',
);
}
closeSync(descriptor);
descriptor = undefined;
linkSync(temporary, filePath);
linked = true;
unlinkSync(temporary);
const parentDescriptor = openSync(
parent,
constants.O_RDONLY | (constants.O_DIRECTORY ?? 0),
);
try {
fsyncSync(parentDescriptor);
} finally {
closeSync(parentDescriptor);
}
} catch (error) {
if (descriptor !== undefined) closeSync(descriptor);
try {
unlinkSync(temporary);
} catch {
// Preserve the original delivery failure.
}
if (error instanceof ClusterAdministrationCommandError) throw error;
throw new ClusterAdministrationCommandError(
linked
? 'credential delivery directory could not be synchronized'
: 'credential delivery could not be published',
error,
);
}
}
export function normalizeClusterAdministrationCommandPaths(
value: ClusterAdministrationCommandPaths,
requiresDelivery: boolean,
): Readonly<ClusterAdministrationCommandPaths> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new ClusterAdministrationCommandError(
'command paths must be an object',
);
}
const expected = [
'assertionFile',
'commandFile',
'keysetFile',
'pepperFile',
...(requiresDelivery ? ['deliveryFile'] : []),
].sort();
const actual = Object.keys(value).sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
throw new ClusterAdministrationCommandError(
'command paths shape is invalid',
);
}
return Object.freeze({
commandFile: boundedClusterAdministrationFile(
value.commandFile,
'command file',
),
assertionFile: boundedClusterAdministrationFile(
value.assertionFile,
'assertion file',
),
keysetFile: boundedClusterAdministrationFile(
value.keysetFile,
'identity keyset file',
),
pepperFile: boundedClusterAdministrationFile(
value.pepperFile,
'pepper file',
),
...(requiresDelivery
? {
deliveryFile: boundedClusterAdministrationFile(
value.deliveryFile,
'delivery file',
),
}
: {}),
});
}
export function clusterAdministrationCommandFileBeforeAdmission(
value: unknown,
): string {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new ClusterAdministrationCommandError(
'command paths must be an object',
);
}
const candidate = value as Record<string, unknown>;
const required = ['assertionFile', 'commandFile', 'keysetFile', 'pepperFile'];
if (
required.some((key) => !Object.hasOwn(candidate, key)) ||
Object.keys(candidate).some(
(key) => !required.includes(key) && key !== 'deliveryFile',
)
) {
throw new ClusterAdministrationCommandError(
'command paths shape is invalid',
);
}
return boundedClusterAdministrationFile(
candidate.commandFile,
'command file',
);
}
export const CLUSTER_ADMINISTRATION_COMMAND_RUNTIME_DEPENDENCIES: ClusterAdministrationCommandDependencies =
Object.freeze({
openAuthority: openDefaultAuthority,
authenticate: authenticateDefault,
readFile: readStableFile,
publishDelivery: publishClusterAdministrationCredentialDelivery,
});
@@ -0,0 +1,361 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
createClusterAdministrationCommandRunner,
normalizeClusterAdministrationCommand,
publishClusterAdministrationCredentialDelivery,
} = require('@qinglong/cluster-admin/administration-command');
const PRINCIPAL = Object.freeze({
subject: { type: 'user', id: 'security-owner' },
authenticationId: 'assertion:security-command-1',
authenticatedAtMs: 900,
expiresAtMs: 2_000,
assurance: 'multi_factor',
});
const SUBJECT = Object.freeze({ type: 'api_app', id: 'automation-client' });
const PATHS = Object.freeze({
commandFile: '/private/command.json',
assertionFile: '/private/assertion.jwt',
keysetFile: '/private/keyset.json',
pepperFile: '/private/pepper',
});
function identityCommand(overrides = {}) {
return {
schemaVersion: 1,
operation: 'identity.register',
request: {
mutationId: '123e4567-e89b-42d3-a456-426614174301',
requestId: 'security-identity-register-1',
expectedCurrentVersion: 0,
subject: SUBJECT,
},
...overrides,
};
}
function credentialCommand(overrides = {}) {
return {
schemaVersion: 1,
operation: 'credential.issue',
request: {
mutationId: '123e4567-e89b-42d3-a456-426614174302',
requestId: 'security-credential-issue-1',
expectedCurrentVersion: 0,
credentialId: 'automation-primary',
subject: SUBJECT,
notBeforeAtMs: 1_000,
expiresAtMs: 2_000,
},
...overrides,
};
}
function authority(overrides = {}) {
const calls = [];
let closes = 0;
const credential = {
credentialId: 'automation-primary',
subject: SUBJECT,
secretDigest: 'digest',
pepperKeyId: 'legacy-v1',
state: 'active',
version: 1,
createdAtMs: 1_000,
notBeforeAtMs: 1_000,
expiresAtMs: 2_000,
};
const value = {
administration: {
async registerIdentity(request) {
calls.push(['identity.register', request]);
return {
status: 'inserted',
identity: {
subject: request.subject,
status: 'active',
version: 1,
createdAtMs: 1_000,
updatedAtMs: 1_000,
},
mutation: {},
};
},
async enableIdentity() {
throw new Error('unexpected enable');
},
async disableIdentity() {
throw new Error('unexpected disable');
},
async issueCredential(request) {
calls.push(['credential.issue', request]);
return {
status: 'inserted',
credential,
mutation: {},
token: 'ql3c_automation-primary_private-secret',
};
},
async rotateCredential() {
throw new Error('unexpected rotate');
},
async revokeCredential() {
throw new Error('unexpected revoke');
},
},
audit: {
async list(query) {
calls.push(['audit.list', query]);
return { records: [], nextCursor: null };
},
},
async close() {
closes += 1;
},
...overrides,
};
return { value, calls, closes: () => closes };
}
function runner(command, authorityValue, published = []) {
const buffers = [];
const pepper = 'A'.repeat(43);
const files = new Map([
[PATHS.commandFile, `${JSON.stringify(command)}\n`],
[PATHS.assertionFile, 'signed.assertion.value'],
[PATHS.pepperFile, pepper],
]);
const authentications = [];
const opens = [];
const instance = createClusterAdministrationCommandRunner({
async openAuthority(environment, candidatePepper) {
opens.push({ environment, pepper: candidatePepper });
return authorityValue;
},
async authenticate(keysetFile, assertion) {
authentications.push({ keysetFile, assertion });
return PRINCIPAL;
},
readFile(filePath) {
const value = files.get(filePath);
if (value === undefined) throw new Error(`unexpected file: ${filePath}`);
const buffer = Buffer.from(value);
buffers.push(buffer);
return buffer;
},
publishDelivery(filePath, bytes) {
published.push({ filePath, bytes: Buffer.from(bytes) });
},
});
return { instance, buffers, authentications, opens };
}
test('executes one strongly authenticated identity mutation and closes authority', async () => {
const target = authority();
const execution = runner(identityCommand(), target.value);
const result = await execution.instance.run(PATHS, { deployment: 'test' });
assert.deepEqual(result, {
schemaVersion: 1,
operation: 'identity.register',
status: 'inserted',
subject: SUBJECT,
version: 1,
identityStatus: 'active',
});
assert.deepEqual(execution.authentications, [
{
keysetFile: PATHS.keysetFile,
assertion: 'signed.assertion.value',
},
]);
assert.equal(execution.opens[0].pepper, 'A'.repeat(43));
assert.equal(target.calls[0][1].principal, PRINCIPAL);
assert.equal(target.closes(), 1);
assert.equal(
execution.buffers.every((value) => value.every((byte) => byte === 0)),
true,
);
});
test('publishes a credential token only to the private delivery boundary', async () => {
const target = authority();
const published = [];
const execution = runner(credentialCommand(), target.value, published);
const paths = { ...PATHS, deliveryFile: '/private/delivery.json' };
const result = await execution.instance.run(paths, {});
assert.equal(result.operation, 'credential.issue');
assert.equal(result.status, 'inserted');
assert.equal('token' in result, false);
assert.deepEqual(result.delivery.fileName, 'delivery.json');
assert.match(result.delivery.digest, /^[0-9a-f]{64}$/);
assert.equal(published.length, 1);
const delivery = JSON.parse(published[0].bytes.toString('utf8'));
assert.equal(delivery.token, 'ql3c_automation-primary_private-secret');
assert.equal(delivery.mutationId, credentialCommand().request.mutationId);
assert.equal(target.closes(), 1);
});
test('does not recreate lost token material during exact credential replay', async () => {
const base = authority();
base.value.administration.issueCredential = async () => ({
status: 'existing',
credential: {
credentialId: 'automation-primary',
subject: SUBJECT,
state: 'active',
version: 1,
createdAtMs: 1_000,
notBeforeAtMs: 1_000,
expiresAtMs: 2_000,
},
mutation: {},
token: null,
});
const published = [];
const execution = runner(credentialCommand(), base.value, published);
const result = await execution.instance.run(
{ ...PATHS, deliveryFile: '/private/delivery.json' },
{},
);
assert.equal(result.status, 'existing');
assert.equal('delivery' in result, false);
assert.deepEqual(published, []);
assert.equal(base.closes(), 1);
});
test('revokes a credential without requiring or publishing a delivery file', async () => {
const target = authority();
target.value.administration.revokeCredential = async (request) => ({
status: 'inserted',
credential: {
credentialId: request.credentialId,
subject: request.subject,
state: 'revoked',
version: 2,
createdAtMs: 1_000,
notBeforeAtMs: 1_000,
expiresAtMs: 2_000,
},
mutation: {},
});
const published = [];
const execution = runner(
{
schemaVersion: 1,
operation: 'credential.revoke',
request: {
mutationId: '123e4567-e89b-42d3-a456-426614174303',
requestId: 'security-credential-revoke-1',
expectedCurrentVersion: 1,
credentialId: 'automation-primary',
subject: SUBJECT,
},
},
target.value,
published,
);
const result = await execution.instance.run(PATHS, {});
assert.deepEqual(result, {
schemaVersion: 1,
operation: 'credential.revoke',
status: 'inserted',
subject: SUBJECT,
credentialId: 'automation-primary',
version: 2,
state: 'revoked',
});
assert.deepEqual(published, []);
assert.equal(target.closes(), 1);
});
test('keeps audit query bounded and rejects widened command shapes before admission', async () => {
const query = {
schemaVersion: 1,
operation: 'audit.list',
request: { limit: 25, filter: { outcome: 'allowed' } },
};
const target = authority();
const execution = runner(query, target.value);
const result = await execution.instance.run(PATHS, {});
assert.deepEqual(result, {
schemaVersion: 1,
operation: 'audit.list',
page: { records: [], nextCursor: null },
});
assert.deepEqual(target.calls, [
['audit.list', { limit: 25, filter: { outcome: 'allowed' } }],
]);
assert.throws(
() => normalizeClusterAdministrationCommand({ ...query, debug: true }),
/command shape is invalid/,
);
assert.throws(
() =>
normalizeClusterAdministrationCommand({
...query,
request: { limit: 201, filter: {} },
}),
/audit query is invalid/,
);
});
test('rejects widened path authority before reading a command file', async () => {
let reads = 0;
const instance = createClusterAdministrationCommandRunner({
async openAuthority() {
throw new Error('must not open authority');
},
async authenticate() {
throw new Error('must not authenticate');
},
readFile() {
reads += 1;
throw new Error('must not read');
},
publishDelivery() {
throw new Error('must not publish');
},
});
await assert.rejects(
instance.run({ ...PATHS, ambientCredential: true }, {}),
/command paths shape is invalid/,
);
assert.equal(reads, 0);
});
test('publishes a 0600 no-replace delivery and leaves an existing target intact', (t) => {
const directory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-security-delivery-'),
);
fs.chmodSync(directory, 0o700);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const filePath = path.join(directory, 'credential.json');
const bytes = Buffer.from('{"token":"secret"}\n');
publishClusterAdministrationCredentialDelivery(filePath, bytes);
assert.equal(fs.statSync(filePath).mode & 0o777, 0o600);
assert.deepEqual(fs.readFileSync(filePath), bytes);
assert.throws(
() =>
publishClusterAdministrationCredentialDelivery(
filePath,
Buffer.from('{"token":"replacement"}\n'),
),
/could not be published/,
);
assert.deepEqual(fs.readFileSync(filePath), bytes);
});
@@ -13,6 +13,7 @@ const {
createClusterApprovalIdentityKeysetFile,
createClusterModelProviderCredentialIdentityKeysetFile,
createClusterRunIdentityKeysetFile,
createClusterSecurityAdministrationIdentityKeysetFile,
} = require('@qinglong/cluster-admin/plugin-package-identity-keyset');
const NOW_MS = 1_700_000_000_000;
@@ -249,6 +250,38 @@ function runAssertion(key, overrides = {}) {
).toString('base64url')}`;
}
function securityAdministrationAssertion(key, overrides = {}) {
const header = Buffer.from(
JSON.stringify({
alg: 'EdDSA',
kid: key.kid,
typ: 'ql3-security-administration+jwt',
}),
).toString('base64url');
const now = Math.floor(NOW_MS / 1000);
const payload = Buffer.from(
JSON.stringify({
acr: 'urn:ql3:mfa',
amr: ['pwd', 'otp'],
aud: 'qinglong3-security-administration',
auth_time: now - 10,
exp: now + 120,
iat: now,
iss: ISSUER,
jti: `security-administration-assertion-${key.kid}`,
ql3_purpose: 'security-administration',
sub: 'security-owner-1',
...overrides,
}),
).toString('base64url');
const signed = `${header}.${payload}`;
return `${signed}.${sign(
null,
Buffer.from(signed, 'ascii'),
key.privateKey,
).toString('base64url')}`;
}
async function atomicWrite(filePath, document) {
const nextPath = `${filePath}.next`;
await writeFile(nextPath, `${JSON.stringify(document)}\n`, { mode: 0o644 });
@@ -337,10 +370,9 @@ test('loads an automation keyset with a purpose isolated from other management p
type: 'user',
id: 'automation-operator-1',
});
await assert.rejects(
provider.bind(workerAssertion(key)).authenticate(),
{ code: 'CLUSTER_PLUGIN_PACKAGE_IDENTITY_ASSERTION_INVALID' },
);
await assert.rejects(provider.bind(workerAssertion(key)).authenticate(), {
code: 'CLUSTER_PLUGIN_PACKAGE_IDENTITY_ASSERTION_INVALID',
});
await assert.rejects(provider.bind(assertion(key)).authenticate(), {
code: 'CLUSTER_PLUGIN_PACKAGE_IDENTITY_ASSERTION_INVALID',
});
@@ -358,14 +390,19 @@ test('loads an Approval keyset isolated by type, purpose and audience', async ()
filePath,
now: () => NOW_MS,
});
const principal = await provider.bind(approvalAssertion(key)).authenticate();
const principal = await provider
.bind(approvalAssertion(key))
.authenticate();
assert.deepEqual(principal.subject, {
type: 'user',
id: 'approval-owner-1',
});
await assert.rejects(provider.bind(automationAssertion(key)).authenticate(), {
code: 'CLUSTER_PLUGIN_PACKAGE_IDENTITY_ASSERTION_INVALID',
});
await assert.rejects(
provider.bind(automationAssertion(key)).authenticate(),
{
code: 'CLUSTER_PLUGIN_PACKAGE_IDENTITY_ASSERTION_INVALID',
},
);
await assert.rejects(provider.bind(assertion(key)).authenticate(), {
code: 'CLUSTER_PLUGIN_PACKAGE_IDENTITY_ASSERTION_INVALID',
});
@@ -435,6 +472,40 @@ test('loads a Run keyset isolated from every other management purpose', async ()
});
});
test('loads a Security Administration keyset isolated from other management purposes', async () => {
await fixture(async ({ filePath }) => {
const key = reviewedKey('security-administration-key-1');
await atomicWrite(filePath, {
...keyset(1, [key]),
audience: 'qinglong3-security-administration',
});
const provider = createClusterSecurityAdministrationIdentityKeysetFile({
filePath,
now: () => NOW_MS,
});
const principal = await provider
.bind(securityAdministrationAssertion(key))
.authenticate();
assert.deepEqual(principal.subject, {
type: 'user',
id: 'security-owner-1',
});
await assert.rejects(provider.bind(runAssertion(key)).authenticate(), {
code: 'CLUSTER_PLUGIN_PACKAGE_IDENTITY_ASSERTION_INVALID',
});
await assert.rejects(
provider
.bind(
securityAdministrationAssertion(key, {
ql3_purpose: 'run-management',
}),
)
.authenticate(),
{ code: 'CLUSTER_PLUGIN_PACKAGE_IDENTITY_ASSERTION_INVALID' },
);
});
});
test('supports overlap rotation then immediately revokes the previous key', async () => {
await fixture(async ({ filePath }) => {
const first = reviewedKey('issuer-key-1');
@@ -336,7 +336,7 @@ function validContextFixture(t) {
test('catalog exposes only reviewed product entrypoints from the same package', () => {
assert.equal(manifest.bin['ql3-cluster-admin'], 'dist/product-cli/cli.js');
assert.equal(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length, 12);
assert.equal(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length, 13);
assert.equal(
new Set(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.map(({ name }) => name)).size,
QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length,
@@ -354,6 +354,7 @@ test('catalog exposes only reviewed product entrypoints from the same package',
);
assert.equal(
command.binary.includes('-client') ||
command.binary === 'ql3-security-admin' ||
command.binary === 'ql3-copilot-mcp' ||
command.binary === 'ql3-copilot-console' ||
command.binary === 'ql3-copilot-evidence-verify',
@@ -391,6 +392,7 @@ test('help and version are bounded installation-derived product facts', () => {
/\n evidence-verify\s+verify one redacted Console evidence/,
);
assert.match(help, /Server, migration, recovery, executor and key-custody/);
assert.match(help, /\n security\s+administer identities, API credentials/);
assert.equal(help.includes('plugin-package-manage'), false);
assert.equal(
loadQingLong3ClusterProductVersion(moduleDirectory),
@@ -923,6 +925,11 @@ test('binary exposes help/version and delegates without a shell', () => {
assert.match(delegatedHelp.stdout, /^Usage: ql3-run-client /);
assert.equal(delegatedHelp.stderr, '');
const securityHelp = runCli(['security', '--help']);
assert.equal(securityHelp.status, 0);
assert.match(securityHelp.stdout, /^Usage: ql3-security-admin /);
assert.equal(securityHelp.stderr, '');
const rejected = runCli(['../../tmp/not-a-command']);
assert.equal(rejected.status, 64);
assert.equal(rejected.stdout, '');