Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/pip/boto3-1.34.156
Browse files Browse the repository at this point in the history
  • Loading branch information
paulschwarzenberger authored Aug 11, 2024
2 parents 79bffcd + bec56c0 commit a0803b9
Show file tree
Hide file tree
Showing 34 changed files with 452 additions and 0 deletions.
Binary file added docs/assets/images/api/add-certificate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/images/api/add-new-mapping.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/images/api/api-ca-architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/images/api/api-gateway-no-auth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/images/api/api-gw-acm-config.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/images/api/certificate-manager.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/images/api/client-auth-success.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/images/api/cloudwatch-logs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/images/api/deploy-api.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/images/api/dns-record.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/images/api/lambda-function.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/images/api/postman-no-auth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
195 changes: 195 additions & 0 deletions docs/how-to-guides/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# API Gateway mTLS with open-source cloud CA

A step-by-step guide on implementing mTLS for Amazon API Gateway using our [open-source private cloud CA](https://github.com/serverless-ca/terraform-aws-ca), also published as a [blog post](https://medium.com/@paulschwarzenberger/api-gateway-mtls-with-open-source-cloud-ca-3362438445de).

![Alt text](../assets/images/api/api-ca-architecture.png?raw=true "API Gateway mTLS Architecture")

## Introduction

Programmatic communications between systems at different organisations usually use APIs, in most cases requiring client authentication before providing an API response. Client certificate authentication is an effective and scalable way of ensuring an API is only available to authorised systems.

Amazon API Gateway can be configured to require mutual Transport Layer Security (mTLS) using client certificate authentication. This requires a private Certificate Authority (CA) to issue client certificates to authorise systems to use the API service. We use our [open-source serverless cloud CA](https://serverlessca.com), a cost-effective, secure private CA which is straightforward to deploy as a Terraform module.

## Deploy API Gateway without authentication

We’ll start by setting up an API Gateway open to the world. While we’d never actually do this for a confidential API, it’s useful to do so here for demonstration and learning purposes.

The following resources will be deployed to your AWS account:

* REST API Gateway
* Lambda function
* CloudWatch log groups
* IAM policies and roles

```bash
git clone https://github.com/serverless-ca/api-gateway.git
```

* update `backend.tf` with your Terraform state S3 bucket details

```bash
cd api-gateway
terraform init
terraform plan
terraform apply
```

In the AWS console, select API Gateway, and view the deployed `cloud-app-api` REST API:

![Alt text](../assets/images/api/api-gateway-no-auth.png?raw=true "API Gateway REST API details")

For the purposes of this how-to guide, we’ll use a Lambda function to provide the response to an API Gateway request.

In the AWS console, choose Lambda, then the `api-response` Lambda function:

![Alt text](../assets/images/api/lambda-function.png?raw=true "API Gateway REST API details")

## Test API Gateway without authentication

Select the API Gateway link from the Lambda console to view trigger details:

![Alt text](../assets/images/api/api-gateway-execution-endpoint.png?raw=true "API Endpoint details shown in Lambda console")

Note that the HTTP method configured is POST, and that the API Gateway is set up with a publicly accessible execute API endpoint, and no authorisation.

Install [Postman](https://www.postman.com/downloads) on your laptop. You’ll be encouraged to open an account with Postman, however you don’t need to for the purposes of this tutorial.

Copy the API Endpoint execute API from the AWS console Lambda trigger details above, choose the POST method, and test:

![Alt text](../assets/images/api/postman-no-auth.png?raw=true "API response with no authentication using Postman")

You should see the message “successful response from API Gateway lambda function”.

## Implement open-source serverless CA

If you haven’t already, set up the [open-source serverless CA](https://serverlessca.com) as detailed in the [Getting Started](../getting-started.md) guide. From a security perspective, a production CA should be in a dedicated AWS account, separate from the AWS account used for the REST API Gateway.

In this case, you’ll need to update the serverless CA Terraform configuration to allow the user or role logged in to the API Gateway AWS account to access the CA bundle in the external S3 bucket within your CA AWS account. For example, if you’re deploying via an IAM user, add in the optional variable below when calling the serverless CA Terraform module, and then deploy using Terraform.

```bash
s3_aws_principals = ["arn:aws:iam::<API_GATEWAY_AWS_ACCOUNT_ID>:user/<YOUR_IAM_USER_NAME>"]
```

See the [Cloud CA repository](https://github.com/serverless-ca/cloud-ca) as an example of how this can be done in practice.

The above configuration step isn’t required if you installed the API Gateway in the same AWS account as the serverless CA.

## Configure custom domain name for API Gateway

We’ll do the next steps manually, for the purposes of understanding and learning. However in a real environment, these should all be implemented using infrastructure-as-code such as Terraform.

From a domain which you own, choose an appropriate subdomain for the API gateway. Then create a TLS certificate using AWS Certificate Manager with DNS validation. This will be the API Gateway server certificate, which doesn’t need to be issued by the serverless private CA.

![Alt text](../assets/images/api/certificate-manager.png?raw=true "AWS Certificate Manager")

* At API Gateway, Custom Domain Names, press Create
* Enter the custom domain name you’ve chosen for your API Gateway
* Slide mutual TLS authentication to on
* Copy the S3 URI of the bundle PEM file in the CA External S3 bucket
* Copy the Version ID of the bundle PEM file in the CA External S3 bucket

![Alt text](../assets/images/api/api-gw-truststore-config.png?raw=true "API Gateway mTLS configuration")

* Choose the ACM certificate issued previously

![Alt text](../assets/images/api/api-gw-acm-config.png?raw=true "Selection of ACM certificate for API Gateway")

* Press Create domain name

## Map API custom domain name to API Gateway

The newly created API custom domain name must now be mapped to the API Gateway created earlier.

* At your newly created API custom domain name, select API mappings
* Press Configure API mappings, Add new mappings
* Select the already configured API Gateway and environment

![Alt text](../assets/images/api/default-api-endpoint-warning.png?raw=true "API Gateway warning of potential mTLS bypass")

* You’ll see a warning that the default execute API endpoint must be disabled to prevent bypass of mutual TLS
* Select the `cloud-app-api` API Gateway, API Settings, Edit
* Change the default endpoint to Inactive

![Alt text](../assets/images/api/default-api-endpoint-inactive.png?raw=true "API Gateway settings with default execute endpoint disabled")

* Press Save changes
* Return to the `cloud-app-api` resources screen

![Alt text](../assets/images/api/deploy-api.png?raw=true "Deploy API")

* Press Deploy API
* Choose the dev stage and press Deploy
* Return to the Add new mapping screen which should no longer show the warning

![Alt text](../assets/images/api/add-new-mapping.png?raw=true "Warning no longer shown")

* Press Save
* View the custom domain name, now configured for mTLS

![Alt text](../assets/images/api/custom-domain-name-configured.png?raw=true "API Gateway custom domain mapping")

## Create DNS entry for API custom domain name

We need to create a public DNS record to the new API custom domain name.

* within Route53 for your hosted zone, create a DNS record for the custom domain name
* the CNAME value should be the API endpoint as listed in the custom domain name configuration

![Alt text](../assets/images/api/dns-record.png?raw=true "Route53 entry for API Gateway custom domain")

## Test default API endpoint disabled

First, let’s confirm that the default execute API endpoint is disabled.

* Open Postman
* Repeat the API call made earlier

![Alt text](../assets/images/api/default-endpoint-failure.png?raw=true "Test using Postman without a certificate results in a 403 response")

* You should see a `"Forbidden"` message
* If you still get the previous response, check you’ve deployed the API

## Test mutual TLS

* Issue a client certificate to your laptop using the utils\client-cert.py script as described in the serverless CA [Getting Started](../getting-started.md) guide
* this will create the following files in your home directory:

```bash
certs/client-key.pem
certs/client-cert.pem
certs/client-cert.crt
certs/client-cert-key.pem
```

* open Postman
* select Settings, Certificates, Client Certificates, Add Certificate
* Enter the custom domain name
* navigate to the `client-cert.crt` and `client-key.pem` files

![Alt text](../assets/images/api/add-certificate.png?raw=true "Configuring Postman with client certificates")

* press Add
* close Settings
* Send a POST request to your custom domain name adding the `/api` path

![Alt text](../assets/images/api/client-auth-success.png?raw=true "Successful response using Postman with client certificate")

* The success message should be returned indicating a successful response

## View certificate details in CloudWatch logs

Details of the connection can be viewed within CloudWatch logs

* view the api-gateway-access CloudWatch log

![Alt text](../assets/images/api/cloudwatch-logs.png?raw=true "API Gateway access logs shows certificate details")

* details of your certificate connection can be viewed

👏 🎉 🎊 Congratulations, you’ve set up and tested API Gateway mTLS with the open-source serverless CA 🎆 🌟 🎇

## Certificate Revocation

Amazon API Gateway mTLS doesn’t by default support Certificate Revocation List (CRL) checking.

This can be implemented using an API Gateway Lambda authorizer, checking against the latest CRL issued by the serverless CA. The Lambda authorizer may also perform additional checks to require the client certificate to have particular certificate distinguished name fields such as a specific Organization Unit (OU).
8 changes: 8 additions & 0 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
| <a name="module_rsa_tls_cert_lambda"></a> [rsa\_tls\_cert\_lambda](#module\_rsa\_tls\_cert\_lambda) | ./modules/terraform-aws-ca-lambda | n/a |
| <a name="module_scheduler"></a> [scheduler](#module\_scheduler) | ./modules/terraform-aws-ca-scheduler | n/a |
| <a name="module_scheduler-role"></a> [scheduler-role](#module\_scheduler-role) | ./modules/terraform-aws-ca-iam | n/a |
| <a name="module_sns-ca-notifications"></a> [sns-ca-notifications](#module\_sns-ca-notifications) | ./modules/terraform-aws-ca-sns | n/a |
| <a name="module_step-function"></a> [step-function](#module\_step-function) | ./modules/terraform-aws-ca-step-function | n/a |
| <a name="module_step-function-role"></a> [step-function-role](#module\_step-function-role) | ./modules/terraform-aws-ca-iam | n/a |
| <a name="module_tls_keygen_iam"></a> [tls\_keygen\_iam](#module\_tls\_keygen\_iam) | ./modules/terraform-aws-ca-iam | n/a |
Expand All @@ -55,6 +56,8 @@
| <a name="input_bucket_prefix"></a> [bucket\_prefix](#input\_bucket\_prefix) | First part of s3 bucket name to ensure uniqueness, if left blank a random suffix will be used instead | `string` | `""` | no |
| <a name="input_cert_info_files"></a> [cert\_info\_files](#input\_cert\_info\_files) | List of file names to be uploaded to internal S3 bucket for processing | `list` | `[]` | no |
| <a name="input_csr_files"></a> [csr\_files](#input\_csr\_files) | List of CSR file names to be uploaded to internal S3 bucket for processing | `list` | `[]` | no |
| <a name="input_custom_sns_topic_display_name"></a> [custom\_sns\_topic\_display\_name](#input\_custom\_sns\_topic\_display\_name) | Customised SNS topic display name, leave empty to use standard naming convention | `string` | `""` | no |
| <a name="input_custom_sns_topic_name"></a> [custom\_sns\_topic\_name](#input\_custom\_sns\_topic\_name) | Customised SNS topic name, leave empty to use standard naming convention | `string` | `""` | no |
| <a name="input_env"></a> [env](#input\_env) | Environment name, e.g. dev | `string` | `"dev"` | no |
| <a name="input_filter_pattern"></a> [filter\_pattern](#input\_filter\_pattern) | Filter pattern for CloudWatch logs subscription filter | `string` | `""` | no |
| <a name="input_hosted_zone_domain"></a> [hosted\_zone\_domain](#input\_hosted\_zone\_domain) | Hosted zone domain, e.g. dev.ca.example.com | `string` | `""` | no |
Expand All @@ -79,6 +82,11 @@
| <a name="input_runtime"></a> [runtime](#input\_runtime) | Lambda language runtime | `string` | `"python3.12"` | no |
| <a name="input_s3_aws_principals"></a> [s3\_aws\_principals](#input\_s3\_aws\_principals) | List of AWS Principals to allow access to external S3 bucket | `list` | `[]` | no |
| <a name="input_schedule_expression"></a> [schedule\_expression](#input\_schedule\_expression) | Step function schedule in cron format, interval should normally be the same as issuing\_crl\_days | `string` | `"cron(15 8 * * ? *)"` | no |
| <a name="input_sns_email_subscriptions"></a> [sns\_email\_subscriptions](#input\_sns\_email\_subscriptions) | List of email addresses to subscribe to SNS topic | `list(string)` | `[]` | no |
| <a name="input_sns_lambda_subscriptions"></a> [sns\_lambda\_subscriptions](#input\_sns\_lambda\_subscriptions) | A map of lambda names to arns to subscribe to SNS topic | `map(string)` | `{}` | no |
| <a name="input_sns_policy"></a> [sns\_policy](#input\_sns\_policy) | A string containing the SNS policy, if used | `string` | `""` | no |
| <a name="input_sns_policy_template"></a> [sns\_policy\_template](#input\_sns\_policy\_template) | Name of SNS policy template file, if used | `string` | `"default"` | no |
| <a name="input_sns_sqs_subscriptions"></a> [sns\_sqs\_subscriptions](#input\_sns\_sqs\_subscriptions) | A map of SQS names to arns to subscribe to thSNSis topic | `map(string)` | `{}` | no |
| <a name="input_subscription_filter_destination"></a> [subscription\_filter\_destination](#input\_subscription\_filter\_destination) | CloudWatch log subscription filter destination, last section of ARN | `string` | `""` | no |
| <a name="input_timeout"></a> [timeout](#input\_timeout) | Amount of time Lambda Function has to run in seconds | `number` | `180` | no |

Expand Down
2 changes: 2 additions & 0 deletions examples/rsa-public-crl/ca.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ module "certificate_authority" {
public_crl = true
cert_info_files = ["tls", "revoked", "revoked-root-ca"]

custom_sns_topic_display_name = "My Company CA Notifications Production"

providers = {
aws = aws
aws.us-east-1 = aws.us-east-1 # certificates for CloudFront must be in this region
Expand Down
19 changes: 19 additions & 0 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ module "create_rsa_root_ca_lambda" {
domain = var.hosted_zone_domain
runtime = var.runtime
public_crl = var.public_crl
sns_topic_arn = module.sns-ca-notifications.sns_topic_arn
}

module "create_rsa_issuing_ca_lambda" {
Expand All @@ -210,6 +211,7 @@ module "create_rsa_issuing_ca_lambda" {
domain = var.hosted_zone_domain
runtime = var.runtime
public_crl = var.public_crl
sns_topic_arn = module.sns-ca-notifications.sns_topic_arn
}

module "rsa_root_ca_crl_lambda" {
Expand All @@ -232,6 +234,7 @@ module "rsa_root_ca_crl_lambda" {
domain = var.hosted_zone_domain
runtime = var.runtime
public_crl = var.public_crl
sns_topic_arn = module.sns-ca-notifications.sns_topic_arn
}

module "rsa_issuing_ca_crl_lambda" {
Expand All @@ -254,6 +257,7 @@ module "rsa_issuing_ca_crl_lambda" {
domain = var.hosted_zone_domain
runtime = var.runtime
public_crl = var.public_crl
sns_topic_arn = module.sns-ca-notifications.sns_topic_arn
}

module "rsa_tls_cert_lambda" {
Expand All @@ -276,6 +280,7 @@ module "rsa_tls_cert_lambda" {
public_crl = var.public_crl
max_cert_lifetime = var.max_cert_lifetime
allowed_invocation_principals = var.aws_principals
sns_topic_arn = module.sns-ca-notifications.sns_topic_arn
}

module "cloudfront_certificate" {
Expand Down Expand Up @@ -369,3 +374,17 @@ module "db-reader-role" {
policy = "db_reader"
assume_role_policy = "db_reader"
}

module "sns-ca-notifications" {
source = "./modules/terraform-aws-ca-sns"

project = var.project
function = "ca-notifications"
env = var.env
custom_sns_topic_display_name = var.custom_sns_topic_display_name
custom_sns_topic_name = var.custom_sns_topic_name
kms_key_arn = coalesce(var.kms_arn_resource, module.kms_tls_keygen.kms_arn)
email_subscriptions = var.sns_email_subscriptions
lambda_subscriptions = var.sns_lambda_subscriptions
sqs_subscriptions = var.sns_sqs_subscriptions
}
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ nav:
- Revocation: revocation.md
- Security: security.md
- How-to guides:
- API Gateway: how-to-guides/api.md
- Application load balancer: how-to-guides/alb.md
- IAM Roles Anywhere: how-to-guides/iam.md
- Terraform reference: reference.md
Expand Down
1 change: 1 addition & 0 deletions modules/terraform-aws-ca-lambda/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ resource "aws_lambda_function" "lambda" {
ROOT_CA_INFO = jsonencode(var.root_ca_info)
ROOT_CRL_DAYS = tostring(var.root_crl_days)
ROOT_CRL_SECONDS = tostring(var.root_crl_seconds)
SNS_TOPIC_ARN = var.sns_topic_arn
}
}

Expand Down
4 changes: 4 additions & 0 deletions modules/terraform-aws-ca-lambda/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ variable "runtime" {
description = "Lambda language runtime"
}

variable "sns_topic_arn" {
description = "SNS Topic ARN for Lambda function to publish to"
}

variable "subscription_filter_destination" {
description = "CloudWatch log subscription filter destination, last section of ARN"
default = ""
Expand Down
3 changes: 3 additions & 0 deletions modules/terraform-aws-ca-sns/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
data "aws_caller_identity" "current" {}

data "aws_region" "current" {}
9 changes: 9 additions & 0 deletions modules/terraform-aws-ca-sns/locals.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
locals {
sns_topic_display_name = coalesce(var.custom_sns_topic_name, title(replace("${var.project}-${var.function}-${var.env}", "-", " ")))
sns_topic_name = coalesce(var.custom_sns_topic_name, "${var.project}-${var.function}-${var.env}")

tags = merge(var.tags, {
Terraform = "true"
Name = local.sns_topic_name,
})
}
37 changes: 37 additions & 0 deletions modules/terraform-aws-ca-sns/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
resource "aws_sns_topic" "sns_topic" {
name = local.sns_topic_name
display_name = local.sns_topic_display_name
policy = coalesce(var.sns_policy, templatefile("${path.module}/templates/${var.sns_policy_template}.json", { region = data.aws_region.current.id, account_id = data.aws_caller_identity.current.account_id, sns_topic_name = local.sns_topic_name }))

tags = merge(
var.tags,
tomap(
{ "Name" = local.sns_topic_name }
)
)
kms_master_key_id = var.kms_key_arn
}

resource "aws_sns_topic_subscription" "email_subscriptions" {
for_each = toset(var.email_subscriptions)
endpoint = each.key
protocol = "email"
topic_arn = aws_sns_topic.sns_topic.arn
raw_message_delivery = false
}

resource "aws_sns_topic_subscription" "lambda_subscriptions" {
for_each = var.lambda_subscriptions
endpoint = each.value
protocol = "lambda"
topic_arn = aws_sns_topic.sns_topic.arn
raw_message_delivery = false
}

resource "aws_sns_topic_subscription" "sqs_subscriptions" {
for_each = var.sqs_subscriptions
endpoint = each.value
protocol = "sqs"
topic_arn = aws_sns_topic.sns_topic.arn
raw_message_delivery = true
}
3 changes: 3 additions & 0 deletions modules/terraform-aws-ca-sns/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
output "sns_topic_arn" {
value = aws_sns_topic.sns_topic.arn
}
Loading

0 comments on commit a0803b9

Please sign in to comment.