Skip to content

Commit

Permalink
feat: Manage user ip in security groups
Browse files Browse the repository at this point in the history
  • Loading branch information
niallmccullagh committed Dec 11, 2018
0 parents commit 5c4c565
Show file tree
Hide file tree
Showing 10 changed files with 9,250 additions and 0 deletions.
13 changes: 13 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
22 changes: 22 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module.exports = {
'env': {
'node': true,
'mocha': true,
'es6': true,
},
'plugins': [
'security'
],
'extends': ['airbnb-base', 'plugin:security/recommended'],
'rules': {
"no-console": "off",
"security/detect-non-literal-fs-filename": "off",
'no-restricted-syntax': [
2,
'BreakStatement',
'DebuggerStatement',
'LabeledStatement',
'WithStatement',
]
}
};
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/node_modules/
.idea
7 changes: 7 additions & 0 deletions .markdownlint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"line-length": false,
"no-inline-html": {},
"no-trailing-punctuation": {
"punctuation": ".,;:!"
}
}
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
10.8.0
12 changes: 12 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
language: node_js
cache:
directories:
- ~/.npm
notifications:
email: false
node_js:
- '10'
after_success:
- npm run travis-deploy-once "npm run semantic-release"
branches:
if: branch = master
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# AWS Security Group Manager

`aws-manage-sg` is a utility to manage multi security group rules for a remote worker.
It revokes old rules, and grants new rules with the user's current ip address.

## Running

* Create a config file to contain

```json
{
"username": "johndoe",
"rules": [
{
"name": "basiton",
"securityGroupId": "sg-396jk989f",
"ports": [22]
},
{
"name": "kibana",
"securityGroupId": "sg-3960686b",
"ports": [443]
}
],
"region": "us-east-1"
}
```

* Install aws-manage-sg `npm install -g aws-manage-sg`
* Run to remove old rules and whitelist new ip. `aws-manage-sg -f config.json`

## Notes

* It is recommended to use the AWS username to ensure that users don't override each others settings
* By default the cli will try to authenticate using details from environment variables, to use a specific profile set the profile explicitly.
* The AWS user must have the following permissions: `ec2:AuthorizeSecurityGroupIngress` and `ec2:DescribeSecurityGroups`

## Command Line

Find out the full range of options by running `aws-manage-sg -h`

```bash
$ aws-manage-sg -h
Usage: aws-manage-sg [options]

Options:
--version Show version number [boolean]
-f, --file Path to config file [required]
-g, --grant Run only the grant
-r, --revoke Run only the revoke
-p, --profile AWS profile to use
-h Show help [boolean]
```
157 changes: 157 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#!/usr/local/bin/node
const AWS = require('aws-sdk');
const delay = require('timeout-as-promise');
const fs = require('fs');
const request = require('request-promise');
const yargs = require('yargs');

function EC2(config) {
const region = config.region || 'us-east-1';
return new AWS.EC2({ apiVersion: '2016-11-15', region });
}

async function getSecurityGroups(config) {
const params = {
GroupIds: config.rules.map(({ securityGroupId }) => securityGroupId),
};

const { SecurityGroups: securityGroups } = await EC2(config)
.describeSecurityGroups(params)
.promise();

if (securityGroups === undefined) {
throw new Error(`No security groups found with for ${JSON.stringify(params)}`);
}

return securityGroups;
}
function isRangeForUser(username, range) {
return range.Description === username;
}

function hasPermissionARangeForUser(username, permission) {
return (
permission.IpRanges
&& permission.IpRanges.some(range => isRangeForUser(username, range))
);
}

async function revokePermission(config, securityGroupId, permission) {
const revokeParam = {
IpRanges: permission.IpRanges.filter(range => isRangeForUser(config.username, range)),
FromPort: permission.FromPort,
ToPort: permission.ToPort,
IpProtocol: permission.IpProtocol,
};

console.log(`Revoking rule ${JSON.stringify(revokeParam)} on ${securityGroupId}`);

return EC2(config).revokeSecurityGroupIngress({
GroupId: securityGroupId,
IpPermissions: [revokeParam],
}).promise();
}

async function grantPermission(config, ipAddress, { securityGroupId, ports }) {
console.log(`Granting rule to ${securityGroupId} on ports ${ports} for IP ${ipAddress}`);

const permissions = await ports.map((port) => {
const p = parseInt(port, 10);
return {
IpRanges: [
{
CidrIp: `${ipAddress}/32`,
Description: `${config.username}`,
},
],
FromPort: p,
ToPort: p,
IpProtocol: 'tcp',
};
});

return EC2(config).authorizeSecurityGroupIngress({
GroupId: securityGroupId,
IpPermissions: permissions,
}).promise();
}

async function getIPAddress() {
const response = await request('http://checkip.amazonaws.com/');
return response.replace(/\s/g, '');
}


async function revokePermissions(config) {
const results = [];

for (const securityGroup of await getSecurityGroups(config)) {
const result = securityGroup.IpPermissions
.filter(permission => hasPermissionARangeForUser(config.username, permission))
.map(permission => revokePermission(config, securityGroup.GroupId, permission));
results.push(result);
}
return Promise.all(results);
}

function useAWSProfile(profile) {
const credentials = new AWS.SharedIniFileCredentials({ profile });
AWS.config.credentials = credentials;
}

async function run(options, config) {
async function giveRevocationTimeToSet() {
await delay(1000);
}

function shouldRunByDefault() {
return !options.revoke && !options.grant;
}

const ipAddress = await getIPAddress();
const revoke = options.revoke || shouldRunByDefault();
const grant = options.grant || shouldRunByDefault();

if (options.profile) {
useAWSProfile(options.profile);
}

if (revoke) {
await revokePermissions(config);
await giveRevocationTimeToSet();
}

if (grant) {
const results = [];
for (const rule of config.rules) {
grantPermission(config, ipAddress, rule);
}
await Promise.all(results);
}
}

function getOptions() {
return yargs
.usage('Usage: $0 <command> [options]')
.alias('f', 'file')
.describe('f', 'Path to config file')
.alias('g', 'grant')
.describe('g', 'Run only the grant')
.alias('r', 'revoke')
.describe('r', 'Run only the revoke')
.alias('p', 'profile')
.describe('p', 'AWS profile to use')
.demandOption(['file'], 'Please provide a path to a config file')
.help('h').argv;
}

(async () => {
try {
const options = getOptions();
const config = JSON.parse(fs.readFileSync(options.file));
await run(options, config);
} catch (e) {
console.log(`ERROR: ${e.message}`);
process.exit(1);
}
})();
Loading

0 comments on commit 5c4c565

Please sign in to comment.