diff --git a/.checkov.yml b/.checkov.yml new file mode 100644 index 0000000..02f3a56 --- /dev/null +++ b/.checkov.yml @@ -0,0 +1,13 @@ +block-list-secret-scan: [] +compact: true +directory: + - . +download-external-modules: false +evaluate-variables: true +framework: + - all +output: + - cli +quiet: true +soft-fail: true +summary-position: top diff --git a/.github/workflows/opentofu.yml b/.github/workflows/opentofu.yml new file mode 100644 index 0000000..80a5bfa --- /dev/null +++ b/.github/workflows/opentofu.yml @@ -0,0 +1,19 @@ +name: OpenTofu + +on: + pull_request: + branches: + - main + push: + branches: + - main + +permissions: + contents: read + pull-requests: write + +jobs: + opentofu: + uses: makeitworkcloud/shared-workflows/.github/workflows/opentofu.yml@main + secrets: + SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45a1d23 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# vim swap files +**/*.sw[po] + +# don't commit terraform state or lock. the repo code is the only state we care about. +# the provider state cache is auto-upgraded by default to ensure compatibility with upstream cloud provider APIs +**/.terraform.lock.hcl +**/.terraform + +# IDE Folders +**/.vscode + +# Mac Finder cache +**/.DS_Store + +# Plan output +plan-output.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f3e633f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,36 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-case-conflict + - id: check-merge-conflict + - id: check-symlinks + - id: check-vcs-permalinks + - id: destroyed-symlinks + - id: detect-private-key + - id: mixed-line-ending + - id: trailing-whitespace +- repo: https://github.com/antonbabenko/pre-commit-terraform + rev: v1.104.0 + hooks: + - id: terraform_validate + args: + - --hook-config=--retry-once-with-cleanup=true + - --args=-no-color + - --tf-init-args=-reconfigure + - --tf-init-args=-upgrade + - id: terraform_tflint + args: + - --args=--minimum-failure-severity=error + - --args=--config=__GIT_WORKING_DIR__/.tflint.hcl + - id: terraform_checkov + args: + - --args=--config-file __GIT_WORKING_DIR__/.checkov.yml + - id: terraform_fmt + args: + - --args=-no-color + - --args=-diff + - --args=-recursive + - id: terraform_docs + args: + - --args=--config=.terraform-docs.yml diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..8967c45 --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,3 @@ +--- +creation_rules: + - age: age152ek83tm4fj5u70r3fecytn4kg7c5xca24erjchxexx4pfqg6das7q763l diff --git a/.terraform-docs.yml b/.terraform-docs.yml new file mode 100644 index 0000000..2cbbb30 --- /dev/null +++ b/.terraform-docs.yml @@ -0,0 +1,18 @@ +formatter: "markdown" + +output: + file: "README.md" + mode: replace + +settings: + color: false + lockfile: false + +sort: + enabled: true + by: name + +# recursive can't be enabled until this bug is fixed: +# https://github.com/terraform-docs/terraform-docs/issues/654 +recursive: + enabled: false diff --git a/.tflint.hcl b/.tflint.hcl new file mode 100644 index 0000000..062eb57 --- /dev/null +++ b/.tflint.hcl @@ -0,0 +1,12 @@ +plugin "terraform" { + enabled = true + preset = "recommended" +} + +rule "terraform_required_providers" { + enabled = false +} + +rule "terraform_required_version" { + enabled = false +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..739d18b --- /dev/null +++ b/Makefile @@ -0,0 +1,79 @@ +SHELL := /bin/bash +TERRAFORM := $(shell which tofu) +S3_REGION := $(shell sops decrypt secrets/secrets.yaml | grep ^s3_region | cut -d ' ' -f 2) +S3_BUCKET := $(shell sops decrypt secrets/secrets.yaml | grep ^s3_bucket | cut -d ' ' -f 2) +S3_KEY := $(shell sops decrypt secrets/secrets.yaml | grep ^s3_key | cut -d ' ' -f 2) +S3_ACCESS_KEY := $(shell sops decrypt secrets/secrets.yaml | grep ^s3_access_key | cut -d ' ' -f 2) +S3_SECRET_KEY := $(shell sops decrypt secrets/secrets.yaml | grep ^s3_secret_key | cut -d ' ' -f 2) + +.PHONY: help init plan apply migrate test pre-commit-check-deps pre-commit-install-hooks argcd-login + +help: + @echo "General targets" + @echo "----------------" + @echo + @echo "\thelp: show this help text" + @echo "\tclean: removes all .terraform directories" + @echo + @echo "Terraform targets" + @echo "-----------------" + @echo + @echo "\tinit: run 'terraform init'" + @echo "\ttest: run pre-commmit checks" + @echo "\tplan: run 'terraform plan'" + @echo "\tapply: run 'terraform apply'" + @echo "\tmigrate; run terraform init -migrate-state" + @echo + @echo "One-time repo init targets" + @echo "--------------------------" + @echo + @echo "\tpre-commit-install-hooks: install pre-commit hooks" + @echo "\tpre-commit-check-deps: check pre-commit dependencies" + @echo + +clean: + @find . -name .terraform -type d | xargs -I {} rm -rf {} + +init: clean .terraform/terraform.tfstate + +.terraform/terraform.tfstate: + @${TERRAFORM} init -reconfigure -upgrade -input=false -backend-config="key=${S3_KEY}" -backend-config="bucket=${S3_BUCKET}" -backend-config="region=${S3_REGION}" -backend-config="access_key=${S3_ACCESS_KEY}" -backend-config="secret_key=${S3_SECRET_KEY}" + +plan: init .terraform/plan + +.terraform/plan: + @${TERRAFORM} plan -compact-warnings -no-color -out tfplan.bin + @${TERRAFORM} show -no-color tfplan.bin | tee plan-output.txt + @rm -f tfplan.bin + +apply: init .terraform/apply + +.terraform/apply: + @${TERRAFORM} apply -auto-approve -compact-warnings + +migrate: + @echo "First use -make init- using the old S3 backend, then run -make migrate- to use the new one." + @${TERRAFORM} init -migrate-state -backend-config="key=${S3_KEY}" -backend-config="bucket=${S3_BUCKET}" -backend-config="region=${S3_REGION}" -backend-config="access_key=${S3_ACCESS_KEY}" -backend-config="secret_key=${S3_SECRET_KEY}" + +test: .git/hooks/pre-commit + @pre-commit run -a + +DEPS_PRE_COMMIT=$(shell which pre-commit || echo "pre-commit not found") +DEPS_TERRAFORM_DOCS=$(shell which terraform-docs || echo "terraform-docs not found") +DEPS_TFLINT=$(shell which tflint || echo "tflint not found,") +DEPS_CHECKOV=$(shell which checkov || echo "checkov not found,") +DEPS_JQ=$(shell which jq || echo "jq not found,") +pre-commit-check-deps: + @echo "Checking for pre-commit and its dependencies:" + @echo " pre-commit: ${DEPS_PRE_COMMIT}" + @echo " terraform-docs: ${DEPS_TERRAFORM_DOCS}" + @echo " tflint: ${DEPS_TFLINT}" + @echo " checkov: ${DEPS_CHECKOV}" + @echo " jq: ${DEPS_JQ}" + @echo "" + +pre-commit-install-hooks: .git/hooks/pre-commit + +.git/hooks/pre-commit: pre-commit-check-deps + @pre-commit install --install-hooks + diff --git a/aws-iam.tf b/aws-iam.tf new file mode 100644 index 0000000..ba27a80 --- /dev/null +++ b/aws-iam.tf @@ -0,0 +1,19 @@ +resource "aws_iam_user" "admin" { + for_each = local.admin_users + name = each.value + force_destroy = false + tags = { + ManagedBy = "Terraform" + } +} + +resource "aws_iam_user_policy_attachment" "admin_attach" { + for_each = local.admin_users + user = aws_iam_user.admin[each.key].name + policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" +} + +resource "aws_iam_access_key" "admin_key" { + for_each = local.admin_users + user = aws_iam_user.admin[each.key].name +} \ No newline at end of file diff --git a/aws-s3.tf b/aws-s3.tf new file mode 100644 index 0000000..f36079c --- /dev/null +++ b/aws-s3.tf @@ -0,0 +1,107 @@ +resource "aws_s3_bucket" "private" { + for_each = local.s3_private_buckets + bucket = each.value + + tags = { + ManagedBy = "Terraform" + } + + lifecycle { + prevent_destroy = true + } +} + +resource "aws_s3_bucket" "public" { + for_each = local.s3_public_buckets + bucket = each.value + + tags = { + ManagedBy = "Terraform" + } + + lifecycle { + prevent_destroy = true + } +} + +resource "aws_s3_bucket_public_access_block" "public" { + for_each = aws_s3_bucket.public + bucket = each.value.id + block_public_acls = false + block_public_policy = false + ignore_public_acls = false + restrict_public_buckets = false +} + +resource "aws_s3_bucket_policy" "public" { + for_each = aws_s3_bucket.public + + bucket = each.value.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = "*" + Action = [ + "s3:GetObject" + ] + Resource = "${each.value.arn}/*" + } + ] + }) +} + +resource "aws_s3_bucket" "web" { + for_each = local.s3_web_buckets + bucket = each.value + + tags = { + ManagedBy = "Terraform" + } + + lifecycle { + prevent_destroy = true + } +} + +# Make "web" buckets publicly accessible +resource "aws_s3_bucket_public_access_block" "web" { + for_each = aws_s3_bucket.web + bucket = each.value.id + block_public_acls = false + block_public_policy = false + ignore_public_acls = false + restrict_public_buckets = false +} + +resource "aws_s3_bucket_policy" "web" { + for_each = aws_s3_bucket.web + + bucket = each.value.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = "*" + Action = [ + "s3:GetObject" + ] + Resource = "${each.value.arn}/*" + } + ] + }) +} + +resource "aws_s3_bucket_website_configuration" "web" { + for_each = aws_s3_bucket.web + + bucket = each.value.id + + index_document { + suffix = "index.html" + } +} diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..abd44e6 --- /dev/null +++ b/main.tf @@ -0,0 +1,18 @@ +data "sops_file" "secret_vars" { + source_file = "${path.module}/secrets/secrets.yaml" +} + +locals { + admin_users = toset(["svc-terraform-admin"]) + s3_private_buckets = toset([ + "mitw-tf-aws-infra", + "mitw-tf-cloudflare-infra", + "mitw-tf-github-repos", + "mitw-tf-libvirt-infra" + ]) + s3_public_buckets = toset([]) + s3_web_buckets = toset([ + "makeitwork.cloud", + "onion.makeitwork.cloud" + ]) +} \ No newline at end of file diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..e422bb0 --- /dev/null +++ b/outputs.tf @@ -0,0 +1,16 @@ +output "admin_access_keys" { + description = "Admin IAM user access keys" + value = { for user, key in aws_iam_access_key.admin_key : user => { + access_key_id = key.id + secret_access_key = key.secret + } } + sensitive = true +} + +output "web_bucket_endpoints" { + value = { + for k, b in aws_s3_bucket.web : + k => aws_s3_bucket_website_configuration.web[k].website_endpoint + } + description = "Website endpoints for public web S3 buckets" +} \ No newline at end of file diff --git a/providers.tf b/providers.tf new file mode 100644 index 0000000..11c57a8 --- /dev/null +++ b/providers.tf @@ -0,0 +1,22 @@ +terraform { + required_version = "> 1.3" + + backend "s3" {} + + required_providers { + sops = { + source = "carlpett/sops" + } + aws = { + source = "hashicorp/aws" + } + } +} + +provider "sops" {} + +provider "aws" { + region = data.sops_file.secret_vars.data["s3_region"] + access_key = data.sops_file.secret_vars.data["s3_access_key"] + secret_key = data.sops_file.secret_vars.data["s3_secret_key"] +} diff --git a/secrets/secrets.yaml b/secrets/secrets.yaml new file mode 100644 index 0000000..1a3d38e --- /dev/null +++ b/secrets/secrets.yaml @@ -0,0 +1,20 @@ +s3_bucket: ENC[AES256_GCM,data:CEGBa/BA32DPnZktC2HZJsI=,iv:BZ1JLkLBmEUVIw6/vkngFrqnlDxlN5Y399WyRQQg594=,tag:DBWZJfDa9nkNopAAtSP5Gw==,type:str] +s3_key: ENC[AES256_GCM,data:8Qc1VRUtW2h6wrm/NiE25LQ=,iv:ztMxBM7DkgHMvRxuQSqRRzPO1fFHonmcHmKooVXclxk=,tag:VfC7rroltntqT1tf9vD/gw==,type:str] +s3_region: ENC[AES256_GCM,data:eOyGm9ay1WsF,iv:wDUtASNnplZ76JJh54xlKKcXNKAtosepQTGgC1Oi4qQ=,tag:Mz6bm1z9S8e4/1cLy/2NVw==,type:str] +s3_access_key: ENC[AES256_GCM,data:wQL1zGnkAtWM1EWgooXMMq5dRHI=,iv:wmRjCt28jloov7zKNs4TaZ9GeKhO6/nqPqY1sxFmaQ4=,tag:ZDIeBorMgwkVrdv80SYiGw==,type:str] +s3_secret_key: ENC[AES256_GCM,data:0EcLqSBTSCQ83LJ1RIuPjv7jzBm4KDeJVyUnGFVzxsCSCo1d+no7lQ==,iv:TuG8ATqDozZ5TsuBmHPNQC7pg1GXfrHgWxDIpjbFsWQ=,tag:46XhMpFqfNoPkteVcFZDeQ==,type:str] +sops: + age: + - recipient: age152ek83tm4fj5u70r3fecytn4kg7c5xca24erjchxexx4pfqg6das7q763l + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqa1V4WGpuWGVlY2xMcVk5 + QlJFeHk3ZjdrcVpnRDFRSmlRQ2FTZnE4WTFRCjQ5UmRONDV3a2xTc3Mya0wvekN6 + RmJrQ09nZzk4VkZxRGlremcrUWU1Z2MKLS0tIHpFZFVpV3lpOUJNSDFwdDRpazJK + d2pVcDh3cDNqY1gzSVR5Z2NXcld1Qm8KRdv8vKhMBi1R8fGIphdmY4pfHV1sAqSb + nAXWA6Ut5/KAPIluSnBtWFkcakulcXYT01XorziztVS0X4nJDzEvMg== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2025-09-27T23:45:19Z" + mac: ENC[AES256_GCM,data:AcSRYq32GptizEXMujOIh4cC29BVNQj4lad85BWfHl6TGQA7oWN5A32xoXqCWqpWEuVDpndUAPZXvMRK8n53djXEXb/A6G9R8vEzaKOUB9zijItanUXkVK63d7KE9X8JrgtV7KQcWZx01qQitMzezMTkouvkfYpodPr1hkV2PIk=,iv:o5Q3VQG55jeCVHTNAAV1HgN91SU6jbx+IFZVGx2vN7o=,tag:GX/v69ggBNUpnLGFPBINdg==,type:str] + unencrypted_suffix: _unencrypted + version: 3.10.2