Skip to content

Commit

Permalink
fix: new GPG binaries don't work on Node 14 Runtime (#1706)
Browse files Browse the repository at this point in the history
The Node 14 runtime is based on Amazon Linux 2, but the updated GPG
binaries require Amazon Linux 2023.

Make the following changes:

- Move the runtime to Node 20 (based on Amazon Linux 2023).
- Use `gpg` from the base Amazon Linux 2023 layer that's already there
- Update to `gpg-agent` and `openssl` CLI from an Amazon Linux 2023.

-----

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
  • Loading branch information
rix0rrr authored Jul 23, 2024
1 parent 7c610aa commit 071dec5
Show file tree
Hide file tree
Showing 12 changed files with 64 additions and 24 deletions.
2 changes: 1 addition & 1 deletion lib/__tests__/custom-resource-handlers/pgp-secret.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jest.spyOn(fs, 'mkdtemp')
const writeFile = fs.writeFile = jest.fn().mockName('fs.writeFile')
.mockImplementation((_pth, _data, _opts, cb) => cb()) as any;
jest.mock('../../custom-resource-handlers/src/_exec', () => async (cmd: string, ...args: string[]) => {
await expect(cmd).toBe('/opt/gpg');
await expect(cmd).toBe('gpg');
const process = require('process');
await expect(process.env.GNUPGHOME).toBe(mockTmpDir);
await expect(args).toContain('--batch');
Expand Down
2 changes: 1 addition & 1 deletion lib/change-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export class ChangeController extends Construct {
const fn = new nodejs.NodejsFunction(this, 'Function', {
description: `Enforces a Change Control Policy into CodePipeline's ${props.pipelineStage.stageName} stage`,
entry: path.join(__dirname, 'change-control-lambda', 'index.ts'),
runtime: lambda.Runtime.NODEJS_14_X,
runtime: lambda.Runtime.NODEJS_20_X,
environment: {
// CAPITAL punishment 👌🏻
CHANGE_CONTROL_BUCKET_NAME: changeControlBucket.bucketName,
Expand Down
2 changes: 1 addition & 1 deletion lib/chime-notifier/chime-notifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class ChimeNotifier extends Construct {
handler: 'index.handler',
uuid: '0f4a3ee0-692e-4249-932f-a46a833886d8',
code: lambda.Code.fromAsset(path.join(__dirname, 'handler')),
runtime: lambda.Runtime.NODEJS_14_X,
runtime: lambda.Runtime.NODEJS_20_X,
timeout: Duration.minutes(5),
});

Expand Down
4 changes: 2 additions & 2 deletions lib/code-signing/certificate-signing-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ export class CertificateSigningRequest extends Construct {
uuid: '541F6782-6DCF-49A7-8C5A-67715ADD9E4C',
lambdaPurpose: 'CreateCSR',
description: 'Creates a Certificate Signing Request document for an x509 certificate',
runtime: lambda.Runtime.NODEJS_14_X,
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: new lambda.AssetCode(codeLocation),
timeout: Duration.seconds(300),
// add the layer that contains the OpenSSL CLI binary
layers: [new lambda.LayerVersion(this, 'OpenSslCliLayer', {
code: lambda.Code.fromAsset(path.join(__dirname, '..', 'custom-resource-handlers', 'layers', 'openssl-cli-layer.zip')),
code: lambda.Code.fromAsset(path.join(__dirname, '..', 'custom-resource-handlers', 'layers', 'openssl-cli-al2023.zip')),
})],
});

Expand Down
4 changes: 2 additions & 2 deletions lib/code-signing/private-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,13 @@ export class RsaPrivateKeySecret extends Construct {
lambdaPurpose: 'RSAPrivate-Key',
uuid: '72FD327D-3813-4632-9340-28EC437AA486',
description: 'Generates an RSA Private Key and stores it in AWS Secrets Manager',
runtime: lambda.Runtime.NODEJS_14_X,
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: new lambda.AssetCode(codeLocation),
timeout: Duration.seconds(300),
// add the layer that contains the OpenSSL CLI binary
layers: [new lambda.LayerVersion(this, 'OpenSslCliLayer', {
code: lambda.Code.fromAsset(path.join(__dirname, '..', 'custom-resource-handlers', 'layers', 'openssl-cli-layer.zip')),
code: lambda.Code.fromAsset(path.join(__dirname, '..', 'custom-resource-handlers', 'layers', 'openssl-cli-al2023.zip')),
})],
});

Expand Down
52 changes: 44 additions & 8 deletions lib/custom-resource-handlers/layers/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,47 @@
This directory contains ZIP files that are used as Lambda layers by our custom resources
(private-key, pgp-secret and certificate-signing-request).
Those Lambdas shell out to the `openssl` and `gpg` tools,
which are not shipped with Node Lambda version older than 8.
This directory contains ZIP files that are used as Lambda layers by our custom
resources (private-key, pgp-secret and certificate-signing-request). Those
Lambdas shell out to the following tools:

If you ever need to update these,
unzip these files, add any necessary binaries to it,
and then zip them back up again.
- `gpg`
- `gpg-agent`
- `openssl`

Only `gpg` is installed on the Lambda Runtime by default, the others are not
(inspect Docker image `public.ecr.aws/lambda/nodejs:20` to be sure).

If you ever need to update these, unzip these files, add any necessary binaries
to it, and then zip them back up again.

The binaries contained in these files were downloaded from an EC2 instance
running Amazon Linux 2.
running Amazon Linux 2023.

N.B:

- Make sure the binaries are copied from a version of Amazon Linux that matches
the Lambda Runtime version that is being used, see here:
<https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html>.
- Make sure that the file structure in the ZIP file does not contain an extra
directory, but looks like:
- `gpg`
- `lib/libgcrypt.so.X`
- etc.
- `gpg` is probably linked against the major version dependencies only, so it will
depend on `libgcrypt.so.8` (and not `libgcrypt.so.8.4.1`). Confirm with `ldd` and
rename the files if necessary.

# Potential update procedure

```shell
host$ exec docker run --net=host \
--rm -it \
-v $HOME:$HOME -w $PWD \
public.ecr.aws/amazonlinux/amazonlinux:2023

# Replace 'gnupg2-minimal' with 'gnupg2', copy gpg-agent out to the current directory
container$ yum install gnupg2 -y --allowerasing
container$ cp /usr/bin/gpg-agent .

# Install openssl, copy CLI out
container$ yum install -y openssl
container$ cp /usr/bin/openssl .
```
Binary file not shown.
Binary file removed lib/custom-resource-handlers/layers/gpg-layer.zip
Binary file not shown.
Binary file not shown.
14 changes: 9 additions & 5 deletions lib/custom-resource-handlers/src/pgp-secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const ssm = new aws.SSM();

exports.handler = cfn.customResourceHandler(handleEvent);

// Used to be /opt/gpg, but now is just plain gpg
const GPG_BIN = 'gpg';


interface ResourceAttributes extends cfn.ResourceAttributes {
SecretArn: string;
PublicKey: string;
Expand Down Expand Up @@ -88,9 +92,9 @@ async function _createNewKey(event: cfn.CreateEvent | cfn.UpdateEvent, context:
].join('\n'), { encoding: 'utf8' });

const gpgCommonArgs = [`--homedir=${tempDir}`, '--agent-program=/opt/gpg-agent'];
await _exec('/opt/gpg', ...gpgCommonArgs, '--batch', '--gen-key', keyConfig);
const keyMaterial = await _exec('/opt/gpg', ...gpgCommonArgs, '--batch', '--yes', '--export-secret-keys', '--armor');
const publicKey = await _exec('/opt/gpg', ...gpgCommonArgs, '--batch', '--yes', '--export', '--armor');
await _exec(GPG_BIN, ...gpgCommonArgs, '--batch', '--gen-key', keyConfig);
const keyMaterial = await _exec(GPG_BIN, ...gpgCommonArgs, '--batch', '--yes', '--export-secret-keys', '--armor');
const publicKey = await _exec(GPG_BIN, ...gpgCommonArgs, '--batch', '--yes', '--export', '--armor');
const secretOpts = {
ClientRequestToken: context.awsRequestId,
Description: event.ResourceProperties.Description,
Expand Down Expand Up @@ -153,8 +157,8 @@ async function _getPublicKey(secretArn: string): Promise<string> {
await writeFile(privateKeyFile, keyData.PrivateKey, { encoding: 'utf-8' });
const gpgCommonArgs = [`--homedir=${tempDir}`, '--agent-program=/opt/gpg-agent'];
// Note: importing a private key does NOT require entering it's passphrase!
await _exec('/opt/gpg', ...gpgCommonArgs, '--batch', '--yes', '--import', privateKeyFile);
return await _exec('/opt/gpg', ...gpgCommonArgs, '--batch', '--yes', '--export', '--armor');
await _exec(GPG_BIN, ...gpgCommonArgs, '--batch', '--yes', '--import', privateKeyFile);
return await _exec(GPG_BIN, ...gpgCommonArgs, '--batch', '--yes', '--export', '--armor');
} finally {
await _rmrf(tempDir);
}
Expand Down
6 changes: 3 additions & 3 deletions lib/open-pgp-key-pair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,10 @@ export class OpenPGPKeyPair extends Construct implements ICredentialPair {
code: new lambda.AssetCode(codeLocation),
handler: 'index.handler',
timeout: Duration.seconds(300),
runtime: lambda.Runtime.NODEJS_14_X,
// add the layer that contains the GPG binary (+ shared libraries)
runtime: lambda.Runtime.NODEJS_20_X,
// add the layer that contains the gpg-agent binary (gpg command itself is already on the host)
layers: [new lambda.LayerVersion(this, 'GpgLayer', {
code: lambda.Code.fromAsset(path.join(__dirname, 'custom-resource-handlers', 'layers', 'gpg-layer.zip')),
code: lambda.Code.fromAsset(path.join(__dirname, 'custom-resource-handlers', 'layers', 'gpg-agent-al2023.zip')),
})],
});

Expand Down
2 changes: 1 addition & 1 deletion lib/pipeline-watcher/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class PipelineWatcher extends Construct {

const pipelineWatcher = new lambda.Function(this, 'Poller', {
handler: 'watcher-handler.handler',
runtime: lambda.Runtime.NODEJS_14_X,
runtime: lambda.Runtime.NODEJS_20_X,
code: lambda.Code.fromAsset(path.join(__dirname, 'handler')),
environment: {
METRIC_NAMESPACE: props.metricNamespace,
Expand Down

0 comments on commit 071dec5

Please sign in to comment.