@k9securityio/k9-cdk
Version:
Provision strong AWS security policies easily using the AWS CDK.
270 lines (207 loc) • 11.2 kB
Markdown
# k9 AWS CDK policy library #
k9 Security's `k9-cdk` for CDKv2 ([CDKv1](https://github.com/k9securityio/k9-cdk/tree/main)) makes strong security usable and helps you provision best practice AWS security policies
defined using the simplified [k9 access capability model](https://k9security.io/docs/k9-access-capability-model/) and
safe defaults. In CDK terms, this library provides [Curated (L2) constructs](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) that wrap core CloudFormation resources (L1) to simplify security.
Supported services:
* S3
* KMS
* DynamoDB
* SQS
* EventBridge
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 [support@k9security.io](mailto:support@k9security.io?subject=k9-cdk).
## Usage
Use the k9 CDK to generate a policy and use it in your existing code base.
For example, the following code will:
1. provision an S3 Bucket
2. allow the `ci` and `person1` users to administer the bucket
3. allow administrators and `k9-auditor` to read bucket configuration
4. allow the `app-backend` role to write data into the bucket
5. allow the `app-backend` and `customer-service` role to read data in the bucket
```typescript
import * as cdk from "aws-cdk-lib";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as k9 from "@k9securityio/k9-cdk";
// Define which principals may access the bucket and what capabilities they should have
const administerResourceArns = [
"arn:aws:iam::123456789012:user/ci",
"arn:aws:iam::123456789012:user/person1"
];
const readConfigArns = administerResourceArns.concat([
"arn:aws:iam::123456789012:role/k9-auditor",
"arn:aws:iam::123456789012:role/aws-service-role/access-analyzer.amazonaws.com/AWSServiceRoleForAccessAnalyzer"
]);
const app = new cdk.App();
const stack = new cdk.Stack(app, 'K9Example');
const bucket = new s3.Bucket(stack, 'TestBucket', {});
const k9BucketPolicyProps: k9.s3.K9BucketPolicyProps = {
bucket: bucket,
k9DesiredAccess: new Array<k9.k9policy.IAccessSpec>(
{ // declare access capabilities individually
accessCapability: k9.k9policy.AccessCapability.ADMINISTER_RESOURCE,
allowPrincipalArns: administerResourceArns,
},
{
accessCapability: k9.k9policy.AccessCapability.READ_CONFIG,
allowPrincipalArns: readConfigArns,
},
{ // or declare multiple access capabilities at once
accessCapabilities: [
k9.k9policy.AccessCapability.READ_DATA,
k9.k9policy.AccessCapability.WRITE_DATA
],
allowPrincipalArns: [
"arn:aws:iam::123456789012:role/app-backend",
],
},
{
accessCapability: k9.k9policy.AccessCapability.READ_DATA,
allowPrincipalArns: [
"arn:aws:iam::123456789012:role/customer-service"
],
}
// omit access spec for delete-data because it is unneeded
)
};
k9.s3.grantAccessViaResourcePolicy(stack, "S3Bucket", k9BucketPolicyProps);
```
Granting access to an SQS queue works the same way, using the `k9.sqs.grantAccessViaResourcePolicy` function:
```typescript
import * as sqs from 'aws-cdk-lib/aws-sqs';
const queue = new sqs.Queue(stack, '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:
```typescript
import * as kms from "aws-cdk-lib/aws-kms";
import {PolicyDocument} from "aws-cdk-lib/aws-iam";
const k9KeyPolicyProps: k9.kms.K9KeyPolicyProps = {
k9DesiredAccess: k9BucketPolicyProps.k9DesiredAccess
};
const keyPolicy: PolicyDocument = k9.kms.makeKeyPolicy(k9KeyPolicyProps);
new kms.Key(stack, 'KMSKey', {
alias: 'app-key-with-k9-policy',
policy: keyPolicy
});
```
Protecting a DynamoDB table follows the same path as KMS, generating a policy then providing it to the DynamoDB table construct via props:
```typescript
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
const ddbResourcePolicyProps: k9.dynamodb.K9DynamoDBResourcePolicyProps = {
k9DesiredAccess: k9BucketPolicyProps.k9DesiredAccess
};
const ddbResourcePolicy = k9.dynamodb.makeResourcePolicy(ddbResourcePolicyProps);
const table = new dynamodb.TableV2(stack, 'app-table-with-k9-policy', {
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
resourcePolicy: ddbResourcePolicy,
});
```
Granting access to an EventBridge event bus works like SQS, using the `k9.events.grantAccessViaResourcePolicy` function.
EventBridge supports the `administer-resource`, `read-config`, and `write-data` capabilities:
```typescript
import * as events from "aws-cdk-lib/aws-events";
const bus = new events.EventBus(stack, 'AppEventBus', {
eventBusName: 'app-bus-with-k9-policy',
});
const k9EventBusProps: k9.events.K9EventBusResourcePolicyProps = {
bus: bus,
k9DesiredAccess: new Array<k9.k9policy.IAccessSpec>(
{
accessCapabilities: [
k9.k9policy.AccessCapability.ADMINISTER_RESOURCE,
k9.k9policy.AccessCapability.READ_CONFIG,
],
allowPrincipalArns: administerResourceArns,
},
{
accessCapabilities: k9.k9policy.AccessCapability.WRITE_DATA,
allowPrincipalArns: [
"arn:aws:iam::123456789012:role/app-backend",
],
},
),
};
k9.events.grantAccessViaResourcePolicy(k9EventBusProps);
```
## Example stack
The example stack demonstrates full use of the k9 S3, KMS, DynamoDB, SQS, and EventBridge policy generators. Generated policies:
S3 Bucket Policy:
* [Templatized Bucket Policy](examples/generated.bucket-policy.json)
* [BucketPolicy resource in CFn template](examples/K9Example.template.json)
SQS Queue Policy:
* [Templatized Queue Policy](examples/generated.queue-policy.json)
* [TestQueuePolicy resource in CFn template](examples/K9Example.template.json)
KMS Key Policy:
* [Templatized Key Policy](examples/generated.key-policy.json)
* [KeyPolicy attribute of Key resource in CFn template](examples/K9Example.template.json)
DynamoDB Resource Policy:
* [Templatized DynamoDB Resource Policy](examples/generated.dynamodb-policy.json)
* [ResourcePolicy attribute of GlobalTable resource in CFn template](examples/K9Example.template.json)
EventBridge Event Bus Policy:
* [Templatized Event Bus Policy](examples/generated.eventbus-policy.json)
* [EventBusPolicy resources in CFn template](examples/K9Example.template.json)
## Restricting Access to Specific Organizations
You can restrict access capabilities to principals within specific AWS Organizations by setting `restrictToPrincipalOrgIDs` on an `IAccessSpec`. When set, k9-cdk will:
1. Add a `StringEquals` condition on `aws:PrincipalOrgID` to the Allow statements for those capabilities
2. Generate a `DenyUntrustedOrgs` statement that explicitly denies the org-restricted actions for principals outside the specified organizations
This provides defense-in-depth: even if another Allow statement is added to the policy without an org constraint, the explicit Deny prevents principals from untrusted organizations from gaining access _for those permissions_.
`restrictToPrincipalOrgIDs` can be combined with specific principal ARNs (both conditions must be satisfied) or with a wildcard `*` principal to allow any principal within the organization:
```typescript
const k9BucketPolicyProps: k9.s3.K9BucketPolicyProps = {
bucket: bucket,
k9DesiredAccess: new Array<k9.k9policy.IAccessSpec>(
{
accessCapabilities: [
k9.k9policy.AccessCapability.ADMINISTER_RESOURCE,
k9.k9policy.AccessCapability.READ_CONFIG,
],
allowPrincipalArns: administerResourceArns,
},
{ // restrict write-data to specific principals within the org
accessCapabilities: k9.k9policy.AccessCapability.WRITE_DATA,
allowPrincipalArns: [
"arn:aws:iam::123456789012:role/app-backend",
],
restrictToPrincipalOrgIDs: ["o-abc123"],
},
{ // allow any principal in the org to read data
accessCapabilities: k9.k9policy.AccessCapability.READ_DATA,
allowPrincipalArns: ["*"],
restrictToPrincipalOrgIDs: ["o-abc123"],
},
),
};
```
In this example, the `write-data` Allow statement requires the caller to match both the specific principal ARN and the org ID. The `read-data` Allow statement allows any principal from `o-abc123`. Both capabilities are covered by the `DenyUntrustedOrgs` statement, which denies the corresponding actions for principals outside `o-abc123`.
**Caveat:** When you use a wildcard `*` principal with `restrictToPrincipalOrgIDs`, k9-cdk will _not_ generate a `DenyEveryoneElse` statement. The `DenyEveryoneElse` statement works by excepting specific principal ARNs from the deny, but a `*` wildcard principal cannot be meaningfully excepted because exempting `*` would exempt everyone and render the deny ineffective. In this case, access is constrained by the `aws:PrincipalOrgID` condition on the Allow statements and the `DenyUntrustedOrgs` deny statement rather than `DenyEveryoneElse`. As an alternative, you can specify principal ARNs with wildcards and test with `ArnLike`:
```typescript
{ // restrict write-data to all 'publisher' principals within the org
accessCapabilities: k9.k9policy.AccessCapability.WRITE_DATA,
allowPrincipalArns: [
"arn:aws:iam::*:role/*publisher*",
],
restrictToPrincipalOrgIDs: ["o-abc123"],
}
```
That's not every principal in the org, but it may be closer to what you want in practice.
## Specialized Use Cases
k9-cdk can be configured to support specialized use cases, including:
* [Public Bucket](docs/use-case-public-bucket.md) - Publicly readable objects, least privilege for all other actions
## Local Development and Testing
The high level build commands for this project are driven by `make`:
* `make all` - build library, run tests, and deploy
* `make build` - build the library
* `make converge` - deploy the integration test resources
* `make destroy` - destroy the integration test resources
The low level build commands for this project are:
* `npx projen build` compile typescript to js, lint, transpile with JSII, execute tests
* `cdk synth` emits the synthesized CloudFormation template
* `cdk deploy` deploy this stack to your default AWS account/region
* `cdk diff` compare deployed stack with current state