Skip to content

Commit

Permalink
feat: Auto rotate site bucket keys #153
Browse files Browse the repository at this point in the history
  • Loading branch information
apburnes committed Nov 14, 2023
1 parent 4038751 commit 5da027b
Show file tree
Hide file tree
Showing 22 changed files with 223 additions and 39 deletions.
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18.12
20.9
2 changes: 1 addition & 1 deletion Dockerfile-app
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:18.15
FROM node:20.9-bullseye

WORKDIR /app

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile-pw
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:18.15
FROM node:20.9-bullseye

WORKDIR /app

Expand Down
2 changes: 1 addition & 1 deletion admin-client/.nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18.12
20.9
2 changes: 1 addition & 1 deletion admin-client/Dockerfile-admin
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:18
FROM node:20.9-bullseye

WORKDIR /app

Expand Down
3 changes: 1 addition & 2 deletions admin-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
"start": "sirv public -p 3000 --host --single --dev"
},
"engines": {
"node": "^18.x.x",
"npm": "^8.1.2"
"node": "^20.x.x"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^12.0.0",
Expand Down
5 changes: 5 additions & 0 deletions api/models/site.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@ module.exports = (sequelize, DataTypes) => {
type: DataTypes.STRING,
allowNull: false,
},
awsBucketKeyUpdatedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
},
config: {
type: DataTypes.JSONB,
defaultValue: {},
Expand Down
54 changes: 54 additions & 0 deletions api/services/SiteBucketKeyRotator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const { Site } = require('../models');
const CFApiClient = require('../utils/cfApiClient');
const CloudFoundryAuthClient = require('../utils/cfAuthClient');

async function rotateBucketKey(site, cfApi) {
const serviceName = site.s3ServiceName;
const serviceBindingName = `${serviceName}-key`;
const serviceInstance = await cfApi.fetchServiceInstance(serviceName);
const credentials = await cfApi
.fetchCredentialBindingsInstance(serviceBindingName)
.catch((error) => {
// Return null to skip credentials delete if not found
// fetchCredentialBindingsInstance throws error
// with name string starting "Not found" when credentials
// do not exist
if (error.name.toLowerCase().trim().startswith('not found')) {
return null;
}

throw error;
});

if (credentials) {
// Delete existing credential service if they exist
await cfApi.deleteServiceInstanceCredentials(credentials.guid);
await cfApi.sleep('3000');
}

await cfApi.createServiceKey(serviceInstance.name, serviceInstance.guid);

const now = new Date();
await site.update({ awsBucketKeyUpdatedAt: now });

return site;
}

async function rotateSitesBucketKeys({
limit = 20, username, password, tokenUrl, apiUrl,
}) {
const authClient = new CloudFoundryAuthClient({ username, password, tokenUrl });
const cfApi = new CFApiClient({ apiUrl, authClient });

const sites = await Site.findAll({
order: [['awsBucketKeyUpdatedAt', 'ASC']],
limit,
});

return Promise.allSettled(sites.map(site => rotateBucketKey(site, cfApi)));
}

module.exports = {
rotateBucketKey,
rotateSitesBucketKeys,
};
12 changes: 10 additions & 2 deletions api/utils/cfApiClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,20 @@ class CloudFoundryAPIClient {
}

fetchCredentialBindingsInstance(name) {
const path = `/v3/service_credential_bindings?names=${name}`;
const endpoint = `/v3/service_credential_bindings?names=${name}`;

return this.accessToken()
.then(token => this.request('GET', path, token))
.then(token => this.request('GET', endpoint, token))
.then(res => findEntity(res, name));
}

deleteServiceInstanceCredentials(guid) {
const endpoint = `/v3/service_credential_bindings/${guid}`;

return this.accessToken()
.then(token => this.request('DELETE', endpoint, token));
}

fetchServiceInstanceCredentials(name) {
return this.accessToken()
.then(token => this.request(
Expand Down Expand Up @@ -332,5 +339,6 @@ CloudFoundryAPIClient.findEntity = findEntity;
CloudFoundryAPIClient.firstEntity = firstEntity;
CloudFoundryAPIClient.objToQueryParams = objToQueryParams;
CloudFoundryAPIClient.buildRequestBody = buildRequestBody;
CloudFoundryAPIClient.sleep = sleep;

module.exports = CloudFoundryAPIClient;
2 changes: 1 addition & 1 deletion apps/metrics/ci/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ node-image: &node-image
type: docker-image
source:
repository: node
tag: 18
tag: 20.9-bullseye

cf-image: &cf-image
platform: linux
Expand Down
2 changes: 1 addition & 1 deletion apps/metrics/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"license": "CC0-1.0",
"author": "[email protected]",
"engines": {
"node": "^18.x.x"
"node": "^20.x.x"
},
"scripts": {
"dev": "NODE_ENV=development npx nodemon -r dotenv/config src/index.js",
Expand Down
2 changes: 1 addition & 1 deletion ci/docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: '3'

services:
app:
image: node:18.15
image: node:20.9-bullseye
volumes:
- ../..:/app
depends_on:
Expand Down
7 changes: 7 additions & 0 deletions ci/partials/rotate-bucket-keys.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
platform: linux
inputs: [name: src]
outputs: [name: src]
run:
dir: src
path: bash
args: [-c, node --env-file=.env scripts/rotate-bucket-keys.js]
38 changes: 37 additions & 1 deletion ci/pipeline-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,42 @@ jobs:
username: ((slack-username))
icon_url: ((slack-icon-url))

- name: nightly-site-bucket-key-rotator
plan:
- get: src
resource: pr-((deploy-env))
passed: [set-pipeline]
# - get: nightly
# trigger: true
- get: node
- get: cf-image
- task: install-deps-api
file: src/ci/partials/install-deps-api.yml
image: node
- task: get-app-env
file: src/ci/partials/get-app-env.yml
image: cf-image
params:
<<: *env-cf
APP_ENV: ((deploy-env))
CF_APP_NAME: pages-((deploy-env))
- task: rotate-keys
file: src/ci/partials/rotate-bucket-keys.yml
image: node
params:
<<: *env-cf
APP_ENV: ((deploy-env))
CF_APP_NAME: pages-((deploy-env))
on_failure:
put: slack
params:
text: |
:x: FAILED: Rotate site bucket keys in ((deploy-env))
<$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME?vars.deploy-env="((deploy-env))"|View build details>
channel: ((slack-channel))
username: ((slack-username))
icon_url: ((slack-icon-url))


- name: set-pipeline
plan:
Expand Down Expand Up @@ -391,7 +427,7 @@ resources:
type: docker-image
source:
repository: node
tag: 18
tag: 20.9-bullseye

- name: slack
type: slack-notification
Expand Down
2 changes: 1 addition & 1 deletion ci/pipeline-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ resources:
type: docker-image
source:
repository: node
tag: 18.15
tag: 20.9-bullseye

- name: slack
type: slack-notification
Expand Down
2 changes: 1 addition & 1 deletion ci/pipeline-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ resources:
type: docker-image
source:
repository: node
tag: 18.15
tag: 20.9-bullseye

- name: slack
type: slack-notification
Expand Down
7 changes: 5 additions & 2 deletions ci/tasks/get-app-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ cf auth

cf t -o $CF_ORG -s $CF_SPACE

echo "VCAP_SERVICES={`cf env $CF_APP_NAME | tail -n+3 | awk -v RS= 'NR==1' | tail -n+2 | tr -d '\n'`" >> .env
echo "VCAP_APPLICATION={`cf env $CF_APP_NAME | tail -n+3 | awk -v RS= 'NR==2' | tail -n+2 | tr -d '\n'`" >> .env
CF_APP_GUID=`cf app $CF_APP_NAME --guid`

cf curl /v3/apps/$CF_APP_GUID/env | \
jq -r 'to_entries | .[] | .value | to_entries | map({key, value: (.value | tostring) }) | .[] | join("=")' \
> .env
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ services:
- 6379:6379
command: redis-server --loglevel warning
echo:
image: node:18
image: node:20.9-bullseye
volumes:
- yarn:/usr/local/share/.cache/yarn
- .:/app
Expand Down
15 changes: 15 additions & 0 deletions migrations/20231027154728-site-aws-bucket-key-updated-at.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const TABLE = 'site';
const COLUMN_NAME = 'awsBucketKeyUpdatedAt';
const COLUMN_TYPE = {
type: 'timestamp',
notNull: true,
defaultValue: new String('now()'),
};

exports.up = async (db) => {
await db.addColumn(TABLE, COLUMN_NAME, COLUMN_TYPE);
};

exports.down = async (db) => {
await db.removeColumn(TABLE, COLUMN_NAME);
};
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@
"queued-builds-check": "node ./scripts/queued-builds-check.js",
"test:e2e": "yarn playwright test",
"create-test-users": "DOTENV_CONFIG_PATH=.env node -r dotenv/config ./scripts/create-test-users.js",
"remove-test-users": "DOTENV_CONFIG_PATH=.env node -r dotenv/config ./scripts/remove-test-users.js"
"remove-test-users": "DOTENV_CONFIG_PATH=.env node -r dotenv/config ./scripts/remove-test-users.js",
"rotate-bucket-keys": "node ./scripts/rotate-bucket-keys.js"
},
"main": "index.js",
"repository": {
Expand Down Expand Up @@ -188,8 +189,8 @@
"webpack-manifest-plugin": "^5.0.0"
},
"engines": {
"node": "^18.x.x",
"npm": "^8.1.2"
"node": "^20.x.x",
"npm": "^10.x.x"
},
"lint-staged": {
"*.js": "eslint",
Expand Down
33 changes: 33 additions & 0 deletions scripts/rotate-bucket-keys.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const cfenv = require('cfenv');
const {
rotateSitesBucketKeys,
} = require('../api/services/SiteBucketKeyRotator');

const appDeployEnv = process.env.APP_ENV;

async function main() {
const appEnv = cfenv.getAppEnv();
const uaaCredentials = appEnv.getServiceCreds(`app-${appDeployEnv}-uaa-client`);
const rotatedKeyServices = await rotateSitesBucketKeys({
username: uaaCredentials.clientID,
password: uaaCredentials.clientSecret,
tokenUrl: uaaCredentials.tokenURL,
apiUrl: appEnv.app.cf_api,
});

const rejected = rotatedKeyServices.filter(
result => result.status === 'rejected'
);

if (rejected.length > 0) {
// eslint-disable-next-line
console.log('Failed to rotate site keys');
// eslint-disable-next-line
console.log(JSON.stringify(rejected, null, 2));
process.exit(1);
}

process.exit(0);
}

main();
Loading

0 comments on commit 5da027b

Please sign in to comment.