Skip to content

Commit 912a036

Browse files
authored
WTL-852 Add Terraform configuration (#22)
1 parent 18b00ac commit 912a036

File tree

20 files changed

+468
-44
lines changed

20 files changed

+468
-44
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@ tests/logs/*
1313
tests/config
1414
/distributions
1515
packaged.yaml
16+
.terraform*
17+
terraform.tfstate*
18+
/infra/terraform/modules/_lambda/packages/*

README.md

Lines changed: 27 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,7 @@ The build script prompts you for the required configuration parameters, which ar
3030

3131
A generic package retrieves its configuration at runtime from the [AWS Systems Manager](https://aws.amazon.com/systems-manager/) Parameter Store and [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/). Currently, support for this type of package has been added for OKTA Native only. To disable all issued JWTs, rotate the key pair using the Secrets Manager console.
3232

33-
Generic packages are available for download from the Releases page of this GitHub repository. Or, to build a generic package yourself, execute:
34-
35-
```bash
36-
./build.sh package
37-
```
38-
39-
The supported values of `package` are:
40-
41-
* `okta_native` - builds a generic Lambda package for OKTA Native authentication
42-
* `rotate_key_pair` - builds a Lambda package for rotating the RSA keys in AWS Secrets Manager
33+
Generic packages are pre-built and available for download from the Releases page of this GitHub repository. [Terraform modules](./infra/terraform/README.md) are available for downloading and deploying generic packages and their configuration resources.
4334

4435
## Identity Provider Guides
4536

@@ -146,43 +137,15 @@ The supported values of `package` are:
146137
1. Client Id from the application created in our previous step (can be found at the bottom of the general tab)
147138
1. Base Url
148139
1. This is named the 'Org URL' and can be found in the top right of the Dashboard tab.
149-
1. Decide on whether you want to use a custom package or a generic package
150-
* For a custom package:
151-
1. Execute `./build.sh` in the downloaded directory. NPM will run to download dependencies and an RSA key will be generated.
152-
1. Choose `OKTA Native` as the authorization method and enter the values for Base URL (Org URL), Client ID, PKCE Code Verifier Length, Redirect URI, and Session Duration
153-
1. Find the resulting `zip` file in your distribution folder
154-
* For a generic package:
155-
1. Create the parameters below in the AWS Systems Manager Parameter Store in the `us-east-1` region. Replace `{name}` with the name that you will give the Lambda authentication function.
156-
* `/{name}/base-url` (e.g. `https://my-org.okta.com/oauth2/default`)
157-
* `/{name}/client-id` (from the OKTA application)
158-
* `/{name}/domain-name` (e.g. `my-site.cloudfront.net`)
159-
* `/{name}/callback-path` (e.g. `/callback`)
160-
* `/{name}/session-duration` (in seconds)
161-
* `/{name}/pkce-code-verifier-length` (from 43 to 128)
162-
* `/{name}/scope` (e.g. `openid email`)
163-
1. Download the latest `okta_native_*.zip` asset from the Releases page
164-
1. Upload the `zip` file using the AWS Lambda console and jump to the [configuration step](#configure-lambda-and-cloudfront)
140+
1. To use the [generic package](#generic-packages), jump straight to [Terraform Modules for CloudFront Authentication](./infra/terraform/README.md).
141+
1. Execute `./build.sh` in the downloaded directory. NPM will run to download dependencies and a RSA key will be generated.
142+
1. Choose `OKTA Native` as the authorization method and enter the values for Base URL (Org URL), Client ID, PKCE Code Verifier Length, Redirect URI, and Session Duration
143+
1. Upload the resulting `zip` file found in your distribution folder using the AWS Lambda console and jump to the [configuration step](#configure-lambda-and-cloudfront)
165144

166145
## Configure Lambda and CloudFront
167146

168147
See [Manual Deployment](https://github.com/Widen/cloudfront-auth/wiki/Manual-Deployment) __*or*__ [AWS SAM Deployment](https://github.com/Widen/cloudfront-auth/wiki/AWS-SAM-Deployment)
169148

170-
If deploying a generic package, you also need to follow the steps below in the `us-east-1` region. In these steps, replace `{name}` with the name of the Lambda authentication function you created and `{account_id}` with the AWS Account ID number.
171-
172-
1. Modify the role on the Lambda authentication function to include a policy that:
173-
* allows the action `secretsmanager:GetSecretValue` on resource `arn:aws:secretsmanager:us-east-1:{account_id}:secret:{name}/*`
174-
* allows the action `ssm:GetParametersByPath` on resource `arn:aws:ssm:us-east-1:{account_id}:parameter/{name}`
175-
1. Execute `./build.sh rotate_key_pair`
176-
1. Create a Lambda rotation function with the following configuration:
177-
* Function code: Use the `rotate_key_pair.zip` file found in the distributions folder
178-
* Runtime: **Node.js 14.x** or later
179-
* Handler: `index.handler`
180-
* Timeout: 30 sec
181-
1. Modify the role on the Lambda rotation function to include a policy that:
182-
* allows the action `secretsmanager:PutSecretValue` on resource `arn:aws:secretsmanager:us-east-1:{account_id}:secret:{name}/*`
183-
1. Create a secret in AWS Secrets Manager named `{name}/key-pair`
184-
1. Enable secret rotation on the secret and associate it with the Lambda rotation function
185-
186149
## Authorization Method Examples
187150

188151
* [Use Google Groups to authorize users](https://github.com/Widen/cloudfront-auth/wiki/Google-Groups-Setup)
@@ -199,10 +162,30 @@ Detailed instructions on testing your function can be found [in the Wiki](https:
199162

200163
## Build Requirements
201164

202-
* [npm](https://www.npmjs.com/) ^5.6.0
203-
* [node](https://nodejs.org/en/) ^10.0
165+
* [npm](https://www.npmjs.com/) ^7.20.0
166+
* [node](https://nodejs.org/en/) ^14.0
204167
* [openssl](https://www.openssl.org)
205168

169+
## Building Generic Packages
170+
171+
If you need to build a generic package yourself, execute:
172+
173+
```bash
174+
./build.sh package
175+
```
176+
177+
The supported values of `package` are:
178+
179+
* `okta_native` - builds a generic Lambda package for OKTA Native authentication
180+
* `rotate_key_pair` - builds a Lambda package for rotating the RSA keys in AWS Secrets Manager
181+
182+
GitHub Actions automatically creates a new GitHub release when the repository owner pushes a tag that begins with `v`:
183+
184+
```sh
185+
git tag -a -m "Target AWS Lambda Node.js 14.x runtime" v3.0.0
186+
git push origin v3.0.0
187+
```
188+
206189
## Contributing
207190

208191
All contributions are welcome. Please create an issue in order open up communication with the community.

infra/terraform/README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Terraform Modules for CloudFront Authentication
2+
3+
This directory contains the _Terraform_ configuration for adding authentication to a CloudFront distribution. Currently only OKTA Native authentication is supported.
4+
5+
## Usage
6+
7+
The Terraform modules for each identity provider are in the [modules](./modules) directory. Refer to the [examples](./examples) directory for Terraform configuration that you can include in your project and adapt. Refer to the `variables.tf` file of the module to see all the available input variables. Below is an example for OKTA Native.
8+
9+
1. Call the module in your Terraform configuration. CloudFront uses the `us-east-1` region, so you must pass a `us-east-1` provider to the module.
10+
11+
```hcl
12+
module "cloudfront_auth_okta_native" {
13+
source = "github.com/iress/cloudfront-auth//infra/terraform/modules/okta_native"
14+
15+
# Lambda function version to deploy (see the Releases page of this GitHub repository)
16+
release_version = "v3.0.0"
17+
18+
name = "my-website-auth"
19+
org_url = "https://my-org.okta.com/oauth2/default"
20+
client_id = "Nf2qSD9wXKU9ph8an22T"
21+
domain_name = "my-cloudfront-site.example.com"
22+
23+
# aws.global_services is a us-east-1 provider
24+
providers = {
25+
aws = aws.global_services
26+
}
27+
}
28+
```
29+
30+
1. Add a [lambda_function_association](https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html#lambda_function_association) to your [aws_cloudfront_distribution](https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html) resource:
31+
32+
```hcl
33+
resource "aws_cloudfront_distribution" "distribution" {
34+
# ... other configuration ...
35+
36+
# lambda_function_association is also supported by ordered_cache_behavior
37+
default_cache_behavior {
38+
# ... other configuration ...
39+
40+
lambda_function_association {
41+
event_type = "viewer-request"
42+
lambda_arn = module.cloudfront_auth_okta_native.auth_lambda_arn
43+
include_body = false
44+
}
45+
}
46+
}
47+
```
48+
49+
## Requirements
50+
51+
This module requires [wget](https://www.gnu.org/software/wget/) to be installed on the machine or container that runs Terraform.
52+
53+
## Logs
54+
55+
Logs are written to CloudWatch. The table below shows where the logs can be found, where {name} is the value of the `name` input variable in the Terraform module.
56+
57+
| Function | Log group name | Region |
58+
|----------|----------------|--------|
59+
| Authentication | /aws/lambda/us-east-1.{name} | The region closest to the user who made the request to the website
60+
| Secret rotation | /aws/lambda/{name}-rotation | us-east-1
61+
62+
## Destroying
63+
64+
The first time you run `terraform destroy` you may receive the following error:
65+
66+
*Lambda was unable to delete arn:aws:lambda:us-east-1:553479592532:function:my-website-auth:1 because it is a replicated function. Please see our [documentation](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-delete-replicas.html) for Deleting Lambda@Edge Functions and Replicas.*
67+
68+
When this occurs, wait (up to a few hours) for CloudFront to delete the Lambda function replicas, then run `terraform destroy` again.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module "cloudfront_auth_okta_native" {
2+
source = "github.com/iress/cloudfront-auth//infra/terraform/modules/okta_native"
3+
4+
release_version = "v3.0.0"
5+
name = "my-website-auth"
6+
org_url = "https://my-org.okta.com/oauth2/default"
7+
client_id = "Nf2qSD9wXKU9ph8an22T"
8+
domain_name = "my-cloudfront-site.example.com"
9+
10+
providers = {
11+
aws = aws.global_services
12+
}
13+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
terraform {
2+
required_version = ">= 1.0.0"
3+
}
4+
5+
provider "aws" {
6+
region = "us-east-1"
7+
alias = "global_services"
8+
}

infra/terraform/modules/_auth/main.tf

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
data "aws_caller_identity" "current" {}
2+
3+
data "aws_region" "current" {}
4+
5+
data "aws_iam_policy_document" "auth" {
6+
statement {
7+
actions = ["ssm:GetParametersByPath"]
8+
resources = ["arn:aws:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:parameter/${var.name}"]
9+
}
10+
11+
statement {
12+
actions = ["secretsmanager:GetSecretValue"]
13+
resources = ["arn:aws:secretsmanager:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:secret:${var.name}/*"]
14+
}
15+
}
16+
17+
data "aws_iam_policy_document" "rotation" {
18+
statement {
19+
actions = ["secretsmanager:PutSecretValue"]
20+
resources = ["arn:aws:secretsmanager:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:secret:${var.name}/*"]
21+
}
22+
}
23+
24+
module "auth" {
25+
source = "../_lambda"
26+
27+
name = var.name
28+
tags = var.tags
29+
package_url = var.package_url
30+
timeout = 5
31+
iam_policy_override_json = data.aws_iam_policy_document.auth.json
32+
lambda_at_edge = true
33+
}
34+
35+
module "rotation" {
36+
source = "../_lambda"
37+
38+
name = "${var.name}-rotation"
39+
tags = var.tags
40+
package_url = "https://github.com/iress/cloudfront-auth/releases/download/${var.release_version}/rotate_key_pair.zip"
41+
timeout = 30
42+
iam_policy_override_json = data.aws_iam_policy_document.rotation.json
43+
}
44+
45+
resource "aws_lambda_permission" "allow_secrets_manager" {
46+
statement_id = "AllowExecutionFromSecretsManager"
47+
action = "lambda:InvokeFunction"
48+
function_name = module.rotation.lambda_arn
49+
principal = "secretsmanager.amazonaws.com"
50+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
output "auth_lambda_arn" {
2+
value = module.auth.lambda_arn
3+
description = "The Amazon Resource Name (ARN) identifying the authentication Lambda Function Version"
4+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Provider passed in should always be us-east-1
2+
3+
terraform {
4+
required_providers {
5+
aws = {
6+
source = "hashicorp/aws"
7+
version = "~> 3"
8+
}
9+
}
10+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
resource "aws_secretsmanager_secret" "key_pair" {
2+
name = "${var.name}/key-pair"
3+
recovery_window_in_days = 0
4+
tags = var.tags
5+
}
6+
7+
resource "aws_secretsmanager_secret_rotation" "key_pair" {
8+
secret_id = aws_secretsmanager_secret.key_pair.id
9+
rotation_lambda_arn = module.rotation.lambda_arn
10+
11+
rotation_rules {
12+
automatically_after_days = var.key_pair_rotation_period_days
13+
}
14+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
variable "release_version" {
2+
description = "The name of the GitHub release version to deploy"
3+
type = string
4+
}
5+
6+
variable "name" {
7+
description = "A name for the AWS resources created by this module"
8+
type = string
9+
}
10+
11+
variable "tags" {
12+
description = "Tags to add to each resource"
13+
type = map(string)
14+
}
15+
16+
variable "package_url" {
17+
description = "The URL of the Lambda authentication function package"
18+
type = string
19+
}
20+
21+
variable "key_pair_rotation_period_days" {
22+
description = "The number of days between automatic scheduled rotations of the key pair"
23+
type = number
24+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
data "aws_iam_policy_document" "assume_role" {
2+
statement {
3+
actions = ["sts:AssumeRole"]
4+
5+
principals {
6+
type = "Service"
7+
identifiers = var.lambda_at_edge ? ["lambda.amazonaws.com", "edgelambda.amazonaws.com"] : ["lambda.amazonaws.com"]
8+
}
9+
}
10+
}
11+
12+
data "aws_iam_policy_document" "execution" {
13+
override_json = var.iam_policy_override_json
14+
15+
statement {
16+
sid = "logs"
17+
18+
actions = [
19+
"logs:CreateLogGroup",
20+
"logs:CreateLogStream",
21+
"logs:PutLogEvents"
22+
]
23+
24+
resources = ["arn:aws:logs:*:*:*"]
25+
}
26+
}
27+
28+
resource "aws_iam_role" "lambda" {
29+
name = var.name
30+
assume_role_policy = data.aws_iam_policy_document.assume_role.json
31+
tags = var.tags
32+
}
33+
34+
resource "aws_iam_role_policy" "execution" {
35+
name = var.name
36+
role = aws_iam_role.lambda.id
37+
policy = data.aws_iam_policy_document.execution.json
38+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
locals {
2+
package_directory = "${path.module}/packages"
3+
package_filename = basename(var.package_url)
4+
}
5+
6+
# Always download the lambda package if it does not exist
7+
resource "null_resource" "download" {
8+
triggers = {
9+
always_run = uuid()
10+
}
11+
12+
provisioner "local-exec" {
13+
command = "test -f ${local.package_directory}/${local.package_filename} || (mkdir -p ${local.package_directory} && wget -P ${local.package_directory} ${var.package_url})"
14+
}
15+
}
16+
17+
resource "aws_lambda_function" "main" {
18+
filename = "${local.package_directory}/${local.package_filename}"
19+
function_name = var.name
20+
role = aws_iam_role.lambda.arn
21+
handler = "index.handler"
22+
source_code_hash = base64sha256(var.package_url)
23+
runtime = "nodejs14.x"
24+
timeout = var.timeout
25+
publish = var.lambda_at_edge
26+
tags = var.tags
27+
28+
# Ensure the lambda function is created after the package is downloaded
29+
depends_on = [
30+
null_resource.download
31+
]
32+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
output "lambda_arn" {
2+
value = var.lambda_at_edge ? aws_lambda_function.main.qualified_arn : aws_lambda_function.main.arn
3+
description = "The Amazon Resource Name (ARN) identifying the Lambda Function Version"
4+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Provider passed in should always be us-east-1
2+
3+
terraform {
4+
required_providers {
5+
aws = {
6+
source = "hashicorp/aws"
7+
version = "~> 3"
8+
}
9+
}
10+
}

0 commit comments

Comments
 (0)