diff --git a/.github/labels.yaml b/.github/labels.yaml new file mode 100644 index 0000000..e961fe2 --- /dev/null +++ b/.github/labels.yaml @@ -0,0 +1,28 @@ +--- +- name: breaking + color: "b60205" + description: This change is not backwards compatible +- name: bug + color: "d93f0b" + description: Something isn't working +- name: documentation + color: "0075ca" + description: Improvements or additions to documentation +- name: enhancement + color: "0e8a16" + description: New feature or request +- name: feature + color: "0e8a16" + description: New feature or request +- name: fix + color: "d93f0b" + description: Something isn't working +- name: misc + color: "#6B93D3" + description: Miscellaneous task not covered by something else +- name: no-changelog + color: "cccccc" + description: No entry should be added to the release notes and changelog +- name: security + color: "5319e7" + description: Solving a security issue diff --git a/.github/release-drafter-config.yaml b/.github/release-drafter-config.yaml new file mode 100644 index 0000000..3d4047a --- /dev/null +++ b/.github/release-drafter-config.yaml @@ -0,0 +1,86 @@ +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' +version-template: '$MAJOR.$MINOR.$PATCH' +change-title-escapes: '\<*_&' + +categories: + - title: '๐Ÿš€ Features' + labels: + - 'breaking' + - 'enhancement' + - 'feature' + - title: '๐Ÿ› Bug Fixes' + labels: + - 'bug' + - 'fix' + - 'security' + - title: '๐Ÿ“– Documentation' + labels: + - 'documentation' + - title: '๐Ÿงบ Miscellaneous' + labels: + - 'misc' + +version-resolver: + major: + labels: + - 'breaking' + minor: + labels: + - 'enhancement' + - 'feature' + patch: + labels: + - 'bug' + - 'documentation' + - 'fix' + - 'security' + default: 'minor' + +autolabeler: + - label: 'documentation' + body: + - '/documentation/' + branch: + - '/docs\/.+/' + title: + - '/documentation/i' + - '/docs/i' + - label: 'bug' + body: + - '/bug/' + branch: + - '/bug\/.+/' + - '/fix\/.+/' + title: + - '/bug/i' + - '/fix/i' + - label: 'feature' + branch: + - '/feature\/.+/' + - '/enhancement\/.+/' + title: + - '/feature/i' + - '/feat/i' + - '/enhancement/i' + - label: 'breaking' + body: + - '/breaking/' + branch: + - '/breaking\/.+/' + title: + - '/breaking/i' + - '/major/i' + +exclude-contributors: + - 'github-actions[bot]' + +exclude-labels: + - 'no-changelog' + +template: | + # What's Changed + + $CHANGES + + **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION diff --git a/.github/workflows/label-synchronization.yaml b/.github/workflows/label-synchronization.yaml new file mode 100644 index 0000000..0c241b8 --- /dev/null +++ b/.github/workflows/label-synchronization.yaml @@ -0,0 +1,29 @@ +name: label-synchronization +on: + workflow_dispatch: + push: + branches: + - main + - master + paths: + - .github/labels.yaml + - .github/workflows/label-sync.yaml + +permissions: + # write permission is required to edit issue labels + issues: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Synchronize labels + uses: crazy-max/ghaction-github-labeler@v5 + with: + dry-run: false + github-token: ${{ secrets.GITHUB_TOKEN }} + skip-delete: false + yaml-file: .github/labels.yaml diff --git a/.github/workflows/pr-validation.yaml b/.github/workflows/pr-validation.yaml new file mode 100644 index 0000000..dbbdeae --- /dev/null +++ b/.github/workflows/pr-validation.yaml @@ -0,0 +1,104 @@ +name: "pr-validation" + +on: + pull_request: + +permissions: + checks: write + contents: read + pull-requests: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + autolabeler: + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v6 + with: + config-name: release-drafter-config.yaml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + title-checker: + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + id: lint_pr_title + with: + types: | + breaking + bug + docs + documentation + enhancement + feat + feature + fix + misc + security + requireScope: false + ignoreLabels: | + skip-changelog + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: marocchino/sticky-pull-request-comment@v2 + # When the previous steps fails, the workflow would stop. By adding this + # condition you can continue the execution with the populated error message. + if: always() && (steps.lint_pr_title.outputs.error_message != null) + with: + header: pr-title-lint-error + message: | + Hey there and thank you for opening this pull request! ๐Ÿ‘‹๐Ÿผ + + We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. + + Examples for valid PR titles: + feat(ui): Add button component. + fix: Correct typo. + _type(scope): subject._ + + Adding a scope is optional + + Details: + ``` + ${{ steps.lint_pr_title.outputs.error_message }} + ``` + + # Delete a previous comment when the issue has been resolved + - if: ${{ steps.lint_pr_title.outputs.error_message == null }} + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pr-title-lint-error + delete: true + + label-checker: + needs: autolabeler + runs-on: ubuntu-latest + steps: + - uses: danielchabr/pr-labels-checker@v3.3 + id: lint_pr_labels + with: + hasSome: breaking,bug,documentation,enhancement,feature,fix,misc,security + githubToken: ${{ secrets.GITHUB_TOKEN }} + + - uses: marocchino/sticky-pull-request-comment@v2 + # When the previous steps fails, the workflow would stop. By adding this + # condition you can continue the execution with the populated error message. + if: always() && (steps.lint_pr_labels.outputs.passed == false) + with: + header: pr-labels-lint-error + message: | + Hey there and thank you for opening this pull request! ๐Ÿ‘‹๐Ÿผ + + The PR needs to have at least one of the following labels: breaking, bug, documentation, enhancement, feature, fix, misc, security. + + # Delete a previous comment when the issue has been resolved + - if: ${{ steps.lint_pr_labels.outputs.passed != false }} + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pr-labels-lint-error + delete: true diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml new file mode 100644 index 0000000..b5e0cc3 --- /dev/null +++ b/.github/workflows/release-drafter.yaml @@ -0,0 +1,29 @@ +name: "release-drafter" + +on: + push: + branches: + - main + - master + paths-ignore: + - .github/** + - .pre-commit-config.yaml + - CHANGELOG.md + - CONTRIBUTING.md + - LICENSE + +permissions: + # write permission is required to create a github release + contents: write + +jobs: + draft: + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v6 + with: + publish: false + prerelease: false + config-name: release-drafter-config.yaml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/terraform-validation.yaml b/.github/workflows/terraform-validation.yaml new file mode 100644 index 0000000..dc704dd --- /dev/null +++ b/.github/workflows/terraform-validation.yaml @@ -0,0 +1,163 @@ +name: "terraform" + +on: + pull_request: + +permissions: + contents: write + pull-requests: write + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TF_IN_AUTOMATION: 1 + +jobs: + fmt-lint-validate: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v2 + + - name: Setup Terraform Linters + uses: terraform-linters/setup-tflint@v4 + with: + github_token: ${{ github.token }} + + - name: Terraform Format + id: fmt + run: terraform fmt -check -recursive + + - name: Terraform Lint + id: lint + run: | + echo "Checking ." + tflint --format compact + + for d in examples/*/; do + echo "Checking ${d} ..." + tflint --chdir=$d --format compact + done + + - name: Terraform Validate + id: validate + if: ${{ !vars.SKIP_TERRAFORM_VALIDATE }} + run: | + for d in examples/*/; do + echo "Checking ${d} ..." + terraform -chdir=$d init + terraform -chdir=$d validate -no-color + done + env: + AWS_DEFAULT_REGION: eu-west-1 + + - uses: actions/github-script@v6 + if: github.event_name == 'pull_request' || always() + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + // 1. Retrieve existing bot comments for the PR + 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 => { + return comment.user.type === 'Bot' && comment.body.includes('Terraform Format and Style') + }) + + // 2. Prepare format of the comment + const output = `#### Terraform Format and Style ๐Ÿ–Œ\`${{ steps.fmt.outcome }}\` + #### Terraform Initialization โš™๏ธ\`${{ steps.init.outcome }}\` + #### Terraform Lint ๐Ÿ“–\`${{ steps.lint.outcome }}\` + #### Terraform Validation ๐Ÿค–\`${{ steps.validate.outcome }}\` +
Validation Output + + \`\`\`\n + ${{ steps.validate.outputs.stdout }} + \`\`\` + +
`; + + // 3. If we have a comment, update it, otherwise create a new one + 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 + }) + } + + docs: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + + - name: Render terraform docs inside the README.md and push changes back to PR branch + uses: terraform-docs/gh-actions@v1.1.0 + with: + args: --sort-by required + git-commit-message: "docs(readme): update module usage" + git-push: true + output-file: README.md + output-method: inject + working-dir: . + continue-on-error: true # added this to prevent a PR from a remote fork failing the workflow + + tfsec: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Terraform security scan + uses: aquasecurity/tfsec-action@v1.0.3 + with: + github_token: ${{ github.token }} + soft_fail: false + tfsec_args: --concise-output --force-all-dirs + + - name: Terraform pr commenter + uses: aquasecurity/tfsec-pr-commenter-action@v1.3.1 + with: + github_token: ${{ github.token }} + tfsec_args: --concise-output --force-all-dirs + + checkov: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Run Checkov + uses: bridgecrewio/checkov-action@v12 + with: + container_user: 1000 + directory: "/" + download_external_modules: false + framework: terraform + output_format: sarif + quiet: true + skip_check: "CKV_GIT_5,CKV_GLB_1,CKV_TF_1" + soft_fail: false + skip_path: "examples/" + +### SKIP REASON ### +# Check | Description | Reason + +# CKV_GIT_5 | Ensure GitHub pull requests have at least 2 approvals | We strive for at least 1 approval +# CKV_GLB_1 | Ensure at least two approving reviews are required to merge a GitLab MR | We strive for at least 1 approval +# CKV_TF_1 | Ensure Terraform module sources use a commit hash | We think this check is too restrictive and that versioning should be preferred over commit hash diff --git a/.github/workflows/update-changelog.yaml b/.github/workflows/update-changelog.yaml new file mode 100644 index 0000000..82f347d --- /dev/null +++ b/.github/workflows/update-changelog.yaml @@ -0,0 +1,31 @@ +name: "update-changelog" + +on: + release: + types: + - published + +permissions: + contents: write + +jobs: + update: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.MCAF_GITHUB_TOKEN }} + + - name: Update Changelog + uses: stefanzweifel/changelog-updater-action@v1 + with: + latest-version: ${{ github.event.release.tag_name }} + release-notes: ${{ github.event.release.body }} + + - name: Commit updated Changelog + uses: stefanzweifel/git-auto-commit-action@v5 + with: + branch: ${{ github.event.repository.default_branch }} + commit_message: "docs(changelog): update changelog" + file_pattern: CHANGELOG.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2faf43d --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Ignore transient lock info files created by terraform apply +.terraform.tfstate.lock.info + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..1aa6827 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,40 @@ +# .pre-commit-config.yaml +default_stages: [commit] +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-json + - id: check-merge-conflict + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: pretty-format-json + args: + - --autofix + - id: detect-aws-credentials + args: + - --allow-missing-credentials + - id: detect-private-key + - repo: https://github.com/antonbabenko/pre-commit-terraform + rev: v1.88.4 + hooks: + - id: terraform_fmt + - id: terraform_tflint + - id: terraform_docs + - id: terraform_validate + - repo: https://github.com/bridgecrewio/checkov.git + rev: 3.2.60 + hooks: + - id: checkov + verbose: false + args: + - --download-external-modules + - "true" + - --quiet + - --compact + - --skip-check + - CKV_GIT_5,CKV_GLB_1,CKV_TF_1 + - --skip-path + - examples/* diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl new file mode 100644 index 0000000..daeac63 --- /dev/null +++ b/.terraform.lock.hcl @@ -0,0 +1,21 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "4.3.0" + hashes = [ + "h1:vxkNugAhv9mTwDNJaEG/S1E3J98JQ8AThd/6IcTa8zQ=", + "zh:117f843126f7a045ef4401103243ef53245a5c60b3fcf1f5f22bcb3a472c71fd", + "zh:4ae400db15d43a181527a585e51a237569631d49d685f9946212d1d9830f97ec", + "zh:53d9e7c9f42918e9cefe6469898c08975504a565e684a049365c43037ac9e3e3", + "zh:80f72cd97defcef1b23de85c5778499be44d5f034e3ecffdca161e1348602ffd", + "zh:826f716d13fd567bcd2db27cdab3c08fceb96542958512a6406ce389e82532ed", + "zh:9cd1ae99efa21bd90d8be47254c25b16f6e7ff9b3ba3ca2da5aaaa1695e9db16", + "zh:a2b78223937b5d7445e9d567f109044f94ffe178200559ed1401f4371b72b25f", + "zh:c7b5b4bfa05d90bc46cf300ec8d17a4554caef986c4c5fcf2610a492b78d65e7", + "zh:ccb3ebed6c701fd502cc41c486603e443c62086dbc1cee6f69c97fcb49e2181f", + "zh:d4d0edbdc373cbb94feffd0297289da2c1f5da36c1776f692151e98b7eadb1dd", + "zh:ee63964ad68a720e3ec399228db40e40a8321639adf3fbf47716252ee6e2f070", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..61f161f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +All notable changes to this project will automatically be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a139bae --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,39 @@ +# Contributing + +## Coding Guidelines + +- The terraform language has some [style conventions](https://developer.hashicorp.com/terraform/language/syntax/style) which must be followed for consistency between files and modules written by different teams. + +## Opening a pull request + +- We require pull request titles to follow the [conventional commits specification](https://www.conventionalcommits.org/en/v1.0.0/) + +- Labels are automatically added to your PR based on certain keywords in the `title`, `body`, and `branch` . You are able to manually add or remove labels from your PR, the following labels are allowed: `breaking`, `enhancement`, `feature`, `bug`, `fix`, `security`, `documentation`. + +## Release flow + +1. Every time a PR is merged, a draft release note is created or updated to add an entry for this PR. The release version is automatically incremented based on the labels specified. + +2. When you are ready to publish the release, you can use the drafted release note to do so. `MCAF Contributors` are able to publish releases. If you are an `MCAF Contributor` and want to publish a drafted release: + - Browse to the release page + - Edit the release you want to publish (click on the pencil) + - Click `Update release` (the green button at the bottom of the page) + +If a PR should not be added to the release notes and changelog, add the label `no-changelog` to your PR. + +## Local Development + +To ease local development, [pre-commit](https://pre-commit.com/) configuration has been added to the repository. Pre-commit is useful for identifying simple issues before creating a PR: + +To use it, follow these steps: + +1. Installation: + - Using Brew: `brew install tflint` + - Using Python: `pip3 install pre-commit --upgrade` + - Using Conda: `conda install -c conda-forge pre-commit` + +2. Run the pre-commit hooks against all the files (the first time run might take a few minutes): +`pre-commit run -a` + +3. (optional) Install the pre-commit hooks to run before each commit: +`pre-commit install` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 944b60e..0b1a468 100644 --- a/README.md +++ b/README.md @@ -1 +1,67 @@ -# terraform-azure-mcaf-key-vault \ No newline at end of file +# terraform-azure-mcaf-key-vault +Terraform module to deploy a key vault with defaults, and optionaly some customer managed keys keys. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.7 | +| [azurerm](#requirement\_azurerm) | >= 4 | + +## Providers + +| Name | Version | +|------|---------| +| [azurerm](#provider\_azurerm) | 4.3.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [azurerm_key_vault.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault) | resource | +| [azurerm_key_vault_key.cmkrsa](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_key) | resource | +| [azurerm_key_vault_key.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_key) | resource | +| [azurerm_role_assignment.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [key\_vault](#input\_key\_vault) | This object describes the configuration for an Azure Key Vault.

The following arguments are supported:

- `name` - (Required) The name of the Key Vault.
- `tenant_id` - (Required) The Azure Active Directory tenant ID that should be used for authenticating requests to the Key Vault.
- `resource_group_name` - (Optional) The name of the resource group in which to create the Key Vault. If not provided, the resource group of the calling module will be used.
- `location` - (Optional) The location of the Key Vault. If not provided, the location of the calling module will be used.
- `enabled_for_disk_encryption` - (Optional) Specifies whether Azure Disk Encryption is permitted to retrieve secrets from the vault and unwrap keys.
- `enabled_for_deployment` - (Optional) Specifies whether Azure Resource Manager is permitted to retrieve secrets from the vault.
- `enabled_for_template_deployment` - (Optional) Specifies whether Azure Resource Manager is permitted to retrieve secrets from the vault.
- `enable_rbac_authorization` - (Optional) Specifies whether Azure RBAC is permitted to retrieve secrets from the vault.
- `purge_protection` - (Optional) Specifies whether protection against purge is enabled for this Key Vault.
- `soft_delete_retention_days` - (Optional) The number of days that items should be retained for once soft deleted.
- `sku` - (Optional) The SKU of the Key Vault.
- `ip_rules` - (Optional) List of IP addresses that are permitted to access the key vault.
- `subnet_id` - (Optional) List of subnet IDs that are permitted to access the key vault.
- `network_bypass` - (Optional) Specifies which traffic can bypass the network rules.
- `cmkrsa_keyname` - (Optional) The name of the customer managed key with RSA algorithm to create.
- `cmkec_keyname` - (Optional) The name of the customer managed key with EC algorithm to create.
- `cmk_keys_create` - (Optional) Specifies whether to create custom managed keys.

Example Inputs:
hcl
key_vault = {
name = "my-key-vault"
tenant_id = "00000000-0000-0000-0000-000000000000"
enabled_for_disk_encryption = true
enabled_for_deployment = true
enabled_for_template_deployment = true
enable_rbac_authorization = true
purge_protection = true
soft_delete_retention_days = 30
sku = "standard"
cmkrsa_keyname = "cmkrsa"
cmkec_keyname = "cmkec"
cmk_keys_create = true
|
object({
name = string
tenant_id = string
resource_group_name = optional(string, null)
location = optional(string, null)
enabled_for_disk_encryption = optional(bool, false)
enabled_for_deployment = optional(bool, false)
enabled_for_template_deployment = optional(bool, false)
enable_rbac_authorization = optional(bool, true)
purge_protection = optional(bool, true)
soft_delete_retention_days = optional(number, 30)
sku = optional(string, "standard")
ip_rules = optional(list(string), [])
subnet_id = optional(list(string), [])
network_bypass = optional(string, "None")
cmkrsa_keyname = optional(string, "cmkrsa")
cmkec_keyname = optional(string, "cmkec")
cmk_keys_create = optional(bool, false)
cmk_rotation_period = optional(string, "P90D")
tags = optional(map(string), {})
})
| n/a | yes | +| [tags](#input\_tags) | A mapping of tags to assign to the resources. | `map(string)` | n/a | yes | +| [key\_vault\_key](#input\_key\_vault\_key) | This map describes the configuration for Azure Key Vault keys.

- `key_vault_id` - (Required) The ID of the Key Vault.
- `key_type` - (Required) The type of the key.
- `key_size` - (Required) The size of the key.
- `key_opts` - (Required) The key operations that are permitted.

Example Inputs:
hcl
key_vault_key = {
key_name = {
type = "RSA"
size = 4096
opts = ["encrypt", "decrypt", "sign", "verify", "wrapKey", "unwrapKey"]
}
key_ec = {
type = "EC"
curve = "P-256"
opts = ["sign", "verify"]
}
}
|
map(object({
name = optional(string, null)
curve = optional(string, null)
size = optional(number, null)
type = optional(string, null)
opts = optional(list(string), null)
expiration_date = optional(string, null)
not_before_date = optional(string, null)
rotation_policy = optional(object({
automatic = optional(object({
time_after_creation = optional(string, null)
time_before_expiry = optional(string, null)
}), null)
expire_after = optional(string, null)
notify_before_expiry = optional(string, null)
}), null)
tags = optional(map(string), {})
}))
| `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [key\_vault\_cmkrsa\_id](#output\_key\_vault\_cmkrsa\_id) | CMK RSA Key ID | +| [key\_vault\_cmkrsa\_keyname](#output\_key\_vault\_cmkrsa\_keyname) | CMK RSA Key Name | +| [key\_vault\_id](#output\_key\_vault\_id) | n/a | +| [key\_vault\_name](#output\_key\_vault\_name) | n/a | +| [key\_vault\_uri](#output\_key\_vault\_uri) | n/a | + + +## License + +**Copyright:** Schuberg Philis + +```text +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..2fb412b --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,34 @@ +# https://taskfile.dev + +version: "3" + +env: + TF_IN_AUTOMATION: 1 + +tasks: + default: + cmds: + - cmd: task --list + ignore_error: true + silent: true + + clean: + desc: Clean lock files and cache directories + cmds: + - rm -rf .terraform.lock.hcl .terraform + - rm -rf **/.terraform.lock.hcl **/.terraform + silent: true + + test: + desc: Run Terraform tests + cmds: + - terraform init + - terraform test + silent: true + + verbose-test: + desc: Run verbose Terraform tests + cmds: + - terraform init + - terraform test -verbose + silent: true diff --git a/examples/basic/main.tf b/examples/basic/main.tf new file mode 100644 index 0000000..4b49e5b --- /dev/null +++ b/examples/basic/main.tf @@ -0,0 +1,34 @@ +terraform { + required_version = ">= 1.7" +} + +module "azure_core" { + source = "../.." + + resource_group = { + name = "my-resource-group" + location = "East US" + } + + key_vault = { + name = "my-key-vault" + tenant_id = "your-tenant-id" + enabled_for_disk_encryption = true + enabled_for_deployment = false + enabled_for_template_deployment = false + enable_rbac_authorization = true + purge_protection = true + soft_delete_retention_days = 30 + sku = "standard" + ip_rules = [] + subnet_id = [] + network_bypass = "AzureServices" + cmkrsa_keyname = "cmkrsa" + cmkec_keyname = "cmkec" + cmk_keys_create = true + } + + tags = { + Environment = "Production" + } +} diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..bc65727 --- /dev/null +++ b/main.tf @@ -0,0 +1,96 @@ +data "azurerm_client_config" "current" {} + +resource "azurerm_key_vault" "this" { + resource_group_name = var.key_vault.resource_group_name + location = var.key_vault.location + name = var.key_vault.name + tenant_id = var.key_vault.tenant_id + sku_name = var.key_vault.sku + enabled_for_disk_encryption = var.key_vault.enabled_for_disk_encryption + enabled_for_deployment = var.key_vault.enabled_for_deployment + enabled_for_template_deployment = var.key_vault.enabled_for_template_deployment + enable_rbac_authorization = var.key_vault.enable_rbac_authorization + purge_protection_enabled = var.key_vault.purge_protection + soft_delete_retention_days = var.key_vault.soft_delete_retention_days + + network_acls { + default_action = length(var.key_vault.ip_rules) == 0 && length(var.key_vault.subnet_id) == 0 ? "Allow" : "Deny" + ip_rules = var.key_vault.ip_rules + virtual_network_subnet_ids = var.key_vault.subnet_id + bypass = var.key_vault.network_bypass + } + + tags = merge( + try(var.tags), + try(var.key_vault.tags), + tomap({ + "Resource Type" = "Key vault" + }) + ) +} + +resource "azurerm_role_assignment" "this" { + scope = azurerm_key_vault.this.id + role_definition_name = "Key Vault Administrator" + principal_id = data.azurerm_client_config.current.object_id +} + +resource "azurerm_key_vault_key" "cmkrsa" { + count = var.key_vault.cmk_keys_create ? 1 : 0 + + name = var.key_vault.cmkrsa_keyname + key_vault_id = azurerm_key_vault.this.id + key_type = "RSA" + key_size = 4096 + key_opts = [ + "unwrapKey", + "wrapKey" + ] + + rotation_policy { + automatic { + time_after_creation = var.key_vault.cmk_rotation_period + } + } + + depends_on = [ + azurerm_role_assignment.this + ] +} + +resource "azurerm_key_vault_key" "this" { + for_each = var.key_vault_key != null ? var.key_vault_key : {} + + key_opts = each.value.opts + key_type = each.value.type + key_vault_id = azurerm_key_vault.this.id + name = each.value.name == null ? each.key : each.value.name + curve = each.value.curve + expiration_date = each.value.expiration_date + key_size = each.value.size + not_before_date = each.value.not_before_date + + dynamic "rotation_policy" { + for_each = each.value.rotation_policy != null ? [each.value.rotation_policy] : [] + content { + expire_after = rotation_policy.value.expire_after + notify_before_expiry = rotation_policy.value.notify_before_expiry + + automatic { + time_before_expiry = rotation_policy.value.automatic.time_before_expiry + } + } + } + + tags = merge( + try(var.tags), + try(each.value.tags), + tomap({ + "Resource Type" = "Key vault key" + }) + ) + + depends_on = [ + azurerm_role_assignment.this + ] +} \ No newline at end of file diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..55654eb --- /dev/null +++ b/outputs.tf @@ -0,0 +1,21 @@ +output "key_vault_id" { + value = azurerm_key_vault.this.id +} + +output "key_vault_name" { + value = azurerm_key_vault.this.name +} + +output "key_vault_uri" { + value = azurerm_key_vault.this.vault_uri +} + +output "key_vault_cmkrsa_keyname" { + value = one(azurerm_key_vault_key.cmkrsa[*].name) + description = "CMK RSA Key Name" +} + +output "key_vault_cmkrsa_id" { + value = one(azurerm_key_vault_key.cmkrsa[*].id) + description = "CMK RSA Key ID" +} diff --git a/terraform.tf b/terraform.tf new file mode 100644 index 0000000..ca9101b --- /dev/null +++ b/terraform.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.7" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 4" + } + } +} diff --git a/tests/basic.tftest.hcl b/tests/basic.tftest.hcl new file mode 100644 index 0000000..db981a4 --- /dev/null +++ b/tests/basic.tftest.hcl @@ -0,0 +1,47 @@ +run "basic" { + variables { + resource_group = { + name = "my-resource-group" + location = "East US" + } + + key_vault = { + name = "kv001" + tenant_id = "your-tenant-id" + enabled_for_disk_encryption = false + enabled_for_deployment = false + enabled_for_template_deployment = false + enable_rbac_authorization = true + purge_protection = true + soft_delete_retention_days = 30 + sku = "standard" + ip_rules = [] + subnet_id = [] + network_bypass = "None" + cmkrsa_keyname = "cmkrsa" + cmkec_keyname = "cmkec" + cmk_keys_create = true + } + + tags = { + Environment = "Production" + } + } + + module { + source = "./" + } + + command = plan + + assert { + condition = output.key_vault_name == "kv001" + error_message = "Unexpected output.key_vault_name value" + } + + assert { + condition = output.cmk_ec_keyname == "cmkec" + error_message = "Unexpected output.cmk_ec_keyname value" + } + +} \ No newline at end of file diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..88ef172 --- /dev/null +++ b/variables.tf @@ -0,0 +1,118 @@ +variable "key_vault" { + type = object({ + name = string + tenant_id = string + resource_group_name = optional(string, null) + location = optional(string, null) + enabled_for_disk_encryption = optional(bool, false) + enabled_for_deployment = optional(bool, false) + enabled_for_template_deployment = optional(bool, false) + enable_rbac_authorization = optional(bool, true) + purge_protection = optional(bool, true) + soft_delete_retention_days = optional(number, 30) + sku = optional(string, "standard") + ip_rules = optional(list(string), []) + subnet_id = optional(list(string), []) + network_bypass = optional(string, "None") + cmkrsa_keyname = optional(string, "cmkrsa") + cmkec_keyname = optional(string, "cmkec") + cmk_keys_create = optional(bool, false) + cmk_rotation_period = optional(string, "P90D") + tags = optional(map(string), {}) + }) + nullable = false + description = <