Skip to content

Commit

Permalink
adds functionality to import a public key (#44)
Browse files Browse the repository at this point in the history
* adds functionality to import a public key

* check if secret exists, before attempting deletion
  • Loading branch information
udondan authored Jan 29, 2022
1 parent a140dbf commit f57fbd7
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 39 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.1.0
3.2.0
133 changes: 96 additions & 37 deletions lambda/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ function Update(event: Event): Promise<Event | AWS.AWSError> {
'A Key Pair cannot be renamed. Please create a new Key Pair instead'
)
);
}

if (
} else if (
event.ResourceProperties.StorePublicKey !==
event.OldResourceProperties.StorePublicKey
) {
Expand All @@ -59,6 +57,15 @@ function Update(event: Event): Promise<Event | AWS.AWSError> {
'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)
Expand Down Expand Up @@ -97,28 +104,59 @@ function Delete(event: any): Promise<Event | AWS.AWSError> {

function createKeyPair(event: Event): Promise<Event> {
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);
}
);
}
});
}

Expand Down Expand Up @@ -232,6 +270,10 @@ function deleteKeyPair(event: Event): Promise<Event> {

function createPrivateKeySecret(event: Event): Promise<Event> {
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)`,
Expand All @@ -257,10 +299,14 @@ function createPrivateKeySecret(event: Event): Promise<Event> {
function createPublicKeySecret(event: Event): Promise<Event> {
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') {
Expand Down Expand Up @@ -462,7 +508,12 @@ function exposePublicKey(event: Event): Promise<Event> {
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);
Expand Down Expand Up @@ -513,13 +564,21 @@ function updateSecretRemoveTags(

function deletePrivateKeySecret(event: Event): Promise<Event> {
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);
Expand Down
21 changes: 20 additions & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down Expand Up @@ -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),
Expand Down
4 changes: 4 additions & 0 deletions test/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions test/lib/test-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down

0 comments on commit f57fbd7

Please sign in to comment.