diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..88cb251 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,30 @@ +# EditorConfig is awesome: http://EditorConfig.org +# Uses editorconfig to maintain consistent coding styles + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +max_line_length = 80 +trim_trailing_whitespace = true + +[*.{tf,tfvars}] +indent_size = 2 +indent_style = space + +[*.md] +max_line_length = 0 +trim_trailing_whitespace = false + +[Makefile] +tab_width = 2 +indent_style = tab + +[COMMIT_EDITMSG] +max_line_length = 0 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 231c9a6..57bdcfa 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ # These owners will be the default owners for everything in the repo. -* @anmolnagpal @clouddrove/approvers @clouddrove-ci +* @anmolnagpal @clouddrove/approvers @clouddrove-ci diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ee2752d..440534d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,6 +5,17 @@ version: 2 updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 3 + assignees: + - "clouddrove-ci" + reviewers: + - "approvers" + - package-ecosystem: "terraform" # See documentation for possible values directory: "/" # Location of package manifests schedule: @@ -15,8 +26,11 @@ updates: # Add reviewer reviewers: - "approvers" + # Allow up to 3 open pull requests for pip dependencies + open-pull-requests-limit: 3 + - package-ecosystem: "terraform" # See documentation for possible values - directory: "_example/" # Location of package manifests + directory: "/_example" # Location of package manifests schedule: interval: "weekly" # Add assignees @@ -25,5 +39,7 @@ updates: # Add reviewer reviewers: - "approvers" - + # Allow up to 3 open pull requests for pip dependencies + open-pull-requests-limit: 3 + diff --git a/.github/workflows/auto_assignee.yml b/.github/workflows/auto_assignee.yml index f8b8bcd..1f126cf 100644 --- a/.github/workflows/auto_assignee.yml +++ b/.github/workflows/auto_assignee.yml @@ -1,9 +1,7 @@ name: Auto Assign PRs - on: pull_request: types: [opened, reopened] - workflow_dispatch: jobs: assignee: @@ -11,4 +9,4 @@ jobs: secrets: GITHUB: ${{ secrets.GITHUB }} with: - assignees: 'clouddrove-ci' + assignees: 'clouddrove-ci' \ No newline at end of file diff --git a/.github/workflows/readme.yml b/.github/workflows/readme.yml index 9961649..2ff513e 100644 --- a/.github/workflows/readme.yml +++ b/.github/workflows/readme.yml @@ -3,7 +3,6 @@ on: push: branches: - master - jobs: readme: name: 'readme-create' diff --git a/.github/workflows/tf-checks.yml b/.github/workflows/tf-checks.yml index 0f02313..c0e9a69 100644 --- a/.github/workflows/tf-checks.yml +++ b/.github/workflows/tf-checks.yml @@ -8,4 +8,4 @@ jobs: example: uses: clouddrove/github-shared-workflows/.github/workflows/tf-checks.yml@master with: - working_directory: './_example/' \ No newline at end of file + working_directory: './_example/' diff --git a/.github/workflows/tflint.yml b/.github/workflows/tflint.yml index 5b8aa91..ee98182 100644 --- a/.github/workflows/tflint.yml +++ b/.github/workflows/tflint.yml @@ -5,7 +5,7 @@ on: pull_request: workflow_dispatch: jobs: - tflint: - uses: clouddrove/test-tfsec/.github/workflows/tflint.yaml@master + tf-lint: + uses: clouddrove/github-shared-workflows/.github/workflows/tf-lint.yml@master secrets: - GITHUB: ${{ secrets.GITHUB }} + GITHUB: ${{ secrets.GITHUB }} \ No newline at end of file diff --git a/README.yaml b/README.yaml index 3f61a65..cd2598e 100644 --- a/README.yaml +++ b/README.yaml @@ -24,6 +24,10 @@ badges: image: "https://img.shields.io/badge/License-APACHE-blue.svg" url: "LICENSE.md" +prerequesties: + - name: Terraform 1.5.3 + url: https://learn.hashicorp.com/terraform/getting-started/install.html + # description of this project description: |- Terraform module to create Client VPN resource on AWS. @@ -41,17 +45,15 @@ usage: |- module "vpn" { source = "clouddrove/client-vpn/aws" version = "1.0.5" - name = "test-vpn" - enabled = true + name = local.name + environment = local.environment split_tunnel_enable = true - environment = "example" - label_order = ["name", "environment"] cidr_block = "172.0.0.0/16" + vpc_id = module.vpc.vpc_id subnet_ids = module.subnets.public_subnet_id route_cidr = ["0.0.0.0/0", "0.0.0.0/0"] - security_group_ids = [""] + security_group_ids = [""] route_subnet_ids = module.subnets.public_subnet_id network_cidr = ["0.0.0.0/0"] - - } - ``` + } + ``` diff --git a/_example/main.tf b/_example/example.tf similarity index 53% rename from _example/main.tf rename to _example/example.tf index 7ee4aed..30d34b1 100644 --- a/_example/main.tf +++ b/_example/example.tf @@ -1,8 +1,10 @@ -##--------------------------------------------------------------------------------------------------------------------------- -## Provider block added, Use the Amazon Web Services (AWS) provider to interact with the many resources supported by AWS. -##-------------------------------------------------------------------------------------------------------------------------- provider "aws" { - region = "eu-west-1" + region = "us-east-1" +} + +locals { + name = "vpn" + environment = "test" } ##--------------------------------------------------------------------------------------------------------------------------- @@ -12,33 +14,30 @@ module "vpc" { source = "clouddrove/vpc/aws" version = "2.0.0" + name = local.name + environment = local.environment enable_flow_log = false - name = "vpc" - environment = "example" - label_order = ["name", "environment"] - - cidr_block = "10.0.0.0/16" + cidr_block = "10.0.0.0/16" } ##----------------------------------------------------- ## A subnet is a range of IP addresses in your VPC. ##----------------------------------------------------- +#tfsec:ignore:aws-ec2-no-excessive-port-access +#tfsec:ignore:aws-ec2-no-public-ingress-acl module "subnets" { source = "clouddrove/subnet/aws" version = "2.0.0" nat_gateway_enabled = true - - name = "subnets" - environment = "example" - label_order = ["name", "environment"] - - availability_zones = ["eu-west-1a", "eu-west-1b"] - vpc_id = module.vpc.vpc_id - type = "public-private" - igw_id = module.vpc.igw_id - cidr_block = module.vpc.vpc_cidr_block - ipv6_cidr_block = module.vpc.ipv6_cidr_block + name = local.name + environment = local.environment + availability_zones = ["us-east-1a", "us-east-1b"] + vpc_id = module.vpc.vpc_id + type = "public-private" + igw_id = module.vpc.igw_id + cidr_block = module.vpc.vpc_cidr_block + ipv6_cidr_block = module.vpc.ipv6_cidr_block } ##----------------------------------------------------------------------------- @@ -47,16 +46,13 @@ module "subnets" { module "vpn" { source = "../" - name = "test-vpn" - enabled = true + name = local.name + environment = local.environment split_tunnel_enable = true - environment = "example" - label_order = ["name", "environment"] cidr_block = "172.0.0.0/16" + vpc_id = module.vpc.vpc_id subnet_ids = module.subnets.public_subnet_id route_cidr = ["0.0.0.0/0", "0.0.0.0/0"] - security_group_ids = [""] route_subnet_ids = module.subnets.public_subnet_id network_cidr = ["0.0.0.0/0"] - -} \ No newline at end of file +} diff --git a/_example/outputs.tf b/_example/outputs.tf index 347ca1b..7803f94 100644 --- a/_example/outputs.tf +++ b/_example/outputs.tf @@ -14,3 +14,23 @@ output "tags" { value = module.vpc.tags description = "A mapping of tags to assign to the resource." } + +output "sg_id" { + value = module.vpn.sg_id + description = "The ID of the SG for Client VPN." +} + +output "vpn_id" { + value = module.vpn.vpn_id + description = "The ID of the Client VPN endpoint." +} + +output "vpn_arn" { + value = module.vpn.vpn_arn + description = "The ARN of the Client VPN endpoint." +} + +output "vpn_dns_name" { + value = module.vpn.vpn_dns_name + description = "VPN DNS name" +} diff --git a/_example/versions.tf b/_example/versions.tf index 5a227cd..c95376a 100644 --- a/_example/versions.tf +++ b/_example/versions.tf @@ -1,11 +1,11 @@ # Terraform version terraform { - required_version = ">= 1.5.0" + required_version = ">= 1.5.5" required_providers { aws = { source = "hashicorp/aws" - version = ">= 5.9.0" + version = ">= 5.13.1" } } -} \ No newline at end of file +} diff --git a/main.tf b/main.tf index 3ee0539..e87b804 100644 --- a/main.tf +++ b/main.tf @@ -14,7 +14,8 @@ module "labels" { resource "tls_private_key" "ca" { count = var.enabled ? 1 : 0 - algorithm = "RSA" + algorithm = var.algorithm + rsa_bits = var.rsa_bits } ##----------------------------------------------------------------------------- @@ -25,14 +26,15 @@ resource "tls_self_signed_cert" "ca" { private_key_pem = join("", tls_private_key.ca[*].private_key_pem) subject { + common_name = format("%s-ca", module.labels.id) organization = var.organization_name } dns_names = var.dns_names - validity_period_hours = 87600 - is_ca_certificate = true + validity_period_hours = var.validity_period_hours + is_ca_certificate = var.is_ca_certificate allowed_uses = [ "cert_signing", @@ -47,26 +49,28 @@ resource "aws_acm_certificate" "ca" { count = var.enabled ? 1 : 0 private_key = join("", tls_private_key.ca[*].private_key_pem) certificate_body = join("", tls_self_signed_cert.ca[*].cert_pem) + + lifecycle { + create_before_destroy = true + } } resource "tls_private_key" "root" { count = var.enabled ? 1 : 0 - algorithm = "RSA" + algorithm = var.algorithm } ##----------------------------------------------------------------------------- ## Generates a Certificate Signing Request (CSR) in PEM format, which is the typical format used to request a certificate from a certificate authority. ##----------------------------------------------------------------------------- resource "tls_cert_request" "root" { - count = var.enabled ? 1 : 0 - #key_algorithm = "RSA" - private_key_pem = join("", tls_private_key.root[*].private_key_pem) + count = var.enabled ? 1 : 0 + private_key_pem = join("", tls_private_key.server[*].private_key_pem) subject { common_name = format("%s-client", module.labels.id) organization = var.organization_name } - dns_names = var.dns_names } @@ -74,13 +78,11 @@ resource "tls_cert_request" "root" { ## Generates a Certificate Signing Request (CSR) in PEM format, which is the typical format used to request a certificate from a certificate authority. ##----------------------------------------------------------------------------- resource "tls_locally_signed_cert" "root" { - count = var.enabled ? 1 : 0 - cert_request_pem = join("", tls_cert_request.root[*].cert_request_pem) - #ca_key_algorithm = "RSA" - ca_private_key_pem = join("", tls_private_key.ca[*].private_key_pem) - ca_cert_pem = join("", tls_self_signed_cert.ca[*].cert_pem) - - validity_period_hours = 87600 + count = var.enabled ? 1 : 0 + cert_request_pem = join("", tls_cert_request.root[*].cert_request_pem) + ca_private_key_pem = join("", tls_private_key.ca[*].private_key_pem) + ca_cert_pem = join("", tls_self_signed_cert.ca[*].cert_pem) + validity_period_hours = var.validity_period_hours allowed_uses = [ "key_encipherment", @@ -94,22 +96,25 @@ resource "tls_locally_signed_cert" "root" { ##----------------------------------------------------------------------------- resource "aws_acm_certificate" "root" { count = var.certificate_enabled ? 1 : 0 - private_key = join("", tls_private_key.root[*].private_key_pem) + private_key = join("", tls_private_key.server[*].private_key_pem) certificate_body = join("", tls_locally_signed_cert.root[*].cert_pem) certificate_chain = join("", tls_self_signed_cert.ca[*].cert_pem) + + lifecycle { + create_before_destroy = true + } } resource "tls_private_key" "server" { count = var.enabled ? 1 : 0 - algorithm = "RSA" + algorithm = var.algorithm } ##----------------------------------------------------------------------------- ## Generates a Certificate Signing Request (CSR) in PEM format, which is the typical format used to request a certificate from a certificate authority. ##----------------------------------------------------------------------------- resource "tls_cert_request" "server" { - count = var.enabled ? 1 : 0 - #key_algorithm = "RSA" + count = var.enabled ? 1 : 0 private_key_pem = join("", tls_private_key.server[*].private_key_pem) subject { @@ -126,11 +131,10 @@ resource "tls_cert_request" "server" { resource "tls_locally_signed_cert" "server" { count = var.enabled ? 1 : 0 - cert_request_pem = join("", tls_cert_request.server[*].cert_request_pem) - ca_private_key_pem = join("", tls_private_key.ca[*].private_key_pem) - ca_cert_pem = join("", tls_self_signed_cert.ca[*].cert_pem) - - validity_period_hours = 87600 + cert_request_pem = join("", tls_cert_request.server[*].cert_request_pem) + ca_private_key_pem = join("", tls_private_key.ca[*].private_key_pem) + ca_cert_pem = join("", tls_self_signed_cert.ca[*].cert_pem) + validity_period_hours = var.validity_period_hours allowed_uses = [ "key_encipherment", @@ -147,6 +151,10 @@ resource "aws_acm_certificate" "server" { private_key = join("", tls_private_key.server[*].private_key_pem) certificate_body = join("", tls_locally_signed_cert.server[*].cert_pem) certificate_chain = join("", tls_self_signed_cert.ca[*].cert_pem) + + lifecycle { + create_before_destroy = true + } } ##----------------------------------------------------------------------------- @@ -161,7 +169,8 @@ resource "aws_ec2_client_vpn_endpoint" "default" { vpc_id = var.vpc_id session_timeout_hours = var.session_timeout_hours security_group_ids = concat([aws_security_group.this.id], var.security_group_ids) - + vpn_port = var.vpn_port + self_service_portal = var.self_service_portal authentication_options { type = var.authentication_type @@ -182,29 +191,38 @@ resource "aws_ec2_client_vpn_endpoint" "default" { authentication_options ] } - } + ##----------------------------------------------------------------------------- ## aws_security_group. Provides a security group resource. ##----------------------------------------------------------------------------- +#tfsec:ignore:aws-ec2-no-public-egress-sgr +#tfsec:ignore:aws-ec2-add-description-to-security-group +#tfsec:ignore:aws-ec2-add-description-to-security-group-rule resource "aws_security_group" "this" { name_prefix = var.name vpc_id = var.vpc_id tags = module.labels.tags - ingress { - from_port = 0 - protocol = -1 - self = true - to_port = 0 - + dynamic "ingress" { + for_each = var.security_group_ingress + content { + self = lookup(ingress.value, "self", true) + from_port = lookup(ingress.value, "from_port", 0) + to_port = lookup(ingress.value, "to_port", 0) + protocol = lookup(ingress.value, "protocol", "-1") + description = lookup(ingress.value, "description", "") + } } - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] + dynamic "egress" { + for_each = var.security_group_egress + content { + cidr_blocks = compact(split(",", lookup(egress.value, "cidr_blocks", "0.0.0.0/0"))) + from_port = lookup(egress.value, "from_port", 0) + to_port = lookup(egress.value, "to_port", 0) + protocol = lookup(egress.value, "protocol", "-1") + } } } @@ -220,12 +238,12 @@ resource "aws_ec2_client_vpn_network_association" "default" { ##----------------------------------------------------------------------------- ## aws_cloudwatch_log_group Provides a CloudWatch Log Group resource. ##----------------------------------------------------------------------------- +#tfsec:ignore:aws-cloudwatch-log-group-customer-key resource "aws_cloudwatch_log_group" "vpn" { count = var.enabled ? 1 : 0 name = format("/aws/vpn/%s/logs", module.labels.id) retention_in_days = var.logs_retention - - tags = module.labels.tags + tags = module.labels.tags } ##----------------------------------------------------------------------------- @@ -244,7 +262,7 @@ resource "aws_ec2_client_vpn_authorization_rule" "vpn_auth" { count = length(var.network_cidr) client_vpn_endpoint_id = join("", aws_ec2_client_vpn_endpoint.default[*].id) target_network_cidr = element(var.network_cidr, count.index) - authorize_all_groups = true + authorize_all_groups = var.authorize_all_groups } ##----------------------------------------------------------------------------- @@ -253,7 +271,7 @@ resource "aws_ec2_client_vpn_authorization_rule" "vpn_auth" { resource "aws_ec2_client_vpn_authorization_rule" "vpn_group_auth" { count = length(var.group_ids) client_vpn_endpoint_id = join("", aws_ec2_client_vpn_endpoint.default[*].id) - target_network_cidr = "0.0.0.0/0" + target_network_cidr = element(var.target_network_cidr, count.index) access_group_id = element(var.group_ids, count.index) } @@ -266,4 +284,4 @@ resource "aws_ec2_client_vpn_route" "vpn_route" { destination_cidr_block = element(var.route_cidr, count.index) target_vpc_subnet_id = element(var.route_subnet_ids, count.index) depends_on = [aws_ec2_client_vpn_network_association.default] -} \ No newline at end of file +} diff --git a/outputs.tf b/outputs.tf index 0ee6fba..dddd922 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,5 +1,5 @@ output "key" { - value = join("", tls_private_key.root[*].private_key_pem) + value = join("", tls_private_key.server[*].private_key_pem) description = "A mapping of tags to assign to the key." sensitive = true } @@ -14,3 +14,23 @@ output "tags" { value = module.labels.tags description = "A mapping of tags to assign to the resource." } + +output "sg_id" { + value = join("", aws_security_group.this[*].id) + description = "The ID of the SG for Client VPN." +} + +output "vpn_id" { + value = join("", aws_ec2_client_vpn_endpoint.default[*].id) + description = "The ID of the Client VPN endpoint." +} + +output "vpn_arn" { + value = join("", aws_ec2_client_vpn_endpoint.default[*].arn) + description = "The ARN of the Client VPN endpoint." +} + +output "vpn_dns_name" { + value = join("", aws_ec2_client_vpn_endpoint.default[*].dns_name) + description = "VPN DNS name" +} \ No newline at end of file diff --git a/variables.tf b/variables.tf index 6a2bc41..b90d75d 100644 --- a/variables.tf +++ b/variables.tf @@ -24,7 +24,7 @@ variable "environment" { variable "label_order" { type = list(any) - default = [] + default = ["name", "environment"] description = "Label order, e.g. `name`,`application`." } @@ -76,7 +76,6 @@ variable "network_cidr" { description = "Client Network CIDR" } - variable "split_tunnel_enable" { type = bool default = false @@ -91,7 +90,7 @@ variable "dns_names" { variable "authentication_type" { type = string - default = "federated-authentication" + default = "certificate-authentication" description = "The type of client authentication to be used. " } @@ -140,4 +139,78 @@ variable "Connection_logging" { type = bool default = true description = "Connection logging is a feature of AWS client VPN that enables you to capture connection logs for your client VPN endpoint. Before you enable, you must have a CloudWatch Logs log group in your account." -} \ No newline at end of file +} + +variable "vpn_port" { + type = number + default = 443 + description = "The port number for the Client VPN endpoint. Valid values are 443 and 1194. Default value is 443." +} + +variable "self_service_portal" { + type = string + default = "disabled" + description = "Optionally specify whether the VPC Client self-service portal is enabled or disabled. Default is disabled" +} + +variable "rsa_bits" { + type = number + default = 2048 + description = "When algorithm is RSA, the size of the generated RSA key, in bits (default: 2048)." +} + +variable "algorithm" { + type = string + default = "RSA" + description = "Name of the algorithm to use when generating the private key. Currently-supported values are: RSA, ECDSA, ED25519." +} + +variable "validity_period_hours" { + type = number + default = 87600 + description = "Number of hours, after initial issuing, that the certificate will remain valid for." +} + +variable "is_ca_certificate" { + type = bool + default = true + description = "Is the generated certificate representing a Certificate Authority (CA)." +} + +variable "authorize_all_groups" { + type = bool + default = true + description = "Indicates whether the authorization rule grants access to all clients. One of access_group_id or authorize_all_groups must be set." +} + +variable "target_network_cidr" { + type = list(string) + default = ["0.0.0.0/0"] + description = "List of CIDR ranges from which access is allowed" +} + +variable "security_group_ingress" { + type = list(map(string)) + default = [ + { + from_port = 0 + protocol = -1 + self = true + to_port = 0 + } + ] + description = "List of maps of ingress rules to set on the default security group" +} + +variable "security_group_egress" { + type = list(map(string)) + default = [ + { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = "0.0.0.0/0" + } + ] + description = "List of maps of egress rules to set on the default security group" +} diff --git a/versions.tf b/versions.tf index 5a227cd..17bb12b 100644 --- a/versions.tf +++ b/versions.tf @@ -1,11 +1,15 @@ # Terraform version terraform { - required_version = ">= 1.5.0" + required_version = ">= 1.5.5" required_providers { aws = { source = "hashicorp/aws" - version = ">= 5.9.0" + version = ">= 5.13.1" + } + tls = { + source = "hashicorp/tls" + version = ">= 4.0.4" } } -} \ No newline at end of file +}