Skip to content

Instantly share code, notes, and snippets.

@seibar
Created March 22, 2022 17:23
Show Gist options
  • Save seibar/fa6f9b07aa04f8a8a26bfcaa358c7791 to your computer and use it in GitHub Desktop.
Save seibar/fa6f9b07aa04f8a8a26bfcaa358c7791 to your computer and use it in GitHub Desktop.
AWS secret rotation lambda node js
// Based on: https://github.com/aws-samples/aws-secrets-manager-rotation-lambdas/blob/master/SecretsManagerRotationTemplate/lambda_function.py
import { SecretsManagerRotationEvent } from 'aws-lambda';
import { DescribeSecretCommandOutput, SecretsManager, ResourceExistsException, GetSecretValueCommandOutput } from "@aws-sdk/client-secrets-manager";
import { inspect } from 'util';
export interface ILogger {
info: (...data: any[]) => void;
error: (...data: any[]) => void;
}
export interface SecretRotationHandlerResult {
/** The previous value of the secret. This will be undefined if the secret has not rotated before. */
previousSecret?: string;
/** The current value of the secret. */
currentSecret: string;
/** The pending value of the secret, i.e. what the secret value will be after the rotation is completed. */
pendingSecret: string;
/** The version of the pending secret. */
pendingSecretVersionId: string;
}
export interface SecretRotationHandlerOptions {
/** An optional logger. Defaults to `console.log()`/`console.error()`. */
logger?: ILogger;
/** A function that generates a new secret string value for this secret. When the rotation happens, the secret will be updated with this value. */
secretValueFactory: () => Promise<string>;
/** Any action that needs to be performed before the secret is finalized. For example if something needs to be done in a database with the new secret value, this is where that would happen. */
sideEffect: (args: SecretRotationHandlerResult) => Promise<void>;
/** An optional function that validates that the the sideEffect did what it was supposed to do with the pending secret value. If this function does not return `true` then the secret's new value does not get finalized. */
testSecret?: (pendingSecret: string) => Promise<boolean>;
}
const AWSPREVIOUS = 'AWSPREVIOUS';
const AWSCURRENT = 'AWSCURRENT';
const AWSPENDING = 'AWSPENDING';
export class SecretRotationHandler {
private readonly logger: ILogger;
constructor (
private readonly secretsManager: SecretsManager,
private readonly options: SecretRotationHandlerOptions
) {
if (!this.options?.secretValueFactory) {
throw new Error('secretValueFactory is required');
}
if (!this.options?.sideEffect) {
throw new Error('sideEffect is required');
}
if (!options?.logger) {
this.logger = {
info: console.log,
error: console.error
};
}
}
async handle(event: SecretsManagerRotationEvent): Promise<void> {
if (!event) {
throw new Error('event is required');
}
let secret: DescribeSecretCommandOutput;
try {
secret = await this.secretsManager.describeSecret({
SecretId: event.SecretId,
});
if (!secret) {
throw new Error('Secret is undefined');
}
} catch (e) {
this.logger.error(`Error retrieving secret ${event.SecretId}`, inspect(e));
throw new Error(`Error retrieving secret ${event.SecretId}`);
}
if (!secret.RotationEnabled) {
this.logger.error(`Secret ${event.SecretId} is not enabled for rotation`);
throw new Error(`Secret ${event.SecretId} is not enabled for rotation`);
}
if (Object.keys(secret.VersionIdsToStages).indexOf(event.ClientRequestToken) === -1) {
this.logger.error(`Secret version ${event.ClientRequestToken} has no stage for rotation of secret ${event.SecretId}`);
throw new Error(`Secret version ${event.ClientRequestToken} has no stage for rotation of secret ${event.SecretId}`);
}
if (secret.VersionIdsToStages[event.ClientRequestToken].indexOf(AWSCURRENT) !== -1) {
this.logger.info(`Secret version ${event.ClientRequestToken} already set as AWSCURRENT for secret ${event.SecretId}.`);
return;
}
if (secret.VersionIdsToStages[event.ClientRequestToken].indexOf(AWSPENDING) === -1) {
this.logger.error(`Secret version ${event.ClientRequestToken} not set as AWSPENDING for rotation of secret ${event.SecretId}.`);
throw new Error(`Secret version ${event.ClientRequestToken} not set as AWSPENDING for rotation of secret ${event.SecretId}.`);
}
if (event.Step === 'createSecret') {
await this.createSecret(event);
} else if (event.Step === 'setSecret') {
await this.setSecret(event);
} else if (event.Step === 'testSecret') {
await this.testSecret(event);
} else if (event.Step === 'finishSecret') {
await this.finishSecret(event);
} else {
throw new Error(`Invalid step parameter: ${event.Step} for secret ${event.SecretId}`);
}
}
private async createSecret(event: SecretsManagerRotationEvent): Promise<void> {
try {
await this.secretsManager.getSecretValue({
SecretId: event.SecretId,
VersionId: event.ClientRequestToken,
VersionStage: AWSPENDING
});
this.logger.info(`createSecret: Secret already exists. ${event.SecretId}`);
} catch (e) {
if (e instanceof ResourceExistsException) {
try {
const secretString = await this.options.secretValueFactory();
if (!secretString) {
throw new Error('The result of secretValueFactory is invalid.')
}
await this.secretsManager.putSecretValue({
SecretId: event.SecretId,
ClientRequestToken: event.ClientRequestToken,
SecretString: secretString,
VersionStages: [AWSPENDING]
});
this.logger.info(`createSecret: Successfully put secret for ARN ${event.SecretId} and version ${event.ClientRequestToken}.`);
} catch (e2) {
this.logger.error(`createSecret: Error putting secret value for ${event.SecretId}`, inspect(e2));
throw e2;
}
} else {
this.logger.info(`createSecret: error retrieving secret ${event.SecretId}`, inspect(e));
throw e;
}
}
}
private async setSecret(event: SecretsManagerRotationEvent): Promise<void> {
let previousSecret: GetSecretValueCommandOutput;
let currentSecret: GetSecretValueCommandOutput;
let pendingSecret: GetSecretValueCommandOutput;
try {
previousSecret = await this.secretsManager.getSecretValue({
SecretId: event.SecretId,
VersionStage: AWSPREVIOUS
});
} catch (e) {
// The previous stage will not always exist, for instance if this is the very first rotation.
if (!(e instanceof ResourceExistsException)) {
this.logger.error(`setSecret: Error retrieving AWSPREVIOUS secret stage for ${event.SecretId}`, inspect(e));
throw e;
}
}
try {
currentSecret = await this.secretsManager.getSecretValue({
SecretId: event.SecretId,
VersionStage: AWSCURRENT,
VersionId: event.ClientRequestToken
});
} catch (e) {
this.logger.error(`setSecret: Error retrieving AWSCURRENT secret stage for ${event.SecretId}`, inspect(e));
throw e;
}
try {
pendingSecret = await this.secretsManager.getSecretValue({
SecretId: event.SecretId,
VersionStage: AWSPENDING,
VersionId: event.ClientRequestToken
});
} catch (e) {
this.logger.error(`setSecret: Error retrieving AWSPENDING secret stage for ${event.SecretId}`, inspect(e));
throw e;
}
try {
await this.options.sideEffect({
previousSecret: previousSecret?.SecretString,
currentSecret: currentSecret.SecretString,
pendingSecret: pendingSecret.SecretString,
pendingSecretVersionId: event.ClientRequestToken
});
} catch (e) {
this.logger.error(`setSecret: error invoking sideEffect function for ${event.SecretId}`, inspect(e));
throw e;
}
}
private async testSecret(event: SecretsManagerRotationEvent): Promise<void> {
if (this.options.testSecret) {
let pendingSecret: GetSecretValueCommandOutput;
try {
pendingSecret = await this.secretsManager.getSecretValue({
SecretId: event.SecretId,
VersionStage: AWSPENDING,
VersionId: event.ClientRequestToken
});
} catch (e) {
this.logger.error(`testSecret: Error retrieving AWSPENDING secret stage for ${event.SecretId}`, inspect(e));
throw e;
}
let testResult = false;
try {
testResult = await this.options.testSecret(pendingSecret.SecretString);
} catch (e) {
this.logger.error(`testSecret function throw an error for ${event.SecretId}`, inspect(e));
throw e;
}
if (testResult !== true) {
this.logger.error(`testSecret returned ${testResult}. The secret ${event.SecretId} will not be finalized.`);
throw new Error(`testSecret returned ${testResult}. The secret ${event.SecretId} will not be finalized.`);
}
}
}
private async finishSecret(event: SecretsManagerRotationEvent): Promise<void> {
const secret = await this.secretsManager.describeSecret({
SecretId: event.SecretId
});
// First find the existing current version so that it can be marked as previous.
let currentVersion: string;
for (const entry of Object.entries(secret.VersionIdsToStages)) {
const version = entry[0];
const stages = entry[1] as string[];
if (stages.indexOf(AWSCURRENT) !== -1) {
if (version === event.ClientRequestToken) {
// The correct version is already marked as current, return
this.logger.info(`finishSecret: Version ${version} is already marked as AWSCURRENT from ${event.SecretId}`);
return;
}
currentVersion = version;
break;
}
}
// Finalize the secret
await this.secretsManager.updateSecretVersionStage({
SecretId: event.SecretId,
VersionStage: AWSCURRENT,
MoveToVersionId: event.ClientRequestToken,
RemoveFromVersionId: currentVersion
});
this.logger.info(`finishSecret: Successfully set AWSCURRENT stage to version ${event.ClientRequestToken} for secret ${event.SecretId}`);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment