mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
285 lines
11 KiB
TypeScript
285 lines
11 KiB
TypeScript
import { CommandModule } from "yargs";
|
|
import { db, idpOidcConfig, licenseKey } from "@server/db";
|
|
import { encrypt, decrypt } from "@server/lib/crypto";
|
|
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
|
import { eq } from "drizzle-orm";
|
|
import fs from "fs";
|
|
import yaml from "js-yaml";
|
|
|
|
type RotateServerSecretArgs = {
|
|
"old-secret": string;
|
|
"new-secret": string;
|
|
force?: boolean;
|
|
};
|
|
|
|
export const rotateServerSecret: CommandModule<
|
|
{},
|
|
RotateServerSecretArgs
|
|
> = {
|
|
command: "rotate-server-secret",
|
|
describe:
|
|
"Rotate the server secret by decrypting all encrypted values with the old secret and re-encrypting with a new secret",
|
|
builder: (yargs) => {
|
|
return yargs
|
|
.option("old-secret", {
|
|
type: "string",
|
|
demandOption: true,
|
|
describe: "The current server secret (for verification)"
|
|
})
|
|
.option("new-secret", {
|
|
type: "string",
|
|
demandOption: true,
|
|
describe: "The new server secret to use"
|
|
})
|
|
.option("force", {
|
|
type: "boolean",
|
|
default: false,
|
|
describe:
|
|
"Force rotation even if the old secret doesn't match the config file. " +
|
|
"Use this if you know the old secret is correct but the config file is out of sync. " +
|
|
"WARNING: This will attempt to decrypt all values with the provided old secret. " +
|
|
"If the old secret is incorrect, the rotation will fail or corrupt data."
|
|
});
|
|
},
|
|
handler: async (argv: {
|
|
"old-secret": string;
|
|
"new-secret": string;
|
|
force?: boolean;
|
|
}) => {
|
|
try {
|
|
// Determine which config file exists
|
|
const configPath = fs.existsSync(configFilePath1)
|
|
? configFilePath1
|
|
: fs.existsSync(configFilePath2)
|
|
? configFilePath2
|
|
: null;
|
|
|
|
if (!configPath) {
|
|
console.error(
|
|
"Error: Config file not found. Expected config.yml or config.yaml in the config directory."
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Read current config
|
|
const configContent = fs.readFileSync(configPath, "utf8");
|
|
const config = yaml.load(configContent) as any;
|
|
|
|
if (!config?.server?.secret) {
|
|
console.error(
|
|
"Error: No server secret found in config file. Cannot rotate."
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const configSecret = config.server.secret;
|
|
const oldSecret = argv["old-secret"];
|
|
const newSecret = argv["new-secret"];
|
|
const force = argv.force || false;
|
|
|
|
// Verify that the provided old secret matches the one in config
|
|
if (configSecret !== oldSecret) {
|
|
if (!force) {
|
|
console.error(
|
|
"Error: The provided old secret does not match the secret in the config file."
|
|
);
|
|
console.error(
|
|
"\nIf you are certain the old secret is correct and the config file is out of sync,"
|
|
);
|
|
console.error(
|
|
"you can use the --force flag to bypass this check."
|
|
);
|
|
console.error(
|
|
"\nWARNING: Using --force with an incorrect old secret will cause the rotation to fail"
|
|
);
|
|
console.error(
|
|
"or corrupt encrypted data. Only use --force if you are absolutely certain."
|
|
);
|
|
process.exit(1);
|
|
} else {
|
|
console.warn(
|
|
"\nWARNING: Using --force flag. Bypassing old secret verification."
|
|
);
|
|
console.warn(
|
|
"The provided old secret does not match the config file, but proceeding anyway."
|
|
);
|
|
console.warn(
|
|
"If the old secret is incorrect, this operation will fail or corrupt data.\n"
|
|
);
|
|
}
|
|
}
|
|
|
|
// Validate new secret
|
|
if (newSecret.length < 8) {
|
|
console.error(
|
|
"Error: New secret must be at least 8 characters long"
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (oldSecret === newSecret) {
|
|
console.error("Error: New secret must be different from old secret");
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log("Starting server secret rotation...");
|
|
console.log("This will decrypt and re-encrypt all encrypted values in the database.");
|
|
|
|
// Read all data first
|
|
console.log("\nReading encrypted data from database...");
|
|
const idpConfigs = await db.select().from(idpOidcConfig);
|
|
const licenseKeys = await db.select().from(licenseKey);
|
|
|
|
console.log(`Found ${idpConfigs.length} OIDC IdP configuration(s)`);
|
|
console.log(`Found ${licenseKeys.length} license key(s)`);
|
|
|
|
// Prepare all decrypted and re-encrypted values
|
|
console.log("\nDecrypting and re-encrypting values...");
|
|
|
|
type IdpUpdate = {
|
|
idpOauthConfigId: number;
|
|
encryptedClientId: string;
|
|
encryptedClientSecret: string;
|
|
};
|
|
|
|
type LicenseKeyUpdate = {
|
|
oldLicenseKeyId: string;
|
|
newLicenseKeyId: string;
|
|
encryptedToken: string;
|
|
encryptedInstanceId: string;
|
|
};
|
|
|
|
const idpUpdates: IdpUpdate[] = [];
|
|
const licenseKeyUpdates: LicenseKeyUpdate[] = [];
|
|
|
|
// Process idpOidcConfig entries
|
|
for (const idpConfig of idpConfigs) {
|
|
try {
|
|
// Decrypt with old secret
|
|
const decryptedClientId = decrypt(idpConfig.clientId, oldSecret);
|
|
const decryptedClientSecret = decrypt(
|
|
idpConfig.clientSecret,
|
|
oldSecret
|
|
);
|
|
|
|
// Re-encrypt with new secret
|
|
const encryptedClientId = encrypt(decryptedClientId, newSecret);
|
|
const encryptedClientSecret = encrypt(
|
|
decryptedClientSecret,
|
|
newSecret
|
|
);
|
|
|
|
idpUpdates.push({
|
|
idpOauthConfigId: idpConfig.idpOauthConfigId,
|
|
encryptedClientId,
|
|
encryptedClientSecret
|
|
});
|
|
} catch (error) {
|
|
console.error(
|
|
`Error processing IdP config ${idpConfig.idpOauthConfigId}:`,
|
|
error
|
|
);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Process licenseKey entries
|
|
for (const key of licenseKeys) {
|
|
try {
|
|
// Decrypt with old secret
|
|
const decryptedLicenseKeyId = decrypt(key.licenseKeyId, oldSecret);
|
|
const decryptedToken = decrypt(key.token, oldSecret);
|
|
const decryptedInstanceId = decrypt(key.instanceId, oldSecret);
|
|
|
|
// Re-encrypt with new secret
|
|
const encryptedLicenseKeyId = encrypt(
|
|
decryptedLicenseKeyId,
|
|
newSecret
|
|
);
|
|
const encryptedToken = encrypt(decryptedToken, newSecret);
|
|
const encryptedInstanceId = encrypt(
|
|
decryptedInstanceId,
|
|
newSecret
|
|
);
|
|
|
|
licenseKeyUpdates.push({
|
|
oldLicenseKeyId: key.licenseKeyId,
|
|
newLicenseKeyId: encryptedLicenseKeyId,
|
|
encryptedToken,
|
|
encryptedInstanceId
|
|
});
|
|
} catch (error) {
|
|
console.error(
|
|
`Error processing license key ${key.licenseKeyId}:`,
|
|
error
|
|
);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Perform all database updates in a single transaction
|
|
console.log("\nUpdating database in transaction...");
|
|
await db.transaction(async (trx) => {
|
|
// Update idpOidcConfig entries
|
|
for (const update of idpUpdates) {
|
|
await trx
|
|
.update(idpOidcConfig)
|
|
.set({
|
|
clientId: update.encryptedClientId,
|
|
clientSecret: update.encryptedClientSecret
|
|
})
|
|
.where(
|
|
eq(
|
|
idpOidcConfig.idpOauthConfigId,
|
|
update.idpOauthConfigId
|
|
)
|
|
);
|
|
}
|
|
|
|
// Update licenseKey entries (delete old, insert new)
|
|
for (const update of licenseKeyUpdates) {
|
|
// Delete old entry
|
|
await trx
|
|
.delete(licenseKey)
|
|
.where(eq(licenseKey.licenseKeyId, update.oldLicenseKeyId));
|
|
|
|
// Insert new entry with re-encrypted values
|
|
await trx.insert(licenseKey).values({
|
|
licenseKeyId: update.newLicenseKeyId,
|
|
token: update.encryptedToken,
|
|
instanceId: update.encryptedInstanceId
|
|
});
|
|
}
|
|
});
|
|
|
|
console.log(`Rotated ${idpUpdates.length} OIDC IdP configuration(s)`);
|
|
console.log(`Rotated ${licenseKeyUpdates.length} license key(s)`);
|
|
|
|
// Update config file with new secret
|
|
console.log("\nUpdating config file...");
|
|
config.server.secret = newSecret;
|
|
const newConfigContent = yaml.dump(config, {
|
|
indent: 2,
|
|
lineWidth: -1
|
|
});
|
|
fs.writeFileSync(configPath, newConfigContent, "utf8");
|
|
|
|
console.log(`Updated config file: ${configPath}`);
|
|
|
|
console.log("\nServer secret rotation completed successfully!");
|
|
console.log(`\nSummary:`);
|
|
console.log(` - OIDC IdP configurations: ${idpUpdates.length}`);
|
|
console.log(` - License keys: ${licenseKeyUpdates.length}`);
|
|
console.log(
|
|
`\n IMPORTANT: Restart the server for the new secret to take effect.`
|
|
);
|
|
|
|
process.exit(0);
|
|
} catch (error) {
|
|
console.error("Error rotating server secret:", error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
};
|
|
|