Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 1 addition & 5 deletions .eslintrc.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion .projenrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,18 @@ const project = new awscdk.AwsCdkConstructLibrary({
docgen: false,
});

project.synth();
project.eslint?.addRules({
'import/order': [
'warn',
{
groups: [
'builtin',
'external',
],
},
],
});

project.eslint?.

project.synth();
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Supported services:
* S3
* KMS
* DynamoDB
* SQS

This library [simplifies IAM as described in Effective IAM for AWS](https://www.effectiveiam.com/simplify-aws-iam) and is fully-supported by k9 Security. We're happy to answer questions or help you integrate it via a [GitHub issue](https://github.com/k9securityio/k9-cdk/issues) or email to [[email protected]](mailto:[email protected]?subject=k9-cdk).

Expand Down Expand Up @@ -77,6 +78,21 @@ const k9BucketPolicyProps: k9.s3.K9BucketPolicyProps = {
k9.s3.grantAccessViaResourcePolicy(stack, "S3Bucket", k9BucketPolicyProps);
```

Granting access to an SQS queue works the same way, using the `k9.sqs.grantAccessViaResourcePolicy` function:
```typescript
const queue = new sqs.Queue(stack, 'k9-cdk-v2-int-test-queue', {
queueName: 'app-queue-with-k9-policy',
});

const k9SQSResourcePolicyProps: K9SQSResourcePolicyProps = {
queue: queue,
// reuse bucket's desired access for brevity; configure k9DesiredAccess however you need
k9DesiredAccess: k9BucketPolicyProps.k9DesiredAccess,
};

k9.sqs.grantAccessViaResourcePolicy(k9SQSResourcePolicyProps);
```

Granting access to a KMS key is similar, but the custom resource policy is created first
so it can be set via `props` per CDK convention:

Expand Down Expand Up @@ -113,6 +129,8 @@ const table = new dynamodb.TableV2(stack, 'app-table-with-k9-policy', {
});
```

## Example stack

The example stack demonstrates full use of the k9 S3, KMS, and DynamoDB policy generators. Generated policies:

S3 Bucket Policy:
Expand Down
38 changes: 37 additions & 1 deletion bin/k9-cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import * as cdk from "aws-cdk-lib";
import {RemovalPolicy, Tags} from "aws-cdk-lib";
// import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
// import * as cforigins from "aws-cdk-lib/aws-cloudfront-origins";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import * as kms from "aws-cdk-lib/aws-kms";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import {BlockPublicAccess, BucketEncryption} from "aws-cdk-lib/aws-s3";
import * as sqs from 'aws-cdk-lib/aws-sqs';

import * as k9 from "../lib";
import {K9SQSResourcePolicyProps} from "../src/sqs";

const administerResourceArns = [
// for development
Expand Down Expand Up @@ -221,6 +223,39 @@ const table = new dynamodb.TableV2(stack, 'k9-cdk-v2-int-test', {
});


const queue = new sqs.Queue(stack, 'k9-cdk-v2-int-test-queue', {
queueName: 'k9-cdk-v2-int-test',
})
const k9SQSResourcePolicyProps: K9SQSResourcePolicyProps = {
queue: queue,
k9DesiredAccess: new Array<k9.k9policy.IAccessSpec>(
{
accessCapabilities: k9.k9policy.AccessCapability.ADMINISTER_RESOURCE,
allowPrincipalArns: administerResourceArns,
},
{
accessCapabilities: k9.k9policy.AccessCapability.READ_CONFIG,
allowPrincipalArns: readConfigArns.concat([
"arn:aws:iam::139710491120:role/aws-service-role/access-analyzer.amazonaws.com/AWSServiceRoleForAccessAnalyzer",
]),
},
{
accessCapabilities: k9.k9policy.AccessCapability.READ_DATA,
allowPrincipalArns: readWriteDataArns,
},
{
accessCapabilities: k9.k9policy.AccessCapability.WRITE_DATA,
allowPrincipalArns: readWriteDataArns,
},
{
accessCapabilities: k9.k9policy.AccessCapability.DELETE_DATA,
allowPrincipalArns: readWriteDataArns,
},
)
}

k9.sqs.grantAccessViaResourcePolicy(k9SQSResourcePolicyProps);

for (let construct of [bucket,
websiteBucket,
autoDeleteBucket,
Expand All @@ -229,6 +264,7 @@ for (let construct of [bucket,
cloudfrontOACBucket,
cloudfrontOACKey,
table,
queue,
]) {
Tags.of(construct).add('k9security:analysis', 'include');
}
20 changes: 11 additions & 9 deletions package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions resources/capability_summary.json
Original file line number Diff line number Diff line change
Expand Up @@ -761,24 +761,26 @@
"SQS": {
"administer-resource": [
"sqs:AddPermission",
"sqs:CancelMessageMoveTask",
"sqs:CreateQueue",
"sqs:DeleteQueue",
"sqs:PurgeQueue",
"sqs:RemovePermission",
"sqs:SetQueueAttributes",
"sqs:StartMessageMoveTask",
"sqs:TagQueue",
"sqs:UntagQueue"
],
"delete-data": [
"sqs:DeleteMessage",
"sqs:DeleteMessageBatch",
"sqs:DeleteQueue",
"sqs:PurgeQueue"
],
"read-config": [
"sqs:GetQueueAttributes",
"sqs:GetQueueUrl",
"sqs:ListDeadLetterSourceQueues",
"sqs:ListMessageMoveTasks",
"sqs:ListQueues",
"sqs:ListQueueTags"
],
Expand All @@ -787,9 +789,7 @@
],
"write-data": [
"sqs:ChangeMessageVisibility",
"sqs:ChangeMessageVisibilityBatch",
"sqs:SendMessage",
"sqs:SendMessageBatch"
"sqs:SendMessage"
]
},
"STS": {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * as k9policy from './k9policy';
export * as dynamodb from './dynamodb';
export * as kms from './kms';
export * as s3 from './s3';
export * as sqs from './sqs';
1 change: 1 addition & 0 deletions src/k9policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export class K9PolicyFactory {
'S3',
'KMS',
'DynamoDB',
'SQS',
]);

/** @internal */
Expand Down
158 changes: 158 additions & 0 deletions src/sqs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import {
AccountRootPrincipal,
AddToResourcePolicyResult,
Effect,
PolicyDocument,
PolicyStatement,
} from 'aws-cdk-lib/aws-iam';
import * as iam from 'aws-cdk-lib/aws-iam';
import { IQueue } from 'aws-cdk-lib/aws-sqs';
import {
AccessCapability,
canPrincipalsManageResources,
getAccessCapabilityFromValue,
IAccessSpec,
K9PolicyFactory,
} from './k9policy';


export interface K9SQSResourcePolicyProps {
readonly queue: IQueue;
readonly k9DesiredAccess: Array<IAccessSpec>;
}

let SUPPORTED_CAPABILITIES = new Array<AccessCapability>(
AccessCapability.ADMINISTER_RESOURCE,
AccessCapability.READ_CONFIG,
AccessCapability.READ_DATA,
AccessCapability.WRITE_DATA,
AccessCapability.DELETE_DATA,
);

export const SID_DENY_EVERYONE_ELSE = 'DenyEveryoneElse';

function partitionArray<T>(arr: T[], maxLength: number): T[][] {
const result: T[][] = [];
for (let i = 0; i < arr.length; i += maxLength) {
result.push(arr.slice(i, i + maxLength));
}
return result;
}

/**
* Generate a SQS resource policy from the provided props that can be attached to a queue.
*
* @param props specifying desired access
* @return a PolicyDocument that can be attached to an SQS queue
*/
export function makeResourcePolicy(props: K9SQSResourcePolicyProps): PolicyDocument {
const policyFactory = new K9PolicyFactory();
const policy = new iam.PolicyDocument();

const resourceArns = ['*'];

let accessSpecsByCapabilityRecs = policyFactory.mergeDesiredAccessSpecsByCapability(SUPPORTED_CAPABILITIES, props.k9DesiredAccess);
let accessSpecsByCapability: Map<AccessCapability, IAccessSpec> = new Map();

for (let [capabilityStr, accessSpec] of Object.entries(accessSpecsByCapabilityRecs)) {
accessSpecsByCapability.set(getAccessCapabilityFromValue(capabilityStr), accessSpec);
}

if (!canPrincipalsManageResources(accessSpecsByCapability)) {
throw Error('At least one principal must be able to administer and read-config for SQS resources' +
' so data remains accessible; found:\n' +
`administer-resource: '${accessSpecsByCapability.get(AccessCapability.ADMINISTER_RESOURCE)?.allowPrincipalArns}'\n` +
`read-config: '${accessSpecsByCapability.get(AccessCapability.READ_CONFIG)?.allowPrincipalArns}'`,
);
}

const allowStatements = policyFactory.makeAllowStatements('SQS',
SUPPORTED_CAPABILITIES,
Array.from(accessSpecsByCapability.values()),
resourceArns);

const max_actions_in_statement = 7;
for (let allowStatement of allowStatements) {
//SQS resource policy has a limit of 7 actions per statement (Really).
//But you can have as many statements as you want up to the queue policy size limit.
//So, if an allowStatement has more than 7 actions (like the administer-resource statement does),
//then create additional statements and spread the original statement's permissions across them
if (allowStatement.actions.length > max_actions_in_statement) {
const partitionedActions = partitionArray(allowStatement.actions, max_actions_in_statement);
partitionedActions.forEach((actions, index) => {
const newStatement = allowStatement.copy({
sid: `${allowStatement.sid} ${index + 1}`,
actions: actions,
});
policy.addStatements(newStatement);
});
} else {
policy.addStatements(allowStatement);
}
}

const denyEveryoneElseStatement = new PolicyStatement({
sid: SID_DENY_EVERYONE_ELSE,
effect: Effect.DENY,
principals: policyFactory.makeDenyEveryoneElsePrincipals(),
actions: ['sqs:*'],
resources: resourceArns,
});
denyEveryoneElseStatement.addCondition('Bool', {
'aws:PrincipalIsAWSService': ['false'],
});
const denyEveryoneElseTest = policyFactory.wasLikeUsed(props.k9DesiredAccess) ?
'ArnNotLike' :
'ArnNotEquals';
const allAllowedPrincipalArns = policyFactory.getAllowedPrincipalArns(props.k9DesiredAccess);
const accountRootPrincipal = new AccountRootPrincipal();
denyEveryoneElseStatement.addCondition(denyEveryoneElseTest, {
'aws:PrincipalArn': [
// Place Root Principal arn in stable, prominent position;
// will render as an object Fn::Join'ing Partition & AccountId
accountRootPrincipal.arn,
...allAllowedPrincipalArns,
],
});

policy.addStatements(
denyEveryoneElseStatement,
);

policy.validateForResourcePolicy();

return policy;
}

/**
* Grant access to a queue via resource policy using k9 IAccessSpec definitions. This function
* is the preferred interface for granting access to a queue.
*
* The grant and make operations are split because SQS policies can only be managed via the
* IQueue.addToResourcePolicy method but IQueue does not offer a way to read the policy.
* So making the policy is done in a separate function so policy generation can be tested.
*
* @param props specifying the queue and desired access
*
* @return the results for adding each statement
*/
export function grantAccessViaResourcePolicy(props: K9SQSResourcePolicyProps):
AddToResourcePolicyResult[] {
const resourcePolicy = makeResourcePolicy(props);

resourcePolicy.validateForResourcePolicy();

const policyJson = resourcePolicy.toJSON();
const k9Statements = policyJson.Statement;
const queue = props.queue;
const addToResourcePolicyResults = new Array<AddToResourcePolicyResult>();

for (let statement of k9Statements) {
let addToResourcePolicyResult = queue.addToResourcePolicy(
PolicyStatement.fromJson(statement),
);
addToResourcePolicyResults.push(addToResourcePolicyResult);
}

return addToResourcePolicyResults;
}
Loading
Loading