├── .gitignore ├── LICENSE ├── README.md └── modules ├── aikido ├── README.md ├── ec2-scanner.tf ├── ecr-scanner.tf ├── main.tf ├── output.tf └── variables.tf ├── alb-listener ├── listener.tf ├── vars.tf └── versions.tf ├── alb-rule ├── rule.tf ├── vars.tf └── versions.tf ├── alb ├── README.md ├── lb.tf ├── output.tf ├── s3.tf ├── securitygroups.tf ├── vars.tf └── versions.tf ├── bastion ├── bastion.tf ├── iam.tf ├── output.tf ├── securitygroups.tf └── vars.tf ├── cis ├── README.md ├── cis-cloudtrail │ ├── cloudtrail-s3-access-logs.tf │ ├── cloudtrail-s3.tf │ ├── cloudtrail.tf │ ├── cloudwatch.tf │ ├── outputs.tf │ └── variables.tf ├── cis-config-aggregator │ ├── aggregator.tf │ └── variables.tf ├── cis-config │ ├── config.tf │ └── variables.tf ├── cis-general-organization │ ├── security-hub.tf │ └── variables.tf ├── cis-general │ ├── policies.tf │ └── variables.tf ├── cis-log-alarms │ ├── cloudwatch-log-filter.tf │ └── variables.tf ├── main.tf └── variables.tf ├── cloudfront ├── README.md ├── cloudfront.tf ├── output.tf └── variables.tf ├── dynamodb ├── LICENSE ├── autoscaling.tf ├── dynamodb.tf ├── output.tf └── vars.tf ├── ecs-cluster ├── cloudwatch.tf ├── ecs.tf ├── iam.tf ├── output.tf ├── securitygroups.tf ├── templates │ └── ecs_init.tpl ├── vars.tf └── versions.tf ├── ecs-codedeploy ├── codedeploy.tf ├── iam.tf ├── output.tf └── vars.tf ├── ecs-service ├── README.md ├── alb.tf ├── ecs-service.json.tpl ├── ecs-service.tf ├── output.tf ├── securitygroup.tf ├── vars.tf └── versions.tf ├── ecs-sqs-scaling ├── README.md ├── docs │ ├── README_TF.md │ └── ecs-1.png ├── main.tf ├── src │ └── lambda │ │ ├── functions.js │ │ └── index.js └── variables.tf ├── ecs-task ├── task.tf ├── vars.tf └── versions.tf ├── efs ├── README.md ├── main.tf ├── outputs.tf └── variables.tf ├── elasticache-redis ├── README.md ├── main.tf ├── output.tf ├── variables.tf └── versions.tf ├── fargate-autoscale ├── main.tf └── variables.tf ├── fargate-cluster ├── cloudwatch.tf ├── ecs.tf ├── iam.tf ├── output.tf └── vars.tf ├── kinesis ├── iam.tf ├── kinesis.tf ├── kms.tf ├── output.tf ├── s3.tf └── variables.tf ├── log-alert ├── alerter_code.tf ├── iam.tf ├── log-subscribers.tf ├── main.tf └── variables.tf ├── log-s3-export ├── .gitignore ├── README.md ├── main.tf ├── outputs.tf ├── src │ └── lambda │ │ ├── functions.js │ │ └── index.js └── variables.tf ├── oidc-github ├── LICENSE.md ├── NOTICE ├── README.md ├── data.tf ├── main.tf ├── outputs.tf ├── variables.tf └── versions.tf ├── openvpn ├── README.md ├── configuration-files.tf ├── ecs-openvpn-access-iam.tf ├── ecs-openvpn-access.tf ├── ecs.tf ├── examples │ └── openvpn-client.tpl ├── lb-rules.tf ├── output.tf ├── s3-configuration.tf ├── ssm.tf ├── tpl │ ├── onelogin-conf.tpl │ ├── openvpn-vars.tpl │ └── vpn-userdata.tpl ├── variables.tf ├── vpn-instance-iam.tf └── vpn-instance.tf ├── rds ├── kms.tf ├── output.tf ├── rds.tf ├── secret │ ├── main.tf │ ├── output.tf │ └── vars.tf ├── securitygroups.tf └── vars.tf ├── s3-replica ├── main.tf ├── outputs.tf ├── replication-iam.tf ├── s3-replica.tf ├── s3-source.tf └── variables.tf ├── s3 ├── README.md ├── outputs.tf ├── s3.tf └── variables.tf ├── sftp-transfer-server ├── iam.tf ├── output.tf ├── transfer.tf └── vars.tf ├── ssm-parameters ├── kms.tf ├── output.tf ├── parameters.tf └── vars.tf ├── ssm-replicator ├── continuous-replica-code.tf ├── full-replica-code.tf ├── iam.tf ├── main.tf └── vars.tf ├── terraform-backend ├── dynamo.tf ├── kms.tf ├── output.tf ├── s3.tf └── vars.tf ├── tgw-routes ├── route.tf └── variables.tf ├── waf ├── README.md ├── associations.tf ├── vars.tf └── waf.tf ├── wireguard-legacy ├── config-files.tf ├── efs.tf ├── output.tf ├── rds.tf ├── s3.tf ├── ssm.tf ├── templates │ ├── docker-compose.yml │ └── userdata.sh ├── variables.tf ├── vpn-instance-iam.tf └── vpn-instance.tf ├── wireguard-site2site ├── output.tf ├── ssm.tf ├── templates │ └── userdata.sh ├── variables.tf ├── vpn-instance-iam.tf └── vpn-instance.tf ├── wireguard-vpn-server ├── README.md ├── efs.tf ├── iam.tf ├── output.tf ├── templates │ └── userdata.sh ├── variables.tf └── vpn-server.tf └── wireguard ├── rds.tf └── variables.tf /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .terraform -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 in4it 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /modules/aikido/README.md: -------------------------------------------------------------------------------- 1 | ## Requirements 2 | 3 | No requirements. 4 | 5 | ## Providers 6 | 7 | | Name | Version | 8 | |------|---------| 9 | | [aws](#provider\_aws) | 5.80.0 | 10 | 11 | ## Modules 12 | 13 | No modules. 14 | 15 | ## Resources 16 | 17 | | Name | Type | 18 | |------|------| 19 | | [aws_iam_role.aikido-security-ec2-hard](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 20 | | [aws_iam_role.aikido-security-ecr-readonly-role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 21 | | [aws_iam_role.aikido-security-readonly-role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 22 | | [aws_iam_role_policy.aikido-security-ec2-policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | 23 | | [aws_iam_role_policy.aikido-security-ecr-policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | 24 | | [aws_iam_role_policy.aikido-security-policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | 25 | | [aws_iam_role_policy_attachment.aikido-managed-role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | 26 | 27 | ## Inputs 28 | 29 | | Name | Description | Type | Default | Required | 30 | |------|-------------|------|---------|:--------:| 31 | | [aikido-principal-arn](#input\_aikido-principal-arn) | aikido IAM principal to assume the role | `string` | `"arn:aws:iam::881830977366:role/service-role/lambda-aws-cloud-findings-role-uox26vzd"` | no | 32 | | [aikido-principal-arn-ec2](#input\_aikido-principal-arn-ec2) | aikido IAM principal to assume the role for ECR | `string` | `"arn:aws:iam::881830977366:role/service-role/lambda-container-image-scanner-role-pb0qotst"` | no | 33 | | [aikido-principal-arn-ecr](#input\_aikido-principal-arn-ecr) | aikido IAM principal to assume the role for ECR | `string` | `"arn:aws:iam::881830977366:role/aws-ebs-scanner-role"` | no | 34 | | [aikido-role-external-id](#input\_aikido-role-external-id) | The external ID for the IAM role - it is generated by Aikido | `string` | n/a | yes | 35 | | [ec2\_scanner\_enabled](#input\_ec2\_scanner\_enabled) | Enable EC2 scanner | `bool` | `false` | no | 36 | | [ecr\_scanner\_enabled](#input\_ecr\_scanner\_enabled) | Enable ECR scanner | `bool` | `false` | no | 37 | 38 | ## Outputs 39 | 40 | | Name | Description | 41 | |------|-------------| 42 | | [aikido-role-arn](#output\_aikido-role-arn) | n/a | 43 | | [aikido-role-ec2-arn](#output\_aikido-role-ec2-arn) | n/a | 44 | | [aikido-role-ecr-arn](#output\_aikido-role-ecr-arn) | n/a | 45 | -------------------------------------------------------------------------------- /modules/aikido/ec2-scanner.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "aikido-security-ec2-hard" { 2 | count = var.ec2_scanner_enabled ? 1 : 0 3 | 4 | name = "aikido-security-ec2-hard" 5 | 6 | assume_role_policy = jsonencode({ 7 | Version = "2012-10-17", 8 | Statement = [ 9 | { 10 | Effect = "Allow", 11 | Principal = { 12 | AWS = var.aikido-principal-arn-ec2 13 | }, 14 | Action = "sts:AssumeRole", 15 | Condition = { 16 | StringEquals = { 17 | "sts:ExternalId" = var.aikido-role-external-id 18 | } 19 | } 20 | } 21 | ] 22 | }) 23 | } 24 | 25 | resource "aws_iam_role_policy" "aikido-security-ec2-policy" { 26 | count = var.ec2_scanner_enabled ? 1 : 0 27 | 28 | name = "aikido-security-ec2-policy" 29 | role = aws_iam_role.aikido-security-ec2-hard[0].id 30 | 31 | policy = jsonencode({ 32 | Version = "2012-10-17", 33 | Statement = [ 34 | { 35 | Action = [ 36 | "ec2:DescribeInstances", 37 | "ec2:DescribeVolumes", 38 | "ec2:DescribeVolumeStatus", 39 | "ec2:DescribeSnapshots", 40 | "ec2:CreateSnapshot", 41 | "ec2:DeleteSnapshot", 42 | "ec2:CreateTags", 43 | "ebs:ListSnapshotBlocks", 44 | "ebs:GetSnapshotBlock", 45 | "kms:Decrypt", 46 | "kms:GenerateDataKey" 47 | ], 48 | Resource = "*", 49 | Effect = "Allow" 50 | } 51 | ] 52 | }) 53 | } 54 | 55 | -------------------------------------------------------------------------------- /modules/aikido/ecr-scanner.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "aikido-security-ecr-readonly-role" { 2 | count = var.ecr_scanner_enabled ? 1 : 0 3 | 4 | name = "aikido-security-ecr-readonly-role" 5 | 6 | assume_role_policy = jsonencode({ 7 | Version = "2012-10-17", 8 | Statement = [ 9 | { 10 | Effect = "Allow", 11 | Principal = { 12 | AWS = var.aikido-principal-arn-ecr 13 | }, 14 | Action = "sts:AssumeRole", 15 | Condition = { 16 | StringEquals = { 17 | "sts:ExternalId" = var.aikido-role-external-id 18 | } 19 | } 20 | } 21 | ] 22 | }) 23 | } 24 | 25 | resource "aws_iam_role_policy" "aikido-security-ecr-policy" { 26 | count = var.ecr_scanner_enabled ? 1 : 0 27 | 28 | name = "aikido-security-ecr-policy" 29 | role = aws_iam_role.aikido-security-ecr-readonly-role[0].id 30 | policy = jsonencode({ 31 | Version = "2012-10-17", 32 | Statement = [ 33 | { 34 | Action = [ 35 | "ecr:GetDownloadUrlForLayer", 36 | "ecr:BatchGetImage", 37 | "ecr:BatchCheckLayerAvailability", 38 | "ecr:ListImages", 39 | "ecr:DescribeImages", 40 | "ecr:DescribeRepositories", 41 | "ecr:DescribeRegistry", 42 | "ecr:GetAuthorizationToken" 43 | ], 44 | Resource = "*", 45 | Effect = "Allow" 46 | } 47 | ] 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /modules/aikido/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "aikido-security-readonly-role" { 2 | name = "aikido-security-readonly-role" 3 | assume_role_policy = jsonencode({ 4 | Version = "2012-10-17", 5 | Statement = [ 6 | { 7 | Effect = "Allow", 8 | Principal = { 9 | AWS = var.aikido-principal-arn 10 | }, 11 | Action = "sts:AssumeRole", 12 | Condition = { 13 | StringEquals = { 14 | "sts:ExternalId" = var.aikido-role-external-id 15 | } 16 | } 17 | } 18 | ] 19 | }) 20 | } 21 | 22 | resource "aws_iam_role_policy_attachment" "aikido-managed-role" { 23 | policy_arn = "arn:aws:iam::aws:policy/SecurityAudit" 24 | role = aws_iam_role.aikido-security-readonly-role.id 25 | } 26 | 27 | resource "aws_iam_role_policy" "aikido-security-policy" { 28 | name = "aikido-security-policy" 29 | role = aws_iam_role.aikido-security-readonly-role.id 30 | policy = jsonencode({ 31 | Version = "2012-10-17", 32 | Statement = [ 33 | { 34 | Action = [ 35 | "iam:CreateServiceLinkedRole", 36 | "inspector2:*", 37 | "budgets:ViewBudget", 38 | "backup:ListBackupPlans", 39 | "backup:GetBackupPlan", 40 | "backup:ListProtectedResources" 41 | ], 42 | Resource = "*", 43 | Effect = "Allow" 44 | } 45 | ] 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /modules/aikido/output.tf: -------------------------------------------------------------------------------- 1 | output "aikido-role-arn" { 2 | value = aws_iam_role.aikido-security-readonly-role.arn 3 | } 4 | output "aikido-role-ec2-arn" { 5 | value = aws_iam_role.aikido-security-ec2-hard[0].arn 6 | } 7 | output "aikido-role-ecr-arn" { 8 | value = aws_iam_role.aikido-security-ecr-readonly-role[0].arn 9 | } 10 | -------------------------------------------------------------------------------- /modules/aikido/variables.tf: -------------------------------------------------------------------------------- 1 | variable "aikido-role-external-id" { 2 | type = string 3 | description = "The external ID for the IAM role - it is generated by Aikido" 4 | } 5 | 6 | variable "aikido-principal-arn" { 7 | type = string 8 | default = "arn:aws:iam::881830977366:role/service-role/lambda-aws-cloud-findings-role-uox26vzd" 9 | description = "aikido IAM principal to assume the role" 10 | } 11 | 12 | variable "aikido-principal-arn-ec2" { 13 | type = string 14 | default = "arn:aws:iam::881830977366:role/service-role/lambda-container-image-scanner-role-pb0qotst" 15 | description = "aikido IAM principal to assume the role for ECR" 16 | } 17 | 18 | variable "aikido-principal-arn-ecr" { 19 | type = string 20 | default = "arn:aws:iam::881830977366:role/aws-ebs-scanner-role" 21 | description = "aikido IAM principal to assume the role for ECR" 22 | } 23 | 24 | variable "ec2_scanner_enabled" { 25 | type = bool 26 | default = false 27 | description = "Enable EC2 scanner" 28 | } 29 | 30 | variable "ecr_scanner_enabled" { 31 | type = bool 32 | default = false 33 | description = "Enable ECR scanner" 34 | } 35 | -------------------------------------------------------------------------------- /modules/alb-listener/listener.tf: -------------------------------------------------------------------------------- 1 | # certificate 2 | data "aws_acm_certificate" "certificate" { 3 | domain = var.DOMAIN 4 | statuses = ["ISSUED", "PENDING_VALIDATION"] 5 | } 6 | 7 | # alb listener (https) 8 | resource "aws_alb_listener" "alb-listener" { 9 | load_balancer_arn = var.ALB_ARN 10 | port = var.ALB_PORT 11 | protocol = var.ALB_PROTOCOL 12 | ssl_policy = var.SSL_POLICY 13 | certificate_arn = data.aws_acm_certificate.certificate.arn 14 | 15 | default_action { 16 | target_group_arn = var.TARGET_GROUP_ARN 17 | type = "forward" 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /modules/alb-listener/vars.tf: -------------------------------------------------------------------------------- 1 | variable "ALB_ARN" { 2 | } 3 | 4 | variable "ALB_PORT" { 5 | } 6 | 7 | variable "DOMAIN" { 8 | } 9 | 10 | variable "TARGET_GROUP_ARN" { 11 | } 12 | 13 | variable "ALB_PROTOCOL" { 14 | default = "HTTPS" 15 | } 16 | 17 | variable "SSL_POLICY" { 18 | type = string 19 | default = "ELBSecurityPolicy-TLS13-1-2-2021-06" 20 | description = "SSL policy to use for the ALB" 21 | } 22 | -------------------------------------------------------------------------------- /modules/alb-listener/versions.tf: -------------------------------------------------------------------------------- 1 | 2 | terraform { 3 | required_version = ">= 0.12" 4 | } 5 | -------------------------------------------------------------------------------- /modules/alb-rule/rule.tf: -------------------------------------------------------------------------------- 1 | resource "aws_lb_listener_rule" "alb_rule" { 2 | listener_arn = var.listener_arn 3 | priority = var.priority 4 | 5 | action { 6 | type = var.action_type 7 | target_group_arn = var.target_group_arn 8 | 9 | dynamic fixed_response { 10 | for_each = var.action_type == "fixed-response" ? [1] : [] 11 | content { 12 | content_type = var.fixed_response.content_type 13 | message_body = var.fixed_response.message_body 14 | status_code = var.fixed_response.status_code 15 | } 16 | } 17 | dynamic redirect { 18 | for_each = var.action_type == "redirect" ? [1] : [] 19 | content { 20 | port = var.redirect.port 21 | protocol = var.redirect.protocol 22 | status_code = var.redirect.status_code 23 | host = var.redirect.host == "" ? "#{host}" : var.redirect.host 24 | path = var.redirect.path == "" ? "/#{path}" : var.redirect.path 25 | query = var.redirect.query == "" ? "#{query}" : var.redirect.query 26 | } 27 | } 28 | } 29 | 30 | # legacy code 31 | condition { 32 | dynamic host_header { 33 | for_each = var.condition_field == "host-header" ? [1] : [] 34 | content { 35 | values = var.condition_values 36 | } 37 | } 38 | dynamic path_pattern { 39 | for_each = var.condition_field == "path-pattern" ? [1] : [] 40 | content { 41 | values = var.condition_values 42 | } 43 | } 44 | } 45 | # more flexible approach 46 | dynamic condition { 47 | for_each = var.conditions 48 | content { 49 | dynamic host_header { 50 | for_each = condition.value.field == "host-header" ? [1] : [] 51 | content { 52 | values = condition.value.values 53 | } 54 | } 55 | dynamic path_pattern { 56 | for_each = condition.value.field == "path-pattern" ? [1] : [] 57 | content { 58 | values = condition.value.values 59 | } 60 | } 61 | dynamic query_string { 62 | for_each = condition.value.field == "query-string" ? [1] : [] 63 | content { 64 | value = condition.value.value 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /modules/alb-rule/vars.tf: -------------------------------------------------------------------------------- 1 | variable "listener_arn" { 2 | } 3 | 4 | variable "priority" { 5 | } 6 | 7 | variable "target_group_arn" { 8 | } 9 | 10 | variable "condition_field" { 11 | default = "" 12 | description = "host-header, path-pattern or ''" 13 | validation { 14 | condition = contains(["host-header", "path-pattern", ""], var.condition_field) 15 | error_message = "Valid values for condition_field are host-header, path-pattern or ''" 16 | } 17 | } 18 | 19 | variable "condition_values" { 20 | default = [] 21 | type = list(string) 22 | } 23 | 24 | variable "conditions" { 25 | description = "ALB rule conditions" 26 | default = [] 27 | type = list(object({ 28 | field = string 29 | values = list(string) 30 | value = string 31 | })) 32 | } 33 | 34 | variable "action_type" { 35 | default = "forward" 36 | } 37 | 38 | variable "fixed-response" { 39 | default = { 40 | content_type = "" 41 | message_body = "" 42 | status_code = "" 43 | } 44 | type = object({ 45 | content_type = string 46 | message_body = string 47 | status_code = string 48 | }) 49 | } 50 | variable "redirect" { 51 | default = { 52 | port = "" 53 | protocol = "" 54 | status_code = "" 55 | host = "" 56 | path = "" 57 | query = "" 58 | } 59 | type = object({ 60 | port = string 61 | protocol = string 62 | status_code = string 63 | host = string 64 | path = string 65 | query = string 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /modules/alb-rule/versions.tf: -------------------------------------------------------------------------------- 1 | 2 | terraform { 3 | required_version = ">= 0.12" 4 | } 5 | -------------------------------------------------------------------------------- /modules/alb/output.tf: -------------------------------------------------------------------------------- 1 | output "lb_arn" { 2 | value = aws_lb.lb.arn 3 | } 4 | 5 | output "dns_name" { 6 | value = aws_lb.lb.dns_name 7 | } 8 | 9 | output "zone_id" { 10 | value = aws_lb.lb.zone_id 11 | } 12 | 13 | output "http_listener_arn" { 14 | value = aws_lb_listener.lb-http.arn 15 | } 16 | 17 | output "https_listener_arn" { 18 | value = length(aws_lb_listener.lb-https) > 0 ? aws_lb_listener.lb-https[0].arn : null 19 | } 20 | 21 | output "security-group-id" { 22 | value = aws_security_group.lb.id 23 | } 24 | -------------------------------------------------------------------------------- /modules/alb/s3.tf: -------------------------------------------------------------------------------- 1 | # LB logs 2 | locals { 3 | create_logging_bucket = lookup(var.access_logs, "bucket", null) == null 4 | } 5 | data "aws_elb_service_account" "main" { 6 | count = local.create_logging_bucket ? 1 : 0 7 | } 8 | 9 | resource "aws_s3_bucket" "lb_logs" { 10 | count = local.create_logging_bucket ? 1 : 0 11 | bucket = "${var.lb_name}-lb-logs" 12 | 13 | tags = { 14 | Name = "${var.lb_name}-lb-logs" 15 | } 16 | } 17 | 18 | resource "aws_s3_bucket_server_side_encryption_configuration" "lb_logs" { 19 | count = local.create_logging_bucket ? 1 : 0 20 | bucket = aws_s3_bucket.lb_logs[0].id 21 | 22 | rule { 23 | apply_server_side_encryption_by_default { 24 | sse_algorithm = "AES256" 25 | } 26 | } 27 | } 28 | 29 | resource "aws_s3_bucket_versioning" "lb_logs" { 30 | count = local.create_logging_bucket ? 1 : 0 31 | bucket = aws_s3_bucket.lb_logs[0].id 32 | versioning_configuration { 33 | status = "Disabled" 34 | } 35 | lifecycle { 36 | ignore_changes = [versioning_configuration] 37 | } 38 | } 39 | 40 | resource "aws_s3_bucket_policy" "lb_logs" { 41 | count = local.create_logging_bucket ? 1 : 0 42 | bucket = aws_s3_bucket.lb_logs[0].id 43 | policy = < /etc/ecs/ecs.config 3 | start ecs 4 | -------------------------------------------------------------------------------- /modules/ecs-cluster/vars.tf: -------------------------------------------------------------------------------- 1 | variable "aws_account_id" { 2 | } 3 | 4 | variable "aws_region" { 5 | } 6 | 7 | variable "log_group" { 8 | } 9 | 10 | variable "vpc_id" { 11 | } 12 | 13 | variable "cluster_name" { 14 | } 15 | 16 | variable "instance_type" { 17 | } 18 | 19 | variable "ssh_key_name" { 20 | } 21 | 22 | variable "vpc_subnets" { 23 | } 24 | 25 | variable "ecs_termination_policies" { 26 | default = "OldestLaunchConfiguration,Default" 27 | } 28 | 29 | variable "ecs_minsize" { 30 | default = 1 31 | } 32 | 33 | variable "ecs_maxsize" { 34 | default = 1 35 | } 36 | 37 | variable "ecs_desired_capacity" { 38 | default = 1 39 | } 40 | 41 | variable "enable_ssh" { 42 | default = false 43 | } 44 | 45 | variable "ssh_sg" { 46 | default = "" 47 | } 48 | 49 | variable "log_retention_days" { 50 | default = 0 51 | } 52 | -------------------------------------------------------------------------------- /modules/ecs-cluster/versions.tf: -------------------------------------------------------------------------------- 1 | 2 | terraform { 3 | required_version = ">= 0.12" 4 | } 5 | -------------------------------------------------------------------------------- /modules/ecs-codedeploy/codedeploy.tf: -------------------------------------------------------------------------------- 1 | resource "aws_codedeploy_app" "codedeploy" { 2 | compute_platform = "ECS" 3 | name = var.name 4 | } 5 | 6 | resource "aws_codedeploy_deployment_group" "codedeploy" { 7 | app_name = aws_codedeploy_app.codedeploy.name 8 | deployment_config_name = var.deployment_config_name 9 | deployment_group_name = var.name 10 | service_role_arn = aws_iam_role.codedeploy.arn 11 | 12 | auto_rollback_configuration { 13 | enabled = true 14 | events = ["DEPLOYMENT_FAILURE"] 15 | } 16 | 17 | blue_green_deployment_config { 18 | deployment_ready_option { 19 | action_on_timeout = "CONTINUE_DEPLOYMENT" 20 | } 21 | 22 | terminate_blue_instances_on_deployment_success { 23 | action = "TERMINATE" 24 | termination_wait_time_in_minutes = 5 25 | } 26 | } 27 | 28 | deployment_style { 29 | deployment_option = var.deployment_option 30 | deployment_type = var.deployment_type 31 | } 32 | 33 | ecs_service { 34 | cluster_name = var.ecs_cluster_name 35 | service_name = var.ecs_service_name 36 | } 37 | 38 | dynamic "load_balancer_info" { 39 | for_each = length(compact(var.listener_arns)) == 0 ? [] : [1] 40 | content { 41 | target_group_pair_info { 42 | prod_traffic_route { 43 | listener_arns = compact(var.listener_arns) 44 | } 45 | 46 | dynamic target_group { 47 | for_each = var.target_group_names 48 | content { 49 | name = target_group.value 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /modules/ecs-codedeploy/iam.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "codedeploy" { 2 | name = "codedeploy-${var.name}" 3 | 4 | assume_role_policy = < 0 ? element([ 11 | for ecs-service in aws_lb_target_group.ecs-service : ecs-service.arn 12 | ], 0) : null 13 | } 14 | 15 | output "target_group_names" { 16 | value = [for ecs-service in aws_lb_target_group.ecs-service : ecs-service.name] 17 | } 18 | 19 | output "task_security_group_id" { 20 | value = aws_security_group.ecs-service.id 21 | } 22 | 23 | output "service_name" { 24 | value = aws_ecs_service.ecs-service.name 25 | } 26 | 27 | output "ecr_url" { 28 | value = length(aws_ecr_repository.ecs-service) > 0 ? aws_ecr_repository.ecs-service.0.repository_url : "" 29 | } 30 | 31 | output "ecr_arn" { 32 | value = length(aws_ecr_repository.ecs-service) > 0 ? aws_ecr_repository.ecs-service.0.arn : "" 33 | } 34 | 35 | output "ecr_name" { 36 | value = length(aws_ecr_repository.ecs-service) > 0 ? aws_ecr_repository.ecs-service.0.name : "" 37 | } 38 | 39 | output "log_group_name" { 40 | value = var.log_group != "" ? var.log_group : aws_cloudwatch_log_group.logs[0].name 41 | } 42 | -------------------------------------------------------------------------------- /modules/ecs-service/securitygroup.tf: -------------------------------------------------------------------------------- 1 | resource "aws_security_group" "ecs-service" { 2 | name = var.application_name 3 | vpc_id = var.vpc_id 4 | description = var.application_name 5 | 6 | dynamic "ingress" { 7 | for_each = var.ingress_rules 8 | content { 9 | from_port = ingress.value.from_port 10 | to_port = ingress.value.to_port 11 | protocol = ingress.value.protocol 12 | security_groups = ingress.value.security_groups 13 | } 14 | } 15 | 16 | egress { 17 | from_port = 0 18 | to_port = 0 19 | protocol = "-1" 20 | cidr_blocks = ["0.0.0.0/0"] 21 | } 22 | } -------------------------------------------------------------------------------- /modules/ecs-service/versions.tf: -------------------------------------------------------------------------------- 1 | 2 | terraform { 3 | required_version = ">= 0.12" 4 | } 5 | -------------------------------------------------------------------------------- /modules/ecs-sqs-scaling/README.md: -------------------------------------------------------------------------------- 1 | # ⚙️ ECS / SQS TargetTracking Scaling using custom metrics 2 | 3 | 4 | ### 📖 Docs 5 | - Terraform docs ➡️ [./docs/README_TF.md](./docs/README_TF.md) 6 | - AWS Javascript SDKv3 ➡️ https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/ 7 | - AWS docs architecture references ➡️ [aws-ref1](https://aws.amazon.com/blogs/containers/amazon-elastic-container-service-ecs-auto-scaling-using-custom-metrics/) 8 | / [aws-ref2](https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-using-sqs-queue.html) 9 | / [aws-ref3](https://aws.amazon.com/blogs/compute/scaling-an-asg-using-target-tracking-with-a-dynamic-sqs-target/) 10 | / [aws-target-tracking-ref](https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-scaling-target-tracking.html) 11 | 12 | --- 13 | 14 | ![solution-diagram](./docs/ecs-1.png) 15 | -------------------------------------------------------------------------------- /modules/ecs-sqs-scaling/docs/ecs-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in4it/terraform-modules/f7dc26a24807a9d2b15d15d5c7141bf2fc897680/modules/ecs-sqs-scaling/docs/ecs-1.png -------------------------------------------------------------------------------- /modules/ecs-sqs-scaling/src/lambda/functions.js: -------------------------------------------------------------------------------- 1 | const checkEnvs = (list) => { 2 | for (const val of list) { 3 | if ( 4 | typeof process.env[val] == "undefined" || 5 | process.env[val] === null || 6 | (typeof process.env[val] == "string" && !process.env[val].trim()) 7 | ) { 8 | throw new Error(`${val} Environment Value is not defined!`); 9 | } 10 | } 11 | }; 12 | module.exports = { 13 | checkEnvs, 14 | }; 15 | -------------------------------------------------------------------------------- /modules/ecs-sqs-scaling/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name_prefix" { 2 | default = "ecs-sqs-scaling" 3 | description = "The prefix to use for all resources created by this module" 4 | } 5 | variable "env" { 6 | description = "The environment this module is being deployed to" 7 | } 8 | 9 | variable "config" { 10 | description = "Configuration for the custom metric generation. Add a new object for each ECS worker service you want to scale based on the SQS queue size." 11 | type = map(object({ 12 | ecs_cluster = string 13 | ecs_service = string 14 | tracking_sqs_queue = string 15 | tracking_sqs_queue_metric = optional(string) 16 | })) 17 | } 18 | 19 | variable "lambda_trigger_period" { 20 | description = "The period to trigger the custom metric generation" 21 | default = "1 minute" 22 | } 23 | 24 | variable "custom_metric_namespace" { 25 | description = "The namespace for the custom metric to be generated" 26 | default = "ECS/CustomerMetrics" 27 | } 28 | 29 | variable "custom_metric_name" { 30 | description = "The name of the custom metric to be generated" 31 | default = "ECSWorkerBacklog" 32 | } 33 | 34 | variable "debug_mode" { 35 | description = "Enable debug mode for the lambda function" 36 | default = false 37 | } 38 | -------------------------------------------------------------------------------- /modules/ecs-task/task.tf: -------------------------------------------------------------------------------- 1 | # 2 | # ECR 3 | # 4 | 5 | resource "aws_ecr_repository" "ecs-task" { 6 | name = "${var.ECR_PREFIX}${var.APPLICATION_NAME}" 7 | } 8 | 9 | # 10 | # get latest active revision 11 | # 12 | data "aws_ecs_task_definition" "ecs-task" { 13 | task_definition = aws_ecs_task_definition.ecs-task-taskdef.family 14 | depends_on = [aws_ecs_task_definition.ecs-task-taskdef] 15 | } 16 | 17 | # 18 | # task definition 19 | # 20 | 21 | resource "aws_ecs_task_definition" "ecs-task-taskdef" { 22 | family = var.APPLICATION_NAME 23 | container_definitions = templatefile(var.TASK_DEF_TEMPLATE, { 24 | APPLICATION_NAME = var.APPLICATION_NAME 25 | APPLICATION_VERSION = var.APPLICATION_VERSION 26 | ECR_URL = aws_ecr_repository.ecs-task.repository_url 27 | AWS_REGION = var.AWS_REGION 28 | CPU_RESERVATION = var.CPU_RESERVATION 29 | MEMORY_RESERVATION = var.MEMORY_RESERVATION 30 | LOG_GROUP = var.LOG_GROUP 31 | }) 32 | task_role_arn = var.TASK_ROLE_ARN 33 | } 34 | 35 | # scheduling 36 | resource "aws_cloudwatch_event_rule" "schedule" { 37 | name = "Run${replace(var.APPLICATION_NAME, "-", "")}" 38 | description = "runs ecs task" 39 | schedule_expression = var.SCHEDULE 40 | } 41 | 42 | resource "aws_cloudwatch_event_target" "schedule" { 43 | rule = aws_cloudwatch_event_rule.schedule.name 44 | target_id = "Run${replace(var.APPLICATION_NAME, "-", "")}" 45 | arn = var.CLUSTER_ARN 46 | role_arn = var.EVENTS_ROLE_ARN 47 | 48 | ecs_target { 49 | task_count = 1 50 | task_definition_arn = aws_ecs_task_definition.ecs-task-taskdef.arn 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /modules/ecs-task/vars.tf: -------------------------------------------------------------------------------- 1 | variable "AWS_REGION" { 2 | } 3 | 4 | variable "APPLICATION_NAME" { 5 | } 6 | 7 | variable "APPLICATION_VERSION" { 8 | } 9 | 10 | variable "CLUSTER_ARN" { 11 | } 12 | 13 | variable "EVENTS_ROLE_ARN" { 14 | } 15 | 16 | variable "DESIRED_COUNT" { 17 | } 18 | 19 | variable "TASK_DEF_TEMPLATE" { 20 | } 21 | 22 | variable "ECR_PREFIX" { 23 | default = "" 24 | } 25 | 26 | variable "SCHEDULE" { 27 | } 28 | 29 | variable "CPU_RESERVATION" { 30 | } 31 | 32 | variable "MEMORY_RESERVATION" { 33 | } 34 | 35 | variable "LOG_GROUP" { 36 | } 37 | 38 | variable "TASK_ROLE_ARN" { 39 | default = "" 40 | } 41 | 42 | -------------------------------------------------------------------------------- /modules/ecs-task/versions.tf: -------------------------------------------------------------------------------- 1 | 2 | terraform { 3 | required_version = ">= 0.12" 4 | } 5 | -------------------------------------------------------------------------------- /modules/efs/README.md: -------------------------------------------------------------------------------- 1 | ## Requirements 2 | 3 | No requirements. 4 | 5 | ## Providers 6 | 7 | | Name | Version | 8 | |------|---------| 9 | | [aws](#provider\_aws) | n/a | 10 | 11 | ## Modules 12 | 13 | No modules. 14 | 15 | ## Resources 16 | 17 | | Name | Type | 18 | |------|------| 19 | | [aws_efs_file_system.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/efs_file_system) | resource | 20 | | [aws_efs_mount_target.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/efs_mount_target) | resource | 21 | | [aws_security_group.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | 22 | 23 | ## Inputs 24 | 25 | | Name | Description | Type | Default | Required | 26 | |------|-------------|------|---------|:--------:| 27 | | [encrypted](#input\_encrypted) | Whether to enable encryption at rest | `bool` | `true` | no | 28 | | [ingress\_security\_groups](#input\_ingress\_security\_groups) | List of security groups to allow ingress from to the EFS | `list(string)` | n/a | yes | 29 | | [lifecycle\_policies](#input\_lifecycle\_policies) | n/a | `any` | `{}` | no | 30 | | [name](#input\_name) | Name of the EFS | `string` | n/a | yes | 31 | | [performance\_mode](#input\_performance\_mode) | 'generalPurpose' or 'maxIO' | `string` | `"generalPurpose"` | no | 32 | | [subnet\_ids](#input\_subnet\_ids) | List of subnet IDs | `list(string)` | n/a | yes | 33 | | [vpc\_id](#input\_vpc\_id) | VPC ID | `string` | n/a | yes | 34 | 35 | ## Outputs 36 | 37 | | Name | Description | 38 | |------|-------------| 39 | | [efs\_dns\_name](#output\_efs\_dns\_name) | DNS name of the EFS | 40 | | [efs\_id](#output\_efs\_id) | EFS ID | 41 | | [efs\_sg\_id](#output\_efs\_sg\_id) | Security group ID of the EFS | 42 | -------------------------------------------------------------------------------- /modules/efs/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_efs_file_system" "this" { 2 | creation_token = var.name 3 | encrypted = var.encrypted 4 | performance_mode = var.performance_mode 5 | 6 | dynamic "lifecycle_policy" { 7 | for_each = [for k, v in var.lifecycle_policies : { (k) = v }] 8 | 9 | content { 10 | transition_to_primary_storage_class = try(lifecycle_policies.value.transition_to_primary_storage_class, null) 11 | transition_to_ia = try(lifecycle_policies.value.transition_to_ia, null) 12 | } 13 | } 14 | tags = { 15 | Name = "${var.name}-efs" 16 | } 17 | } 18 | 19 | resource "aws_efs_mount_target" "this" { 20 | for_each = toset(var.subnet_ids) 21 | 22 | file_system_id = aws_efs_file_system.this.id 23 | subnet_id = each.value 24 | security_groups = [aws_security_group.this.id] 25 | } 26 | 27 | resource "aws_security_group" "this" { 28 | name = "${var.name}-efs-sg" 29 | description = "${var.name}-efs-sg" 30 | vpc_id = var.vpc_id 31 | 32 | ingress { 33 | from_port = 2049 34 | protocol = "tcp" 35 | to_port = 2049 36 | security_groups = var.ingress_security_groups 37 | description = "Allow traffic to EFS" 38 | } 39 | 40 | egress { 41 | from_port = 0 42 | protocol = "-1" 43 | to_port = 0 44 | cidr_blocks = ["0.0.0.0/0"] 45 | } 46 | 47 | tags = { 48 | Name = "${var.name}-efs-sg" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /modules/efs/outputs.tf: -------------------------------------------------------------------------------- 1 | output "efs_id" { 2 | value = aws_efs_file_system.this.id 3 | description = "EFS ID" 4 | } 5 | 6 | output "efs_sg_id" { 7 | value = aws_security_group.this.id 8 | description = "Security group ID of the EFS" 9 | } 10 | 11 | output "efs_dns_name" { 12 | description = "DNS name of the EFS" 13 | value = aws_efs_file_system.this.dns_name 14 | } 15 | -------------------------------------------------------------------------------- /modules/efs/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "Name of the EFS" 3 | type = string 4 | } 5 | variable "ingress_security_groups" { 6 | description = "List of security groups to allow ingress from to the EFS" 7 | type = list(string) 8 | } 9 | variable "vpc_id" { 10 | description = "VPC ID" 11 | type = string 12 | } 13 | variable "subnet_ids" { 14 | description = "List of subnet IDs" 15 | type = list(string) 16 | } 17 | variable "lifecycle_policies" { 18 | type = any 19 | default = {} 20 | } 21 | variable "performance_mode" { 22 | description = "'generalPurpose' or 'maxIO'" 23 | type = string 24 | default = "generalPurpose" 25 | } 26 | variable "encrypted" { 27 | description = "Whether to enable encryption at rest" 28 | type = bool 29 | default = true 30 | } 31 | -------------------------------------------------------------------------------- /modules/elasticache-redis/output.tf: -------------------------------------------------------------------------------- 1 | output "endpoint" { 2 | value = var.cluster_mode_enabled == false ? aws_elasticache_replication_group.redis.primary_endpoint_address : aws_elasticache_replication_group.redis.configuration_endpoint_address 3 | } 4 | output "redis_security_group_id" { 5 | value = var.existing_security_group == "" ? aws_security_group.redis[0].id : var.existing_security_group 6 | } 7 | -------------------------------------------------------------------------------- /modules/elasticache-redis/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 5.0" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /modules/fargate-autoscale/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_appautoscaling_target" "service" { 2 | max_capacity = var.max_capacity 3 | min_capacity = var.min_capacity 4 | resource_id = "service/${var.cluster_name}/${var.service_name}" 5 | scalable_dimension = "ecs:service:DesiredCount" 6 | service_namespace = "ecs" 7 | role_arn = "arn:aws:iam::${var.aws_account_id}:role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService" 8 | } 9 | 10 | resource "aws_appautoscaling_policy" "service-cpu" { 11 | name = "${var.service_name}-cpu-scaling-policy-${var.env}" 12 | policy_type = "TargetTrackingScaling" 13 | resource_id = aws_appautoscaling_target.service.resource_id 14 | scalable_dimension = aws_appautoscaling_target.service.scalable_dimension 15 | service_namespace = aws_appautoscaling_target.service.service_namespace 16 | 17 | target_tracking_scaling_policy_configuration { 18 | predefined_metric_specification { 19 | predefined_metric_type = "ECSServiceAverageCPUUtilization" 20 | } 21 | target_value = var.cpu_util_target 22 | } 23 | depends_on = [aws_appautoscaling_target.service] 24 | } 25 | 26 | resource "aws_appautoscaling_policy" "service-memory" { 27 | name = "${var.service_name}-memory-scaling-policy-${var.env}" 28 | policy_type = "TargetTrackingScaling" 29 | resource_id = aws_appautoscaling_target.service.resource_id 30 | scalable_dimension = aws_appautoscaling_target.service.scalable_dimension 31 | service_namespace = aws_appautoscaling_target.service.service_namespace 32 | 33 | target_tracking_scaling_policy_configuration { 34 | predefined_metric_specification { 35 | predefined_metric_type = "ECSServiceAverageMemoryUtilization" 36 | } 37 | target_value = var.memory_util_target 38 | } 39 | depends_on = [aws_appautoscaling_target.service] 40 | } 41 | -------------------------------------------------------------------------------- /modules/fargate-autoscale/variables.tf: -------------------------------------------------------------------------------- 1 | variable "cluster_name" { 2 | description = "Name of the ECS cluster" 3 | } 4 | variable "service_name" { 5 | description = "Name of the ECS service" 6 | } 7 | variable "env" { 8 | description = "Environment" 9 | } 10 | variable "aws_account_id" { 11 | description = "AWS Account ID" 12 | } 13 | variable "min_capacity" { 14 | description = "Minimum number of tasks to run" 15 | default = 1 16 | } 17 | variable "max_capacity" { 18 | description = "Maximum number of tasks to run" 19 | default = 1 20 | } 21 | variable "cpu_util_target" { 22 | description = "Target CPU utilization threshold" 23 | default = 80 24 | } 25 | variable "memory_util_target" { 26 | description = "Target memory utilization threshold" 27 | default = 80 28 | } 29 | -------------------------------------------------------------------------------- /modules/fargate-cluster/cloudwatch.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Cloudwatch logs 3 | # 4 | 5 | resource "aws_cloudwatch_log_group" "cluster" { 6 | name = var.log_group 7 | retention_in_days = var.log_retention_days 8 | } 9 | 10 | 11 | resource "aws_cloudwatch_log_group" "execute_command_logs" { 12 | count = var.enable_execute_command ? 1 : 0 13 | 14 | name = "${var.log_group}-execute-commands" 15 | retention_in_days = var.log_retention_days 16 | } 17 | -------------------------------------------------------------------------------- /modules/fargate-cluster/ecs.tf: -------------------------------------------------------------------------------- 1 | # 2 | # ECS Fargate cluster 3 | # 4 | 5 | resource "aws_ecs_cluster" "cluster" { 6 | name = var.cluster_name 7 | 8 | setting { 9 | name = "containerInsights" 10 | value = var.ecs_insights 11 | } 12 | 13 | dynamic "configuration" { 14 | for_each = var.enable_execute_command ? [""] : [] 15 | content { 16 | execute_command_configuration { 17 | logging = "OVERRIDE" 18 | 19 | log_configuration { 20 | cloud_watch_log_group_name = aws_cloudwatch_log_group.execute_command_logs[0].name 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /modules/fargate-cluster/iam.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Fargate execution role 3 | # 4 | 5 | resource "aws_iam_role" "ecs-task-execution-role" { 6 | name = "ecs-task-execution-role-${var.cluster_name}" 7 | 8 | assume_role_policy = < { 6 | return String(num).padStart(2, "0"); 7 | }; 8 | 9 | const sleep = (sec) => { 10 | return new Promise((resolve) => setTimeout(resolve, sec)); 11 | }; 12 | 13 | const getExportStatus = async (taskId, cloudwatchlogs) => { 14 | try { 15 | const command = new DescribeExportTasksCommand({ taskId: taskId }); 16 | const res = await cloudwatchlogs.send(command); 17 | return res.exportTasks[0].status.code; 18 | } catch (error) { 19 | const err = `[X] ERROR describing task ${taskId}::: ${error}`; 20 | throw new Error(err); 21 | } 22 | }; 23 | 24 | const calcPrefix = (prefix, name, day) => { 25 | return `${prefix}/${name}/${day.getUTCFullYear()}/${pad( 26 | day.getUTCMonth() + 1 27 | )}/${pad(day.getUTCDate())}/exportedlogs`; 28 | }; 29 | 30 | const checkEnvs = (list) => { 31 | for (const val of list) { 32 | if ( 33 | typeof process.env[val] == "undefined" || 34 | process.env[val] === null || 35 | (typeof process.env[val] == "string" && !process.env[val].trim()) 36 | ) { 37 | throw new Error(`${val} Environment Value is not defined!`); 38 | } 39 | } 40 | }; 41 | 42 | module.exports = { 43 | sleep, 44 | getExportStatus, 45 | calcPrefix, 46 | checkEnvs, 47 | }; 48 | -------------------------------------------------------------------------------- /modules/log-s3-export/src/lambda/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | CloudWatchLogsClient, 3 | CreateExportTaskCommand, 4 | DescribeExportTasksCommand, 5 | } = require("@aws-sdk/client-cloudwatch-logs"); 6 | 7 | const { 8 | sleep, 9 | getExportStatus, 10 | calcPrefix, 11 | checkEnvs, 12 | } = require("./functions"); 13 | 14 | const cwLogs = new CloudWatchLogsClient(); 15 | 16 | exports.handler = async function (event, context) { 17 | // Get env variables 18 | checkEnvs(["EXPORT_BUCKET", "LOG_GROUPS", "BUCKET_PREFIX"]); 19 | 20 | const EXPORT_BUCKET = process.env.EXPORT_BUCKET; 21 | const LOG_GROUPS = JSON.parse(process.env.LOG_GROUPS); // Array of log groups 22 | const BUCKET_PREFIX = process.env.BUCKET_PREFIX; 23 | const DAYS_BEFORE = process.env.DAYS_BEFORE || 1; 24 | const RETRY = process.env.RETRY || 5; 25 | const RETRY_TIMEOUT = process.env.RETRY_TIMEOUT || 5; 26 | 27 | const results = { 28 | success: [], 29 | failed: [], 30 | timeout: [], 31 | }; 32 | 33 | // Set date objects 34 | var today = new Date(); 35 | today.setUTCHours(0, 0, 0, 0); 36 | var fromDate = new Date(today); 37 | fromDate.setDate(today.getDate() - DAYS_BEFORE); 38 | 39 | // For each log group create export task 40 | for (const logGroup of LOG_GROUPS) { 41 | const parsedName = logGroup.replace(/\//g, "."); 42 | const prefix = calcPrefix(BUCKET_PREFIX, parsedName, today); 43 | 44 | const params = { 45 | destination: EXPORT_BUCKET, // required 46 | from: +fromDate, // required 47 | logGroupName: logGroup, // required 48 | to: +today, // required 49 | destinationPrefix: prefix, 50 | taskName: `export-${parsedName}`, 51 | }; 52 | 53 | try { 54 | // Create export task using v3 SDK command pattern 55 | const command = new CreateExportTaskCommand(params); 56 | const res = await cwLogs.send(command); 57 | console.log(`> Created exported task for ${logGroup}`); 58 | 59 | // Status checking, with retry logic 60 | for (let i = 0; i < RETRY; i++) { 61 | // Wait some seconds for it to complete 62 | await sleep(RETRY_TIMEOUT); 63 | 64 | console.log(`> Checking Status.. try No.${i}`); 65 | let currentStatus = await getExportStatus(res.taskId, cwLogs); 66 | console.log(currentStatus); 67 | 68 | if (["FAILED", "CANCELLED"].includes(currentStatus)) { 69 | throw new Error("Export status is FAILED or CANCELLED"); 70 | } 71 | if (currentStatus == "COMPLETED") { 72 | results.success.push(logGroup); 73 | break; 74 | } 75 | if (i == RETRY - 1) { 76 | results.timeout.push(logGroup); 77 | } 78 | } 79 | } catch (error) { 80 | console.log(`[X] ERROR exporting task ${logGroup}`, error); 81 | results.failed.push(logGroup); 82 | } 83 | await sleep(0.25); // Sleep to avoid throttling 84 | } 85 | 86 | console.log("> Lambda Task Completed: ", results); 87 | 88 | if (results.failed.length > 0) { 89 | throw new Error("Some tasks failed"); 90 | } 91 | 92 | return results; 93 | }; 94 | -------------------------------------------------------------------------------- /modules/log-s3-export/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | } 4 | 5 | variable "aws_region" { 6 | type = string 7 | } 8 | 9 | variable "log_groups_list" { 10 | type = list(string) 11 | } 12 | 13 | variable "bucket_prefix" { 14 | type = string 15 | validation { 16 | condition = substr(var.bucket_prefix, -1, 1) != "/" && substr(var.bucket_prefix, 0, 1) != "/" 17 | error_message = "The bucket_prefix must not end or begin with a '/'." 18 | } 19 | } 20 | variable "archive_class" { 21 | type = string 22 | default = "GLACIER" 23 | } 24 | variable "export_days_before" { 25 | type = number 26 | description = "Number of days to export logs from" 27 | default = 1 28 | } 29 | variable "days_to_archive" { 30 | type = number 31 | default = 180 32 | description = "Number of days to keep logs in S3 before moving to Glacier" 33 | } 34 | variable "days_to_expire" { 35 | type = number 36 | default = 365 37 | description = "Number of days to keep logs in S3 before expiring/deleting" 38 | } 39 | variable "check_retry_timeout" { 40 | type = number 41 | default = 5000 42 | description = "Number of milli-seconds to wait before retrying checking status of an export task" 43 | } 44 | variable "check_retry_attempts" { 45 | type = number 46 | default = 5 47 | description = "Number of times to retry checking status of an export task" 48 | } 49 | variable "lambda_timeout" { 50 | type = number 51 | default = 900 52 | description = "Timeout for the Lambda function that exports logs to S3" 53 | } 54 | variable "trigger_schedule_expression" { 55 | type = string 56 | default = "cron(1 0 * * ? *)" # Default to run once a day at midnight UTC 57 | description = "CloudWatch Event rule schedule expression for triggering the Lambda function" 58 | } -------------------------------------------------------------------------------- /modules/oidc-github/NOTICE: -------------------------------------------------------------------------------- 1 | This terraform module includes partial code from the `terraform-aws-oidc-github` library (https://github.com/unfunco/terraform-aws-oidc-github) which is licensed under the Apache License 2.0. 2 | -------------------------------------------------------------------------------- /modules/oidc-github/README.md: -------------------------------------------------------------------------------- 1 | This terraform module includes partial code from the `terraform-aws-oidc-github` library (https://github.com/unfunco/terraform-aws-oidc-github) which is licensed under the Apache License 2.0. 2 | 3 | 4 | 5 | ## Resources 6 | 7 | | Name | Type | 8 | |------|------| 9 | | [aws_iam_openid_connect_provider.github](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_openid_connect_provider) | resource | 10 | | [aws_iam_role.github](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 11 | | [aws_iam_role_policy_attachment.custom](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | 12 | | [aws_iam_openid_connect_provider.github](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_openid_connect_provider) | data source | 13 | | [aws_iam_policy_document.assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 14 | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | 15 | | [tls_certificate.github](https://registry.terraform.io/providers/hashicorp/tls/latest/docs/data-sources/certificate) | data source | 16 | 17 | ## Inputs 18 | 19 | | Name | Description | Type | Default | Required | 20 | |------|-------------|------|---------|:--------:| 21 | | github\_repositories | List of GitHub organization/repository names authorized to assume the role. | `list(string)` | n/a | yes | 22 | | iam\_role\_inline\_policies | Inline policies map with policy name as key and json as value. | `map(string)` | `{}` | no | 23 | | iam\_role\_name | Name of the IAM role to be created. This will be assumable by GitHub. | `string` | `"github"` | no | 24 | | iam\_role\_policy\_arns | List of IAM policy ARNs to attach to the IAM role. | `list(string)` | `[]` | no | 25 | | max\_session\_duration | Maximum session duration in seconds. | `number` | `3600` | no | 26 | | tags | Map of tags to be applied to all resources. | `map(string)` | `{}` | no | 27 | 28 | ## Outputs 29 | 30 | | Name | Description | 31 | |------|-------------| 32 | | iam\_role\_arn | ARN of the IAM role. | 33 | -------------------------------------------------------------------------------- /modules/oidc-github/data.tf: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Daniel Morris 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at: 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // 16 | // Modifications copyright (C) 2023 Vasilis Siourdas 17 | 18 | data "aws_partition" "current" {} 19 | 20 | data "aws_iam_policy_document" "assume_role" { 21 | statement { 22 | actions = ["sts:AssumeRoleWithWebIdentity"] 23 | effect = "Allow" 24 | 25 | condition { 26 | test = "StringLike" 27 | values = [ 28 | for repo in var.github_repositories : 29 | "repo:%{if length(regexall(":+", repo)) > 0}${repo}%{else}${repo}:*%{endif}" 30 | ] 31 | variable = "token.actions.githubusercontent.com:sub" 32 | } 33 | 34 | condition { 35 | test = "StringEquals" 36 | values = ["sts.amazonaws.com"] 37 | variable = "token.actions.githubusercontent.com:aud" 38 | } 39 | 40 | principals { 41 | identifiers = [local.oidc_provider_arn] 42 | type = "Federated" 43 | } 44 | } 45 | 46 | version = "2012-10-17" 47 | } 48 | 49 | data "tls_certificate" "github" { 50 | url = "https://token.actions.githubusercontent.com/.well-known/openid-configuration" 51 | } 52 | -------------------------------------------------------------------------------- /modules/oidc-github/main.tf: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Daniel Morris 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at: 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // 16 | // Modifications copyright (C) 2023 Vasilis Siourdas 17 | 18 | locals { 19 | github_organizations = toset([for repo in var.github_repositories : split("/", repo)[0]]) 20 | oidc_provider_arn = aws_iam_openid_connect_provider.github.arn 21 | partition = data.aws_partition.current.partition 22 | } 23 | 24 | resource "aws_iam_role" "github" { 25 | assume_role_policy = data.aws_iam_policy_document.assume_role.json 26 | description = "Role assumed by the GitHub OIDC provider." 27 | max_session_duration = var.max_session_duration 28 | name = var.iam_role_name 29 | tags = var.tags 30 | 31 | dynamic "inline_policy" { 32 | for_each = var.iam_role_inline_policies 33 | 34 | content { 35 | name = inline_policy.key 36 | policy = inline_policy.value 37 | } 38 | } 39 | } 40 | 41 | resource "aws_iam_role_policy_attachment" "custom" { 42 | count = length(var.iam_role_policy_arns) 43 | 44 | policy_arn = var.iam_role_policy_arns[count.index] 45 | role = aws_iam_role.github.id 46 | } 47 | 48 | resource "aws_iam_openid_connect_provider" "github" { 49 | client_id_list = concat( 50 | [for org in local.github_organizations : "https://github.com/${org}"], 51 | ["sts.amazonaws.com"] 52 | ) 53 | 54 | tags = var.tags 55 | url = "https://token.actions.githubusercontent.com" 56 | thumbprint_list = [data.tls_certificate.github.certificates[0].sha1_fingerprint] 57 | } 58 | -------------------------------------------------------------------------------- /modules/oidc-github/outputs.tf: -------------------------------------------------------------------------------- 1 | output "iam_role_arn" { 2 | depends_on = [aws_iam_role.github] 3 | description = "ARN of the IAM role." 4 | value = aws_iam_role.github.arn 5 | } 6 | -------------------------------------------------------------------------------- /modules/oidc-github/variables.tf: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Daniel Morris 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at: 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // 16 | // Modifications copyright (C) 2023 Vasilis Siourdas 17 | // 18 | 19 | variable "github_repositories" { 20 | description = "List of GitHub organization/repository names authorized to assume the role." 21 | type = list(string) 22 | 23 | validation { 24 | condition = length([ 25 | for repo in var.github_repositories : 1 26 | if length(regexall("^[A-Za-z0-9_.-]+?/([A-Za-z0-9_.:/-]+[*]?|\\*)$", repo)) > 0 27 | ]) == length(var.github_repositories) 28 | error_message = "Repositories must be specified in the organization/repository format." 29 | } 30 | } 31 | 32 | variable "iam_role_name" { 33 | default = "github" 34 | description = "Name of the IAM role to be created. This will be assumable by GitHub." 35 | type = string 36 | } 37 | 38 | variable "iam_role_policy_arns" { 39 | default = [] 40 | description = "List of IAM policy ARNs to attach to the IAM role." 41 | type = list(string) 42 | } 43 | 44 | variable "iam_role_inline_policies" { 45 | default = {} 46 | description = "Inline policies map with policy name as key and json as value." 47 | type = map(string) 48 | } 49 | 50 | variable "max_session_duration" { 51 | default = 3600 52 | description = "Maximum session duration in seconds." 53 | type = number 54 | 55 | validation { 56 | condition = var.max_session_duration >= 3600 && var.max_session_duration <= 43200 57 | error_message = "Maximum session duration must be between 3600 and 43200 seconds." 58 | } 59 | } 60 | 61 | variable "tags" { 62 | default = {} 63 | description = "Map of tags to be applied to all resources." 64 | type = map(string) 65 | } 66 | -------------------------------------------------------------------------------- /modules/oidc-github/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.0" 6 | } 7 | 8 | tls = { 9 | source = "hashicorp/tls" 10 | version = ">= 3.0" 11 | } 12 | } 13 | 14 | required_version = "~> 1.0" 15 | } 16 | -------------------------------------------------------------------------------- /modules/openvpn/README.md: -------------------------------------------------------------------------------- 1 | ## How to renew the openvpn server cert 2 | To renew the cert for the VPN you should do the following steps: 3 | **NOTE:** Replace everywhere ${domain} and ${env} with the correspondent ones. 4 | 5 | ### How to check if the certificate will expire: 6 | ``` 7 | openssl x509 -enddate -noout -in /etc/openvpn/pki/issued/${domain}.crt 8 | ``` 9 | 10 | First go to `/etc/openvpn/pki/issued` and create the `v3.ext` file to include in the cert the correct info. 11 | 12 | ``` 13 | [v3] 14 | authorityKeyIdentifier = keyid, issuer:always 15 | keyUsage = digitalSignature, keyEncipherment 16 | subjectAltName = DNS:${domain} 17 | extendedKeyUsage = serverAuth 18 | subjectKeyIdentifier = hash 19 | basicConstraints = CA:FALSE 20 | nsCertType = server 21 | nsComment = "OpenSSL Generated Server Certificate" 22 | ``` 23 | 24 | and then: 25 | 26 | ``` 27 | openssl x509 -req -days 1825 -in ../reqs/${domain}.req -signkey /etc/openvpn/pki/private/${domain}.key -out ${domain}.crt.new -CA ../ca.crt -CAkey ../private/ca.key -CAcreateserial -extfile v3.ext -extensions v3 -clrext 28 | ``` 29 | 30 | Rename the .new cert to rewrite the old one and reboot the dockers on the EC2: 31 | 32 | ``` 33 | mv ${domain}.crt.new ${domain}.crt 34 | systemctl restart docker-openvpn-1194-udp@${env}.service 35 | systemctl restart docker-openvpn-443-tcp@${env}.service 36 | ``` -------------------------------------------------------------------------------- /modules/openvpn/configuration-files.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_bucket_object" "oneloginconf" { 2 | bucket = aws_s3_bucket.configuration-bucket.id 3 | key = "openvpnconfig/onelogin.conf" 4 | content = templatefile("${path.module}/tpl/onelogin-conf.tpl", { 5 | subdomain = var.onelogin_client_domain 6 | client_id = var.onelogin_client_id 7 | client_secret = var.onelogin_client_secret 8 | }) 9 | etag = md5(templatefile("${path.module}/tpl/onelogin-conf.tpl", { 10 | subdomain = var.onelogin_client_domain 11 | client_id = var.onelogin_client_id 12 | client_secret = var.onelogin_client_secret 13 | })) 14 | } 15 | 16 | resource "aws_s3_bucket_object" "openvpn-vars" { 17 | bucket = aws_s3_bucket.configuration-bucket.id 18 | key = "openvpnconfig/vars" 19 | content = templatefile("${path.module}/tpl/openvpn-vars.tpl", { 20 | domain = var.vpn_domain 21 | req_email = var.cert_req_email 22 | req_city = var.cert_req_city 23 | req_province = var.cert_req_province 24 | req_country = var.cert_req_country 25 | req_org = var.certificate_organization_name 26 | }) 27 | etag = md5(templatefile("${path.module}/tpl/openvpn-vars.tpl", { 28 | domain = var.vpn_domain 29 | req_email = var.cert_req_email 30 | req_city = var.cert_req_city 31 | req_province = var.cert_req_province 32 | req_country = var.cert_req_country 33 | req_org = var.certificate_organization_name 34 | })) 35 | } 36 | 37 | resource "aws_s3_bucket_object" "openvpn-client" { 38 | bucket = aws_s3_bucket.configuration-bucket.id 39 | key = "openvpnconfig/openvpn-client.conf" 40 | content_base64 = var.open_vpn_client_file_base64 41 | etag = md5(var.open_vpn_client_file_base64) 42 | } 43 | 44 | resource "aws_s3_bucket_object" "openvpn-client-pki" { 45 | bucket = aws_s3_bucket.configuration-bucket.id 46 | key = "openvpn/pki/openvpn-client.conf" 47 | content_base64 = var.open_vpn_client_file_base64 48 | etag = md5(var.open_vpn_client_file_base64) 49 | } 50 | -------------------------------------------------------------------------------- /modules/openvpn/ecs-openvpn-access-iam.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "ecs-openvpn-access-task-role" { 2 | name = "ecs-openvpn-access-task-role-${var.env}" 3 | assume_role_policy = data.aws_iam_policy_document.ecs-openvpn-access-task-role-assume-policy.json 4 | } 5 | 6 | data "aws_iam_policy_document" "ecs-openvpn-access-task-role-assume-policy" { 7 | statement { 8 | actions = ["sts:AssumeRole"] 9 | 10 | principals { 11 | type = "Service" 12 | identifiers = ["ecs-tasks.amazonaws.com"] 13 | } 14 | } 15 | } 16 | 17 | resource "aws_iam_role_policy" "ecs-openvpn-access-task-role-policy" { 18 | name = "ecs-openvpn-access-task-role-policy-${var.env}" 19 | policy = data.aws_iam_policy_document.ecs-openvpn-access-task-role-policy.json 20 | role = aws_iam_role.ecs-openvpn-access-task-role.id 21 | } 22 | 23 | data "aws_iam_policy_document" "ecs-openvpn-access-task-role-policy" { 24 | statement { 25 | actions = ["s3:*"] 26 | effect = "Allow" 27 | resources = [ 28 | "${aws_s3_bucket.configuration-bucket.arn}/*", 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /modules/openvpn/ecs-openvpn-access.tf: -------------------------------------------------------------------------------- 1 | module "openvpn-access" { 2 | source = "git@github.com:in4it/terraform-modules.git//modules/ecs-service" 3 | vpc_id = var.vpc_id 4 | cluster_arn = aws_ecs_cluster.cluster.id 5 | execution_role_arn = aws_iam_role.ecs-task-execution-role.arn 6 | task_role_arn = aws_iam_role.ecs-openvpn-access-task-role.arn 7 | aws_region = data.aws_region.current.name 8 | healthcheck_matcher = "200,301" 9 | healthcheck_path = "/" 10 | cpu_reservation = "256" 11 | memory_reservation = "512" 12 | log_group = aws_cloudwatch_log_group.cloudwatch-ecs-openvpn-access.name 13 | desired_count = 1 14 | alb_arn = var.alb_arn 15 | launch_type = "FARGATE" 16 | platform_version = "1.4.0" 17 | fargate_service_subnetids = var.private_subnets 18 | deployment_controller = "ECS" 19 | enable_blue_green = false 20 | 21 | application_name = "openvpn-access" 22 | exposed_container_name = "openvpn-access" 23 | exposed_container_port = 8080 24 | 25 | containers = [ 26 | { 27 | ecr_url = var.openvpn_access_public_ecr 28 | application_name = "openvpn-access" 29 | application_port = "8080" 30 | host_port = null 31 | application_version = "latest" 32 | cpu_reservation = "256" 33 | memory_reservation = "512" 34 | environments = { 35 | STORAGE_TYPE = "S3" 36 | S3_BUCKET = aws_s3_bucket.configuration-bucket.id 37 | S3_PREFIX = "openvpn" 38 | AWS_REGION = data.aws_region.current.name 39 | } 40 | secrets = { 41 | OAUTH2_CLIENT_ID = var.ouath2_client_id_parameter_arn 42 | CSRF_KEY = var.csrf_key_parameter_arn 43 | CLIENT_CERT_ORG = "arn:aws:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:parameter/${var.project_name}-${var.env}/vpn/CLIENT_CERT_ORG" 44 | OAUTH2_CLIENT_SECRET = var.ouath2_client_secret_parameter_arn 45 | OAUTH2_REDIRECT_URL = "arn:aws:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:parameter/${var.project_name}-${var.env}/vpn/OAUTH2_REDIRECT_URL" 46 | OAUTH2_SCOPES = "arn:aws:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:parameter/${var.project_name}-${var.env}/vpn/OAUTH2_SCOPES" 47 | OAUTH2_URL = "arn:aws:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:parameter/${var.project_name}-${var.env}/vpn/OAUTH2_URL" 48 | } 49 | } 50 | ] 51 | 52 | ingress_rules = [ 53 | { 54 | from_port = 8080 55 | to_port = 8080 56 | protocol = "tcp" 57 | security_groups = [var.alb_security_group_id] 58 | } 59 | ] 60 | } 61 | 62 | resource "aws_cloudwatch_log_group" "cloudwatch-ecs-openvpn-access" { 63 | name = "ecs-${var.project_name}-openvpn-access-${var.env}" 64 | retention_in_days = var.log_retention_days 65 | } 66 | -------------------------------------------------------------------------------- /modules/openvpn/ecs.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecs_cluster" "cluster" { 2 | name = "${var.project_name}-vpn-${var.env}" 3 | } 4 | 5 | resource "aws_cloudwatch_log_group" "cluster" { 6 | name = "${var.project_name}-vpn-${var.env}" 7 | retention_in_days = var.log_retention_days 8 | } 9 | 10 | resource "aws_iam_role" "ecs-task-execution-role" { 11 | name = "ecs-task-execution-role-${var.project_name}-vpn-${var.env}" 12 | 13 | assume_role_policy = < 15 | [KEY] 16 | 17 | 18 | [CERT] 19 | 20 | 21 | [CA] 22 | 23 | key-direction 1 24 | 25 | [TLS-AUTH] 26 | 27 | 28 | -------------------------------------------------------------------------------- /modules/openvpn/lb-rules.tf: -------------------------------------------------------------------------------- 1 | module "alb-rule-openvpn-access" { 2 | source = "git@github.com:in4it/terraform-modules.git//modules/alb-rule" 3 | listener_arn = var.alb_https_listener_arn 4 | 5 | priority = var.alb_route_priority 6 | target_group_arn = module.openvpn-access.target_group_arn 7 | condition_field = "host-header" 8 | condition_values = [var.app_domain] 9 | } 10 | 11 | resource "aws_route53_record" "vpn-app-alb-record" { 12 | count = var.create_r53_records ? 1 : 0 13 | 14 | allow_overwrite = true 15 | name = var.app_domain 16 | type = "A" 17 | 18 | alias { 19 | evaluate_target_health = false 20 | name = var.alb_dns_name 21 | zone_id = var.alb_dns_zone_id 22 | } 23 | zone_id = var.hosted_zone_id 24 | } 25 | 26 | 27 | resource "aws_route53_record" "vpn-alb-record" { 28 | count = var.create_r53_records ? 1 : 0 29 | 30 | allow_overwrite = true 31 | name = var.vpn_domain 32 | type = "A" 33 | records = [aws_eip.vpn_ip.public_ip] 34 | ttl = 300 35 | 36 | zone_id = var.hosted_zone_id 37 | } 38 | -------------------------------------------------------------------------------- /modules/openvpn/output.tf: -------------------------------------------------------------------------------- 1 | output "vpn-sg" { 2 | value = aws_security_group.vpn-instance.id 3 | } 4 | -------------------------------------------------------------------------------- /modules/openvpn/s3-configuration.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_bucket" "configuration-bucket" { 2 | bucket = "${var.project_name}-configuration-${var.env}" 3 | 4 | lifecycle { 5 | prevent_destroy = true 6 | } 7 | } 8 | 9 | resource "aws_s3_bucket_public_access_block" "configuration-bucket" { 10 | bucket = aws_s3_bucket.configuration-bucket.id 11 | 12 | block_public_acls = true 13 | block_public_policy = true 14 | ignore_public_acls = true 15 | restrict_public_buckets = true 16 | } 17 | 18 | resource "aws_s3_bucket_versioning" "configuration-bucket" { 19 | bucket = aws_s3_bucket.configuration-bucket.id 20 | versioning_configuration { 21 | status = "Enabled" 22 | } 23 | } 24 | 25 | resource "aws_s3_bucket_server_side_encryption_configuration" "configuration-bucket" { 26 | bucket = aws_s3_bucket.configuration-bucket.id 27 | 28 | rule { 29 | apply_server_side_encryption_by_default { 30 | sse_algorithm = "AES256" 31 | } 32 | } 33 | } 34 | 35 | resource "aws_s3_bucket_policy" "configuration-bucket" { 36 | bucket = aws_s3_bucket.configuration-bucket.id 37 | policy = < >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1 3 | 4 | #login to enter in aws cli 5 | curl http://169.254.169.254/latest/meta-data/iam/info 6 | 7 | apt-get update 8 | mkdir -p /etc/openvpn 9 | apt-get -y install docker.io awscli 10 | 11 | aws s3 sync s3://${project_name}-configuration-${env}/openvpn /etc/openvpn --endpoint https://s3.${aws_region}.amazonaws.com --region ${aws_region} --exclude "*issued/client*" --exclude "*private/client*" 12 | aws s3 cp s3://${project_name}-configuration-${env}/openvpnconfig/openvpn-client.conf /etc/openvpn/openvpn-client.conf --endpoint https://s3.${aws_region}.amazonaws.com --region ${aws_region} 13 | 14 | # Get onelogin auth 15 | aws s3 cp s3://${project_name}-configuration-${env}/openvpnconfig/onelogin.conf /etc/openvpn/onelogin.conf --endpoint https://s3.${aws_region}.amazonaws.com --region ${aws_region} 16 | chown nobody:nogroup /etc/openvpn/onelogin.conf 17 | chmod 600 /etc/openvpn/onelogin.conf 18 | 19 | %{ for listener in listeners ~} 20 | cat > /etc/systemd/system/docker-openvpn-${listener.port}-${listener.protocol}@.service << 'EOF' 21 | [Unit] 22 | Description=OpenVPN Docker Container 23 | Documentation=https://github.com/kylemanna/docker-openvpn 24 | After=network.target docker.service 25 | Requires=docker.service 26 | 27 | [Service] 28 | RestartSec=10 29 | Restart=always 30 | 31 | ExecStartPre=-/usr/bin/docker rm -f openvpn-${listener.port}-${listener.protocol} 32 | 33 | ExecStart=/usr/bin/docker run --privileged --log-driver=awslogs --log-opt awslogs-region=${aws_region} --log-opt awslogs-group=${log_group} -v /etc/openvpn:/etc/openvpn -p ${listener.port}:1194/${listener.protocol} --cap-add=NET_ADMIN --name openvpn-${listener.port}-${listener.protocol} ${openvpn_public_ecr} 34 | 35 | [Install] 36 | WantedBy=multi-user.target 37 | EOF 38 | %{ endfor ~} 39 | 40 | sleep 3 41 | aws ecr get-login-password --region ${aws_region} --endpoint https://api.ecr.${aws_region}.amazonaws.com 42 | if [ ! -e /etc/openvpn/openvpn.conf ]; then 43 | echo "No config files found, generating...." 44 | aws s3 cp s3://${project_name}-configuration-${env}/openvpnconfig/vars /etc/openvpn/vars --endpoint https://s3.${aws_region}.amazonaws.com --region ${aws_region} 45 | docker run -v /etc/openvpn:/etc/openvpn --log-driver=none ${openvpn_public_ecr} ovpn_genconfig -u udp://${domain} 46 | docker run -v /etc/openvpn:/etc/openvpn --rm --log-driver=none -e EASYRSA_VARS_FILE=/etc/openvpn/vars ${openvpn_public_ecr} ovpn_initpki nopass 47 | echo "#Auth Plugin" >> /etc/openvpn/openvpn.conf 48 | echo "auth-user-pass-verify /bin/openvpn-onelogin-auth via-env" >> /etc/openvpn/openvpn.conf 49 | echo "script-security 3" >> /etc/openvpn/openvpn.conf 50 | echo "reneg-sec ${reneg_sec}" >> /etc/openvpn/openvpn.conf 51 | %{ for listener in listeners ~} 52 | systemctl enable --now docker-openvpn-${listener.port}-${listener.protocol}@${env} 53 | %{ endfor ~} 54 | aws s3 sync /etc/openvpn s3://${project_name}-configuration-${env}/openvpn --endpoint https://s3.${aws_region}.amazonaws.com --region ${aws_region} 55 | else 56 | echo "Config files found, starting OpenVPN..." 57 | %{ for listener in listeners ~} 58 | systemctl enable --now docker-openvpn-${listener.port}-${listener.protocol}@${env} 59 | %{ endfor ~} 60 | fi 61 | -------------------------------------------------------------------------------- /modules/openvpn/vpn-instance-iam.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_instance_profile" "vpn_iam_instance_profile" { 2 | name = "${var.project_name}-vpn-iam-instance-profile-${var.env}" 3 | role = aws_iam_role.vpn-iam-role.name 4 | } 5 | 6 | resource "aws_iam_role" "vpn-iam-role" { 7 | name = "${var.project_name}-vpn-iam-role-${var.env}" 8 | path = "/" 9 | 10 | assume_role_policy = < 0 ? module.secret[0].secret_arn : null 37 | } 38 | output "db-secret-name" { 39 | value = length(module.secret) > 0 ? module.secret[0].secret_name : null 40 | } 41 | output "db-secret-id" { 42 | value = length(module.secret) > 0 ? module.secret[0].secret_id : null 43 | } 44 | -------------------------------------------------------------------------------- /modules/rds/secret/main.tf: -------------------------------------------------------------------------------- 1 | resource "random_password" "password" { 2 | length = var.password_length 3 | special = true 4 | override_special = var.password_override_special 5 | } 6 | 7 | resource "aws_secretsmanager_secret" "secret" { 8 | name = var.name 9 | description = var.description 10 | } 11 | 12 | resource "aws_secretsmanager_secret_version" "secret_value" { 13 | secret_id = aws_secretsmanager_secret.secret.id 14 | secret_string = jsonencode({ 15 | username = var.username 16 | password = var.password == null ? random_password.password.result : var.password 17 | engine = var.engine 18 | host = var.host 19 | port = var.port 20 | dbname = var.dbname 21 | dbInstanceIdentifier = var.dbInstanceIdentifier 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /modules/rds/secret/output.tf: -------------------------------------------------------------------------------- 1 | output "secret_id" { 2 | value = aws_secretsmanager_secret.secret.id 3 | } 4 | 5 | output "secret_name" { 6 | value = aws_secretsmanager_secret.secret.name 7 | } 8 | 9 | output "secret_arn" { 10 | value = aws_secretsmanager_secret.secret.arn 11 | } 12 | -------------------------------------------------------------------------------- /modules/rds/secret/vars.tf: -------------------------------------------------------------------------------- 1 | variable "password_length" { 2 | default = 16 3 | } 4 | variable "password_override_special" { 5 | description = "override special characters used for rds password" 6 | default = "!#$%&*()-_=+[]{}<>:?" 7 | } 8 | 9 | variable "name" { 10 | type = string 11 | } 12 | variable "description" { 13 | default = "" 14 | } 15 | 16 | variable "username" { 17 | type = string 18 | } 19 | variable "engine" { 20 | type = string 21 | 22 | validation { 23 | condition = can(regex("^(mysql|postgres|mariadb|oracle|sqlserver|mongo|redshift)$", var.engine)) 24 | error_message = "Engine must be one of mysql, postgres, mariadb, oracle, sqlserver, mongo, redshift" 25 | } 26 | } 27 | variable "host" { 28 | type = string 29 | } 30 | variable "port" { 31 | type = number 32 | } 33 | variable "dbname" { 34 | type = string 35 | } 36 | variable "dbInstanceIdentifier" { 37 | default = "" 38 | type = string 39 | description = "Usually the prefix of the RDS host. Identifier inside AWS region of DB." 40 | } 41 | variable "password" { 42 | type = string 43 | default = null 44 | description = "If not set, a random password will be generated." 45 | } 46 | -------------------------------------------------------------------------------- /modules/rds/securitygroups.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | port = contains(["mysql", "mariadb"], data.aws_rds_engine_version.rds_version.engine) ? 3306 : 5432 3 | } 4 | resource "aws_security_group" "rds" { 5 | name = "${var.name}-rds" 6 | vpc_id = var.vpc_id 7 | description = "${var.name}-rds" 8 | 9 | ingress { 10 | from_port = local.port 11 | to_port = local.port 12 | protocol = "tcp" 13 | security_groups = var.ingress_security_groups 14 | self = var.allow_self 15 | } 16 | 17 | egress { 18 | from_port = 0 19 | to_port = 0 20 | protocol = "-1" 21 | cidr_blocks = ["0.0.0.0/0"] 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /modules/rds/vars.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "RDS name" 3 | default = "mydb" 4 | } 5 | variable "username" { 6 | description = "RDS username" 7 | } 8 | variable "database_name" { 9 | description = "RDS database name" 10 | } 11 | variable "at_rest_encryption" { 12 | description = "enable at rest encryption with KMS" 13 | default = true 14 | } 15 | variable "storage" { 16 | description = "RDS storage in GB" 17 | default = 100 18 | } 19 | variable "max_allocated_storage" { 20 | description = "If greater than storage it will enable storage autoscaling" 21 | default = 0 22 | } 23 | variable "storage_type" { 24 | description = "RDS storage type" 25 | default = "gp2" 26 | } 27 | variable "instance_type" { 28 | description = "RDS instance type" 29 | } 30 | variable "engine" { 31 | description = "RDS engine" 32 | } 33 | variable "engine_version" { 34 | description = "RDS engine version" 35 | } 36 | variable "vpc_id" { 37 | description = " vpc id" 38 | } 39 | variable "subnet_ids" { 40 | description = "subnet ids to launch RDS in" 41 | default = [] 42 | } 43 | variable "subnet_group" { 44 | description = "subnet group to launch RDS in" 45 | default = "" 46 | } 47 | variable "ingress_security_groups" { 48 | description = "Security groups to allow" 49 | default = [] 50 | } 51 | variable "parameters" { 52 | description = "rds parameters to set" 53 | default = [] 54 | type = list(object({ 55 | name = string 56 | value = string 57 | pending_reboot = optional(bool) 58 | })) 59 | } 60 | variable "multi_az" { 61 | default = false 62 | } 63 | variable "backup_retention_period" { 64 | description = "RDS backup retention period" 65 | default = 30 66 | } 67 | variable "iam_database_authentication_enabled" { 68 | description = "enable iam auth" 69 | default = true 70 | } 71 | variable "performance_insight_enabled" { 72 | description = "Enable Performance Insight" 73 | default = false 74 | } 75 | variable "deletion_protection" { 76 | description = "Enable Deletion Protection" 77 | default = true 78 | } 79 | variable "set_password" { 80 | description = "if true, set a random password" 81 | default = true 82 | } 83 | variable "create_secret" { 84 | description = "if true, create a Secret Manager secret with db credentials" 85 | default = false 86 | } 87 | variable "allow_self" { 88 | description = "if true, allows traffic from self" 89 | default = false 90 | } 91 | 92 | variable "password_override_special" { 93 | description = "override special characters used for rds password" 94 | default = "!#$%&*()-_=+[]{}<>:?" 95 | } 96 | variable "allow_major_version_upgrade" { 97 | description = "When upgrading the major version of an engine, allow_major_version_upgrade must be set to true." 98 | default = false 99 | } 100 | 101 | variable "initial_snapshot_id" { 102 | type = string 103 | description = "Initial (Decrypted) snapshot DB to restore from (useful for restoring from a different region or account)" 104 | default = "" 105 | } 106 | -------------------------------------------------------------------------------- /modules/s3-replica/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = ">= 5.40.0" 6 | configuration_aliases = [ aws.source, aws.destination ] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /modules/s3-replica/outputs.tf: -------------------------------------------------------------------------------- 1 | output "source_bucket_arn" { 2 | value = local.source_bucket_arn 3 | } 4 | output "source_bucket_name" { 5 | value = local.source_bucket_name 6 | } 7 | output "source_bucket_regional_domain_name" { 8 | value = local.source_bucket_regional_domain 9 | } 10 | 11 | output "destination_bucket_arn" { 12 | value = module.destination.bucket_arn 13 | } 14 | output "destination_bucket_name" { 15 | value = module.destination.bucket_name 16 | } 17 | output "destination_bucket_regional_domain_name" { 18 | value = module.destination.bucket_regional_domain_name 19 | } 20 | -------------------------------------------------------------------------------- /modules/s3-replica/replication-iam.tf: -------------------------------------------------------------------------------- 1 | data "aws_iam_policy_document" "s3_assume_role" { 2 | statement { 3 | effect = "Allow" 4 | 5 | principals { 6 | type = "Service" 7 | identifiers = ["s3.amazonaws.com"] 8 | } 9 | 10 | actions = ["sts:AssumeRole"] 11 | } 12 | } 13 | 14 | resource "aws_iam_role" "replication" { 15 | name = "${var.bucket_name}-ReplicationRole" 16 | assume_role_policy = data.aws_iam_policy_document.s3_assume_role.json 17 | } 18 | 19 | data "aws_iam_policy_document" "replication" { 20 | statement { 21 | effect = "Allow" 22 | 23 | actions = [ 24 | "s3:GetReplicationConfiguration", 25 | "s3:ListBucket", 26 | ] 27 | 28 | resources = [local.source_bucket_arn] 29 | } 30 | 31 | statement { 32 | effect = "Allow" 33 | 34 | actions = [ 35 | "s3:GetObjectVersionForReplication", 36 | "s3:GetObjectVersionAcl", 37 | "s3:GetObjectVersionTagging", 38 | ] 39 | 40 | resources = ["${local.source_bucket_arn}/*"] 41 | } 42 | 43 | statement { 44 | effect = "Allow" 45 | 46 | actions = [ 47 | "s3:ReplicateObject", 48 | "s3:ReplicateDelete", 49 | "s3:ReplicateTags", 50 | ] 51 | 52 | resources = ["${module.destination.bucket_arn}/*"] 53 | } 54 | } 55 | 56 | resource "aws_iam_role_policy" "replication" { 57 | name = "${var.bucket_name}-ReplicationPolicy" 58 | policy = data.aws_iam_policy_document.replication.json 59 | role = aws_iam_role.replication.id 60 | } 61 | -------------------------------------------------------------------------------- /modules/s3-replica/s3-replica.tf: -------------------------------------------------------------------------------- 1 | module "destination" { 2 | source = "git@github.com:in4it/terraform-modules.git//modules/s3" 3 | providers = { 4 | aws = aws.destination 5 | } 6 | name = "${var.bucket_name}${var.replica_suffix}" 7 | versioning = true 8 | 9 | lifecycle_rules = var.lifecycle_rules 10 | additional_policy_statements = var.additional_policy_statements 11 | cloudfront_origins = var.cloudfront_origins 12 | public_access_block = var.public_access_block 13 | } 14 | 15 | resource "aws_s3_bucket_replication_configuration" "replication" { 16 | provider = aws.source 17 | depends_on = [module.source, module.destination] 18 | 19 | role = aws_iam_role.replication.arn 20 | bucket = local.source_bucket_name 21 | 22 | rule { 23 | status = "Enabled" 24 | destination { 25 | bucket = module.destination.bucket_arn 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /modules/s3-replica/s3-source.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | source_bucket_arn = var.create_source ? module.source[0].bucket_arn : data.aws_s3_bucket.source[0].arn 3 | source_bucket_name = var.create_source ? module.source[0].bucket_name : data.aws_s3_bucket.source[0].bucket 4 | source_bucket_regional_domain = var.create_source ? module.source[0].bucket_regional_domain_name : data.aws_s3_bucket.source[0].bucket_regional_domain_name 5 | } 6 | 7 | module "source" { 8 | count = var.create_source ? 1 : 0 9 | source = "git@github.com:in4it/terraform-modules.git//modules/s3" 10 | providers = { 11 | aws = aws.source 12 | } 13 | name = var.bucket_name 14 | versioning = true 15 | 16 | lifecycle_rules = var.lifecycle_rules 17 | additional_policy_statements = var.additional_policy_statements 18 | cloudfront_origins = var.cloudfront_origins 19 | public_access_block = var.public_access_block 20 | } 21 | 22 | data "aws_s3_bucket" "source" { 23 | count = var.create_source ? 0 : 1 24 | bucket = var.bucket_name 25 | provider = aws.source 26 | } 27 | -------------------------------------------------------------------------------- /modules/s3-replica/variables.tf: -------------------------------------------------------------------------------- 1 | variable "bucket_name" { 2 | description = "name of the s3 bucket" 3 | } 4 | 5 | variable "versioning" { 6 | description = "enable s3 versioning" 7 | default = true 8 | } 9 | 10 | variable "cloudfront_origins" { 11 | description = < 0 ? aws_kms_key.ssm-parameters[0].key_id : null 3 | } 4 | output "kms-key-arn" { 5 | value = length(aws_kms_key.ssm-parameters) > 0 ? aws_kms_key.ssm-parameters[0].arn : null 6 | } 7 | -------------------------------------------------------------------------------- /modules/ssm-parameters/parameters.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ssm_parameter" "parameters" { 2 | for_each = { for p in var.parameters : p.name => p } 3 | name = "${var.prefix}${each.key}" 4 | type = each.value.type 5 | value = each.value.value 6 | key_id = var.at_rest_encryption ? aws_kms_key.ssm-parameters[0].id : "" 7 | } 8 | 9 | -------------------------------------------------------------------------------- /modules/ssm-parameters/vars.tf: -------------------------------------------------------------------------------- 1 | variable "prefix" { 2 | description = "Prefix to use in the parameter store" 3 | default = "" 4 | } 5 | variable "parameters" { 6 | description = "list of parameters" 7 | default = [] 8 | type = list(object({ 9 | name = string 10 | type = string 11 | value = string 12 | })) 13 | } 14 | 15 | variable "at_rest_encryption" { 16 | description = "at rest encryption with KMS" 17 | default = true 18 | } 19 | -------------------------------------------------------------------------------- /modules/ssm-replicator/continuous-replica-code.tf: -------------------------------------------------------------------------------- 1 | data "archive_file" "continuous-replica" { 2 | type = "zip" 3 | output_path = "${path.module}/continuous.zip" 4 | source_content_filename = "continuous.py" 5 | source_content = < 0 ? 1 : 0 15 | key_id = aws_kms_key.terraform-state[0].id 16 | policy = data.aws_iam_policy_document.terraform-state.json 17 | } 18 | 19 | data "aws_iam_policy_document" "terraform-state" { 20 | statement { 21 | sid = "Enable IAM User Permissions" 22 | effect = "Allow" 23 | actions = ["kms:*"] 24 | resources = ["*"] 25 | 26 | dynamic "principals" { 27 | for_each = var.principals 28 | content { 29 | type = principals.value.type 30 | identifiers = principals.value.identifiers 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /modules/terraform-backend/output.tf: -------------------------------------------------------------------------------- 1 | output "kms-key-id" { 2 | value = var.kms-encryption ? aws_kms_key.terraform-state[0].id : null 3 | } 4 | 5 | output "kms-key-arn" { 6 | value = var.kms-encryption ? aws_kms_key.terraform-state[0].arn : null 7 | } 8 | 9 | output "s3-bucket" { 10 | value = aws_s3_bucket.infrastructure.bucket 11 | } 12 | 13 | output "lock-table-name" { 14 | value = var.lock_table_enabled ? aws_dynamodb_table.terraform-state-lock[0].name : null 15 | } 16 | -------------------------------------------------------------------------------- /modules/terraform-backend/s3.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_bucket" "infrastructure" { 2 | bucket = "${var.project}-terraform-${var.env}" 3 | 4 | tags = { 5 | Name = "${var.project} ${var.env} infrastructure bucket" 6 | Project = var.project 7 | Environment = var.env 8 | } 9 | } 10 | 11 | data "aws_iam_policy_document" "terraform-state-storage" { 12 | policy_id = "terraform-s3-${var.env}-state-policy" 13 | statement { 14 | sid = "AllowSSLRequestsOnly" 15 | principals { 16 | type = "*" 17 | identifiers = ["*"] 18 | } 19 | effect = "Deny" 20 | actions = ["s3:*"] 21 | resources = [ 22 | "arn:aws:s3:::${var.project}-terraform-${var.env}/*", 23 | "arn:aws:s3:::${var.project}-terraform-${var.env}" 24 | ] 25 | condition { 26 | test = "Bool" 27 | variable = "aws:SecureTransport" 28 | values = ["false"] 29 | } 30 | } 31 | } 32 | 33 | resource "aws_s3_bucket_policy" "infrastructure" { 34 | bucket = aws_s3_bucket.infrastructure.id 35 | policy = data.aws_iam_policy_document.terraform-state-storage.json 36 | } 37 | 38 | resource "aws_s3_bucket_versioning" "infrastructure" { 39 | bucket = aws_s3_bucket.infrastructure.id 40 | versioning_configuration { 41 | status = "Enabled" 42 | } 43 | } 44 | 45 | resource "aws_s3_bucket_public_access_block" "infrastructure" { 46 | bucket = aws_s3_bucket.infrastructure.id 47 | 48 | block_public_acls = true 49 | block_public_policy = true 50 | ignore_public_acls = true 51 | restrict_public_buckets = true 52 | } 53 | 54 | resource "aws_s3_bucket_server_side_encryption_configuration" "infrastructure" { 55 | bucket = aws_s3_bucket.infrastructure.id 56 | rule { 57 | apply_server_side_encryption_by_default { 58 | sse_algorithm = length(aws_kms_key.terraform-state) > 0 ? "aws:kms" : "AES256" 59 | kms_master_key_id = length(aws_kms_key.terraform-state) > 0 ? aws_kms_key.terraform-state[0].id : null 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /modules/terraform-backend/vars.tf: -------------------------------------------------------------------------------- 1 | variable "env" { 2 | default = "dev" 3 | description = "The environment for the terraform backend" 4 | } 5 | 6 | variable "project" { 7 | type = string 8 | description = "The name of the project/product" 9 | } 10 | 11 | variable "lock_table_enabled" { 12 | default = true 13 | description = "True - to enable locking of the state file with DynamoDB" 14 | } 15 | 16 | variable "kms-encryption" { 17 | default = true 18 | description = "True - to Create KMS key to use for encrypting the S3 bucket. Otherwise - AES256 will be used" 19 | } 20 | 21 | variable "kms-deletion-window" { 22 | default = 30 23 | description = "The duration in days after which the key is deleted after destruction of the resource, must be between 7 and 30 days" 24 | validation { 25 | condition = var.kms-deletion-window >= 7 && var.kms-deletion-window <= 30 26 | error_message = "kms-deletion-window must be between 7 and 30 days" 27 | } 28 | } 29 | 30 | variable "principals" { 31 | description = "List of principals allowed to use kms key" 32 | type = list(object({ 33 | type = string 34 | identifiers = list(string) 35 | })) 36 | default = [] 37 | } 38 | -------------------------------------------------------------------------------- /modules/tgw-routes/route.tf: -------------------------------------------------------------------------------- 1 | resource "aws_route" "source-route" { 2 | for_each = {for route-table in var.source_route_tables: route-table => true } 3 | route_table_id = each.key 4 | destination_cidr_block = var.destination_cidr 5 | transit_gateway_id = var.transit_gateway_id 6 | } 7 | 8 | resource "aws_route" "destination-route" { 9 | for_each = {for route-table in var.destination_route_tables: route-table => true } 10 | route_table_id = each.key 11 | destination_cidr_block = var.source_cidr 12 | transit_gateway_id = var.transit_gateway_id 13 | } 14 | -------------------------------------------------------------------------------- /modules/tgw-routes/variables.tf: -------------------------------------------------------------------------------- 1 | variable "source_route_tables" { 2 | description = "source vpc route tables" 3 | } 4 | 5 | variable "destination_route_tables" { 6 | description = "destination vpc route tables" 7 | } 8 | 9 | variable "source_cidr" { 10 | 11 | } 12 | variable "destination_cidr" { 13 | 14 | } 15 | 16 | variable "transit_gateway_id" { 17 | 18 | } -------------------------------------------------------------------------------- /modules/waf/README.md: -------------------------------------------------------------------------------- 1 | ## Requirements 2 | 3 | No requirements. 4 | 5 | ## Providers 6 | 7 | | Name | Version | 8 | |------|---------| 9 | | [aws](#provider\_aws) | n/a | 10 | 11 | ## Modules 12 | 13 | No modules. 14 | 15 | ## Resources 16 | 17 | | Name | Type | 18 | |------|------| 19 | | [aws_wafv2_ip_set.ratelimit_ipset](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_ip_set) | resource | 20 | | [aws_wafv2_web_acl.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl) | resource | 21 | | [aws_wafv2_web_acl_association.alb-waf-association](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl_association) | resource | 22 | 23 | ## Inputs 24 | 25 | | Name | Description | Type | Default | Required | 26 | |------|-------------|------|---------|:--------:| 27 | | [env](#input\_env) | optional environment | `string` | `""` | no | 28 | | [lb\_arns](#input\_lb\_arns) | ARN of ALBs to associate with WAF | `set(string)` | `[]` | no | 29 | | [managed\_rules](#input\_managed\_rules) | managed rules |
list(object({
name = string
priority = number
managed_rule_name = string
managed_rule_vendor_name = string
block = bool
blocking_rules = optional(list(string))
allowing_rules = optional(list(string))
counting_rules = optional(list(string))
}))
| `[]` | no | 30 | | [name](#input\_name) | name of WAF | `string` | `"alb-waf"` | no | 31 | | [ratelimit\_rules](#input\_ratelimit\_rules) | ratelimiting rules |
list(object({
name = string
limit = number
priority = number
exclude_ip_ranges = list(string)
block = bool
}))
| `[]` | no | 32 | | [regex\_match\_rules](#input\_regex\_match\_rules) | n/a |
list(object({
name = string
priority = number
action = string # "count" or "block"
statement = any
rule_label = optional(list(string), null)
}))
| `[]` | no | 33 | | [scope](#input\_scope) | scope of WAF, use 'CLOUDFRONT' for CloudFront distributions | `string` | `"REGIONAL"` | no | 34 | 35 | ## Outputs 36 | 37 | No outputs. 38 | -------------------------------------------------------------------------------- /modules/waf/associations.tf: -------------------------------------------------------------------------------- 1 | resource "aws_wafv2_web_acl_association" "alb-waf-association" { 2 | for_each = var.lb_arns 3 | resource_arn = each.value 4 | web_acl_arn = aws_wafv2_web_acl.this.arn 5 | } 6 | -------------------------------------------------------------------------------- /modules/waf/vars.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "name of WAF" 3 | default = "alb-waf" 4 | } 5 | variable "scope" { 6 | description = "scope of WAF, use 'CLOUDFRONT' for CloudFront distributions" 7 | default = "REGIONAL" 8 | validation { 9 | condition = var.scope == "REGIONAL" || var.scope == "CLOUDFRONT" 10 | error_message = "scope must be REGIONAL or CLOUDFRONT" 11 | } 12 | } 13 | variable "lb_arns" { 14 | type = set(string) 15 | description = "ARN of ALBs to associate with WAF" 16 | default = [] 17 | } 18 | 19 | variable "env" { 20 | description = "optional environment" 21 | default = "" 22 | } 23 | 24 | variable "ratelimit_rules" { 25 | description = "ratelimiting rules" 26 | type = list(object({ 27 | name = string 28 | limit = number 29 | priority = number 30 | exclude_ip_ranges = list(string) 31 | block = bool 32 | })) 33 | default = [] 34 | } 35 | variable "managed_rules" { 36 | description = "managed rules" 37 | type = list(object({ 38 | name = string 39 | priority = number 40 | managed_rule_name = string 41 | managed_rule_vendor_name = string 42 | block = bool 43 | blocking_rules = optional(list(string)) 44 | allowing_rules = optional(list(string)) 45 | counting_rules = optional(list(string)) 46 | })) 47 | default = [] 48 | } 49 | variable "regex_match_rules" { 50 | type = list(object({ 51 | name = string 52 | priority = number 53 | action = string # "count" or "block" 54 | statement = any 55 | rule_label = optional(list(string), null) 56 | })) 57 | default = [] 58 | } 59 | -------------------------------------------------------------------------------- /modules/wireguard-legacy/config-files.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_object" "docker-compose" { 2 | bucket = aws_s3_bucket.configuration-bucket.id 3 | key = "firezone/docker-compose.yml" 4 | source = "${path.module}/templates/docker-compose.yml" 5 | etag = filemd5("${path.module}/templates/docker-compose.yml") 6 | } -------------------------------------------------------------------------------- /modules/wireguard-legacy/efs.tf: -------------------------------------------------------------------------------- 1 | resource "aws_efs_file_system" "vpn" { 2 | performance_mode = "generalPurpose" 3 | 4 | tags = { 5 | Name = "vpn" 6 | } 7 | } 8 | 9 | resource "aws_efs_mount_target" "vpn" { 10 | count = 2 11 | 12 | file_system_id = aws_efs_file_system.vpn.id 13 | subnet_id = var.efs_subnet_ids[count.index] 14 | security_groups = [aws_security_group.vpn-efs.id] 15 | } 16 | 17 | resource "aws_security_group" "vpn-efs" { 18 | name = "vpn-efs" 19 | description = "vpn-efs" 20 | vpc_id = var.vpc_id 21 | 22 | ingress { 23 | from_port = 2049 24 | protocol = "tcp" 25 | to_port = 2049 26 | security_groups = [aws_security_group.vpn-instance.id] 27 | description = "Allow traffic from vpn to efs" 28 | } 29 | 30 | egress { 31 | from_port = 0 32 | protocol = "-1" 33 | to_port = 0 34 | cidr_blocks = ["0.0.0.0/0"] 35 | } 36 | 37 | tags = { 38 | Name = "vpn-efs" 39 | } 40 | } -------------------------------------------------------------------------------- /modules/wireguard-legacy/output.tf: -------------------------------------------------------------------------------- 1 | output "vpn-ip" { 2 | value = aws_eip.vpn_ip.public_ip 3 | } 4 | -------------------------------------------------------------------------------- /modules/wireguard-legacy/rds.tf: -------------------------------------------------------------------------------- 1 | module "vpn-rds" { 2 | source = "github.com/in4it/terraform-modules//modules/rds" 3 | name = "vpn" 4 | storage = "20" 5 | storage_type = "gp2" 6 | engine = "postgres" 7 | engine_version = "15.2" 8 | username = "vpn" 9 | database_name = "vpn" 10 | vpc_id = var.vpc_id 11 | instance_type = var.db_instance_type 12 | subnet_group = "" 13 | subnet_ids = var.db_subnet_ids 14 | 15 | ingress_security_groups = [aws_security_group.vpn-instance.id] 16 | } 17 | -------------------------------------------------------------------------------- /modules/wireguard-legacy/s3.tf: -------------------------------------------------------------------------------- 1 | 2 | resource "aws_s3_bucket" "configuration-bucket" { 3 | bucket = "vpn-configuration-${data.aws_caller_identity.current.account_id}-${var.env}" 4 | 5 | lifecycle { 6 | prevent_destroy = true 7 | } 8 | } 9 | 10 | resource "aws_s3_bucket_public_access_block" "configuration-bucket" { 11 | bucket = aws_s3_bucket.configuration-bucket.id 12 | 13 | block_public_acls = true 14 | block_public_policy = true 15 | ignore_public_acls = true 16 | restrict_public_buckets = true 17 | } 18 | 19 | resource "aws_s3_bucket_versioning" "configuration-bucket" { 20 | bucket = aws_s3_bucket.configuration-bucket.id 21 | versioning_configuration { 22 | status = "Enabled" 23 | } 24 | } 25 | 26 | resource "aws_s3_bucket_server_side_encryption_configuration" "configuration-bucket" { 27 | bucket = aws_s3_bucket.configuration-bucket.id 28 | 29 | rule { 30 | apply_server_side_encryption_by_default { 31 | sse_algorithm = "AES256" 32 | } 33 | } 34 | } 35 | 36 | resource "aws_s3_bucket_policy" "configuration-bucket" { 37 | bucket = aws_s3_bucket.configuration-bucket.id 38 | policy = < /etc/caddy/Caddyfile && caddy run --config /etc/caddy/Caddyfile 31 | 32 | ${EXTERNAL_URL:-https://} { 33 | log 34 | reverse_proxy * 172.25.0.100:${PHOENIX_PORT:-13000} 35 | ${TLS_OPTS:-} 36 | } 37 | EOF 38 | network_mode: "host" 39 | deploy: 40 | <<: *default-deploy 41 | 42 | firezone: 43 | image: firezone/firezone:${VERSION:-latest} 44 | ports: 45 | - ${WIREGUARD_PORT:-51820}:${WIREGUARD_PORT:-51820}/udp 46 | env_file: 47 | # This should contain a list of env vars for configuring Firezone. 48 | # See https://www.firezone.dev/docs/reference/env-vars for more info. 49 | - ${FZ_INSTALL_DIR:-.}/.env 50 | volumes: 51 | # IMPORTANT: Persists WireGuard private key and other data. If 52 | # /var/firezone/private_key exists when Firezone starts, it is 53 | # used as the WireGuard private. Otherwise, one is generated. 54 | - ${FZ_INSTALL_DIR:-.}/firezone:/var/firezone 55 | cap_add: 56 | # Needed for WireGuard and firewall support. 57 | - NET_ADMIN 58 | - SYS_MODULE 59 | sysctls: 60 | # Needed for masquerading and NAT. 61 | - net.ipv6.conf.all.disable_ipv6=0 62 | - net.ipv4.ip_forward=1 63 | - net.ipv6.conf.all.forwarding=1 64 | networks: 65 | firezone-network: 66 | ipv4_address: 172.25.0.100 67 | ipv6_address: 2001:3990:3990::99 68 | 69 | deploy: 70 | <<: *default-deploy 71 | 72 | networks: 73 | firezone-network: 74 | enable_ipv6: true 75 | driver: bridge 76 | ipam: 77 | config: 78 | - subnet: 172.25.0.0/16 79 | - subnet: 2001:3990:3990::/64 80 | gateway: 2001:3990:3990::1 -------------------------------------------------------------------------------- /modules/wireguard-legacy/templates/userdata.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | apt-get update 4 | apt-get install ca-certificates curl gnupg awscli git binutils -y 5 | install -m 0755 -d /etc/apt/keyrings 6 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg 7 | chmod a+r /etc/apt/keyrings/docker.gpg 8 | 9 | echo \ 10 | "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ 11 | "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \ 12 | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null 13 | 14 | apt-get update 15 | apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y 16 | 17 | # efs helper 18 | cd /root 19 | git clone https://github.com/aws/efs-utils 20 | cd efs-utils 21 | ./build-deb.sh 22 | mv ./build/amazon-efs-utils*deb / 23 | apt-get -y install /amazon-efs-utils*deb 24 | rm /amazon-efs-utils*deb 25 | mkdir /efs 26 | echo -e "${efs_fs_id}\t/efs\tefs\t_netdev,noresvport,tls" >> /etc/fstab 27 | mount /efs 28 | 29 | mkdir /efs/firezone 30 | aws s3 cp s3://${s3_bucket}/firezone/docker-compose.yml /efs/firezone/ 31 | 32 | curl -L -o /bin/aws-env https://github.com/in4it/aws-env/releases/download/v0.7/aws-env-linux-amd64 33 | chmod +x /bin/aws-env 34 | 35 | cd /efs/firezone 36 | AWS_ENV_PATH=${aws_env_path} AWS_REGION=${aws_region} /bin/aws-env --format=dotenv | sed 's#\$#\\$#g' > .env 37 | 38 | echo "# The ability to change the IPv4 and IPv6 address pool will be removed 39 | # in a future Firezone release in order to reduce the possible combinations 40 | # of network configurations we need to handle. 41 | # 42 | # Due to the above, we recommend not changing these unless absolutely 43 | # necessary. 44 | WIREGUARD_IPV4_NETWORK=100.64.0.0/10 45 | WIREGUARD_IPV4_ADDRESS=100.64.0.1 46 | WIREGUARD_IPV6_NETWORK=fd00::/106 47 | WIREGUARD_IPV6_ADDRESS=fd00::1" >> .env 48 | 49 | if [ ! -e .setup-completed ] ; then 50 | docker compose run --rm firezone bin/migrate 51 | docker compose run --rm firezone bin/create-or-reset-admin 52 | fi 53 | 54 | 55 | touch .setup-completed 56 | docker compose up -d -------------------------------------------------------------------------------- /modules/wireguard-legacy/variables.tf: -------------------------------------------------------------------------------- 1 | variable "instance_type" { 2 | default = "t3.micro" 3 | } 4 | variable "db_instance_type" { 5 | default = "db.t4g.micro" 6 | } 7 | variable "vpc_id" { 8 | 9 | } 10 | variable "instance_subnet_id" { 11 | description = "subnet to launch the EC2 in" 12 | } 13 | variable "db_subnet_ids" { 14 | description = "subnets to launch the DB in" 15 | } 16 | variable "efs_subnet_ids" { 17 | description = "subnets to create the efs mountpoints in" 18 | } 19 | variable "external_url" { 20 | description = "external url the VPN is going to be reachable over. Starts with https://" 21 | } 22 | variable "admin_email" { 23 | description = "email of administrator" 24 | } 25 | variable "log_retention_days" { 26 | default = 30 27 | } 28 | 29 | variable "listeners" { 30 | type = list(object({ 31 | port = string 32 | protocol = string 33 | cidr_blocks = list(string) 34 | })) 35 | default = [{ 36 | port = "51820" 37 | protocol = "udp" 38 | cidr_blocks = ["0.0.0.0/0"] 39 | }, 40 | { 41 | port = "80" 42 | protocol = "tcp" 43 | cidr_blocks = ["0.0.0.0/0"] 44 | }, 45 | { 46 | port = "443" 47 | protocol = "tcp" 48 | cidr_blocks = ["0.0.0.0/0"] 49 | }] 50 | } 51 | 52 | variable "env" { 53 | default = "prod" 54 | } 55 | 56 | variable "tags" { 57 | default = {} 58 | type = map(string) 59 | } 60 | -------------------------------------------------------------------------------- /modules/wireguard-legacy/vpn-instance-iam.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_instance_profile" "vpn_iam_instance_profile" { 2 | name = "vpn-iam-instance-profile-${var.env}" 3 | role = aws_iam_role.vpn-iam-role.name 4 | } 5 | 6 | resource "aws_iam_role" "vpn-iam-role" { 7 | name = "vpn-iam-role-${var.env}" 8 | path = "/" 9 | 10 | assume_role_policy = < /etc/wireguard/site2site.key 22 | fi 23 | 24 | if [ -z "$PUBLIC_KEY" ] ; then 25 | cat /etc/wireguard/site2site.key | wg pubkey | tee /etc/wireguard/site2site.pub 26 | aws ssm put-parameter --region ${aws_region} --name ${aws_env_path}PUBLIC_KEY --value $(cat /etc/wireguard/site2site.pub) --key-id $KMS_ID --type SecureString 27 | else 28 | echo $PUBLIC_KEY > /etc/wireguard/site2site.pub 29 | fi 30 | 31 | 32 | 33 | echo "[Interface] 34 | PostUp = wg set %i private-key /etc/wireguard/%i.key 35 | Address = $VPN_INTERNAL_CIDR 36 | ListenPort = 51820 37 | 38 | [Peer] 39 | PublicKey = ${vpn_destination_pubkey} 40 | AllowedIPs = $VPN_DESTINATION_ALLOWED_IPS,$VPN_INTERNAL_CIDR 41 | Endpoint = ${vpn_destination_public_ip}:51820" > /etc/wireguard/site2site.conf 42 | 43 | sysctl -w net.ipv4.ip_forward=1 44 | 45 | echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf 46 | 47 | systemctl enable --now wg-quick@site2site -------------------------------------------------------------------------------- /modules/wireguard-site2site/variables.tf: -------------------------------------------------------------------------------- 1 | variable "identifier" { 2 | default = "site2site" 3 | } 4 | variable "instance_type" { 5 | default = "t3.micro" 6 | } 7 | 8 | variable "source_dest_check" { 9 | default = false 10 | } 11 | 12 | variable "vpc_id" { 13 | 14 | } 15 | variable "instance_subnet_id" { 16 | description = "subnet to launch the EC2 in" 17 | } 18 | variable "log_retention_days" { 19 | default = 30 20 | } 21 | 22 | variable "listeners" { 23 | type = list(object({ 24 | port = string 25 | protocol = string 26 | cidr_blocks = list(string) 27 | })) 28 | default = [{ 29 | port = "51820" 30 | protocol = "udp" 31 | cidr_blocks = ["0.0.0.0/0"] 32 | }] 33 | } 34 | 35 | variable "env" { 36 | default = "prod" 37 | } 38 | 39 | variable "vpn_internal_cidr" { 40 | 41 | } 42 | variable "vpn_destination_allowed_ips" { 43 | 44 | } 45 | variable "vpn_destination_pubkey" { 46 | 47 | } 48 | variable "vpn_destination_public_ip" { 49 | 50 | } 51 | 52 | variable "tags" { 53 | default = {} 54 | type = map(string) 55 | } 56 | -------------------------------------------------------------------------------- /modules/wireguard-site2site/vpn-instance-iam.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_instance_profile" "vpn_iam_instance_profile" { 2 | name = "${var.identifier}-vpn-iam-instance-profile-${var.env}" 3 | role = aws_iam_role.vpn-iam-role.name 4 | } 5 | 6 | resource "aws_iam_role" "vpn-iam-role" { 7 | name = "${var.identifier}-vpn-iam-role-${var.env}" 8 | path = "/" 9 | 10 | assume_role_policy = < /root/rustup.sh && chmod +x /root/rustup.sh && /root/rustup.sh -y 7 | . "$HOME/.cargo/env" 8 | 9 | # reinitialize vpn 10 | systemctl stop vpn-rest-server 11 | systemctl stop vpn-configmanager 12 | wg-quick down vpn 13 | rm -rf /vpn/config 14 | rm -rf /vpn/secrets 15 | rm -rf /vpn/tls-certs 16 | 17 | # efs helper 18 | cd /root 19 | git clone https://github.com/aws/efs-utils 20 | cd efs-utils 21 | git checkout v2.0.4 22 | ./build-deb.sh 23 | chmod 755 ~/efs-utils/build/amazon-efs-utils*deb 24 | mv ~/efs-utils//build/amazon-efs-utils*deb / 25 | apt-get -y install /amazon-efs-utils*deb 26 | rm /amazon-efs-utils*deb 27 | 28 | # Clean up packages after efs installation 29 | apt-get remove --purge -y binutils build-essential git rustc cargo pkg-config libssl-dev 30 | apt-get autoremove -y 31 | apt-get clean 32 | 33 | # set mounts in fstab 34 | echo -e "${efs_fs_id}\t/efs\tefs\t_netdev,noresvport,tls" >> /etc/fstab 35 | echo -e "${efs_fs_id}:/config\t/vpn/config\tefs\t_netdev,noresvport,tls" >> /etc/fstab 36 | echo -e "${efs_fs_id}:/secrets\t/vpn/secrets\tefs\t_netdev,noresvport,tls" >> /etc/fstab 37 | echo -e "${efs_fs_id}:/tls-certs\t/vpn/tls-certs\tefs\t_netdev,noresvport,tls" >> /etc/fstab 38 | echo -e "${efs_fs_id}:/stats\t/vpn/stats\tefs\t_netdev,noresvport,tls" >> /etc/fstab 39 | 40 | # require mounts before starting the vpn server 41 | sed -i 's#\[Unit\]#[Unit]\nRequires=vpn-config.mount vpn-secrets.mount\nAfter=vpn-config.mount vpn-secrets.mount#' /etc/systemd/system/vpn-configmanager.service 42 | sed -i 's#\[Unit\]#[Unit]\nRequires=vpn-config.mount vpn-secrets.mount\nAfter=vpn-config.mount vpn-secrets.mount#' /etc/systemd/system/vpn-rest-server.service 43 | 44 | # reload systemd 45 | systemctl daemon-reload 46 | 47 | # create directories and mount 48 | mkdir -p /efs 49 | mount /efs 50 | mkdir -p /efs/config 51 | mkdir -p /efs/secrets 52 | mkdir -p /efs/tls-certs 53 | mkdir -p /efs/stats 54 | chown vpn:vpn /efs/config 55 | chown vpn:vpn /efs/tls-certs 56 | chown vpn:vpn /efs/stats 57 | chmod 700 /efs 58 | chmod 700 /efs/config 59 | chmod 700 /efs/tls-certs 60 | chmod 700 /efs/secrets 61 | chmod 700 /efs/stats 62 | 63 | mkdir /vpn/config 64 | mount /vpn/config 65 | mkdir /vpn/secrets 66 | mount /vpn/secrets 67 | mkdir /vpn/tls-certs 68 | mount /vpn/tls-certs 69 | mkdir /vpn/stats 70 | mount /vpn/stats 71 | 72 | # restart vpn 73 | systemctl start vpn-rest-server 74 | systemctl start vpn-configmanager 75 | -------------------------------------------------------------------------------- /modules/wireguard-vpn-server/variables.tf: -------------------------------------------------------------------------------- 1 | variable "instance_type" { 2 | default = "t3.small" 3 | } 4 | 5 | variable "vpc_id" { 6 | description = "VPC id to launch the VPN Server in" 7 | 8 | } 9 | variable "instance_subnet_id" { 10 | description = "subnet to launch the VPN Server in" 11 | } 12 | 13 | variable "instance_profile_name" { 14 | default = "" 15 | description = "use a custom instance profile" 16 | } 17 | 18 | variable "efs_subnet_ids" { 19 | description = "subnets to create the efs mountpoints in" 20 | } 21 | 22 | variable "efs_encrypted" { 23 | description = "Enable EFS encryption" 24 | default = false 25 | } 26 | 27 | variable "efs_kms_key_id" { 28 | description = "EFS CMK ID for encryption" 29 | default = "" 30 | } 31 | 32 | variable "efs_backup_enabled" { 33 | description = "Enable EFS backups" 34 | default = false 35 | } 36 | 37 | variable "env" { 38 | default = "prod" 39 | } 40 | 41 | variable "listeners" { 42 | type = list(object({ 43 | port = string 44 | protocol = string 45 | cidr_blocks = list(string) 46 | })) 47 | default = [{ 48 | port = "51820" 49 | protocol = "udp" 50 | cidr_blocks = ["0.0.0.0/0"] 51 | }, 52 | { 53 | port = "80" 54 | protocol = "tcp" 55 | cidr_blocks = ["0.0.0.0/0"] 56 | }, 57 | { 58 | port = "443" 59 | protocol = "tcp" 60 | cidr_blocks = ["0.0.0.0/0"] 61 | }] 62 | } 63 | 64 | variable "tags" { 65 | default = {} 66 | type = map(string) 67 | } 68 | 69 | variable "ami_owner" { 70 | default = "aws-marketplace" 71 | } 72 | 73 | variable "license" { 74 | default = "marketplace" 75 | } 76 | -------------------------------------------------------------------------------- /modules/wireguard-vpn-server/vpn-server.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current" {} 2 | data "aws_caller_identity" "current" {} 3 | 4 | resource "aws_instance" "vpn-server" { 5 | depends_on = [ 6 | aws_efs_mount_target.vpn-server-config 7 | ] 8 | ami = data.aws_ami.vpn-server.id 9 | instance_type = var.instance_type 10 | subnet_id = var.instance_subnet_id 11 | vpc_security_group_ids = [aws_security_group.vpn-server.id] 12 | iam_instance_profile = var.instance_profile_name != "" ? var.instance_profile_name : aws_iam_instance_profile.vpn-server.name 13 | 14 | user_data_base64 = base64encode(templatefile("${path.module}/templates/userdata.sh", { 15 | aws_region = data.aws_region.current.name 16 | efs_fs_id = aws_efs_file_system.vpn-server-config.id 17 | })) 18 | 19 | root_block_device { 20 | encrypted = true 21 | } 22 | 23 | metadata_options { 24 | http_endpoint = "enabled" 25 | http_tokens = "required" 26 | instance_metadata_tags = "enabled" 27 | } 28 | 29 | tags = merge({ Name = "vpn-server-${var.env}", env = var.env, license = var.license }, var.tags) 30 | } 31 | 32 | resource "aws_eip" "vpn-server" { 33 | instance = aws_instance.vpn-server.id 34 | 35 | lifecycle { 36 | prevent_destroy = true 37 | } 38 | } 39 | 40 | data "aws_ami" "vpn-server" { 41 | owners = [var.ami_owner] 42 | most_recent = true 43 | 44 | filter { 45 | name = "name" 46 | values = [var.license == "marketplace" ? "in4it-vpn-server-licensed-*" : "in4it-vpn-server-byol-*"] 47 | } 48 | } 49 | 50 | resource "aws_security_group" "vpn-server" { 51 | name = "vpn-server-${var.env}" 52 | vpc_id = var.vpc_id 53 | 54 | dynamic "ingress" { 55 | for_each = var.listeners 56 | content { 57 | from_port = ingress.value.port 58 | protocol = ingress.value.protocol 59 | to_port = ingress.value.port 60 | cidr_blocks = ingress.value.cidr_blocks 61 | } 62 | } 63 | egress { 64 | from_port = 0 65 | protocol = "-1" 66 | to_port = 0 67 | cidr_blocks = ["0.0.0.0/0"] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /modules/wireguard/rds.tf: -------------------------------------------------------------------------------- 1 | module "vpn-rds" { 2 | source = "github.com/in4it/terraform-modules//modules/rds" 3 | name = "vpn" 4 | storage = "20" 5 | storage_type = "gp3" 6 | engine = "postgres" 7 | engine_version = var.db_engine_version 8 | username = "vpn" 9 | database_name = "vpn" 10 | vpc_id = var.vpc_id 11 | instance_type = var.db_instance_type 12 | subnet_group = "" 13 | subnet_ids = var.db_subnet_ids 14 | 15 | ingress_security_groups = [aws_security_group.vpn-instance.id] 16 | } 17 | -------------------------------------------------------------------------------- /modules/wireguard/variables.tf: -------------------------------------------------------------------------------- 1 | variable "instance_type" { 2 | default = "t3.micro" 3 | } 4 | variable "db_instance_type" { 5 | default = "db.t4g.micro" 6 | } 7 | variable "vpc_id" { 8 | 9 | } 10 | variable "instance_subnet_id" { 11 | description = "subnet to launch the EC2 in" 12 | } 13 | variable "db_subnet_ids" { 14 | description = "subnets to launch the DB in" 15 | } 16 | variable "efs_subnet_ids" { 17 | description = "subnets to create the efs mountpoints in" 18 | } 19 | variable "external_url" { 20 | description = "external url the VPN is going to be reachable over. Starts with https://" 21 | } 22 | variable "admin_email" { 23 | description = "email of administrator" 24 | } 25 | variable "log_retention_days" { 26 | default = 30 27 | } 28 | 29 | variable "listeners" { 30 | type = list(object({ 31 | port = string 32 | protocol = string 33 | cidr_blocks = list(string) 34 | })) 35 | default = [{ 36 | port = "51820" 37 | protocol = "udp" 38 | cidr_blocks = ["0.0.0.0/0"] 39 | }, 40 | { 41 | port = "80" 42 | protocol = "tcp" 43 | cidr_blocks = ["0.0.0.0/0"] 44 | }, 45 | { 46 | port = "443" 47 | protocol = "tcp" 48 | cidr_blocks = ["0.0.0.0/0"] 49 | }] 50 | } 51 | 52 | variable "env" { 53 | default = "prod" 54 | } 55 | 56 | variable "tags" { 57 | default = {} 58 | type = map(string) 59 | } 60 | variable "db_engine_version" { 61 | default = "15.5" 62 | } --------------------------------------------------------------------------------