feat: secure organization provider credentials

This commit is contained in:
2026-07-10 22:04:43 +08:00
parent e049cb6880
commit 848f913597
51 changed files with 3280 additions and 230 deletions
+61 -3
View File
@@ -1,11 +1,13 @@
#!/usr/bin/env node
import { execFile } from "node:child_process";
import { constants } from "node:fs";
import { access, readFile, realpath, stat } from "node:fs/promises";
import { access, lstat, readFile, realpath, stat } from "node:fs/promises";
import { basename, dirname, join, resolve } from "node:path";
import { promisify } from "node:util";
import { PrismaClient } from "@prisma/client";
import { parse } from "dotenv";
import { LocalSecretEnvelope, loadLocalSecretKeyring } from "../security/secretEnvelope.js";
import { verifyStoredProviderEnvelopes } from "../connections/providerConnections.js";
import {
DeploymentPreflightError,
validateDeploymentPreflight,
@@ -18,6 +20,7 @@ const execFileAsync = promisify(execFile);
interface Options {
readonly envFile: string;
readonly keyringFile: string;
readonly nodeBin: string;
readonly runuserBin: string;
readonly setprivBin: string;
@@ -47,6 +50,7 @@ async function main(): Promise<void> {
const canonicalInput = await canonicalizeInput(rawInput);
const result = validateDeploymentPreflight(canonicalInput);
await validateSecretFile(options.envFile);
const secretEnvelope = await validateKeyringFile(options.keyringFile);
await validateNodeVersion(options.nodeBin);
await validateExecutable(options.runuserBin, "runuser");
await validateExecutable(options.setprivBin, "setpriv");
@@ -56,7 +60,7 @@ async function main(): Promise<void> {
await validateExecutable(options.pgIsReadyBin, "pg_isready");
await validateCph(result.cphBin);
await validatePostgres(options.pgIsReadyBin, result);
await validatePostgresQuery(result.databaseUrl);
await validatePostgresQuery(result.databaseUrl, secretEnvelope);
if (options.serviceUid !== undefined && options.serviceGid !== undefined) {
await validateProvisionedDirectories(canonicalInput, options.serviceUid, options.serviceGid);
@@ -125,6 +129,22 @@ async function validateSecretFile(path: string): Promise<void> {
}
}
async function validateKeyringFile(path: string): Promise<LocalSecretEnvelope> {
const metadata = await lstat(path);
if (metadata.isSymbolicLink() || !metadata.isFile()) {
throw new Error(`local secret keyring is not a regular non-symlink file: ${path}`);
}
if ((metadata.mode & 0o077) !== 0) {
throw new Error(`local secret keyring must not be accessible by group or others: ${path}`);
}
const keyring = await loadLocalSecretKeyring({
NODE_ENV: "preflight",
HUB_SECRET_KEYRING_FILE: path,
});
console.log("[preflight] local secret keyring is valid");
return new LocalSecretEnvelope(keyring);
}
async function validateNodeVersion(nodeBin: string): Promise<void> {
let stdout: string;
try {
@@ -189,14 +209,44 @@ async function validatePostgres(pgIsReadyBin: string, result: DeploymentPrefligh
console.log("[preflight] PostgreSQL is accepting connections");
}
async function validatePostgresQuery(databaseUrl: string): Promise<void> {
async function validatePostgresQuery(
databaseUrl: string,
secretEnvelope: LocalSecretEnvelope,
): Promise<void> {
const client = new PrismaClient({
datasources: { db: { url: databaseUrl } },
log: [],
});
let queryFailure: unknown;
let envelopeFailure: unknown;
try {
await client.$queryRawUnsafe("SELECT 1");
const migrationTable = await client.$queryRaw<Array<{ relation: string | null }>>`
SELECT to_regclass('public."_prisma_migrations"')::text AS "relation"
`;
if (migrationTable[0]?.relation === null || migrationTable[0]?.relation === undefined) {
console.log("[preflight] empty database has no migration table or stored provider envelopes");
} else {
const migration = await client.$queryRaw<Array<{ finished_at: Date | null; rolled_back_at: Date | null }>>`
SELECT "finished_at", "rolled_back_at"
FROM "_prisma_migrations"
WHERE "migration_name" = '20260710220000_org_provider_secret_envelopes'
ORDER BY "started_at" DESC
LIMIT 1
`;
const applied = migration[0]?.finished_at !== null && migration[0]?.finished_at !== undefined &&
migration[0]?.rolled_back_at === null;
if (!applied) {
console.log("[preflight] provider envelope migration is pending; no stored envelopes to authenticate");
} else {
try {
const verified = await verifyStoredProviderEnvelopes(client, secretEnvelope);
console.log(`[preflight] authenticated ${verified} stored provider envelope(s)`);
} catch (error) {
envelopeFailure = error;
}
}
}
} catch (error) {
queryFailure = error;
}
@@ -222,6 +272,12 @@ async function validatePostgresQuery(databaseUrl: string): Promise<void> {
{ cause: disconnectFailure },
);
}
if (envelopeFailure !== undefined) {
throw new Error(
`stored provider envelope verification failed (${formatFailure(envelopeFailure)})`,
{ cause: envelopeFailure },
);
}
console.log("[preflight] PostgreSQL authenticated query succeeded");
}
@@ -383,6 +439,7 @@ function parseArgs(argv: readonly string[]): Options {
const allowed = new Set([
"--env-file",
"--keyring-file",
"--node-bin",
"--runuser-bin",
"--setpriv-bin",
@@ -411,6 +468,7 @@ function parseArgs(argv: readonly string[]): Options {
return {
envFile: required(values, "--env-file"),
keyringFile: required(values, "--keyring-file"),
nodeBin: required(values, "--node-bin"),
runuserBin: required(values, "--runuser-bin"),
setprivBin: required(values, "--setpriv-bin"),