diff --git a/README.md b/README.md index 460e8b81..1bff5f10 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,19 @@ const keyPair = new KeyPair(this, 'A-Key-Pair', { }); ``` +### Importing public key + +You can create a key pair by importing the public key. Obviously, in this case the secret key won't be available in secrets manager. + +The public key has to be in OpenSSH format. + +```typescript +new KeyPair(this, 'Test-Key-Pair', { + name: 'imported-key-pair', + publicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCuMmbK...' +}); +``` + ### Using the key pair for CloudFront signed url/cookies You can use this library for generating keys for CloudFront signed url/cookies. diff --git a/VERSION b/VERSION index fd2a0186..944880fa 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.0 +3.2.0 diff --git a/lambda/index.ts b/lambda/index.ts index 41699bfe..5e55a6a1 100644 --- a/lambda/index.ts +++ b/lambda/index.ts @@ -48,9 +48,7 @@ function Update(event: Event): Promise { 'A Key Pair cannot be renamed. Please create a new Key Pair instead' ) ); - } - - if ( + } else if ( event.ResourceProperties.StorePublicKey !== event.OldResourceProperties.StorePublicKey ) { @@ -59,6 +57,15 @@ function Update(event: Event): Promise { 'Once created, a key cannot be modified or accessed. Therefore the public key can only be stored, when the key is created.' ) ); + } else if ( + event.ResourceProperties.PublicKey !== + (event.OldResourceProperties.PublicKey || '') + ) { + reject( + new Error( + 'You cannot change the public key of an exiting key pair. Please delete the key pair and create a new one.' + ) + ); } updateKeyPair(event) @@ -97,28 +104,59 @@ function Delete(event: any): Promise { function createKeyPair(event: Event): Promise { return new Promise(function (resolve, reject) { - const params: AWS.EC2.CreateKeyPairRequest = { - KeyName: event.ResourceProperties.Name, - TagSpecifications: [ - { - ResourceType: 'key-pair', - Tags: makeTags(event, event.ResourceProperties) as AWS.EC2.TagList, - }, - ], - }; - logger.debug(`ec2.createKeyPair: ${JSON.stringify(params)}`); - ec2.createKeyPair( - params, - function (err: AWS.AWSError, data: AWS.EC2.KeyPair) { - if (err) return reject(err); - event.addResponseValue('KeyPairName', data.KeyName); - event.addResponseValue('KeyPairID', data.KeyPairId); - event.KeyFingerprint = data.KeyFingerprint; - event.KeyMaterial = data.KeyMaterial; - event.KeyID = data.KeyPairId; - resolve(event); - } - ); + if ( + // public key provided, let's import + event.ResourceProperties.PublicKey && + event.ResourceProperties.PublicKey.length + ) { + const params: AWS.EC2.ImportKeyPairRequest = { + KeyName: event.ResourceProperties.Name, + PublicKeyMaterial: event.ResourceProperties.PublicKey, + TagSpecifications: [ + { + ResourceType: 'key-pair', + Tags: makeTags(event, event.ResourceProperties) as AWS.EC2.TagList, + }, + ], + }; + logger.debug(`ec2.importKeyPair: ${JSON.stringify(params)}`); + ec2.importKeyPair( + params, + function (err: AWS.AWSError, data: AWS.EC2.KeyPair) { + if (err) return reject(err); + event.addResponseValue('KeyPairName', data.KeyName); + event.addResponseValue('KeyPairID', data.KeyPairId); + event.KeyFingerprint = data.KeyFingerprint; + event.KeyMaterial = data.KeyMaterial; + event.KeyID = data.KeyPairId; + resolve(event); + } + ); + } else { + // no public key provided. create new key + const params: AWS.EC2.CreateKeyPairRequest = { + KeyName: event.ResourceProperties.Name, + TagSpecifications: [ + { + ResourceType: 'key-pair', + Tags: makeTags(event, event.ResourceProperties) as AWS.EC2.TagList, + }, + ], + }; + logger.debug(`ec2.createKeyPair: ${JSON.stringify(params)}`); + ec2.createKeyPair( + params, + function (err: AWS.AWSError, data: AWS.EC2.KeyPair) { + if (err) return reject(err); + event.addResponseValue('KeyPairName', data.KeyName); + event.addResponseValue('KeyPairID', data.KeyPairId); + event.KeyFingerprint = data.KeyFingerprint; + event.KeyMaterial = data.KeyMaterial; + event.KeyID = data.KeyPairId; + resolve(event); + } + ); + } }); } @@ -232,6 +270,10 @@ function deleteKeyPair(event: Event): Promise { function createPrivateKeySecret(event: Event): Promise { return new Promise(function (resolve, reject) { + if (event.ResourceProperties.PublicKey) { + event.addResponseValue('PrivateKeyARN', null); + return resolve(event); + } const params: AWS.SecretsManager.CreateSecretRequest = { Name: `${event.ResourceProperties.SecretPrefix}${event.ResourceProperties.Name}/private`, Description: `${event.ResourceProperties.Description} (Private Key)`, @@ -257,10 +299,14 @@ function createPrivateKeySecret(event: Event): Promise { function createPublicKeySecret(event: Event): Promise { return new Promise(async function (resolve, reject) { let publicKey: string; - try { - publicKey = await makePublicKey(event); - } catch (err) { - return reject(err); + if (event.ResourceProperties.PublicKey.length) + publicKey = event.ResourceProperties.PublicKey; + else { + try { + publicKey = await makePublicKey(event); + } catch (err) { + return reject(err); + } } if (event.ResourceProperties.StorePublicKey !== 'true') { @@ -462,7 +508,12 @@ function exposePublicKey(event: Event): Promise { return new Promise(async function (resolve, reject) { if (event.ResourceProperties.ExposePublicKey == 'true') { try { - const publicKey = await makePublicKey(event); + let publicKey: string; + if (event.ResourceProperties.PublicKey.length) { + publicKey = event.ResourceProperties.PublicKey; + } else { + publicKey = await makePublicKey(event); + } event.addResponseValue('PublicKeyValue', publicKey); } catch (err) { return reject(err); @@ -513,13 +564,21 @@ function updateSecretRemoveTags( function deletePrivateKeySecret(event: Event): Promise { return new Promise(async function (resolve, reject) { - deleteSecret( - `${event.ResourceProperties.SecretPrefix}${event.ResourceProperties.Name}/private`, - event - ) - .then((data) => { - event.addResponseValue('PrivateKeyARN', data.ARN); - resolve(event); + const arn = `${event.ResourceProperties.SecretPrefix}${event.ResourceProperties.Name}/private`; + secretExists(arn) + .then((exists) => { + if (!exists) { + // no private key stored. nothing to do + return resolve(event); + } + deleteSecret(arn, event) + .then((data) => { + event.addResponseValue('PrivateKeyARN', data.ARN); + resolve(event); + }) + .catch((err) => { + reject(err); + }); }) .catch((err) => { reject(err); diff --git a/lib/index.ts b/lib/index.ts index 4e317454..636bd8ac 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -67,6 +67,13 @@ export interface KeyPairProps extends cdk.ResourceProps { */ readonly kmsPublicKey?: kms.Key; + /** + * Import a public key instead of creating it + * + * If no public key is provided, a new key pair will be created. + */ + readonly publicKey?: string; + /** * Store the public key as a secret * @@ -177,6 +184,16 @@ export class KeyPair extends Construct implements cdk.ITaggable { ); } + if ( + props.publicKey?.length && + props.publicKeyFormat !== undefined && + props.publicKeyFormat !== PublicKeyFormat.OPENSSH + ) { + cdk.Annotations.of(this).addError( + 'When importing a key, the format has to be of type OpenSSH' + ); + } + const stack = cdk.Stack.of(this).stackName; this.prefix = props.resourcePrefix || stack; if (this.prefix.length + cleanID.length > 62) @@ -201,6 +218,7 @@ export class KeyPair extends Construct implements cdk.ITaggable { Description: props.description || '', KmsPrivate: kmsPrivate?.keyArn || 'alias/aws/secretsmanager', KmsPublic: kmsPublic?.keyArn || 'alias/aws/secretsmanager', + PublicKey: props.publicKey || '', StorePublicKey: props.storePublicKey || false, ExposePublicKey: props.exposePublicKey || false, PublicKeyFormat: props.publicKeyFormat || PublicKeyFormat.OPENSSH, @@ -253,9 +271,10 @@ export class KeyPair extends Construct implements cdk.ITaggable { new statement.Ec2() // generally allow to inspect key pairs .allow() .toDescribeKeyPairs(), - new statement.Ec2() // allow creation, only if createdByTag is set + new statement.Ec2() // allow creation/import, only if createdByTag is set .allow() .toCreateKeyPair() + .toImportKeyPair() .toCreateTags() .onKeyPair('*', undefined, undefined, stack.partition) .ifAwsRequestTag(createdByTag, ID), diff --git a/test/Makefile b/test/Makefile index 5ef1bf7e..73340d9b 100644 --- a/test/Makefile +++ b/test/Makefile @@ -11,6 +11,10 @@ build: install lambda @echo Building application... @npm run build +diff: build + @echo Running diff... + @AWS_REGION=us-east-1 npm run cdk -- diff + deploy: build @echo Deploying application... @AWS_REGION=us-east-1 npm run cdk -- deploy --require-approval never diff --git a/test/lib/test-stack.ts b/test/lib/test-stack.ts index 80b372bb..e534ed1e 100644 --- a/test/lib/test-stack.ts +++ b/test/lib/test-stack.ts @@ -26,6 +26,22 @@ export class TestStack extends cdk.Stack { value: keyPair.publicKeyValue, }); + // import public key + + const keyPairImport = new KeyPair(this, 'Test-Key-Pair-Import', { + name: 'test-key-pair-import', + description: 'A test Key Pair, imported via public key', + removeKeySecretsAfterDays: 0, + storePublicKey: false, + exposePublicKey: true, + publicKey: keyPair.publicKeyValue, + }); + + new cdk.CfnOutput(this, 'Test-Public-Key-Import', { + exportName: 'TestPublicKeyImport', + value: keyPairImport.publicKeyValue, + }); + // PEM && CloudFront const keyPairPem = new KeyPair(this, 'Test-Key-Pair-PEM', {