Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(route53resolver): DNS Firewall #15031

Merged
merged 24 commits into from
Aug 31, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7e33f62
feat(route53resolver): DNS Firewall
jogold Jun 7, 2021
0936e83
Merge branch 'master' into route53resolver-firewall
jogold Jun 9, 2021
93dec85
FirewallDomains.fromS3()
jogold Jun 10, 2021
2c9c641
Merge branch 'master' into route53resolver-firewall
jogold Jun 10, 2021
7a96386
fromS3 with IBucket
jogold Jun 13, 2021
fd429da
Merge branch 'master' into route53resolver-firewall
jogold Jun 13, 2021
f37c494
Merge branch 'route53resolver-firewall' of github.com:jogold/aws-cdk …
jogold Jun 13, 2021
1a5315a
Merge branch 'master' into route53resolver-firewall
jogold Jun 18, 2021
fee2a6d
FirewallDomains.fromAsset()
jogold Jun 18, 2021
88dbdc8
validate domains
jogold Jun 18, 2021
c62af08
add missing test file
jogold Jun 18, 2021
59b3e5c
README
jogold Jun 18, 2021
29af36f
DomainConfig
jogold Jun 21, 2021
b282f82
Merge branch 'master' into route53resolver-firewall
jogold Jun 21, 2021
7ebe7f7
Update packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group.ts
jogold Jul 7, 2021
7c864a7
Merge branch 'master' into route53resolver-firewall
jogold Jul 7, 2021
aa5de11
Update packages/@aws-cdk/aws-route53resolver/README.md
jogold Jul 7, 2021
9452284
URI -> URL
jogold Jul 7, 2021
583acfc
NODATA comment
jogold Jul 7, 2021
b1f0350
domain and domain list name validation
jogold Jul 7, 2021
484edad
Merge branch 'master' into route53resolver-firewall
jogold Aug 2, 2021
16cdf9c
Merge branch 'master' into route53resolver-firewall
jogold Aug 30, 2021
5b62359
assertions
jogold Aug 30, 2021
f01ceeb
Merge branch 'master' into route53resolver-firewall
mergify[bot] Aug 31, 2021
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
92 changes: 91 additions & 1 deletion packages/@aws-cdk/aws-route53resolver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,100 @@
>
> [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib

![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge)

> The APIs of higher level constructs in this module are experimental and under active development.
> They are subject to non-backward compatible changes or removal in any future version. These are
> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be
> announced in the release notes. This means that while you may use them, you may need to update
> your source code when upgrading to a newer version of this package.

---

<!--END STABILITY BANNER-->

## DNS Firewall

With Route 53 Resolver DNS Firewall, you can filter and regulate outbound DNS traffic for your
virtual private connections (VPCs). To do this, you create reusable collections of filtering rules
in DNS Firewall rule groups and associate the rule groups to your VPC.

DNS Firewall provides protection for outbound DNS requests from your VPCs. These requests route
through Resolver for domain name resolution. A primary use of DNS Firewall protections is to help
prevent DNS exfiltration of your data. DNS exfiltration can happen when a bad actor compromises
an application instance in your VPC and then uses DNS lookup to send data out of the VPC to a domain
that they control. With DNS Firewall, you can monitor and control the domains that your applications
can query. You can deny access to the domains that you know to be bad and allow all other queries
to pass through. Alternately, you can deny access to all domains except for the ones that you
explicitly trust.

### Domain lists

Domain lists can be created using a list of strings, a text file stored in Amazon S3 or a local
text file:

```ts
const blockList = new route53resolver.FirewallDomainList(this, 'BlockList', {
domains: route53resolver.FirewallDomains.fromList(['bad-domain.com', 'bot-domain.net']),
});

const s3List = new route53resolver.FirewallDomainList(this, 'S3List', {
domains: route53resolver.FirewallDomains.fromS3Uri('s3://bucket/prefix/object'),
});

const assetList = new route53resolver.FirewallDomainList(this, 'AssetList', {
domains: route53resolver.FirewallDomains.fromAsset('/path/to/domains.txt'),
});
```

The file must be a text file and must contain a single domain per line.

Use `FirewallDomainList.fromFirewallDomainListId()` to import an existing or [AWS managed domain list](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dns-firewall-managed-domain-lists.html):

```ts
// AWSManagedDomainsMalwareDomainList in us-east-1
const malwareList = route53resolver.FirewallDomainList.fromFirewallDomainListId(this, 'Malware', 'rslvr-fdl-2c46f2ecbfec4dcc');
```

### Rule group

Create a rule group:

```ts
new route53resolver.FirewallRuleGroup(this, 'RuleGroup', {
rules: [
{
priority: 10,
firewallDomainList: myBlockList,
action: FirewallRuleAction.block(), // defaults to NODATA
jogold marked this conversation as resolved.
Show resolved Hide resolved
},
],
});
```

Rules can be added at construction time or using `addRule()`:

```ts
ruleGroup.addRule({
priority: 10,
firewallDomainList: blockList,
// block and reply with NXDOMAIN
action: route53resolver.FirewallRuleAction.block(route53resolver.DnsBlockResponse.nxDomain()),
});

ruleGroup.addRule({
priority: 20,
firewallDomainList: blockList,
// block and override DNS response with a custom domain
action: route53resolver.FirewallRuleAction.block(route53resolver.DnsBlockResponse.override('amazon.com')),
});
```

Use `associate()` to associate a rule group with a VPC:

```ts
import * as route53resolver from '@aws-cdk/aws-route53resolver';
ruleGroup.associate({
priority: 101,
vpc: myVpc,
})
```
230 changes: 230 additions & 0 deletions packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import * as path from 'path';
import { IBucket } from '@aws-cdk/aws-s3';
import { Asset } from '@aws-cdk/aws-s3-assets';
import { IResource, Resource, Token } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnFirewallDomainList } from './route53resolver.generated';

/**
* A Firewall Domain List
*/
export interface IFirewallDomainList extends IResource {
/**
* The ID of the domain list
*
* @attribute
*/
readonly firewallDomainListId: string;
}

/**
* Properties for a Firewall Domain List
*/
export interface FirewallDomainListProps {
/**
* A name for the domain list
*
* @default - a CloudFormation generated name
*/
readonly name?: string;

/**
* A list of domains
*/
readonly domains: FirewallDomains;
}

/**
* A list of domains
*/
export abstract class FirewallDomains {
/**
* Firewall domains created from a list of domains
*
* @param list the list of domains
*/
public static fromList(list: string[]): FirewallDomains {
for (const domain of list) {
if (!/^[\w-.]{1,128}$/.test(domain)) {
throw new Error(`Invalid domain: ${domain}. The name must have 1-128 characters. Valid characters: A-Z, a-z, 0-9, _, -, .`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No underscores are allowed in domain names, IIRC, and your regex isn't checking for them.

Suggested change
if (!/^[\w-.]{1,128}$/.test(domain)) {
throw new Error(`Invalid domain: ${domain}. The name must have 1-128 characters. Valid characters: A-Z, a-z, 0-9, _, -, .`);
if (!/^[\w-.]{1,128}$/.test(domain)) {
throw new Error(`Invalid domain: ${domain}. The name must have 1-128 characters. Valid characters: A-Z, a-z, 0-9, -, .`);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Underscores are unusual but allowed in (sub)domains names and \w includes underscores (\w = [a-zA-Z0-9_])

https://stackoverflow.com/questions/2180465/can-domain-name-subdomains-have-an-underscore-in-it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the {1,128} part which was incorrect and actually applies to the domain list name.

}
}

return {
bind(_scope: Construct): DomainsConfig {
return { domains: list };
},
};
}

/**
* Firewall domains created from the URL of a file stored in Amazon S3.
* The file must be a text file and must contain a single domain per line.
* The content type of the S3 object must be `plain/text`.
*
* @param url S3 bucket url (s3://bucket/prefix/objet).
*/
public static fromS3Url(url: string): FirewallDomains {
if (!Token.isUnresolved(url) && !url.startsWith('s3://')) {
throw new Error(`The S3 URI must start with s3://, got ${url}`);
}

return {
bind(_scope: Construct): DomainsConfig {
return { domainFileUrl: url };
},
};
}

/**
* Firewall domains created from a file stored in Amazon S3.
* The file must be a text file and must contain a single domain per line.
* The content type of the S3 object must be `plain/text`.
*
* @param bucket S3 bucket
* @param key S3 key
*/
public static fromS3(bucket: IBucket, key: string): FirewallDomains {
return this.fromS3Url(bucket.s3UrlForObject(key));
}

/**
* Firewall domains created from a local disk path to a text file.
* The file must be a text file (`.txt` extension) and must contain a single
* domain per line. It will be uploaded to S3.
*
* @param assetPath path to the text file
*/
public static fromAsset(assetPath: string): FirewallDomains {
// cdk-assets will correctly set the content type for the S3 object
// if the file has the correct extension
if (path.extname(assetPath) !== '.txt') {
throw new Error(`FirewallDomains.fromAsset() expects a file with the .txt extension, got ${assetPath}`);
}

return {
bind(scope: Construct): DomainsConfig {
const asset = new Asset(scope, 'Domains', { path: assetPath });

if (!asset.isFile) {
throw new Error('FirewallDomains.fromAsset() expects a file');
}

return { domainFileUrl: asset.s3ObjectUrl };
},
};

}

/** Binds the domains to a domain list */
public abstract bind(scope: Construct): DomainsConfig;
}

/**
* Domains configuration
*/
export interface DomainsConfig {
/**
* The fully qualified URL or URI of the file stored in Amazon S3 that contains
* the list of domains to import. The file must be a text file and must contain
* a single domain per line. The content type of the S3 object must be `plain/text`.
*
* @default - use `domains`
*/
readonly domainFileUrl?: string;

/**
* A list of domains
*
* @default - use `domainFileUrl`
*/
readonly domains?: string[];
}

/**
* A Firewall Domain List
*/
export class FirewallDomainList extends Resource implements IFirewallDomainList {
/**
* Import an existing Firewall Rule Group
*/
public static fromFirewallDomainListId(scope: Construct, id: string, firewallDomainListId: string): IFirewallDomainList {
class Import extends Resource implements IFirewallDomainList {
public readonly firewallDomainListId = firewallDomainListId;
}
return new Import(scope, id);
}

public readonly firewallDomainListId: string;

/**
* The ARN (Amazon Resource Name) of the domain list
* @attribute
*/
public readonly firewallDomainListArn: string;

/**
* The date and time that the domain list was created
* @attribute
*/
public readonly firewallDomainListCreationTime: string;

/**
* The creator request ID
* @attribute
*/
public readonly firewallDomainListCreatorRequestId: string;

/**
* The number of domains in the list
* @attribute
*/
public readonly firewallDomainListDomainCount: number;

/**
* The owner of the list, used only for lists that are not managed by you.
* For example, the managed domain list `AWSManagedDomainsMalwareDomainList`
* has the managed owner name `Route 53 Resolver DNS Firewall`.
* @attribute
*/
public readonly firewallDomainListManagedOwnerName: string;

/**
* The date and time that the domain list was last modified
* @attribute
*/
public readonly firewallDomainListModificationTime: string;

/**
* The status of the domain list
* @attribute
*/
public readonly firewallDomainListStatus: string;

/**
* Additional information about the status of the rule group
* @attribute
*/
public readonly firewallDomainListStatusMessage: string;

constructor(scope: Construct, id: string, props: FirewallDomainListProps) {
super(scope, id);

const domainsConfig = props.domains.bind(this);
const domainList = new CfnFirewallDomainList(this, 'Resource', {
name: props.name,
domainFileUrl: domainsConfig.domainFileUrl,
domains: domainsConfig.domains,
});

this.firewallDomainListId = domainList.attrId;
this.firewallDomainListArn = domainList.attrArn;
this.firewallDomainListCreationTime = domainList.attrCreationTime;
this.firewallDomainListCreatorRequestId = domainList.attrCreatorRequestId;
this.firewallDomainListDomainCount = domainList.attrDomainCount;
this.firewallDomainListManagedOwnerName = domainList.attrManagedOwnerName;
this.firewallDomainListModificationTime = domainList.attrModificationTime;
this.firewallDomainListStatus = domainList.attrStatus;
this.firewallDomainListStatusMessage = domainList.attrStatusMessage;
}
}
Loading