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

Encrypt everywhere #28

Open
wants to merge 28 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c8d77d3
Implement ECR for custom container images
Nov 26, 2023
8e06aee
Implement initial custom application image:
Nov 26, 2023
c3419c8
Wild experiments with ECS deployment of custom image in workflows
Nov 27, 2023
8f51108
Fix amazon-ecr-login Github action name
Nov 27, 2023
9f2d4a3
Initial task definition template for hello-world
Nov 27, 2023
503b47c
Build repository with interpolation instead of output
Nov 27, 2023
cda9190
Tweak IMAGE_TAG
Nov 27, 2023
6bd79b6
Fix repository namespace in build action
Nov 27, 2023
1343855
Remove external Terraform process computing Git SHA
Nov 27, 2023
a07134f
Add image_tag Terraform variable and set during workflow via github v…
Nov 27, 2023
29cbb56
Compute ECS image in locals including ECR url
Nov 27, 2023
b7d08df
Fix redundant namespace in image tag interpolation
Nov 27, 2023
37d9850
Change target group to listen on HTTPS 443
Nov 27, 2023
2c09076
Modify port mappings in task definition for 443
Nov 27, 2023
9a639c3
Modify ECS service ip forwarding and app security group ingress for p…
Nov 27, 2023
d3a7361
Comment deploy workflow sections
Nov 27, 2023
ec51616
Remove ECS task definition template file for now
Nov 27, 2023
fe0ef74
Test deploy workflow on new pr-28 feature branch
Nov 27, 2023
3c525ad
Adjust TODO comment: SSL implemented
Nov 27, 2023
ac653ff
SSL certificate refactoring:
Nov 28, 2023
0029648
Remove commented-out section of deploy workflow referencing ECS deplo…
Nov 28, 2023
8995af5
README and CHANGELOG
Nov 28, 2023
f0218d8
Provision SSL certs to ECS task via secrets in task definition
Nov 28, 2023
48fb5cb
Encrypt SSL SSM parameters with KMS CMK
Nov 28, 2023
fcaec3e
Remove aws-cli from Dockerfile
Nov 28, 2023
df1c16e
Add detail on public SSL feature to README
Nov 28, 2023
4e6ec8c
Audit and adjust comments regarding SSL
Nov 28, 2023
4e6bacc
Whitelabel and namespace changes for landing page
Oct 2, 2024
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
33 changes: 33 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ jobs:
name: Deploy
runs-on: ubuntu-latest
steps:
# Setup section
- name: Clone the Git repository
uses: actions/checkout@v3

Expand All @@ -53,6 +54,7 @@ jobs:
role-to-assume: ${{ secrets.aws_assume_role_arn }}
aws-region: ${{ inputs.aws_region }}

# Terraform setup
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2

Expand All @@ -68,13 +70,44 @@ jobs:
-backend-config="region=${{ inputs.aws_region }}"
working-directory: ./terraform

- name: Terraform Apply - ECR only
id: terraform-apply-ecr
run: |
terraform apply -auto-approve -target=aws_ecr_repository.hello_world \
-var="aws_region=${{ inputs.aws_region }}" \
-var="aws_replication_region=${{ inputs.aws_replication_region }}" \
-var="dns_name=${{ inputs.dns_name }}" \
-var="environment=${{ inputs.environment_name }}" \
-var="image_tag=${{ github.sha }}" \
-var="vpc_cidr_index=${{ inputs.vpc_cidr_index }}"
working-directory: ./terraform

# Docker section
- name: Login to AWS ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2

- name: Build, tag and push docker image to ECR
id: build-image
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: "aws-ecs-fargate-demo-${{ inputs.environment_name }}/hello-world"
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG .
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT

# Terraform provision
- name: Terraform Apply
id: terraform-apply
run: |
terraform apply -auto-approve \
-var="additional_certificate_arns=${{ inputs.additional_certificate_arns }}" \
-var="aws_region=${{ inputs.aws_region }}" \
-var="aws_replication_region=${{ inputs.aws_replication_region }}" \
-var="dns_name=${{ inputs.dns_name }}" \
-var="environment=${{ inputs.environment_name }}" \
-var="image_tag=${{ github.sha }}" \
-var="vpc_cidr_index=${{ inputs.vpc_cidr_index }}"
working-directory: ./terraform
34 changes: 34 additions & 0 deletions .github/workflows/pr-28--encrypt-everywhere.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Deploy PR 28 - Encrypt everywhere

on:
push:
branches:
- feature/encrypt-everywhere--wild-workflow

permissions:
id-token: write # This is required for requesting the JWT
contents: read # This is required for actions/checkout

jobs:
deploy:
name: Deploy to pr-28
uses: ./.github/workflows/deploy.yml
# Originally the workflow implementation was setup to use environment
# variables configured in the Github repository settings. However,
# after moving to a reusable action, it became ugly to pass those values
# into the called action due to this bug:
#
# https://github.com/orgs/community/discussions/26671#discussioncomment-4295807
#
# So now we're hardcoding the values here and using it as a manifest. Please see
# commit 1ec7a0346abc04b73c03e35c0e228e9dba14300c for the previous implementation.
with:
aws_region: us-east-1
aws_replication_region: us-west-2
aws_s3_terraform_state_object_key: pull-requests/pr-28.tfstate
dns_name: pr-28.aws-ecs-fargate-demo.carlucci.network
environment_name: pr-28
vpc_cidr_index: 4
secrets:
aws_assume_role_arn: ${{ secrets.AWS_ASSUME_ROLE_ARN }}
aws_s3_terraform_state_bucket_name: ${{ secrets.AWS_S3_TERRAFORM_STATE_BUCKET_NAME }}
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# Unreleased

* Customized Hello World Nginx container hosted in ECR
* Container deployment CI/CD workflow
* Encryption everywhere for private traffic:
* Self-signed certificate generation
* SSM Parameter Store secrets for key and certificate encrypted using KMS CMK
* Certificate installation and configuration on ECS task bootstrap

# 0.3.0

* Additional ACM certificate assignment
Expand Down
13 changes: 13 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM alpine:3.18.4

RUN apk add nginx openssl

COPY nginx.conf /etc/nginx/http.d/default.conf
COPY index.html /usr/share/nginx/html/index.html

EXPOSE 443

COPY entrypoint.sh /usr/sbin/entrypoint.sh
RUN chmod 755 /usr/sbin/entrypoint.sh

ENTRYPOINT ["/bin/sh", "/usr/sbin/entrypoint.sh"]
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ Route53 hosted zones are provisioned for each environment and DNS name servers a
* Definition of a KMS customer managed key for encryption, with support for cross-region replication to enable future multi-region deployments
* CloudWatch logging with KMS CMK encryption
* DNS name resolution
* SSL traffic encryption terminating at the ALB
* Custom Hello World Nginx container image hosted in ECR with CI/CD deployment workflow
* SSL traffic encryption:
* Public HTTPS traffic with ACM certificate and support for attaching external ACM certificates by ARN for custom domains for CNAME records
* Encryption everywhere for private VPC traffic
* Self-signed certificate generation
* SSM Parameter Store secrets for key and certificate encrypted using KMS CMK
* Certificate installation and configuration on ECS task bootstrap

## Contributing

Expand Down
14 changes: 14 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash

set -e

echo "Provisioning SSL certificate from SSM..."
echo "$SSL_KEY" > /etc/ssl/private/key.pem
echo "$SSL_CERT" > /etc/ssl/certs/cert.pem

if [[ -z ${1} ]]; then
echo "Starting nginx..."
exec $(which nginx) -g "daemon off;" ${EXTRA_ARGS}
else
exec "$@"
fi
10 changes: 10 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>AWS ECS Fargate Demo</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>A demo application by <a href="https://github.com/aaroncarlucci/aws-ecs-fargate-demo">Aaron Carlucci</a> featuring deployment and CI/CD for a Hello World application on AWS ECS Fargate. Infrastructure is defined in Terraform code and deployed automatically.</p>
</body>
</html>
13 changes: 13 additions & 0 deletions nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
server {
# Encrypt everywhere - only listen for HTTPS
listen 443 ssl;
server_name localhost;

ssl_certificate /etc/ssl/certs/cert.pem;
ssl_certificate_key /etc/ssl/private/key.pem;

location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
20 changes: 7 additions & 13 deletions terraform/ec2.tf
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# Security group for the ALB accepting HTTP connections on port 80
#
# TODO: implement SSL encrypted traffic and redirect HTTP to HTTPS
# Security group for the ALB accepting HTTP connections on ports 80 and 443
resource "aws_security_group" "alb" {
name = "${local.namespace}-alb"
vpc_id = aws_vpc.vpc.id
Expand Down Expand Up @@ -39,24 +37,20 @@ resource "aws_security_group_rule" "alb_egress_all" {
type = "egress"
}

# Currently SSL is terminated at the ALB and traffic is unencrypted
# inside the VPC.
#
# TODO: adopt "Encryption Everywhere" policy by protecting internal traffic
# between the ALB and the application service as well (#15)
# Adopt "Encryption Everywhere" policy by protecting internal traffic
# between the ALB and the application service as well.
resource "aws_lb_target_group" "alb" {
# name not specified as it creates conflicts when resource needs to be replaced. Depend
# on tags to identify target groups in the console.
port = 80
protocol = "HTTP"
port = 443
protocol = "HTTPS"
target_type = "ip"

health_check {
# TODO: review health check
enabled = true
path = "/"
port = 80
protocol = "HTTP"
port = 443
protocol = "HTTPS"
}

vpc_id = aws_vpc.vpc.id
Expand Down
75 changes: 62 additions & 13 deletions terraform/ecs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ resource "aws_ecs_cluster" "cluster" {
name = local.namespace
}

# ECR to hold our slightly customized Docker image
resource "aws_ecr_repository" "hello_world" {
encryption_configuration {
encryption_type = "KMS"
kms_key = aws_kms_key.primary.arn
}

image_scanning_configuration {
scan_on_push = true
}
# TODO: Temporary?
image_tag_mutability = "MUTABLE"

name = "${local.namespace}/hello-world"
}

# Task execution assumed role
data "aws_iam_policy_document" "ecs_task_assume_role" {
statement {
Expand All @@ -32,7 +48,7 @@ data "aws_iam_policy_document" "ecs_task_assume_role" {
}

resource "aws_iam_role" "ecs_task_execution" {
name = "${local.namespace}_ecs_task_execution"
name = "${local.namespace}-ecs-task-execution"
assume_role_policy = data.aws_iam_policy_document.ecs_task_assume_role.json
}

Expand All @@ -42,14 +58,38 @@ resource "aws_iam_role_policy_attachment" "hello_world_aws_task_execution_role_p
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

data "aws_iam_policy_document" "hello_world_task_execution_role_policy" {
# Allow access to self-signed SSL certs for nginx on task bootstrap
statement {
actions = ["ssm:GetParameters"]
effect = "Allow"
resources = [
aws_ssm_parameter.app_ssl_key.arn,
aws_ssm_parameter.app_ssl_cert.arn
]
}

statement {
actions = ["kms:Decrypt"]
effect = "Allow"
resources = [aws_kms_key.primary.arn]
}
}

resource "aws_iam_role_policy" "hello_world_task_execution_role_policy" {
name = "${local.namespace}-hello-world-task-execution"
role = aws_iam_role.ecs_task_execution.id
policy = data.aws_iam_policy_document.hello_world_task_execution_role_policy.json
}

# Task definition for Hello World server featuring CloudWatch logs integration
resource "aws_ecs_task_definition" "hello_world" {
container_definitions = jsonencode([
{
# The same value is used for task and service because there is only one task.
cpu = var.cpu
# TODO: parameterize image and/or adjust for a customized container image
image = "nginxdemos/hello:0.3"

image = local.ecs_hello_world_image
logConfiguration = {
logDriver = "awslogs"
options = {
Expand All @@ -64,11 +104,21 @@ resource "aws_ecs_task_definition" "hello_world" {
networkMode = "FARGATE"
portMappings = [
{
hostPort = 80,
containerPort = 80,
hostPort = 443,
containerPort = 443,
protocol = "tcp"
}
]
secrets = [
{
name = "SSL_KEY"
valueFrom = aws_ssm_parameter.app_ssl_key.arn
},
{
name = "SSL_CERT"
valueFrom = aws_ssm_parameter.app_ssl_cert.arn
}
]
}
])

Expand All @@ -80,20 +130,20 @@ resource "aws_ecs_task_definition" "hello_world" {
requires_compatibilities = ["FARGATE"]
}

# Security group for the hello-world ECS service accepts HTTP
# Security group for the hello-world ECS service accepts HTTPS
# connections from the ALB security group
resource "aws_security_group" "app" {
name = "${local.namespace}-app"
vpc_id = aws_vpc.vpc.id
}

resource "aws_security_group_rule" "app_ingress_http" {
description = "Allow HTTP from ALB"
from_port = 80
resource "aws_security_group_rule" "app_ingress_https" {
description = "Allow HTTPS from ALB"
from_port = 443
protocol = "tcp"
security_group_id = aws_security_group.app.id
source_security_group_id = aws_security_group.alb.id
to_port = 80
to_port = 443
type = "ingress"
}

Expand Down Expand Up @@ -121,12 +171,11 @@ resource "aws_ecs_service" "hello_world" {
ignore_changes = [desired_count]
}

# TODO: consider service encrypted internal traffic between
# ALB and ECS container on 443 - requires self-signed cert
# SSL traffic served on 443 using a self-signed cert
load_balancer {
target_group_arn = aws_lb_target_group.alb.arn
container_name = "hello-world"
container_port = 80
container_port = 443
}

network_configuration {
Expand Down
14 changes: 8 additions & 6 deletions terraform/locals.tf
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
locals {
account_id = data.aws_caller_identity.current.account_id
alb_ips = [for v in aws_lb.alb.subnet_mapping : v.private_ipv4_address]
account_id = data.aws_caller_identity.current.account_id
alb_ips = [for v in aws_lb.alb.subnet_mapping : v.private_ipv4_address]
application_name = "aws-ecs-fargate-demo"
default_tags = {
Application = "aws-ecs-fargate-demo"
Application = local.application_name
Environment = var.environment
}
namespace = "aws-ecs-fargate-demo-${var.environment}"
private_subnet_ids = [aws_subnet.private_1.id, aws_subnet.private_2.id]
public_subnet_ids = [aws_subnet.public_1.id, aws_subnet.public_2.id]
ecs_hello_world_image = "${aws_ecr_repository.hello_world.repository_url}:${var.image_tag}"
namespace = "${local.application_name}-${var.environment}"
private_subnet_ids = [aws_subnet.private_1.id, aws_subnet.private_2.id]
public_subnet_ids = [aws_subnet.public_1.id, aws_subnet.public_2.id]
}
5 changes: 5 additions & 0 deletions terraform/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ output "dns_name" {
description = "The DNS name of the application."
value = var.dns_name
}

output "namespace" {
description = "The namespace of the application."
value = local.namespace
}
Loading
Loading