diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 0000000..9d8f551 --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,34 @@ +*.auto.tfvars +.terraform.lock.hcl +terraform.state.* + +# Local .terraform directories (contains provider plugins and modules) +.terraform/ +.terraform.lock.hcl.tmp + +# Terraform state files (may contain sensitive data/passwords) +*.tfstate +*.tfstate.* +*.tfstate.backup + +# Crash logs +crash.log +crash.*.log + +# Variable files containing sensitive values / secrets +# (Uncomment the line below if you keep non-sensitive examples like 'terraform.tfvars.example' in git) +*.tfvars +*.tfvars.json + +# Override files (used for local development overrides) +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# CLI configuration files +.terraformrc +terraform.rc + +# Execution plan files (generated by 'terraform plan -out=...') +*.tfplan diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 0000000..fccba44 --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,4 @@ +# Terraform + +Sandox for learning terraform and hold onto any modules that are easy recipes that I may use other places and willing to share. + diff --git a/terraform/home-ec2/README.md b/terraform/home-ec2/README.md new file mode 100644 index 0000000..9e73e6d --- /dev/null +++ b/terraform/home-ec2/README.md @@ -0,0 +1,96 @@ +# Home EC2 + +Simple Module to create an ec2 instance for you to login to from your home network and only you can access. This can be used for tunneling to various resources in your VPC from your local box. + +## Variables + +| Variable Name | Description | Type | Default Value | Required? | +| --- | --- | --- | --- | --- | +| `aws_profile_name` | Name of your local aws profile | `string` | *None* | **Yes** | +| `aws_key_pair_name` | Name of your AWS key pair that will be used for ssh | `string` | *None* | **Yes** | +| `aws_ami_image_id` | image id for the ami. | `string` | *None* | **Yes** | +| `aws_instance_type` | Size of the image to stand up | `string` | `"t3a.nano"` | No | +| `aws_additional_security_groups` | Additional security groups on top of the ssh only one created by module | `list(string)` | `[]` | No | +| `aws_instance_startup_script` | Startup script for instance. | `string` | `null` | No | +| `aws_subnet_id` | id for the subnet | `string` | `null` | No | +| `aws_ec2_instance_name` | Name for the EC2 Instance | `string` | `"home-ec2"` | No | + +## Outputs + +| Output Name | Description | Type | +| --- | --- | --- | +| `ssh_command` | SSH Command you can run to access the image | `string` | + +## Commands + +### Create the EC2 + +```bash +MacBookPro:home-ec2 mkwyche$ terraform apply +data.external.force_ipv4: Reading... +data.external.local_user: Reading... +data.http.my_public_ip: Reading... +data.external.local_user: Read complete after 0s [id=-] +data.external.force_ipv4: Read complete after 0s [id=-] +data.http.my_public_ip: Read complete after 0s [id=https://ifconfig.me/ip] +data.aws_key_pair.key_pair: Reading... +data.aws_ami.ami: Reading... +data.aws_key_pair.key_pair: Read complete after 0s [id=key-0aacf6008840baa11] +data.aws_ami.ami: Read complete after 0s [id=ami-01edba92f9036f76e] + +Apply complete! Resources: 6 added, 0 changed, 0 destroyed. +``` + +### SSH into the EC2 + +If your key has been added locally to ssh(`ssh-add -l`) you can now easily ssh in. + +```bash +ssh ec2-user@ec2-13-220-44-159.compute-1.amazonaws.com +The authenticity of host 'ec2-13-220-44-159.compute-1.amazonaws.com (13.220.44.159)' can't be established. +Are you sure you want to continue connecting (yes/no/[fingerprint])? yes +Warning: Permanently added 'ec2-13-220-44-159.compute-1.amazonaws.com' (ED25519) to the list of known hosts. +** WARNING: connection is not using a post-quantum key exchange algorithm. +** This session may be vulnerable to "store now, decrypt later" attacks. +** The server may need to be upgraded. See https://openssh.com/pq.html + , #_ + ~\_ ####_ Amazon Linux 2023 + ~~ \_#####\ + ~~ \###| + ~~ \#/ ___ https://aws.amazon.com/linux/amazon-linux-2023 + ~~ V~' '-> + ~~~ / + ~~._. _/ + _/ _/ + _/m/' +[ec2-user@ip-172-31-13-45 ~]$ +``` + +### Destroy the EC2 + +```bash +aws_vpc_security_group_ingress_rule.ssh_ingress_v4: Destroying... [id=sgr-0de0b6d7878d047ff] +aws_vpc_security_group_egress_rule.all_egress_v4: Destroying... [id=sgr-00fe6cc6be11c98e3] +aws_vpc_security_group_ingress_rule.ssh_ingress_v6[0]: Destroying... [id=sgr-014f4d42101b6e6c1] +aws_vpc_security_group_egress_rule.all_egress_v6: Destroying... [id=sgr-0f4ddbc6d5e168af3] +aws_instance.ec2_instance: Destroying... [id=i-03edc197398ff1491] +aws_vpc_security_group_egress_rule.all_egress_v6: Destruction complete after 1s +aws_vpc_security_group_ingress_rule.ssh_ingress_v6[0]: Destruction complete after 1s +aws_vpc_security_group_ingress_rule.ssh_ingress_v4: Destruction complete after 1s +aws_vpc_security_group_egress_rule.all_egress_v4: Destruction complete after 1s +aws_instance.ec2_instance: Still destroying... [id=i-03edc197398ff1491, 00m10s elapsed] +aws_instance.ec2_instance: Still destroying... [id=i-03edc197398ff1491, 00m20s elapsed] +aws_instance.ec2_instance: Still destroying... [id=i-03edc197398ff1491, 00m30s elapsed] +aws_instance.ec2_instance: Destruction complete after 30s +aws_security_group.ssh_restricted: Destroying... [id=sg-08ad998def9119e33] +aws_security_group.ssh_restricted: Destruction complete after 1s + +Destroy complete! Resources: 6 destroyed. +``` + + +## TODO + +- Figure out SSH from ipv6 instead of needing to force ipv4 +- Add a default user so you're not dependent on the default user in the AMI the issue I have with this is it makes passing down userdata a bit more complex but I'm sure it can be handled. +- Add dynamic filtering to just provide a OS name and it'll pull latest AMI for that OS. \ No newline at end of file diff --git a/terraform/home-ec2/main.tf b/terraform/home-ec2/main.tf new file mode 100644 index 0000000..87ba00d --- /dev/null +++ b/terraform/home-ec2/main.tf @@ -0,0 +1,142 @@ +# Pulls the AWS Subnet if provided by the user. +data "aws_subnet" "subnet" { + count = var.aws_subnet_id != null ? 1 : 0 + id = var.aws_subnet_id +} + +locals { + current_timestamp = timestamp() + formatted_timestamp = formatdate("YYYY-MM-DD'T'hh:mmZ", local.current_timestamp) +} + +locals { + # Just pull out the first default subnet + provided_subnet = var.aws_subnet_id != null ? data.aws_subnet.subnet[0].id : null +} + +data "external" "local_user" { + program = ["sh", "-c", "echo \"{\\\"user\\\": \\\"$(whoami)\\\"}\""] +} + +data "external" "force_ipv4" { + program = ["sh", "-c", "echo \"{\\\"ipv4\\\": \\\"$(curl -4 ifconfig.me)\\\"}\""] +} + +# Automatically query your current local workstation's public IP +data "http" "my_public_ip" { + url = "https://ifconfig.me/ip" +} + +locals { + is_ipv6 = can(regex(":", data.http.my_public_ip.response_body)) +} + +# 1. Base Security Group Container (Always created) +resource "aws_security_group" "ssh_restricted" { + name = "ssh-only-my-ip-test" + description = "Block all inbound SSH access except for the deployer workspace" +} + +# ========================================== +# IPv4 Rules (Created when local.is_ipv6 = false) +# ========================================== + +# Ingress: SSH via IPv4 +# TODO: figure out how to make this work. +resource "aws_vpc_security_group_ingress_rule" "ssh_ingress_v4" { + # TODO: If you can figure out the ipv6 rule hanging then you can make this conditional + # count = !local.is_ipv6 ? 1 : 0 + security_group_id = aws_security_group.ssh_restricted.id + description = "SSH from my workstation (IPv4)" + + ip_protocol = "tcp" + from_port = 22 + to_port = 22 + cidr_ipv4 = "${chomp(data.external.force_ipv4.result["ipv4"])}/32" +} + +# Ingress: SSH via IPv6 +# TODO: Below does not work need to figure out proper way to set this up... +resource "aws_vpc_security_group_ingress_rule" "ssh_ingress_v6" { + count = local.is_ipv6 ? 1 : 0 + security_group_id = aws_security_group.ssh_restricted.id + description = "SSH from my workstation (IPv6)" + + ip_protocol = "tcp" + from_port = 22 + to_port = 22 + cidr_ipv6 = "${chomp(data.http.my_public_ip.response_body)}/128" +} + +# Egress: All Outbound via IPv4 +resource "aws_vpc_security_group_egress_rule" "all_egress_v4" { + security_group_id = aws_security_group.ssh_restricted.id + description = "Allow all outbound IPv4 traffic" + + ip_protocol = "-1" + cidr_ipv4 = "0.0.0.0/0" +} + +# ========================================== +# IPv6 Rules (Created when local.is_ipv6 = true) +# ========================================== + +# Egress: All Outbound via IPv6 +resource "aws_vpc_security_group_egress_rule" "all_egress_v6" { + security_group_id = aws_security_group.ssh_restricted.id + description = "Allow all outbound IPv6 traffic" + + ip_protocol = "-1" + cidr_ipv6 = "::/0" +} + +# Key pair ssh +data "aws_key_pair" "key_pair" { + key_name = var.aws_key_pair_name +} + +# Try filtering things out +data "aws_ami" "ami" { + most_recent = true + owners = ["amazon"] # Official Canonical AWS Account ID + + filter { + name = "image-id" + values = [var.aws_ami_image_id] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } +} + +# 2. Infer default username from AMI name +# TODO: Eventually just add the user but below will work in the mean time. +locals { + ami_name = lower(data.aws_ami.ami.name) + + default_user = ( + can(regex("ubuntu", local.ami_name)) ? "ubuntu" : + can(regex("amzn|amazon", local.ami_name)) ? "ec2-user" : + can(regex("centos", local.ami_name)) ? "centos" : + can(regex("rhel|redhat", local.ami_name)) ? "ec2-user" : + can(regex("debian", local.ami_name)) ? "admin" : + can(regex("fedora", local.ami_name)) ? "fedora" : + can(regex("suse|sles", local.ami_name)) ? "ec2-user" : + can(regex("arch", local.ami_name)) ? "arch" : + "ec2-user" # Safe fallback for most custom/Linux AMIs + ) +} + +resource "aws_instance" "ec2_instance" { + ami = data.aws_ami.ami.id + instance_type = var.aws_instance_type + vpc_security_group_ids = concat([aws_security_group.ssh_restricted.id], var.aws_additional_security_groups) + key_name = data.aws_key_pair.key_pair.key_name + user_data = var.aws_instance_startup_script + subnet_id = local.provided_subnet + tags = { + Name = var.aws_ec2_instance_name + } +} \ No newline at end of file diff --git a/terraform/home-ec2/outputs.tf b/terraform/home-ec2/outputs.tf new file mode 100644 index 0000000..32895c4 --- /dev/null +++ b/terraform/home-ec2/outputs.tf @@ -0,0 +1,14 @@ +# output "home_ip" { +# description = "IP of your local workstation" +# value = data.http.my_public_ip.response_body +# } + +# TODO: Bring this back once you add the user on your own. +# output "local_os_username" { +# value = data.external.local_user.result["user"] +# } + +output "ssh_command" { + description = "SSH Command you can run to access the image" + value = "ssh ${local.default_user}@${aws_instance.ec2_instance.public_dns}" +} \ No newline at end of file diff --git a/terraform/home-ec2/providers.tf b/terraform/home-ec2/providers.tf new file mode 100644 index 0000000..bfb2fda --- /dev/null +++ b/terraform/home-ec2/providers.tf @@ -0,0 +1,18 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + external = { + source = "hashicorp/external" + version = "~> 2.3" + } + } +} + +provider "aws" { + profile = var.aws_profile_name +} + +provider "external" {} \ No newline at end of file diff --git a/terraform/home-ec2/variables.tf b/terraform/home-ec2/variables.tf new file mode 100644 index 0000000..9c8179b --- /dev/null +++ b/terraform/home-ec2/variables.tf @@ -0,0 +1,44 @@ +variable "aws_profile_name" { + description = "Name of your local aws profile" + type = string +} + +variable "aws_key_pair_name" { + description = "Name of your AWS key pair that will be used for ssh" + type = string +} + +variable "aws_ami_image_id" { + description = "image id for the ami." + type = string +} + +variable "aws_instance_type" { + description = "Size of the image to stand up" + type = string + default = "t3a.nano" +} + +variable "aws_additional_security_groups" { + description = "Additional security groups on top of the ssh only one created by module" + type = list(string) + default = [] +} + +variable "aws_instance_startup_script" { + description = "Startup script for instance." + type = string + default = null +} + +variable "aws_subnet_id" { + description = "id for the subnet" + type = string + default = null +} + +variable "aws_ec2_instance_name" { + description = "Name for the EC2 Instance" + type = string + default = "home-ec2" +} \ No newline at end of file