Skip to content
This repository has been archived by the owner on May 5, 2024. It is now read-only.

Commit

Permalink
feat: add a new GitHub action to inject SSM secrets into builds
Browse files Browse the repository at this point in the history
  • Loading branch information
marvinpinto committed Sep 9, 2020
1 parent 9f2207d commit f4b98c0
Show file tree
Hide file tree
Showing 14 changed files with 380 additions and 3 deletions.
7 changes: 7 additions & 0 deletions __mocks__/@actions/core.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const core = jest.requireActual('@actions/core');

module.exports = {
...core,
setSecret: jest.fn(),
exportVariable: jest.fn(),
};
11 changes: 11 additions & 0 deletions __mocks__/aws-sdk/clients/ssm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const mockGetParameter = jest.fn().mockImplementation(() => ({
promise: jest.fn().mockResolvedValue({
Parameter: {
Value: 'super-secret-value',
},
}),
}));

export default jest.fn().mockImplementation(() => ({
getParameter: mockGetParameter,
}));
8 changes: 8 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ module.exports = {
testPathIgnorePatterns: ['/__tests__/payloads', '/__tests__/utils/', '/__tests__/assets'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
},
{
name: 'aws-ssm-secrets',
displayName: 'aws-ssm-secrets',
testRegex: 'packages/aws-ssm-secrets/__tests__',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
},
],
coverageReporters: ['text'],
coverageThreshold: {
Expand All @@ -33,11 +39,13 @@ module.exports = {
collectCoverageFrom: [
'**/packages/keybase-notifications/**/*.ts',
'**/packages/automatic-releases/**/*.ts',
'**/packages/aws-ssm-secrets/**/*.ts',
'!**/__tests__/**',
'!**/dist/**',
'!**/packages/keybase-notifications/src/index.ts',
'!**/packages/keybase-notifications/src/utils.ts',
'!**/packages/automatic-releases/src/index.ts',
'!**/packages/automatic-releases/src/uploadReleaseArtifacts.ts',
'!**/packages/aws-ssm-secrets/src/index.ts',
],
};
3 changes: 3 additions & 0 deletions packages/aws-ssm-secrets/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const eslintrc = require('../../.eslintrc');
module.exports = eslintrc;
3 changes: 3 additions & 0 deletions packages/aws-ssm-secrets/.prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const prettierrc = require('../../.prettierrc');
module.exports = prettierrc;
87 changes: 87 additions & 0 deletions packages/aws-ssm-secrets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# AWS SSM Build Secrets for GitHub Actions

This action injects AWS SSM Parameter Store secrets as environment variables into your GitHub Actions builds.

It makes it easier to follow [Amazon IAM best practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) in respect to principle of least privilege and tracking credentials usage. Combined with the `aws-actions/configure-aws-credentials` action, this allows you to inject any combination of secrets from multiple stores, using different credential contexts.

## Contents

1. [Usage Examples](#usage-examples)
1. [Supported Parameters](#supported-parameters)
1. [Event Triggers](#event-triggers)
1. [Versioning](#versioning)
1. [How to get help](#how-to-get-help)
1. [License](#license)

> **NOTE**: The `marvinpinto/action-inject-ssm-secrets` repository is an automatically generated mirror of the [marvinpinto/actions](https://github.com/marvinpinto/actions) monorepo containing this and other actions. Please file issues and pull requests over there.
## Usage Examples

### Inject your production Cloudflare API tokens into a build

```yaml
---
name: "build-and-invalidate-cf-cache"

on:
push:
branches:
- "master"

jobs:
ci:
runs-on: "ubuntu-latest"
env:
BUILD_STAGE: "production"

steps:
# ...

- uses: "aws-actions/configure-aws-credentials@v1"
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: "us-east-1"
role-to-assume: "arn:aws:iam::111111111111:role/build-and-deploy-website"
role-duration-seconds: 1800 # 30 mins

- uses: "marvinpinto/action-inject-ssm-secrets@v1"
with:
ssm_parameter: "/build-secrets/${{ env.BUILD_STAGE }}/cloudflare-account-id"
env_variable_name: "cloudflare_account_id"

- uses: "marvinpinto/action-inject-ssm-secrets@v1"
with:
ssm_parameter: "/build-secrets/${{ env.BUILD_STAGE }}/cloudflare-api-token"
env_variable_name: "cloudflare_api_token"

- name: "Build & Deploy"
run: |
echo "You will now have access to the CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN environment variables in all your subsequent build steps"
```

## Supported Parameters

| Parameter | Description | Default |
| ---------------- | -------------------------------------------------------------------------------------------------------- | ------- |
| `parameters`\*\* | A mapped object consisting of an environment variable (to set), and its corresponding AWS SSM parameter. | `null` |

### Notes:

- Parameters denoted with `**` are required.

## Versioning

Every commit that lands on master for this project triggers an automatic build as well as a tagged release called `latest`. If you don't wish to live on the bleeding edge you may use a stable release instead. See [releases](../../releases/latest) for the available versions.

```yaml
- uses: "marvinpinto/action-inject-ssm-secrets@<VERSION>"
```

## How to get help

The main [README](https://github.com/marvinpinto/actions/blob/master/README.md) for this project has a bunch of information related to debugging & submitting issues. If you're still stuck, try and get a hold of me on [keybase](https://keybase.io/marvinpinto) and I will do my best to help you out.

## License

The source code for this project is released under the [MIT License](/LICENSE). This project is not associated with GitHub or AWS.
37 changes: 37 additions & 0 deletions packages/aws-ssm-secrets/__tests__/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as core from '@actions/core';
import {mockGetParameter} from 'aws-sdk/clients/ssm';
import {main} from '../src/main';

describe('main handler', () => {
beforeEach(() => {
jest.clearAllMocks();
process.env['INPUT_SSM_PARAMETER'] = '/build-secrets/production/cloudflare-account-id';
process.env['INPUT_ENV_VARIABLE_NAME'] = 'cloudflare_account_id';
});

afterEach(() => {
delete process.env.INPUT_SSM_PARAMETER;
delete process.env.INPUT_ENV_VARIABLE_NAME;
});

it('throws an error if any of the required parameters is not supplied', async () => {
delete process.env.INPUT_SSM_PARAMETER;
await expect(main()).rejects.toThrow('Input required and not supplied: ssm_parameter');
});

it('is able to successfully inject a secret as an environment variable', async () => {
await main();

expect(mockGetParameter).toHaveBeenCalledTimes(1);
expect(mockGetParameter).toHaveBeenCalledWith({
Name: '/build-secrets/production/cloudflare-account-id',
WithDecryption: true,
});

expect(core.setSecret).toHaveBeenCalledTimes(1);
expect(core.setSecret).toHaveBeenCalledWith('super-secret-value');

expect(core.exportVariable).toHaveBeenCalledTimes(1);
expect(core.exportVariable).toHaveBeenCalledWith('CLOUDFLARE_ACCOUNT_ID', 'super-secret-value');
});
});
16 changes: 16 additions & 0 deletions packages/aws-ssm-secrets/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: "AWS SSM Build Secrets for GitHub Actions"
author: "marvinpinto"
description: "Inject AWS SSM Parameter Store secrets as enviroment variables into your GitHub Actions builds"
inputs:
ssm_parameter:
description: "The SSM key to look up"
required: true
env_variable_name:
description: "The corresponding environment variable name to assign the secret to"
required: true
runs:
using: "node12"
main: "dist/index.js"
branding:
icon: "lock"
color: "orange"
35 changes: 35 additions & 0 deletions packages/aws-ssm-secrets/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "aws-ssm-secrets",
"version": "1.0.1",
"private": true,
"description": "Inject AWS SSM Parameter Store secrets as enviroment variables into your GitHub Actions builds",
"config": {
"eslintPaths": "src/**/*.ts __tests__/**/*.ts .*.js *.js",
"prettierPaths": "**/*.{json,md,yaml,yml} !package.json"
},
"scripts": {
"build": "webpack --config webpack.config.js --colors",
"clean": "rm -rf node_modules yarn-error.log dist",
"lint": "yarn run lint:eslint && yarn run lint:prettier",
"lint:eslint": "eslint --color --max-warnings=0 $npm_package_config_eslintPaths",
"lint:prettier": "prettier --color --list-different $npm_package_config_prettierPaths",
"lintfix": "yarn run lintfix:eslint && yarn run lintfix:prettier",
"lintfix:eslint": "eslint --color --fix $npm_package_config_eslintPaths",
"lintfix:prettier": "prettier --color --write $npm_package_config_prettierPaths"
},
"dependencies": {
"@actions/core": "^1.2.5",
"aws-sdk": "^2.748.0"
},
"main": "dist/index.js",
"license": "MIT",
"eslintIgnore": [
"!.*.js"
],
"devDependencies": {
"terser-webpack-plugin": "^2.3.1",
"ts-loader": "^6.2.1",
"webpack": "^4.41.4",
"webpack-cli": "^3.3.10"
}
}
2 changes: 2 additions & 0 deletions packages/aws-ssm-secrets/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import {main} from './main';
main();
51 changes: 51 additions & 0 deletions packages/aws-ssm-secrets/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as core from '@actions/core';
import SSM from 'aws-sdk/clients/ssm';

type ActionParams = {
envVariable: string;
ssmParameter: string;
};

const getAndValidateArgs = (): ActionParams => {
core.startGroup('Validating action arguments');

const ssmParameter = core.getInput('ssm_parameter', {required: true});
const envVariable = core.getInput('env_variable_name', {required: true});

const param: ActionParams = {
envVariable,
ssmParameter,
};
core.debug(`Final actionParam: ${JSON.stringify(param)}`);

core.info('Validation successful');
core.endGroup();

return param;
};

export const main = async () => {
const actionParam = getAndValidateArgs();
const ssm = new SSM();
core.startGroup('Injecting secret environment variables');

const result = await ssm
.getParameter({
Name: actionParam.ssmParameter,
WithDecryption: true, // NOTE: this flag is ignored for String and StringList parameter types
})
.promise();

const envVar = actionParam.envVariable.toUpperCase();
const secret = result?.Parameter?.Value;
// istanbul ignore next
if (!secret) {
core.warning(`Secret value for environment variable ${envVar} appears to be empty`);
}

core.setSecret(secret || '');
core.exportVariable(envVar, secret);
core.info(`Successfully set secret environment variable: ${envVar}`);

core.endGroup();
};
4 changes: 4 additions & 0 deletions packages/aws-ssm-secrets/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig-base",
"rootDir": "./src"
}
52 changes: 52 additions & 0 deletions packages/aws-ssm-secrets/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
target: 'node',
mode: 'production',

entry: {
index: './src/index.ts',
},

module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},

resolve: {
extensions: ['.tsx', '.ts', '.js'],
mainFields: ['main'],
},

output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
library: 'main',
libraryTarget: 'commonjs2',
},

optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
output: {
comments: false,
},
},
sourceMap: true,
extractComments: false,
}),
],
},

plugins: [new webpack.IgnorePlugin(/\/iconv-loader$/)],
};

0 comments on commit f4b98c0

Please sign in to comment.