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: Allow update of PGPSecret and PrivateKey #20

Merged
merged 9 commits into from
Dec 27, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 4 additions & 1 deletion lib/code-signing/certificate-signing-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import cfn = require('@aws-cdk/aws-cloudformation');
import lambda = require('@aws-cdk/aws-lambda');
import cdk = require('@aws-cdk/cdk');
import path = require('path');
import { hashFileOrDirectory } from '../util';
import { RsaPrivateKeySecret } from './private-key';

export interface CertificateSigningRequestProps {
Expand Down Expand Up @@ -46,20 +47,22 @@ export class CertificateSigningRequest extends cdk.Construct {
constructor(parent: cdk.Construct, id: string, props: CertificateSigningRequestProps) {
super(parent, id);

const codeLocation = path.join(__dirname, 'certificate-signing-request');
const customResource = new lambda.SingletonFunction(this, 'ResourceHandler', {
uuid: '541F6782-6DCF-49A7-8C5A-67715ADD9E4C',
lambdaPurpose: 'CreateCSR',
description: 'Creates a Certificate Signing Request document for an x509 certificate',
runtime: lambda.Runtime.Python36,
handler: 'index.main',
code: new lambda.AssetCode(path.join(__dirname, 'certificate-signing-request')),
code: new lambda.AssetCode(codeLocation),
timeout: 300,
});

const csr = new cfn.CustomResource(this, 'Resource', {
lambdaProvider: customResource,
resourceType: 'Custom::CertificateSigningRequest',
properties: {
resourceVersion: hashFileOrDirectory(codeLocation),
// Private key
privateKeySecretId: props.privateKey.secretArn,
privateKeySecretVersion: props.privateKey.secretVersion,
Expand Down
47 changes: 15 additions & 32 deletions lib/code-signing/code-signing-certificate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import iam = require('@aws-cdk/aws-iam');
import kms = require('@aws-cdk/aws-kms');
import ssm = require('@aws-cdk/aws-ssm');
import cdk = require('@aws-cdk/cdk');
import { ICredentialPair } from '../credential-pair';
import permissions = require('../permissions');
import { DistinguishedName } from './certificate-signing-request';
import { RsaPrivateKeySecret } from './private-key';
Expand Down Expand Up @@ -53,24 +54,7 @@ interface CodeSigningCertificateProps {
distinguishedName: DistinguishedName;
}

export interface ICodeSigningCertificate {
/**
* The ARN of the AWS Secrets Manager secret that holds the private key for
* this CSC
*/
privateKeySecretArn: string;

/**
* The ID of the version of the AWS Secrets Manager secret that holds the
* private key for this CSC
*/
privateKeySecretVersionId: string;

/**
* The name of the AWS SSM parameter that holds the certificate for this CSC.
*/
certificateParameterName: string;

export interface ICodeSigningCertificate extends ICredentialPair {
/**
* Grant the IAM principal permissions to read the private key and
* certificate.
Expand All @@ -97,23 +81,22 @@ export class CodeSigningCertificate extends cdk.Construct implements ICodeSignin
/**
* The ARN of the AWS Secrets Manager secret that holds the private key for this CSC
*/
public readonly privateKeySecretArn: string;
public readonly secretArn: string;

/**
* The ID of the version of the AWS Secrets Manager secret that holds the private key for this CSC
*/

public readonly privateKeySecretVersionId: string;
public readonly secretVersionId: string;

/**
* The name of the AWS SSM parameter that holds the certificate for this CSC.
* The ARN of the AWS SSM Parameter that holds the certificate for this CSC.
*/
public readonly certificateParameterName: string;
RomainMuller marked this conversation as resolved.
Show resolved Hide resolved
public readonly parameterArn: string;

/**
* The ARN of the AWS SSM Parameter that holds the certificate for this CSC.
* The name of the AWS SSM parameter that holds the certificate for this CSC.
*/
private readonly certificateParameterArn: string;
public readonly parameterName: string;

/**
* KMS key to encrypt the secret.
Expand All @@ -140,8 +123,8 @@ export class CodeSigningCertificate extends cdk.Construct implements ICodeSignin

this.secretEncryptionKey = props.secretEncryptionKey;

this.privateKeySecretArn = privateKey.secretArn;
this.privateKeySecretVersionId = privateKey.secretVersion;
this.secretArn = privateKey.secretArn;
this.secretVersionId = privateKey.secretVersion;

let certificate = props.pemCertificate;

Expand All @@ -163,16 +146,16 @@ export class CodeSigningCertificate extends cdk.Construct implements ICodeSignin
}

const paramName = `${baseName}/Certificate`;
this.certificateParameterName = `/${paramName}`;
this.parameterName = `/${paramName}`;

new ssm.cloudformation.ParameterResource(this, 'Resource', {
description: `A PEM-encoded Code-Signing Certificate (private key in ${privateKey.secretArn} version ${privateKey.secretVersion})`,
name: this.certificateParameterName,
name: this.parameterName,
type: 'String',
value: certificate
});

this.certificateParameterArn = cdk.ArnUtils.fromComponents({
this.parameterArn = cdk.ArnUtils.fromComponents({
service: 'ssm',
resource: 'parameter',
resourceName: paramName
Expand All @@ -188,11 +171,11 @@ export class CodeSigningCertificate extends cdk.Construct implements ICodeSignin

permissions.grantSecretRead({
keyArn: this.secretEncryptionKey && this.secretEncryptionKey.keyArn,
secretArn: this.privateKeySecretArn,
secretArn: this.secretArn,
}, principal);

principal.addToPolicy(new iam.PolicyStatement()
.addAction('ssm:GetParameter')
.addResource(this.certificateParameterArn));
.addResource(this.parameterArn));
}
}
22 changes: 11 additions & 11 deletions lib/code-signing/private-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import iam = require('@aws-cdk/aws-iam');
import kms = require('@aws-cdk/aws-kms');
import lambda = require('@aws-cdk/aws-lambda');
import cdk = require('@aws-cdk/cdk');
import fs = require('fs');
import path = require('path');
import { hashFileOrDirectory } from '../util';
import { CertificateSigningRequest, DistinguishedName } from './certificate-signing-request';

export interface RsaPrivateKeySecretProps {
Expand Down Expand Up @@ -63,28 +63,27 @@ export class RsaPrivateKeySecret extends cdk.Construct {

props.deletionPolicy = props.deletionPolicy || cdk.DeletionPolicy.Retain;

const codeLocation = path.join(__dirname, 'private-key');
const customResource = new lambda.SingletonFunction(this, 'ResourceHandler', {
uuid: '72FD327D-3813-4632-9340-28EC437AA486',
description: 'Generates an RSA Private Key and stores it in AWS Secrets Manager',
runtime: lambda.Runtime.Python36,
handler: 'index.main',
code: new lambda.InlineCode(
fs.readFileSync(path.join(__dirname, 'private-key.py'))
.toString('utf8')
// Remove blank and comment-only lines, to shrink code length
.replace(/^[ \t]*(#[^\n]*)?\n/mg, '')
),
code: new lambda.AssetCode(codeLocation),
timeout: 300,
});

this.secretArnLike = cdk.ArnUtils.fromComponents({
service: 'secretsmanager',
resource: 'secret',
sep: ':',
resourceName: `${props.secretName}-*`
resourceName: `${props.secretName}-??????`
RomainMuller marked this conversation as resolved.
Show resolved Hide resolved
});
customResource.addToRolePolicy(new iam.PolicyStatement()
.addActions('secretsmanager:CreateSecret', 'secretsmanager:DeleteSecret')
.addActions('secretsmanager:CreateSecret',
'secretsmanager:DeleteSecret',
'secretsmanager:ListSecretVersionIds',
'secretsmanager:UpdateSecret')
.addResource(this.secretArnLike));

if (props.secretEncryptionKey) {
Expand All @@ -105,6 +104,7 @@ export class RsaPrivateKeySecret extends cdk.Construct {
lambdaProvider: customResource,
resourceType: 'Custom::RsaPrivateKeySecret',
properties: {
resourceVersion: hashFileOrDirectory(codeLocation),
description: props.description,
keySize: props.keySize,
secretName: props.secretName,
Expand Down Expand Up @@ -132,8 +132,8 @@ export class RsaPrivateKeySecret extends cdk.Construct {
privateKey.options.deletionPolicy = props.deletionPolicy;

this.masterKey = props.secretEncryptionKey;
this.secretArn = privateKey.getAtt('ARN').toString();
this.secretVersion = privateKey.getAtt('VersionId').toString();
this.secretArn = privateKey.getAtt('SecretArn').toString();
this.secretVersion = privateKey.getAtt('SecretVersionId').toString();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# - Description (string): the description to attach to the secret.
#
# Outputs:
# - ARN (string): The AWS SecretsManager secret ARN
# - Arn (string): The AWS SecretsManager secret ARN
# - VersionId (string): The AWS SecretsManager secret VersionId

import logging as log
Expand All @@ -22,10 +22,41 @@ def handle_event(event, aws_request_id):
import boto3, shutil, subprocess, tempfile

props = event['ResourceProperties']
description = props.get('Description')
kmsKeyId = props.get('KmsKeyId')

if event['RequestType'] == 'Update':
# Prohibit updates - you don't want to inadertently cause your private key to change...
raise Exception('X509 Private Key update requires replacement, a new resource must be created!')
old_props = event['OldResourceProperties']
# Prohibit updates to KeySize or SecretName, as those would require re-creating the key...
if old_props['KeySize'] != props['KeySize']:
raise Exception(f'The KeySize property cannot be updated (attempting to change from {old_props["KeySize"]} to {props["KeySize"]})')
if old_props['SecretName'] != props['SecretName']:
raise Exception(f'The SecretName property cannot be updated (attempting to change from {old_props["SecretName"]} to {props["SecretName"]})')

opts = {
'SecretId': event['PhysicalResourceId'],
'ClientRequestToken': aws_request_id
}

if description is not None: opts['Description'] = description
if kmsKeyId: opts['KmsKeyId'] = kmsKeyId

ret = boto3.client('secretsmanager').update_secret(**opts)

# No new version was created - go fetch the current latest VersionId
if ret.get('VersionId') is None:
opts = dict(SecretId=ret['ARN'])
while True:
response = boto3.client('secretsmanager').list_secret_version_ids(**opts)
for version in response['Versions']:
if 'AWSCURRENT' in version['VersionStages']:
ret['VersionId'] = version['VersionId']
break
if ret['VersionId'] is not None or response.get('NextToken') is None:
break
opts['NextToken'] = response['NextToken']

return {'SecretArn': ret['ARN'], 'SecretVersionId': ret['VersionId']}

elif event['RequestType'] == 'Create':
tmpdir = tempfile.mkdtemp()
Expand All @@ -40,16 +71,16 @@ def handle_event(event, aws_request_id):
'SecretString': pkey.read()
}

kmsKeyId = props.get('KmsKeyId')
if description is not None: opts['Description'] = description
if kmsKeyId: opts['KmsKeyId'] = kmsKeyId

ret = boto3.client('secretsmanager').create_secret(**opts)
return {'ARN': ret['ARN'], 'VersionId': ret['VersionId']}
return {'SecretArn': ret['ARN'], 'SecretVersionId': ret['VersionId']}

elif event['RequestType'] == 'Delete':
if event['PhysicalResourceId'].startswith('arn:'): # Only if the resource had been successfully created before
boto3.client('secretsmanager').delete_secret(SecretId=event['PhysicalResourceId'])
return {'ARN': ''}
return {'SecretArn': '', 'SecretVersionId': ''}

else:
raise Exception('Unsupported RequestType: %s' % event['RequestType'])
Expand All @@ -60,8 +91,9 @@ def main(event, context):
try:
log.info('Input event: %s', json.dumps(event))
attributes = handle_event(event, context.aws_request_id)
cfn_send(event, context, CFN_SUCCESS, attributes, attributes['ARN'])
cfn_send(event, context, CFN_SUCCESS, attributes, attributes['SecretArn'])
except KeyError as e:
log.exception(e)
cfn_send(event, context, CFN_FAILED, {}, reason="Invalid request: missing key %s" % str(e))
except Exception as e:
log.exception(e)
Expand Down
35 changes: 35 additions & 0 deletions lib/credential-pair.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* A Credential Pair combines a secret element and a public element. The public
* element is stored in an SSM Parameter, while the secret element is stored in
* AWS Secrets Manager.
*
* For example, this can be:
* - A username and a password
* - A private key and a certificate
* - An OpenPGP Private key and its public part
*/
export interface ICredentialPair {
/**
* The ARN of the SSM parameter containing the public part of this credential
* pair.
*/
readonly parameterArn: string;
RomainMuller marked this conversation as resolved.
Show resolved Hide resolved

/**
* The name of the SSM parameter containing the public part of this credential
* pair.
*/
readonly parameterName: string;

/**
* The ARN of the AWS SecretsManager secret that holds the private part of
* this credential pair.
*/
readonly secretArn: string;

/**
* The VersionId of the AWS SecretsManager secret that holds the private part
* of this credential pair.
*/
readonly secretVersionId: string;
}
Loading