Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: implements IKeyPair interface #279

Merged
merged 8 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 37 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@

[AWS CDK] L3 construct for managing [EC2 Key Pairs].

CloudFormation doesn't directly support creation of EC2 Key Pairs. This construct provides an easy interface for creating Key Pairs through a [custom CloudFormation resource]. The private key is stored in [AWS Secrets Manager].
> ⚠️ Please be aware, CloudFormation now natively supports creating EC2 Key Pairs via [AWS::EC2::KeyPair](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-keypair.html), so you can generally use [CDK's own KeyPair construct](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.KeyPair.html). There are a few differences though and this is why the custom construct remains valuable:
>
> - Instead of SSM Parameter Store, keys are stored in [AWS Secrets Manager]
> - Secrets can be **KMS encrypted** - even different KMS keys for the private and public keys. Of course, SSM parameters _can_ be encrypted too, CloudFormation just doesn't do it
> - Optionally, this construct can store and expose the public key, enabling the user to directly use it as input for other resources, e.g. for CloudFront signed urls

## Installation

Expand All @@ -29,7 +33,7 @@ For TypeScript/NodeJS, add these to your `dependencies` in `package.json`. For P
## CDK compatibility

- Version 3.x is compatible with the CDK v2.
- Version 2.x is compatible with the CDK v1. There won't be regular updates for this.
- Version 2.x is compatible with the CDK v1. There won't be updates for this.

## Usage

Expand All @@ -42,22 +46,22 @@ import { KeyPair } from 'cdk-ec2-key-pair';

// Create the Key Pair
const key = new KeyPair(this, 'A-Key-Pair', {
name: 'a-key-pair',
description: 'This is a Key Pair',
storePublicKey: true, // by default the public key will not be stored in Secrets Manager
name: 'a-key-pair',
description: 'This is a Key Pair',
storePublicKey: true, // by default the public key will not be stored in Secrets Manager
});

// Grant read access to the private key to a role or user
key.grantReadOnPrivateKey(someRole)
key.grantReadOnPrivateKey(someRole);

// Grant read access to the public key to another role or user
key.grantReadOnPublicKey(anotherRole)
key.grantReadOnPublicKey(anotherRole);

// Use Key Pair on an EC2 instance
new ec2.Instance(this, 'An-Instance', {
keyName: key.keyPairName,
// ...
})
keyName: key.keyPairName,
// ...
});
```

The private (and optionally the public) key will be stored in AWS Secrets Manager. The secret names by default are prefixed with `ec2-ssh-key/`. The private key is suffixed with `/private`, the public key is suffixed with `/public`. So in this example they will be stored as `ec2-ssh-key/a-key-pair/private` and `ec2-ssh-key/a-key-pair/public`.
Expand Down Expand Up @@ -97,8 +101,8 @@ To use a custom KMS key you can pass it to the Key Pair:
const kmsKey = new kms.Key(this, 'KMS-key');

const keyPair = new KeyPair(this, 'A-Key-Pair', {
name: 'a-key-pair',
kms: kmsKey,
name: 'a-key-pair',
kms: kmsKey,
});
```

Expand All @@ -111,9 +115,9 @@ const kmsKeyPrivate = new kms.Key(this, 'KMS-key-private');
const kmsKeyPublic = new kms.Key(this, 'KMS-key-public');

const keyPair = new KeyPair(this, 'A-Key-Pair', {
name: 'a-key-pair',
kmsPrivateKey: kmsKeyPrivate,
kmsPublicKey: kmsKeyPublic
name: 'a-key-pair',
kmsPrivateKey: kmsKeyPrivate,
kmsPublicKey: kmsKeyPublic,
});
```

Expand All @@ -126,7 +130,7 @@ The public key has to be in OpenSSH format.
```typescript
new KeyPair(this, 'Test-Key-Pair', {
name: 'imported-key-pair',
publicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCuMmbK...'
publicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCuMmbK...',
});
```

Expand All @@ -138,23 +142,26 @@ Make sure to set `publicKeyFormat` to `PublicKeyFormat.PEM` as that is the forma
You also have to set `exposePublicKey` to `true` so you can actually get the public key.

```typescript
const key = new KeyPair(this, 'Signing-Key-Pair', {
name: 'CFN-signing-key',
exposePublicKey: true,
storePublicKey: true,
publicKeyFormat: PublicKeyFormat.PEM
});

const pubKey = new cloudfront.PublicKey(this, 'Signing-Public-Key', {
encodedKey: key.publicKeyValue,
});
const trustedKeyGroupForCF = new cloudfront.KeyGroup(this, 'Signing-Key-Group', {
items: [ pubKey ]
});
const key = new KeyPair(this, 'Signing-Key-Pair', {
name: 'CFN-signing-key',
exposePublicKey: true,
storePublicKey: true,
publicKeyFormat: PublicKeyFormat.PEM,
});

const pubKey = new cloudfront.PublicKey(this, 'Signing-Public-Key', {
encodedKey: key.publicKeyValue,
});
const trustedKeyGroupForCF = new cloudfront.KeyGroup(
this,
'Signing-Key-Group',
{
items: [pubKey],
},
);
```

[AWS CDK]: https://aws.amazon.com/cdk/
[custom CloudFormation resource]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html
[EC2 Key Pairs]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html
[AWS Secrets Manager]: https://aws.amazon.com/secrets-manager/
[npm]: https://www.npmjs.com/package/cdk-ec2-key-pair
Expand Down
13 changes: 12 additions & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import {
Duration,
ITaggable,
Lazy,
Resource,
ResourceProps,
Stack,
TagManager,
TagType,
} from 'aws-cdk-lib';
import { IKeyPair, OperatingSystemType } from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';
import * as path from 'path';
import { PublicKeyFormat, ResourceProperties } from './types';
Expand Down Expand Up @@ -143,7 +145,7 @@ export interface KeyPairProps extends ResourceProps {
/**
* An EC2 Key Pair
*/
export class KeyPair extends Construct implements ITaggable {
export class KeyPair extends Resource implements ITaggable, IKeyPair {
/**
* The lambda function that is created
*/
Expand Down Expand Up @@ -410,4 +412,13 @@ export class KeyPair extends Construct implements ITaggable {
});
return result;
}

/**
* Used internally to determine whether the key pair is compatible with an OS type.
*
* @internal
*/
public _isOsCompatible(_osType: OperatingSystemType): boolean {
return true; // as we currently only support OpenSSH, we are compatible with all OS types
}
}
23 changes: 22 additions & 1 deletion test/lib/test-stack.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Tags, StackProps, Stack, CfnOutput, aws_iam } from 'aws-cdk-lib';
import {
Tags,
StackProps,
Stack,
CfnOutput,
aws_iam,
aws_ec2,
} from 'aws-cdk-lib';
import cloudfront = require('aws-cdk-lib/aws-cloudfront');
import { Construct } from 'constructs';
import { PublicKeyFormat } from '../../lambda/types';
Expand Down Expand Up @@ -41,6 +48,20 @@ export class TestStack extends Stack {
publicKey: keyPair.publicKeyValue,
});

if (process.env.with_ec2) {
udondan marked this conversation as resolved.
Show resolved Hide resolved
new aws_ec2.Instance(this, 'Test-Instance', {
vpc: aws_ec2.Vpc.fromLookup(this, 'VPC', {
vpcName: 'default',
}),
instanceType: aws_ec2.InstanceType.of(
aws_ec2.InstanceClass.T2,
aws_ec2.InstanceSize.MICRO,
),
machineImage: aws_ec2.MachineImage.latestAmazonLinux2(),
keyPair: keyPairImport,
});
}

new CfnOutput(this, 'Test-Public-Key-Import', {
exportName: 'TestPublicKeyImport',
value: keyPairImport.publicKeyValue,
Expand Down