diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 3f404961b..41f45336c 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -15,6 +15,8 @@ jobs: steps: - uses: actions/checkout@v3 + with: + fetch-depth: 0 - uses: actions/setup-python@v3 - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml new file mode 100644 index 000000000..e5c00d568 --- /dev/null +++ b/.github/workflows/terraform.yml @@ -0,0 +1,128 @@ +--- +name: Terraform Enforcement + +on: + push: + branches: + - main + paths: + - terraform/** + + pull_request: + branches: + - main + paths: + - terraform/** + +permissions: + contents: read + id-token: write + pull-requests: write + +jobs: + terraform_enforcement: + runs-on: ubuntu-latest + + strategy: + matrix: + terraform_module: [aws, github] + + defaults: + run: + shell: bash + working-directory: terraform/${{ matrix.terraform_module }} + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-region: ${{ secrets.DEFAULT_AWS_REGION }} + role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubAction-AssumeRoleWithAction + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v2 + with: + terraform_version: 1.4.2 + + - name: Terraform Init + id: init + run: terraform init + + - name: Terraform Format + id: fmt + run: terraform fmt -check + + - name: Terraform Validate + id: validate + run: terraform validate + + - name: Terraform Plan + id: plan + if: github.event_name == 'pull_request' + run: terraform plan -no-color -input=false -out=terraform-${{ matrix.terraform_module }}.tfplan + env: + SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }} + continue-on-error: true + + - uses: actions/github-script@v6 + if: github.event_name == 'pull_request' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PLAN: "terraform\n${{ steps.plan.outputs.stdout }}" + TERRAFORM_MODULE: ${{ matrix.terraform_module }} + with: + script: | + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }) + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('Terraform Enforcement Summary (${{ env.TERRAFORM_MODULE }})') + ) + + const output = `## Terraform Enforcement Summary (${{ env.TERRAFORM_MODULE }}) + #### Terraform Format and Style: 🖌\`${{ steps.fmt.outcome }}\` + #### Terraform Initialization: ⚙️\`${{ steps.init.outcome }}\` + #### Terraform Validation: 🤖\`${{ steps.validate.outcome }}\` + #### Terraform Plan: 📖\`${{ steps.plan.outcome }}\` + +
Show Plan + + \`\`\`\n + ${process.env.PLAN} + \`\`\` + +
+ + *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Working Directory: \`${{ env.TERRAFORM_MODULE }}\`, Workflow: \`${{ github.workflow }}\`*`; + + if (botComment) { + github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: output + }) + } else { + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + } + + - name: Terraform Plan Status + if: steps.plan.outcome == 'failure' + run: exit 1 + + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -auto-approve -input=false terraform-${{ matrix.terraform_module }}.tfplan diff --git a/.sops.yaml b/.sops.yaml index b87868c9b..ae3a3335f 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -7,6 +7,8 @@ keys: - &host_devbox age1cd8stpd83v0zwfk7ke80nag65mxvxgc8rfjx86a4rk35wevymfwq747zwx # K3s - &k3s_sops_operator age1pwu309yva6z9qs5z4n5dytns3jze850das6y2qntqq5yc8wzc52sk4nufy + # Effect-TS GitHub Organization + - &github_organization age14ge7pvpta4u03gygnz5jcw4lqra4f2txg2emwtu2qlywhxzu59uq92avw8 creation_rules: - encrypted_regex: ^(data|stringData)$ path_regex: k3s/.*/secrets/.*\.ya?ml$ @@ -26,3 +28,4 @@ creation_rules: - age: - *admin_maxwellbrown - *admin_mikearnaldi + - *github_organization diff --git a/terraform/aws/.terraform.lock.hcl b/terraform/aws/.terraform.lock.hcl new file mode 100644 index 000000000..7270f0e97 --- /dev/null +++ b/terraform/aws/.terraform.lock.hcl @@ -0,0 +1,28 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.4.0" + constraints = "5.4.0" + hashes = [ + "h1:4eGsUS3r5eApQc19t8woc6d+sQLaOBaCSaK5GyGcWf0=", + "h1:Jol4lNIzMrREQzUBSveCLX0iQLy7dm0OF+IYY2GKrhY=", + "h1:V2yz+EYWnLU/fWdsk8UX7oFuAKTBLTHDdLKB6NG7th0=", + "h1:ny1YPz2LiHTasDVNh6/HEvh1c9+TN/ftgAHh84bmy1E=", + "zh:1db5f81089216831bb0fdff9ddc3772efa133397c66ec276bc75b96eec06e23f", + "zh:26fe5fdf399192b5724d21854fbec650c158f8ee9eb1dc52a50f7da0f2bc07ac", + "zh:2946d9e333b1efe01588ee9f9771169fd3c3a4a7cb78ed8f91e8b3efd1a73850", + "zh:36ed69e8d3029332c8a52a70940f714fd579b9fd95f5569cc010ef11162f5bf7", + "zh:46ba5ad1c3a3ef98c346356cfa4bdd9c2501c661c2513bb92f4413f2482fb24b", + "zh:46c10aaa9672b54a14b0e0effdd6ecd9b8a539b3bfe273ac54111e7352a7bb4b", + "zh:47d7f57bcbe4fba2f960ab6c4228c5e9e586be2f233a8baa8962b51a63337179", + "zh:47e41c198439ba1c4d933f808b6f47e518f8f0aae25ca42abcac97f149121e90", + "zh:526c5834de71654ee14039cb973322bf5032cb684a2a113b48fb48a0584f46f3", + "zh:6169316517b95677819ba2904dcea204fb9b55e868348e906af9164104fe7198", + "zh:7c063ef2b8d69a8db7e8bf0dcd45793ede22b259b30464ed114d330df304cdbb", + "zh:87c4f2faca636715a08be3121d26b3354415401eab89349077ca9436a0822c23", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:b184b8a268f45258edd27d389ca793708f1bc3ee4d6706d154a45e93deaddde1", + "zh:ba1a998cbf4b639fa3e04b9069f0f5a289662457940726a8a51c81df400aa852", + ] +} diff --git a/terraform/aws/backend.tf b/terraform/aws/backend.tf new file mode 100644 index 000000000..b6aa6dc5b --- /dev/null +++ b/terraform/aws/backend.tf @@ -0,0 +1,9 @@ +terraform { + backend "s3" { + bucket = "effectful-terraform-state" + key = "aws/terraform.tfstate" + region = "us-east-2" + dynamodb_table = "effectful-terraform-state" + encrypt = true + } +} diff --git a/terraform/aws/iam.tf b/terraform/aws/iam.tf new file mode 100644 index 000000000..ca004845e --- /dev/null +++ b/terraform/aws/iam.tf @@ -0,0 +1,113 @@ +################################################################################ +# IAM Policy - Manage Terraform State +################################################################################ + +data "aws_iam_policy_document" "read_write_terraform_state" { + version = "2012-10-17" + + statement { + effect = "Allow" + actions = ["s3:ListBucket"] + resources = ["arn:aws:s3:::effectful-terraform-state"] + } + + statement { + effect = "Allow" + actions = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"] + resources = ["arn:aws:s3:::effectful-terraform-state/*"] + } + + statement { + effect = "Allow" + actions = [ + "dynamodb:DescribeTable", + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:DeleteItem" + ] + resources = ["arn:aws:dynamodb:*:*:table/effectful-terraform-state"] + } +} + +resource "aws_iam_policy" "read_write_terraform_state" { + name = "read-write-terraform-state" + description = "Grant access to S3 bucket and DynamoDB table used to manage Terraform state" + policy = data.aws_iam_policy_document.read_write_terraform_state.json +} + +data "aws_iam_policy_document" "terraform_enforcement" { + version = "2012-10-17" + + statement { + effect = "Allow" + actions = [ + "iam:GetOpenIDConnectProvider", + "iam:GetPolicy", + "iam:GetPolicyVersion", + "iam:GetRole", + "iam:ListAttachedRolePolicies", + "iam:ListRolePolicies", + ] + resources = ["*"] + } +} + +resource "aws_iam_policy" "terraform_enforcement" { + name = "terraform-enforcement" + description = "Grant the least privileged access required for Terraform in AWS" + policy = data.aws_iam_policy_document.terraform_enforcement.json +} + +################################################################################ +# IAM Role - GitHub Actions OIDC +################################################################################ + +resource "aws_iam_openid_connect_provider" "github_actions" { + url = "https://token.actions.githubusercontent.com" + client_id_list = ["sts.amazonaws.com"] + thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"] +} + +data "aws_iam_policy_document" "github_actions" { + version = "2012-10-17" + + statement { + sid = "GithubActionsAssumeRole" + + effect = "Allow" + + principals { + type = "Federated" + identifiers = [aws_iam_openid_connect_provider.github_actions.arn] + } + + actions = ["sts:AssumeRoleWithWebIdentity"] + + condition { + test = "StringEquals" + variable = "token.actions.githubusercontent.com:aud" + values = ["sts.amazonaws.com"] + } + + condition { + test = "StringLike" + variable = "token.actions.githubusercontent.com:sub" + values = ["repo:Effect-TS/infra:*"] + } + } +} + +resource "aws_iam_role" "github_actions" { + name = "GitHubAction-AssumeRoleWithAction" + assume_role_policy = data.aws_iam_policy_document.github_actions.json +} + +resource "aws_iam_role_policy_attachment" "github_actions" { + role = aws_iam_role.github_actions.name + policy_arn = aws_iam_policy.read_write_terraform_state.arn +} + +resource "aws_iam_role_policy_attachment" "terraform_enforcement" { + role = aws_iam_role.github_actions.name + policy_arn = aws_iam_policy.terraform_enforcement.arn +} diff --git a/terraform/aws/terraform.tfvars b/terraform/aws/terraform.tfvars new file mode 100644 index 000000000..f81d21e78 --- /dev/null +++ b/terraform/aws/terraform.tfvars @@ -0,0 +1 @@ +# See https://github.blog/changelog/2022-01-13-github-actions-update-on-oidc-based-deployments-to-aws/ diff --git a/terraform/aws/variables.tf b/terraform/aws/variables.tf new file mode 100644 index 000000000..e69de29bb diff --git a/terraform/aws/versions.tf b/terraform/aws/versions.tf new file mode 100644 index 000000000..ef1c028a9 --- /dev/null +++ b/terraform/aws/versions.tf @@ -0,0 +1,13 @@ +terraform { + required_version = ">= 1.3" + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.4.0" + } + } +} + +provider "aws" { + region = "us-east-2" +} diff --git a/terraform/github/backend.tf b/terraform/github/backend.tf index 89eca54bf..a9efb58f4 100644 --- a/terraform/github/backend.tf +++ b/terraform/github/backend.tf @@ -1,5 +1,3 @@ -# terraform init -migrate-state -# enter "yes" when prompted terraform { backend "s3" { bucket = "effectful-terraform-state" diff --git a/terraform/github/secrets.yaml b/terraform/github/secrets.yaml index 53f067b65..3f7298454 100644 --- a/terraform/github/secrets.yaml +++ b/terraform/github/secrets.yaml @@ -10,20 +10,29 @@ sops: - recipient: age1usc2072gec0plj2q866m0uyeh2rly60xnxts4c3xxh50cwygte8s8avj8a enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMaC9pbUdHUUZXUFhiSm9E - U01JSko0SytDcjhBUGxKb1FaS2Z5RGJld2pvClZGQ3NaNkNVeng3QWJjdGtnTytL - V3c3NWhnSXBENm1Ib1dYeGJpWjltK0EKLS0tIG1EWmdGMDU4ZlJvWmNnTUpYS2V2 - aHdUSTdHR0RJWjVYTFpVeUkwR05KTDQKsxG1OwWulEXXHqUhBWvuNUAoNdeuD6Bz - FiL/h7EHADv++jhW4ie/YO20zkCQp9R8OAc2skJD95cV5W/S47Lc5Q== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrRHR3QzRVM3ozOWxmZWtN + K3VlNnV1ZTlDQ1RyaFladmRDYjNPZmZWcVNFCkdZUDJYYlhPYWhuTzNoRm5BMXMy + Q0JTZnM0ZEVaZ0JzclFxUXB1c1RMc00KLS0tIDU0ckVWUitoUzNXalk4QXFJR0dq + eXhNVFVyd0tXMFNVelZFbGpoSkI4bDQKujVXw1iOPpcc26wzIV+s4lgTSzAnhk7/ + G3CtQvkTe/Apba9kHKX9HlODV8i0CTYhwds+XKeIUYczExESF03I7A== -----END AGE ENCRYPTED FILE----- - recipient: age15umcap7zkzq5g32zykcywjk0l6cwldxqk7e46t2ynkkwy0eghuas5rs6t7 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2cWRHMmVvQTg5ZmxObzM4 - aUdnOU40eUIzNTl4a0ZrM1JaZ3ZKOUdpT1FjCjBsRzBoMTdwcWloVWZXWDdwNndN - VTh1dERGYm4xalFrSHJqTzEzdG9qYm8KLS0tIDh4ZmhQOXNoaWp3TzdTT3NMeXha - YXRibEthSFBUejl3Z0pCWFNWZHVUWmMKbl8hIRecmghnJKShrvd4mX96CGeTzlpf - 7nEAAKvCAfsoO2n+CtZIyU3KLBo/TLWqcU/nhR9LJ2UjFZfhkn5/IQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqckxkVjZidUxPeWlFd3ZZ + QTJQRC91WUR4SkV3aENPNFBBTHdoQW9yV0NjClh0WW5zRkh1Z2hMbzNKaGhnU3Z1 + SG0vYnRCclNqbXByVEo0STJQMHZ0Sk0KLS0tIE11ZzV2R3hEOVA0cnZ3OHEwQ0Iw + cXdYQ25EaHJuMGJKMUo4OVp1V2ZHdXMK0kA6tCvXr9o7aundHJsJNjx2JkIZ8/H4 + abslW6mLvjEEpxk65zpMhJfbiPFTw9b3YNRL7Tog57fhGlwnfb5CPg== + -----END AGE ENCRYPTED FILE----- + - recipient: age14ge7pvpta4u03gygnz5jcw4lqra4f2txg2emwtu2qlywhxzu59uq92avw8 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuSDNhL1k2UTN3Y2ZnZW5z + bFdQS0MrUllDeHpYUm5RMEJ4MWJHdUxkTlFNCjdlcEpzMThpbmxUaVpHSDNmN0kr + cEtQT3RVT0Y4dFlEYytqOEdZV0QwU00KLS0tIFIySXRSQUhRL2lYVi8yclRJVjRI + dU1SUWxYb0UxMlg3bDRBVkxlRlJwSWcKIN3GSk4iNfbU3ySlAe+8PaE7Oai+Ru4e + RtrtpgbEx92D6a0i22E/fE0+FdVu8E2G9UBGkg3R2fJ3kmReSAurKQ== -----END AGE ENCRYPTED FILE----- lastmodified: '2023-04-04T23:30:03Z' mac: ENC[AES256_GCM,data:7QdX20UJRbht2ymv3onzND0QmT69Z7R7rVaCewopMuX1G2egU/XWMtL+VzEjOU/A5OJQw06BOBBTDPmPwVvMNizIn5Tywer3dkYZKnAf+3WC0PlbP/FEVZ21IawFPuWrr32Ba3DSuQsIVucDZL7W6LrJ97Mvs9Jn6Asi++CBjj4=,iv:b8gDcF4OYhbJ6ah/63JPKn1ELmMyxALCWFyrfB9pSTw=,tag:+JUxZH9OHO0te79uxBJ5AQ==,type:str]