diff --git a/.github/workflows/validate_terraform.yml b/.github/workflows/validate_terraform.yml new file mode 100644 index 0000000..cb86c0e --- /dev/null +++ b/.github/workflows/validate_terraform.yml @@ -0,0 +1,63 @@ +name: Validate Terraform + +on: + pull_request: + paths: + - 'terraform2.0/**' + + push: + branches: + - main + paths: + - 'terraform2.0/**' + +jobs: + validate: + runs-on: [self-hosted, Linux, X64] + + defaults: + run: + working-directory: terraform2.0 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: "1.15.5" + + # The .example files are skipped on purpose: their dual-source comment + # pattern (commented local path above the live remote source) makes + # `terraform fmt` always want to realign the block. + - name: Check module formatting + run: terraform fmt -check -recursive modules + + - name: Validate modules + run: | + for module in modules/*/; do + echo "::group::validate $module" + terraform -chdir="$module" init -backend=false -input=false + terraform -chdir="$module" validate + echo "::endgroup::" + done + + - name: Validate examples + run: | + for example in examples/basic examples/existing-vpc; do + echo "::group::validate $example" + cp "$example/main.tf.example" "$example/main.tf" + # Validate against the modules in this checkout rather than the + # published ?ref=main source. A Terraform override file merges into + # the matching module blocks and replaces only their source. + cat > "$example/ci_local_modules_override.tf" <<'EOF' + module "defguard_core" { source = "../../modules/core" } + module "defguard_edge" { source = "../../modules/edge" } + module "defguard_gateway" { source = "../../modules/gateway" } + module "network" { source = "../../modules/network" } + EOF + terraform -chdir="$example" init -backend=false -input=false + terraform -chdir="$example" validate + echo "::endgroup::" + done diff --git a/.gitignore b/.gitignore index 5e9cbfd..655653e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,12 @@ terraform/**/terraform.tfstate terraform/**/terraform.tfstate.backup terraform/**/.* terraform/**/*.tfvars +terraform2.0/**/terraform.tfstate +terraform2.0/**/terraform.tfstate.backup +terraform2.0/**/.* +terraform2.0/**/*.tfvars +terraform2.0/examples/basic/main.tf +terraform2.0/examples/existing-vpc/main.tf .direnv/ .envrc .venv/ diff --git a/terraform2.0/examples/basic/main.tf.example b/terraform2.0/examples/basic/main.tf.example new file mode 100644 index 0000000..c9f03b0 --- /dev/null +++ b/terraform2.0/examples/basic/main.tf.example @@ -0,0 +1,349 @@ +# Deploy Defguard 2.0 into a NEW, self-contained VPC. +# +# This example creates everything from scratch: a VPC with public/private subnets and a NAT +# gateway, the security groups, network interfaces, EIPs and the RDS database (via the shared +# modules/network submodule), plus the three component instances. Edit the locals below and +# apply. To deploy into a VPC you already run instead, use examples/existing-vpc. +# +# Layout: the Core and database stay private (egress via NAT); the Gateway and Edge sit in the +# public subnet with EIPs. First-access steps are documented above the outputs at the bottom. + +locals { + ############################ AWS configuration ############################ + + # The AWS region where the Defguard infrastructure will be deployed. + region = "us-east-1" + + ############################ SSH / access configuration ################### + + # SSH is OFF by default: with ssh_admin_cidr = null no port-22 ingress is created at all. + # To enable SSH (e.g. to tunnel to the private Core via the Gateway), set BOTH: + # - ssh_admin_cidr to your own public IP as a /32 (find it: curl -s https://checkip.amazonaws.com); + # avoid 0.0.0.0/0, which exposes SSH to the whole internet. + # - ssh_key_name to an existing EC2 key pair in this region. + ssh_admin_cidr = null + ssh_key_name = null + + ############################ Core configuration ########################### + + # The gRPC port for the Defguard Core. + core_grpc_port = 50055 + + # The HTTP(S) port for the Defguard Core web UI. + core_http_port = 8000 + + # Whether to allow insecure cookies for the Defguard Core web UI. Set to true if you + # are using HTTP to access the Defguard Core web UI. + core_cookie_insecure = false + + # The deb package version of the Defguard Core that will be installed on the instance. + # Must be a valid, released 2.x version of Defguard Core. + core_package_version = "2.0.1" + + # The architecture of the Defguard Core server instance. + # Supported values: "x86_64", "aarch64" + core_arch = "x86_64" + + # The instance type for the Defguard Core server. + core_instance_type = "t3.micro" + + ############################ Edge configuration ########################### + + # The gRPC port the Defguard Edge listens on. Core dials this to adopt and manage the edge. + edge_grpc_port = 50051 + + # The HTTP port the Defguard Edge listens on for enrollment and client communication. + edge_http_port = 8080 + + # The HTTPS port the Defguard Edge listens on (used after Core provisions TLS). + edge_https_port = 443 + + # The deb package version of the edge that will be installed on the instance. + # Must be a valid, released 2.x version of Defguard Proxy (edge). + edge_package_version = "2.0.1" + + # The architecture of the Defguard Edge server instance. + # Supported values: "x86_64", "aarch64" + edge_arch = "x86_64" + + # The instance type for the Defguard Edge server. + edge_instance_type = "t3.micro" + + ###################### VPN and Gateway configuration ###################### + + # The gRPC port the Defguard Gateway listens on. Core dials this to adopt and manage + # the gateway (initial adoption and the ongoing control stream). + gateway_grpc_port = 50066 + + # The UDP port for the WireGuard VPN. The gateway listens on this port for incoming VPN + # connections. Must match the port of the location auto-created during adoption (51820). + wireguard_port = 51820 + + # Whether to enable NAT (masquerade) on the gateway, allowing VPN clients to reach the + # internet and other resources through the gateway. + gateway_nat = true + + # The gateway deb package version that will be installed on the instance. + # Must be a valid, released 2.x version of Defguard Gateway. + gateway_package_version = "2.0.1" + + # The architecture of the Defguard Gateway server instance. + # Supported values: "x86_64", "aarch64" + gateway_arch = "x86_64" + + # The instance type for the Defguard Gateway server. + gateway_instance_type = "t3.micro" + + ########################## Database configuration ######################### + + # The major PostgreSQL engine version for the RDS instance. The parameter group family + # (postgres) is derived from this, so the two always match. + db_engine_version = "18" + + # The name of the database that will be created for the Defguard Core. + db_name = "defguard" + + # The username for the database that will be created for the Defguard Core. + db_username = "defguard" + + # The port on which the database will listen for incoming connections. + db_port = 5432 + + # The password for the database user. This will be used by the Defguard Core to connect to the database. + db_password = "defguard" + + # The amount of storage allocated for the database in GB. The minimum amount for this example required by AWS is 20 GB. + db_storage = 20 # GB + + # The instance class for the database. This defines the performance characteristics of the database instance. + db_instance_class = "db.t3.micro" + + ############################ VPC configuration ############################ + + # The name of the VPC that will be created for the Defguard infrastructure. + vpc_name = "defguard-vpc" + + # The CIDR block for the VPC. This defines the IP address range for the VPC and its subnets. + # Deliberately off 10.0.0.0/16 so it does NOT overlap the default WireGuard location range + # (10.0.0.1/24) that auto-adoption creates. + vpc_cidr = "10.20.0.0/16" + + # The tags to be applied to the Defguard VPC. + vpc_tags = { + Name = local.vpc_name + } + + # The private subnets for the VPC. The Core runs in the first one (no public IP, egress via + # NAT); both are used by the RDS subnet group. Note: 2 subnets in different AZs are required + # for the database subnet group. + vpc_private_subnets = ["10.20.2.0/24", "10.20.3.0/24"] + + # The public subnet for the internet-facing components: the Gateway (WireGuard UDP) and the + # Edge (HTTPS enrollment). The Core stays private (see vpc_private_subnets above) and is + # reached only from within the VPC. + vpc_public_subnets = ["10.20.1.0/24"] + + # The availability zones for the VPC. This is used mainly for the database instance to ensure high availability. + azs = ["us-east-1a", "us-east-1b"] +} + +terraform { + required_version = ">= 1.5.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + } +} + +# Maps the deb package architecture (x86_64/aarch64) to the Ubuntu AMI name token +# (amd64/arm64), so the AMI a component boots matches the package its setup.sh downloads. +locals { + ubuntu_ami_arch = { + x86_64 = "amd64" + aarch64 = "arm64" + } +} + +# One lookup per distinct component architecture; each component picks +# data.aws_ami.ubuntu[] below. When using aarch64, also set that component's +# instance_type to an arm64 type (e.g. t4g.micro). +data "aws_ami" "ubuntu" { + for_each = toset([local.core_arch, local.edge_arch, local.gateway_arch]) + most_recent = true + owners = ["099720109477"] + + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-${local.ubuntu_ami_arch[each.value]}-server-*"] + } +} + +# Credentials come from the standard AWS provider chain (environment variables, a shared +# profile, SSO, or an instance role). Set e.g. AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY or +# AWS_PROFILE before running terraform; do not hardcode keys here. +provider "aws" { + region = local.region +} + +########################################################################### +########################## Defguard components ############################ +########################################################################### + +module "defguard_core" { + # source = "../../modules/core" + source = "github.com/DefGuard/deployment//terraform2.0/modules/core?ref=main" + instance_type = local.core_instance_type + package_version = local.core_package_version + arch = local.core_arch + ami = data.aws_ami.ubuntu[local.core_arch].id + + grpc_port = local.core_grpc_port + http_port = local.core_http_port + cookie_insecure = local.core_cookie_insecure + + gateway_address = module.network.gateway_private_ip + gateway_grpc_port = local.gateway_grpc_port + + edge_address = module.network.edge_private_ip + edge_grpc_port = local.edge_grpc_port + + db_details = module.network.db_details + key_name = local.ssh_key_name + network_interface_id = module.network.core_network_interface_id + # log_level = "info" + + # Gateway and edge must be listening before Core's one-shot adoption runs. + depends_on = [ + module.defguard_gateway, + module.defguard_edge, + ] +} + +module "defguard_edge" { + # source = "../../modules/edge" + source = "github.com/DefGuard/deployment//terraform2.0/modules/edge?ref=main" + + instance_type = local.edge_instance_type + package_version = local.edge_package_version + arch = local.edge_arch + grpc_port = local.edge_grpc_port + http_port = local.edge_http_port + https_port = local.edge_https_port + # log_level = "info" + + ami = data.aws_ami.ubuntu[local.edge_arch].id + key_name = local.ssh_key_name + network_interface_id = module.network.edge_network_interface_id +} + +module "defguard_gateway" { + # source = "../../modules/gateway" + source = "github.com/DefGuard/deployment//terraform2.0/modules/gateway?ref=main" + + ami = data.aws_ami.ubuntu[local.gateway_arch].id + instance_type = local.gateway_instance_type + package_version = local.gateway_package_version + arch = local.gateway_arch + + grpc_port = local.gateway_grpc_port + nat = local.gateway_nat + key_name = local.ssh_key_name + network_interface_id = module.network.gateway_network_interface_id + # log_level = "info" +} + +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 5.0" + + name = local.vpc_name + cidr = local.vpc_cidr + azs = local.azs + private_subnets = local.vpc_private_subnets + public_subnets = local.vpc_public_subnets + + enable_dns_hostnames = true + + # Private Core needs NAT for outbound (deb download at boot, license checks). Gateway/Edge + # are public and egress via their own EIPs. + enable_nat_gateway = true + single_nat_gateway = true + + tags = local.vpc_tags +} + +########################################################################### +############### Network, security groups and database ##################### +########################################################################### + +# All networking (security groups, NICs, EIPs) and the RDS database live in the shared +# modules/network submodule, fed by the VPC created above. The same submodule is reused by +# examples/existing-vpc, where the vpc_id and subnet IDs come from a customer's own VPC. +module "network" { + # source = "../../modules/network" + source = "github.com/DefGuard/deployment//terraform2.0/modules/network?ref=main" + + vpc_id = module.vpc.vpc_id + vpc_cidr = local.vpc_cidr + public_subnet_id = module.vpc.public_subnets[0] + core_subnet_id = module.vpc.private_subnets[0] + db_subnet_ids = module.vpc.private_subnets + + ssh_admin_cidr = local.ssh_admin_cidr + core_http_port = local.core_http_port + gateway_grpc_port = local.gateway_grpc_port + wireguard_port = local.wireguard_port + edge_grpc_port = local.edge_grpc_port + edge_http_port = local.edge_http_port + edge_https_port = local.edge_https_port + + db_name = local.db_name + db_username = local.db_username + db_password = local.db_password + db_port = local.db_port + db_engine_version = local.db_engine_version + db_storage = local.db_storage + db_instance_class = local.db_instance_class +} + +########################################################################### +################################# Outputs ################################# +########################################################################### +# First-access steps after apply: +# 1. Core is private; reach its web UI and finish the setup wizard (2.0 has no admin-password +# env var). If you already have access to the private network (VPN, peering, bastion), +# open http://: directly. Otherwise enable +# SSH (set ssh_admin_cidr + ssh_key_name above) and tunnel through the gateway: +# ssh -L 8000:: ubuntu@ +# open http://localhost:8000 +# 2. In Core, set the WireGuard location endpoint to defguard_gateway_public_address +# (adoption stores the gateway's private IP) so external clients can connect. +# 3. In Core settings, set the enrollment URL to the edge public address; users enroll via +# the public Edge with an admin-generated token (no Core access needed). + +output "defguard_core_private_address" { + description = "IP address of Defguard Core instance in the internal network" + value = module.network.core_private_ip +} + +output "defguard_edge_public_address" { + description = "Public IP address of Defguard Edge instance" + value = module.network.edge_public_ip +} + +output "defguard_edge_private_address" { + description = "Private IP address of Defguard Edge instance" + value = module.network.edge_private_ip +} + +output "defguard_gateway_public_address" { + description = "Public IP address of Defguard Gateway instance" + value = module.network.gateway_public_ip +} + +output "defguard_gateway_private_address" { + description = "Private IP address of Defguard Gateway instance" + value = module.network.gateway_private_ip +} diff --git a/terraform2.0/examples/existing-vpc/main.tf.example b/terraform2.0/examples/existing-vpc/main.tf.example new file mode 100644 index 0000000..23acd4a --- /dev/null +++ b/terraform2.0/examples/existing-vpc/main.tf.example @@ -0,0 +1,277 @@ +# Deploy Defguard 2.0 into an EXISTING VPC. +# +# Unlike examples/basic (which creates its own VPC + subnets + NAT), this example takes a +# VPC and subnets you already run and places the Defguard components into them. It creates +# only the security groups, network interfaces, EIPs and the RDS database (via the shared +# modules/network submodule) plus the three component instances. +# +# Your network must satisfy these requirements: +# - core_subnet_id : a PRIVATE subnet with outbound internet (a NAT gateway/instance). +# Core has no public IP and must download its deb + reach the license +# server on first boot. +# - public_subnet_id : a PUBLIC subnet (internet gateway route) so the Gateway and Edge +# EIPs work. Gateway needs inbound UDP; Edge needs inbound HTTPS. To put +# them in separate subnets, also set gateway_subnet_id / edge_subnet_id. +# - db_subnet_ids : at least TWO subnets in DIFFERENT availability zones (RDS subnet +# group requirement). Private subnets recommended. + +locals { + ############################ Core configuration ########################### + core_grpc_port = 50055 + core_http_port = 8000 + core_cookie_insecure = false + core_package_version = "2.0.1" + core_arch = "x86_64" + core_instance_type = "t3.micro" + + ############################ Edge configuration ########################### + edge_grpc_port = 50051 + edge_http_port = 8080 + edge_https_port = 443 + edge_package_version = "2.0.1" + edge_arch = "x86_64" + edge_instance_type = "t3.micro" + + ###################### VPN and Gateway configuration ###################### + gateway_grpc_port = 50066 + wireguard_port = 51820 + gateway_nat = true + gateway_package_version = "2.0.1" + gateway_arch = "x86_64" + gateway_instance_type = "t3.micro" + + ########################## Database configuration ######################### + db_engine_version = "18" + db_name = "defguard" + db_username = "defguard" + db_password = "defguard" + db_port = 5432 + db_storage = 20 + db_instance_class = "db.t3.micro" +} + +variable "region" { + description = "AWS region the existing VPC lives in" + type = string +} + +variable "vpc_id" { + description = "ID of your existing VPC" + type = string +} + +variable "public_subnet_id" { + description = "Existing PUBLIC subnet (IGW route) for the Gateway and Edge. Used for whichever of the two is not given its own subnet below." + type = string +} + +variable "gateway_subnet_id" { + description = "Optional separate PUBLIC subnet for the Gateway. Defaults to public_subnet_id when null." + type = string + default = null +} + +variable "edge_subnet_id" { + description = "Optional separate PUBLIC subnet for the Edge. Defaults to public_subnet_id when null." + type = string + default = null +} + +variable "core_subnet_id" { + description = "Existing PRIVATE subnet with NAT egress for the Core" + type = string +} + +variable "db_subnet_ids" { + description = "At least two existing subnets in different AZs for the RDS subnet group" + type = list(string) +} + +variable "ssh_key_name" { + description = "Existing EC2 key pair (in this region) attached to all components for SSH. null (default) launches without a key. Required if you enable SSH via ssh_admin_cidr." + type = string + default = null +} + +variable "ssh_admin_cidr" { + description = "CIDR allowed to SSH into the components. null (default) disables SSH entirely (no port-22 ingress). Set a /32 to allow a single host; avoid 0.0.0.0/0." + type = string + default = null +} + +terraform { + required_version = ">= 1.5.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + } +} + +# Credentials come from the standard AWS provider chain (environment variables, a shared +# profile, SSO, or an instance role). Set e.g. AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY or +# AWS_PROFILE before running terraform. +provider "aws" { + region = var.region +} + +# The VPC CIDR is read from the existing VPC and used for the VPC-internal security group +# rules (Core UI/SSH, Edge HTTP). +data "aws_vpc" "selected" { + id = var.vpc_id +} + +# Maps the deb package architecture (x86_64/aarch64) to the Ubuntu AMI name token +# (amd64/arm64), so the AMI a component boots matches the package its setup.sh downloads. +locals { + ubuntu_ami_arch = { + x86_64 = "amd64" + aarch64 = "arm64" + } +} + +# One lookup per distinct component architecture; each component picks +# data.aws_ami.ubuntu[] below. When using aarch64, also set that component's +# instance_type to an arm64 type (e.g. t4g.micro). +data "aws_ami" "ubuntu" { + for_each = toset([local.core_arch, local.edge_arch, local.gateway_arch]) + most_recent = true + owners = ["099720109477"] + + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-${local.ubuntu_ami_arch[each.value]}-server-*"] + } +} + +########################################################################### +############### Network, security groups and database ##################### +########################################################################### + +module "network" { + # source = "../../modules/network" + source = "github.com/DefGuard/deployment//terraform2.0/modules/network?ref=main" + + vpc_id = var.vpc_id + vpc_cidr = data.aws_vpc.selected.cidr_block + public_subnet_id = var.public_subnet_id + gateway_subnet_id = var.gateway_subnet_id + edge_subnet_id = var.edge_subnet_id + core_subnet_id = var.core_subnet_id + db_subnet_ids = var.db_subnet_ids + + ssh_admin_cidr = var.ssh_admin_cidr + core_http_port = local.core_http_port + gateway_grpc_port = local.gateway_grpc_port + wireguard_port = local.wireguard_port + edge_grpc_port = local.edge_grpc_port + edge_http_port = local.edge_http_port + edge_https_port = local.edge_https_port + + db_name = local.db_name + db_username = local.db_username + db_password = local.db_password + db_port = local.db_port + db_engine_version = local.db_engine_version + db_storage = local.db_storage + db_instance_class = local.db_instance_class +} + +########################################################################### +########################## Defguard components ############################ +########################################################################### + +module "defguard_core" { + # source = "../../modules/core" + source = "github.com/DefGuard/deployment//terraform2.0/modules/core?ref=main" + instance_type = local.core_instance_type + package_version = local.core_package_version + arch = local.core_arch + ami = data.aws_ami.ubuntu[local.core_arch].id + + grpc_port = local.core_grpc_port + http_port = local.core_http_port + cookie_insecure = local.core_cookie_insecure + + gateway_address = module.network.gateway_private_ip + gateway_grpc_port = local.gateway_grpc_port + edge_address = module.network.edge_private_ip + edge_grpc_port = local.edge_grpc_port + + db_details = module.network.db_details + key_name = var.ssh_key_name + network_interface_id = module.network.core_network_interface_id + + # Gateway and edge must be listening before Core runs its one-shot adoption. + depends_on = [ + module.defguard_gateway, + module.defguard_edge, + ] +} + +module "defguard_edge" { + # source = "../../modules/edge" + source = "github.com/DefGuard/deployment//terraform2.0/modules/edge?ref=main" + + instance_type = local.edge_instance_type + package_version = local.edge_package_version + arch = local.edge_arch + grpc_port = local.edge_grpc_port + http_port = local.edge_http_port + https_port = local.edge_https_port + + ami = data.aws_ami.ubuntu[local.edge_arch].id + key_name = var.ssh_key_name + network_interface_id = module.network.edge_network_interface_id +} + +module "defguard_gateway" { + # source = "../../modules/gateway" + source = "github.com/DefGuard/deployment//terraform2.0/modules/gateway?ref=main" + + ami = data.aws_ami.ubuntu[local.gateway_arch].id + instance_type = local.gateway_instance_type + package_version = local.gateway_package_version + arch = local.gateway_arch + + grpc_port = local.gateway_grpc_port + nat = local.gateway_nat + key_name = var.ssh_key_name + network_interface_id = module.network.gateway_network_interface_id +} + +########################################################################### +################################# Outputs ################################# +########################################################################### +# Core is private: if you already have access to +# the private network (VPN, peering, bastion) open http://:8000 directly, +# otherwise enable SSH (set ssh_admin_cidr + ssh_key_name) and tunnel through the public +# Gateway (ssh -L 8000::8000 ubuntu@). +# Then complete the setup wizard, set the enrollment URL to the Edge public address, and adopt +# a WireGuard location whose CIDR does NOT overlap your VPC CIDR. + +output "defguard_core_private_address" { + description = "Private IP of the Defguard Core instance" + value = module.network.core_private_ip +} + +output "defguard_edge_public_address" { + description = "Public IP of the Defguard Edge instance" + value = module.network.edge_public_ip +} + +output "defguard_edge_private_address" { + description = "Private IP of the Defguard Edge instance" + value = module.network.edge_private_ip +} + +output "defguard_gateway_public_address" { + description = "Public IP of the Defguard Gateway instance" + value = module.network.gateway_public_ip +} + +output "defguard_gateway_private_address" { + description = "Private IP of the Defguard Gateway instance" + value = module.network.gateway_private_ip +} diff --git a/terraform2.0/modules/core/main.tf b/terraform2.0/modules/core/main.tf new file mode 100644 index 0000000..1f407d6 --- /dev/null +++ b/terraform2.0/modules/core/main.tf @@ -0,0 +1,32 @@ +resource "aws_instance" "defguard_core" { + ami = var.ami + instance_type = var.instance_type + key_name = var.key_name + + user_data = templatefile("${path.module}/setup.sh", { + db_address = var.db_details.address + db_password = var.db_details.password + db_username = var.db_details.username + db_name = var.db_details.name + db_port = var.db_details.port + grpc_port = var.grpc_port + http_port = var.http_port + gateway_address = var.gateway_address + gateway_grpc_port = var.gateway_grpc_port + edge_address = var.edge_address + edge_grpc_port = var.edge_grpc_port + package_version = var.package_version + arch = var.arch + cookie_insecure = var.cookie_insecure + log_level = var.log_level + }) + user_data_replace_on_change = true + + primary_network_interface { + network_interface_id = var.network_interface_id + } + + tags = { + Name = "defguard-core-instance" + } +} diff --git a/terraform2.0/modules/core/outputs.tf b/terraform2.0/modules/core/outputs.tf new file mode 100644 index 0000000..967ef1a --- /dev/null +++ b/terraform2.0/modules/core/outputs.tf @@ -0,0 +1,4 @@ +output "instance_id" { + description = "ID of Defguard Core instance" + value = aws_instance.defguard_core.id +} diff --git a/terraform2.0/modules/core/setup.sh b/terraform2.0/modules/core/setup.sh new file mode 100755 index 0000000..b7996df --- /dev/null +++ b/terraform2.0/modules/core/setup.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -e + +LOG_FILE="/var/log/defguard.log" + +log() { + echo "$(date '+%Y-%m-%d %H:%M:%S') $1" +} + +# Wait until a TCP host:port accepts connections, up to a bounded number of attempts. +# Core auto-adoption runs once on startup with a short per-target timeout and no retry, +# so the gateway and edge gRPC servers must be listening before core starts. +wait_for_port() { + local host="$1" + local port="$2" + local attempts="$3" + local i=0 + while [ "$i" -lt "$attempts" ]; do + if timeout 2 bash -c "echo > /dev/tcp/$host/$port" 2>/dev/null; then + log "Reachable: $host:$port" + return 0 + fi + i=$((i + 1)) + sleep 5 + done + log "WARNING: $host:$port not reachable after $attempts attempts; auto-adoption may fail" + return 1 +} + +( +log "Updating apt repositories..." +apt update + +log "Installing curl..." +apt install -y curl + +log "Downloading defguard-core package..." +curl -fsSL -o /tmp/defguard-core.deb https://github.com/DefGuard/defguard/releases/download/v${package_version}/defguard-${package_version}-${arch}-unknown-linux-gnu.deb + +log "Installing defguard-core package..." +# apt-get resolves the deb's dependencies (dpkg -i would not). +apt-get install -y /tmp/defguard-core.deb + +log "Writing Core configuration to /etc/defguard/core.conf..." +tee /etc/defguard/core.conf <&1 | tee -a "$LOG_FILE" diff --git a/terraform2.0/modules/core/variables.tf b/terraform2.0/modules/core/variables.tf new file mode 100644 index 0000000..9dd2139 --- /dev/null +++ b/terraform2.0/modules/core/variables.tf @@ -0,0 +1,88 @@ +variable "ami" { + description = "AMI ID for the instance" + type = string +} + +variable "instance_type" { + description = "Instance type for the instance" + type = string + default = "t3.micro" +} + +variable "db_details" { + description = "Details of the database connection" + sensitive = true + type = object({ + name = string + username = string + password = string + port = number + address = string + }) +} + +variable "grpc_port" { + description = "Port the Defguard Core gRPC server listens on" + type = number + default = 50055 +} + +variable "http_port" { + description = "Port to be used to access Defguard Core via HTTP" + type = number + default = 8000 +} + +variable "gateway_address" { + description = "Address Core dials to adopt the gateway. Also reused as the WireGuard location endpoint; if a private address is passed, set the location endpoint in the Core web UI after adoption so external clients can connect." + type = string +} + +variable "gateway_grpc_port" { + description = "Port the Defguard Gateway gRPC server listens on" + type = number + default = 50066 +} + +variable "edge_address" { + description = "Address Core dials to adopt the edge. Used only for internal core->edge gRPC, so the private address is preferred." + type = string +} + +variable "edge_grpc_port" { + description = "Port the Defguard Edge gRPC server listens on" + type = number + default = 50051 +} + +variable "network_interface_id" { + description = "Network interface ID for the instance" + type = string +} + +variable "package_version" { + description = "Version of the Defguard Core package to be installed" + type = string +} + +variable "arch" { + description = "Architecture of the Defguard Core package to be installed" + type = string +} + +variable "cookie_insecure" { + description = "Whether to use insecure cookies for the Defguard Core" + type = bool +} + +variable "log_level" { + description = "Log level for Defguard Core. Possible values: trace, debug, info, warn, error" + type = string + default = "info" +} + +variable "key_name" { + description = "Name of an existing EC2 key pair to attach (for SSH access). Leave null to launch without a key." + type = string + default = null +} diff --git a/terraform2.0/modules/edge/main.tf b/terraform2.0/modules/edge/main.tf new file mode 100644 index 0000000..763a4bf --- /dev/null +++ b/terraform2.0/modules/edge/main.tf @@ -0,0 +1,23 @@ +resource "aws_instance" "defguard_edge" { + ami = var.ami + instance_type = var.instance_type + key_name = var.key_name + + user_data = templatefile("${path.module}/setup.sh", { + grpc_port = var.grpc_port + arch = var.arch + package_version = var.package_version + http_port = var.http_port + https_port = var.https_port + log_level = var.log_level + }) + user_data_replace_on_change = true + + primary_network_interface { + network_interface_id = var.network_interface_id + } + + tags = { + Name = "defguard-edge-instance" + } +} diff --git a/terraform2.0/modules/edge/outputs.tf b/terraform2.0/modules/edge/outputs.tf new file mode 100644 index 0000000..df5a1c2 --- /dev/null +++ b/terraform2.0/modules/edge/outputs.tf @@ -0,0 +1,9 @@ +output "edge_private_address" { + description = "Private IP address of Defguard Edge instance" + value = aws_instance.defguard_edge.private_ip +} + +output "instance_id" { + description = "ID of Defguard Edge instance" + value = aws_instance.defguard_edge.id +} diff --git a/terraform2.0/modules/edge/setup.sh b/terraform2.0/modules/edge/setup.sh new file mode 100644 index 0000000..53f2bde --- /dev/null +++ b/terraform2.0/modules/edge/setup.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -e + +LOG_FILE="/var/log/defguard.log" + +log() { + echo "$(date '+%Y-%m-%d %H:%M:%S') $1" +} + +( +log "Updating apt repositories..." +apt update + +log "Installing curl..." +apt install -y curl + +log "Downloading defguard-proxy package..." +curl -fsSL -o /tmp/defguard-proxy.deb https://github.com/DefGuard/proxy/releases/download/v${package_version}/defguard-proxy-${package_version}-${arch}-unknown-linux-gnu.deb + +log "Installing defguard-proxy package..." +# apt-get resolves the deb's dependencies (dpkg -i would not). +apt-get install -y /tmp/defguard-proxy.deb + +# The edge runs as the 'defguard' user, so the cert dir must be writable by it. +log "Ensuring certificate directory exists..." +mkdir -p /etc/defguard/certs +chown -R defguard:defguard /etc/defguard/certs + +log "Writing edge configuration to /etc/defguard/proxy.toml..." +tee /etc/defguard/proxy.toml <&1 | tee -a "$LOG_FILE" diff --git a/terraform2.0/modules/edge/variables.tf b/terraform2.0/modules/edge/variables.tf new file mode 100644 index 0000000..5185163 --- /dev/null +++ b/terraform2.0/modules/edge/variables.tf @@ -0,0 +1,55 @@ +variable "ami" { + description = "AMI ID for the instance" + type = string +} + +variable "instance_type" { + description = "Instance type for the instance" + type = string + default = "t3.micro" +} + +variable "grpc_port" { + description = "Port the Defguard Edge gRPC server listens on (Core dials this)" + type = number + default = 50051 +} + +variable "http_port" { + description = "Port to be used to access the Defguard Edge enrollment server via HTTP" + type = number + default = 8080 +} + +variable "https_port" { + description = "Port the Defguard Edge HTTPS server listens on" + type = number + default = 443 +} + +variable "network_interface_id" { + description = "Network interface ID for the instance" + type = string +} + +variable "arch" { + description = "Architecture of the Defguard Edge package to be installed" + type = string +} + +variable "package_version" { + description = "Version of the Defguard Edge package to be installed" + type = string +} + +variable "log_level" { + description = "Log level for Defguard Edge. Possible values: trace, debug, info, warn, error" + type = string + default = "info" +} + +variable "key_name" { + description = "Name of an existing EC2 key pair to attach (for SSH access). Leave null to launch without a key." + type = string + default = null +} diff --git a/terraform2.0/modules/gateway/main.tf b/terraform2.0/modules/gateway/main.tf new file mode 100644 index 0000000..418394c --- /dev/null +++ b/terraform2.0/modules/gateway/main.tf @@ -0,0 +1,22 @@ +resource "aws_instance" "defguard_gateway" { + ami = var.ami + instance_type = var.instance_type + key_name = var.key_name + + user_data = templatefile("${path.module}/setup.sh", { + grpc_port = var.grpc_port + package_version = var.package_version + nat = var.nat + arch = var.arch + log_level = var.log_level + }) + user_data_replace_on_change = true + + primary_network_interface { + network_interface_id = var.network_interface_id + } + + tags = { + Name = "defguard-gateway-instance" + } +} diff --git a/terraform2.0/modules/gateway/outputs.tf b/terraform2.0/modules/gateway/outputs.tf new file mode 100644 index 0000000..4f144a9 --- /dev/null +++ b/terraform2.0/modules/gateway/outputs.tf @@ -0,0 +1,4 @@ +output "instance_id" { + description = "ID of Defguard Gateway instance" + value = aws_instance.defguard_gateway.id +} diff --git a/terraform2.0/modules/gateway/setup.sh b/terraform2.0/modules/gateway/setup.sh new file mode 100644 index 0000000..1dce883 --- /dev/null +++ b/terraform2.0/modules/gateway/setup.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -e + +LOG_FILE="/var/log/defguard.log" + +log() { + echo "$(date '+%Y-%m-%d %H:%M:%S') $1" +} + +( +log "Updating apt repositories..." +apt update + +log "Installing curl..." +apt install -y curl + +log "Downloading defguard-gateway package..." +curl -fsSL -o /tmp/defguard-gateway.deb https://github.com/DefGuard/gateway/releases/download/v${package_version}/defguard-gateway-${package_version}-${arch}-unknown-linux-gnu.deb + +log "Installing defguard-gateway package..." +# apt-get resolves the deb's dependencies (dpkg -i would not). +apt-get install -y /tmp/defguard-gateway.deb + +log "Ensuring certificate directory exists..." +mkdir -p /etc/defguard/certs + +log "Writing gateway configuration to /etc/defguard/gateway.toml..." +tee /etc/defguard/gateway.toml <&1 | tee -a "$LOG_FILE" diff --git a/terraform2.0/modules/gateway/variables.tf b/terraform2.0/modules/gateway/variables.tf new file mode 100644 index 0000000..ac8fb8d --- /dev/null +++ b/terraform2.0/modules/gateway/variables.tf @@ -0,0 +1,49 @@ +variable "ami" { + description = "AMI ID for the instance" + type = string +} + +variable "instance_type" { + description = "Instance type for the instance" + type = string + default = "t3.micro" +} + +variable "grpc_port" { + description = "Port the Defguard Gateway gRPC server listens on (Core dials this)" + type = number + default = 50066 +} + +variable "network_interface_id" { + description = "Network interface ID for the instance" + type = string +} + +variable "package_version" { + description = "Version of the Defguard Gateway package to be installed" + type = string +} + +variable "arch" { + description = "Architecture of the Defguard Gateway package to be installed" + type = string +} + +variable "nat" { + description = "Enable masquerading" + type = bool + default = true +} + +variable "key_name" { + description = "Name of an existing EC2 key pair to attach (for SSH/tunnel access). Leave null to launch without a key." + type = string + default = null +} + +variable "log_level" { + description = "Log level for Defguard Gateway. Possible values: trace, debug, info, warn, error" + type = string + default = "info" +} diff --git a/terraform2.0/modules/network/main.tf b/terraform2.0/modules/network/main.tf new file mode 100644 index 0000000..5039111 --- /dev/null +++ b/terraform2.0/modules/network/main.tf @@ -0,0 +1,241 @@ +########################################################################### +# Security groups +########################################################################### + +# Core is the control plane and stays entirely private. SSH and the web UI are reachable +# only from within the VPC (hop through the public Gateway, a bastion, or SSM). +resource "aws_security_group" "core" { + name = "${var.name_prefix}-core-sg" + description = "Core access" + vpc_id = var.vpc_id + + dynamic "ingress" { + for_each = var.ssh_admin_cidr == null ? [] : [1] + content { + description = "SSH from within the VPC" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = [var.vpc_cidr] + } + } + + ingress { + description = "Web UI from within the VPC / over the VPN" + from_port = var.core_http_port + to_port = var.core_http_port + protocol = "tcp" + cidr_blocks = [var.vpc_cidr] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +# Gateway exposes only its WireGuard UDP port publicly. gRPC is reachable only from Core. +resource "aws_security_group" "gateway" { + name = "${var.name_prefix}-gateway-sg" + description = "Gateway access" + vpc_id = var.vpc_id + + dynamic "ingress" { + for_each = var.ssh_admin_cidr == null ? [] : [1] + content { + description = "SSH from the admin IP" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = [var.ssh_admin_cidr] + } + } + + ingress { + description = "WireGuard VPN traffic from clients" + from_port = var.wireguard_port + to_port = var.wireguard_port + protocol = "udp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + description = "gRPC from Core (adoption + control stream)" + from_port = var.gateway_grpc_port + to_port = var.gateway_grpc_port + protocol = "tcp" + security_groups = [aws_security_group.core.id] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +# Edge is the only public HTTPS interface. Plain HTTP is VPC-internal; 80 is for ACME. +resource "aws_security_group" "edge" { + name = "${var.name_prefix}-edge-sg" + description = "Edge access" + vpc_id = var.vpc_id + + dynamic "ingress" { + for_each = var.ssh_admin_cidr == null ? [] : [1] + content { + description = "SSH from the admin IP" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = [var.ssh_admin_cidr] + } + } + + ingress { + description = "Public HTTPS (enrollment + client communication)" + from_port = var.edge_https_port + to_port = var.edge_https_port + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + description = "ACME HTTP-01 challenge for the :443 certificate" + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + description = "Plain HTTP API, VPC-internal only (pre-TLS testing)" + from_port = var.edge_http_port + to_port = var.edge_http_port + protocol = "tcp" + cidr_blocks = [var.vpc_cidr] + } + + ingress { + description = "gRPC from Core (adoption + control stream)" + from_port = var.edge_grpc_port + to_port = var.edge_grpc_port + protocol = "tcp" + security_groups = [aws_security_group.core.id] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_security_group" "db" { + name = "${var.name_prefix}-db-sg" + description = "Access to the database" + vpc_id = var.vpc_id + + ingress { + description = "PostgreSQL from Core" + from_port = var.db_port + to_port = var.db_port + protocol = "tcp" + security_groups = [aws_security_group.core.id] + } + + tags = { + Name = "${var.name_prefix}-db-sg" + } +} + +########################################################################### +# Network interfaces and elastic IPs +########################################################################### + +resource "aws_network_interface" "core" { + subnet_id = var.core_subnet_id + security_groups = [aws_security_group.core.id] + + tags = { + Name = "${var.name_prefix}-core-network-interface" + } +} + +resource "aws_network_interface" "gateway" { + subnet_id = coalesce(var.gateway_subnet_id, var.public_subnet_id) + security_groups = [aws_security_group.gateway.id] + + tags = { + Name = "${var.name_prefix}-gateway-network-interface" + } +} + +resource "aws_network_interface" "edge" { + subnet_id = coalesce(var.edge_subnet_id, var.public_subnet_id) + security_groups = [aws_security_group.edge.id] + + tags = { + Name = "${var.name_prefix}-edge-network-interface" + } +} + +# Gateway needs a public IP so external WireGuard clients can reach its UDP port. +resource "aws_eip" "gateway" { + domain = "vpc" +} + +resource "aws_eip_association" "gateway" { + network_interface_id = aws_network_interface.gateway.id + allocation_id = aws_eip.gateway.id +} + +# Edge is the public-facing component for enrollment and client communication. +resource "aws_eip" "edge" { + domain = "vpc" +} + +resource "aws_eip_association" "edge" { + network_interface_id = aws_network_interface.edge.id + allocation_id = aws_eip.edge.id +} + +########################################################################### +# Database +########################################################################### + +resource "aws_db_instance" "core" { + engine = "postgres" + engine_version = var.db_engine_version + instance_class = var.db_instance_class + username = var.db_username + password = var.db_password + db_name = var.db_name + port = var.db_port + skip_final_snapshot = true + allocated_storage = var.db_storage + db_subnet_group_name = aws_db_subnet_group.core.name + vpc_security_group_ids = [aws_security_group.db.id] + parameter_group_name = aws_db_parameter_group.core.name + storage_encrypted = true + backup_retention_period = 7 + deletion_protection = false +} + +resource "aws_db_parameter_group" "core" { + name = "${var.name_prefix}-db-parameter-group" + family = "postgres${var.db_engine_version}" + + parameter { + name = "rds.force_ssl" + value = "1" + } +} + +resource "aws_db_subnet_group" "core" { + name = "${var.name_prefix}-db-subnet-group" + subnet_ids = var.db_subnet_ids +} diff --git a/terraform2.0/modules/network/outputs.tf b/terraform2.0/modules/network/outputs.tf new file mode 100644 index 0000000..9285005 --- /dev/null +++ b/terraform2.0/modules/network/outputs.tf @@ -0,0 +1,51 @@ +output "core_network_interface_id" { + description = "Network interface ID for the Core instance" + value = aws_network_interface.core.id +} + +output "gateway_network_interface_id" { + description = "Network interface ID for the Gateway instance" + value = aws_network_interface.gateway.id +} + +output "edge_network_interface_id" { + description = "Network interface ID for the Edge instance" + value = aws_network_interface.edge.id +} + +output "core_private_ip" { + description = "Private IP of the Core network interface" + value = aws_network_interface.core.private_ip +} + +output "gateway_private_ip" { + description = "Private IP of the Gateway network interface (used as Core's adoption target)" + value = aws_network_interface.gateway.private_ip +} + +output "edge_private_ip" { + description = "Private IP of the Edge network interface (used as Core's adoption target)" + value = aws_network_interface.edge.private_ip +} + +output "gateway_public_ip" { + description = "Public EIP of the Gateway (WireGuard endpoint for clients)" + value = aws_eip.gateway.public_ip +} + +output "edge_public_ip" { + description = "Public EIP of the Edge (enrollment / client HTTPS)" + value = aws_eip.edge.public_ip +} + +output "db_details" { + description = "Database connection details, in the shape the Core module expects" + sensitive = true + value = { + name = var.db_name + username = var.db_username + password = var.db_password + port = var.db_port + address = aws_db_instance.core.address + } +} diff --git a/terraform2.0/modules/network/variables.tf b/terraform2.0/modules/network/variables.tf new file mode 100644 index 0000000..f627088 --- /dev/null +++ b/terraform2.0/modules/network/variables.tf @@ -0,0 +1,126 @@ +variable "vpc_id" { + description = "ID of the VPC the Defguard components are deployed into" + type = string +} + +variable "vpc_cidr" { + description = "CIDR of the VPC. Used for security group rules that allow VPC-internal access (Core UI/SSH, Edge plain HTTP)." + type = string +} + +variable "name_prefix" { + description = "Prefix for all created resource names (security groups, RDS parameter/subnet group, network interfaces). Override it to run more than one deployment in the same account/region/VPC without name collisions." + type = string + default = "defguard" +} + +variable "public_subnet_id" { + description = "Default public subnet (with an internet gateway route) for the Gateway and Edge network interfaces. Used for whichever of the two is not given its own subnet below." + type = string +} + +variable "gateway_subnet_id" { + description = "Optional public subnet for the Gateway NIC. Defaults to public_subnet_id when null." + type = string + default = null +} + +variable "edge_subnet_id" { + description = "Optional public subnet for the Edge NIC. Defaults to public_subnet_id when null." + type = string + default = null +} + +variable "core_subnet_id" { + description = "Private subnet for the Core network interface. Must have outbound internet (NAT) for the deb download and license checks." + type = string +} + +variable "db_subnet_ids" { + description = "Subnets for the RDS subnet group. Must span at least two availability zones." + type = list(string) +} + +variable "ssh_admin_cidr" { + description = "CIDR allowed to SSH into the components. null (default) disables SSH entirely (no port-22 ingress is created). Set a /32 to allow SSH from a single host; avoid 0.0.0.0/0." + type = string + default = null +} + +variable "core_http_port" { + description = "Core web UI port (reachable only from within the VPC)" + type = number + default = 8000 +} + +variable "gateway_grpc_port" { + description = "Gateway gRPC port that Core dials for adoption/control" + type = number + default = 50066 +} + +variable "wireguard_port" { + description = "UDP port the WireGuard VPN listens on (public)" + type = number + default = 51820 +} + +variable "edge_grpc_port" { + description = "Edge gRPC port that Core dials for adoption/control" + type = number + default = 50051 +} + +variable "edge_http_port" { + description = "Edge plain HTTP API port (reachable only from within the VPC)" + type = number + default = 8080 +} + +variable "edge_https_port" { + description = "Edge public HTTPS port" + type = number + default = 443 +} + +variable "db_name" { + description = "Name of the database created for Defguard Core" + type = string + default = "defguard" +} + +variable "db_username" { + description = "Database username" + type = string + default = "defguard" +} + +variable "db_password" { + description = "Database password" + type = string + sensitive = true +} + +variable "db_port" { + description = "Database port" + type = number + default = 5432 +} + +variable "db_engine_version" { + description = "Major PostgreSQL engine version. The parameter group family is derived from this." + type = string + default = "18" +} + +variable "db_storage" { + description = "Allocated storage for the database in GB" + type = number + default = 20 +} + +variable "db_instance_class" { + description = "RDS instance class" + type = string + default = "db.t3.micro" +}