├── .config ├── sast_python_bandit_cli.yml ├── sast_python_bandit_json.yml ├── sast_terraform_checkov_cli.yml └── sast_terraform_checkov_json.yml ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── checkov.yml │ ├── codeql.yml │ ├── dependency-review.yml │ ├── ecdsa_default.yml │ ├── python.yml │ ├── rsa_public_crl.yml │ ├── s3_backup.yml │ ├── scorecards.yml │ ├── secrets.yml │ ├── terraform.yml │ ├── terraform_docs.yml │ └── website.yml ├── .gitignore ├── CNAME ├── CONTRIBUTING.md ├── LICENSE.md ├── MAINTAINERS.md ├── README.md ├── docs ├── CNAME ├── assets │ └── images │ │ ├── alb │ │ ├── alb-mtls.png │ │ ├── alb-resource-map.png │ │ ├── alb-resources.png │ │ ├── edit-listener.png │ │ ├── firefox-cert-needed.png │ │ ├── hello-with-border.png │ │ ├── hello.png │ │ ├── listener-no-mtls.png │ │ ├── mtls-config.png │ │ ├── postman-certs.png │ │ ├── postman-hello.png │ │ ├── postman-settings.png │ │ ├── trust-store-config.png │ │ └── trust-store-created.png │ │ ├── api │ │ ├── add-certificate.png │ │ ├── add-new-mapping.png │ │ ├── api-ca-architecture.png │ │ ├── api-gateway-execution-endpoint.png │ │ ├── api-gateway-no-auth.png │ │ ├── api-gw-acm-config.png │ │ ├── api-gw-truststore-config.png │ │ ├── certificate-manager.png │ │ ├── client-auth-success.png │ │ ├── cloudwatch-logs.png │ │ ├── custom-domain-name-configured.png │ │ ├── default-api-endpoint-inactive.png │ │ ├── default-api-endpoint-warning.png │ │ ├── default-endpoint-failure.png │ │ ├── deploy-api.png │ │ ├── disable-default-api-endpoint.png │ │ ├── dns-record.png │ │ ├── lambda-function.png │ │ └── postman-no-auth.png │ │ ├── ca-architecture-options.png │ │ ├── ca-architecture.png │ │ ├── cache.png │ │ ├── cdp.png │ │ ├── cert-chain.png │ │ ├── cert-details.png │ │ ├── cert-logo.png │ │ ├── cert-logo.svg │ │ ├── costs.png │ │ ├── crl-revoked.png │ │ ├── crl.png │ │ ├── deployment-workflow.png │ │ ├── docs-development.png │ │ ├── dynamodb-test-server-cert-details.png │ │ ├── dynamodb-test-server-cert.png │ │ ├── external-s3.png │ │ ├── iam │ │ ├── add-permissions.png │ │ ├── aws-iam-roles-anywhere.png │ │ ├── confidential.png │ │ ├── create-trust-anchor-1.png │ │ ├── create-trust-anchor-2.png │ │ ├── download-bundle.png │ │ ├── profile.png │ │ ├── roles-anywhere-intro.png │ │ ├── subject-activity.png │ │ ├── trust-anchor-and-profile.png │ │ └── trust-anchor-created.png │ │ ├── open-cloud-security.png │ │ ├── q-solution.png │ │ ├── sans-cloudsecnext.png │ │ ├── server-cert-1.png │ │ ├── server-cert-2.png │ │ ├── sns-cert-issued.png │ │ ├── step-function.png │ │ ├── trusted-cert.png │ │ └── untrusted-cert.png ├── automation.md ├── client-certificates.md ├── development.md ├── faq.md ├── getting-started.md ├── how-to-guides │ ├── alb.md │ ├── api.md │ └── iam.md ├── index.md ├── locations.md ├── options.md ├── reference.md ├── revocation.md └── security.md ├── examples ├── default │ ├── README.md │ ├── backend.tf │ ├── ca.tf │ ├── certs │ │ └── dev │ │ │ ├── csrs │ │ │ └── .gitkeep │ │ │ ├── revoked-root-ca.json │ │ │ ├── revoked.json │ │ │ └── tls.json │ ├── data.tf │ ├── locals.tf │ ├── provider.tf │ └── variables.tf └── rsa-public-crl │ ├── README.md │ ├── backend.tf │ ├── ca.tf │ ├── certs │ └── prod │ │ ├── csrs │ │ └── .gitkeep │ │ ├── revoked-root-ca.json │ │ ├── revoked.json │ │ └── tls.json │ ├── data.tf │ ├── locals.tf │ ├── provider.tf │ └── variables.tf ├── main.tf ├── mkdocs.yml ├── modules ├── terraform-aws-ca-acm │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf ├── terraform-aws-ca-cloudfront │ ├── README.md │ ├── cache.tf │ ├── headers.tf │ ├── locals.tf │ ├── main.tf │ ├── outputs.tf │ ├── request.tf │ └── variables.tf ├── terraform-aws-ca-dynamodb │ ├── README.md │ ├── locals.tf │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── terraform-aws-ca-iam │ ├── README.md │ ├── data.tf │ ├── locals.tf │ ├── main.tf │ ├── outputs.tf │ ├── templates │ │ ├── db_reader_policy.json.tpl │ │ ├── db_reader_role.json.tpl │ │ ├── issuing_ca_policy.json.tpl │ │ ├── issuing_crl_policy.json.tpl │ │ ├── lambda_policy.json.tpl │ │ ├── lambda_role.json.tpl │ │ ├── root_ca_policy.json.tpl │ │ ├── root_crl_policy.json.tpl │ │ ├── scheduler_policy.json.tpl │ │ ├── scheduler_role.json.tpl │ │ ├── state_policy.json.tpl │ │ ├── state_role.json.tpl │ │ └── tls_cert_policy.json.tpl │ └── variables.tf ├── terraform-aws-ca-kms │ ├── README.md │ ├── data.tf │ ├── locals.tf │ ├── main.tf │ ├── outputs.tf │ ├── templates │ │ ├── ca.json.tpl │ │ └── default.json.tpl │ └── variables.tf ├── terraform-aws-ca-lambda │ ├── README.MD │ ├── build │ │ └── .gitkeep │ ├── cloudwatch.tf │ ├── data.tf │ ├── lambda_code │ │ ├── __init__.py │ │ ├── create_issuing_ca │ │ │ ├── __init__.py │ │ │ ├── create_issuing_ca.py │ │ │ └── requirements.txt │ │ ├── create_root_ca │ │ │ ├── __init__.py │ │ │ ├── create_root_ca.py │ │ │ └── requirements.txt │ │ ├── issuing_ca_crl │ │ │ ├── __init__.py │ │ │ ├── issuing_ca_crl.py │ │ │ └── requirements.txt │ │ ├── root_ca_crl │ │ │ ├── __init__.py │ │ │ ├── requirements.txt │ │ │ └── root_ca_crl.py │ │ └── tls_cert │ │ │ ├── __init__.py │ │ │ ├── requirements.txt │ │ │ └── tls_cert.py │ ├── locals.tf │ ├── main.tf │ ├── requirements-dev.txt │ ├── scripts │ │ └── lambda-build │ │ │ └── create-package.sh │ ├── unittests │ │ ├── __init__.py │ │ ├── test_tls_cert.py │ │ ├── test_types.py │ │ └── test_validate_sans.py │ ├── utils │ │ ├── __init__.py │ │ ├── aws │ │ │ ├── __init__.py │ │ │ ├── kms.py │ │ │ ├── lambdas.py │ │ │ └── sns.py │ │ └── certs │ │ │ ├── __init__.py │ │ │ ├── ca.py │ │ │ ├── crypto.py │ │ │ ├── crypto_kms_classes.py │ │ │ ├── db.py │ │ │ ├── kms.py │ │ │ ├── s3.py │ │ │ └── types.py │ └── variables.tf ├── terraform-aws-ca-s3 │ ├── README.md │ ├── data.tf │ ├── locals.tf │ ├── main.tf │ ├── outputs.tf │ ├── templates │ │ ├── cloudfront-with-principals.json.tpl │ │ ├── cloudfront.json.tpl │ │ ├── secure-transport-with-principals.json.tpl │ │ └── secure-transport.json.tpl │ └── variables.tf ├── terraform-aws-ca-scheduler │ ├── README.md │ ├── main.tf │ └── variables.tf ├── terraform-aws-ca-sns │ ├── data.tf │ ├── locals.tf │ ├── main.tf │ ├── outputs.tf │ ├── templates │ │ ├── default.json │ │ └── eventbridge.json │ └── variables.tf └── terraform-aws-ca-step-function │ ├── README.md │ ├── data.tf │ ├── locals.tf │ ├── main.tf │ ├── outputs.tf │ ├── templates │ ├── ca-no-gitops.json.tpl │ └── ca.json.tpl │ └── variables.tf ├── outputs.tf ├── providers.tf ├── requirements-dev.txt ├── requirements-docs.txt ├── scripts ├── delete_db_table_items.py ├── requirements.txt └── start_ca_step_function.py ├── tests ├── __init__.py ├── helper.py ├── test_cert_revocation.py └── test_issued_certs.py ├── utils ├── client-cert.py ├── generate-cert.py ├── generate-csr.py ├── modules │ ├── __init__.py │ ├── aws │ │ ├── kms.py │ │ ├── lambdas.py │ │ └── s3.py │ └── certs │ │ ├── crypto.py │ │ └── kms.py ├── requirements.txt ├── server-cert.py └── variables │ ├── client.json │ └── server.json └── variables.tf /.config/sast_python_bandit_cli.yml: -------------------------------------------------------------------------------- 1 | [bandit] 2 | format: screen 3 | recursive: true 4 | skips: B101 5 | -------------------------------------------------------------------------------- /.config/sast_python_bandit_json.yml: -------------------------------------------------------------------------------- 1 | [bandit] 2 | format: json 3 | recursive: true 4 | ignore-nosec: true 5 | skips: B101 6 | -------------------------------------------------------------------------------- /.config/sast_terraform_checkov_cli.yml: -------------------------------------------------------------------------------- 1 | download-external-modules: true 2 | skip-download: true 3 | evaluate-variables: true 4 | framework: 5 | - terraform 6 | output: 7 | - cli 8 | quiet: true -------------------------------------------------------------------------------- /.config/sast_terraform_checkov_json.yml: -------------------------------------------------------------------------------- 1 | skip-download: true 2 | evaluate-variables: true 3 | framework: 4 | - terraform 5 | soft-fail: true -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @serverless-ca/admins 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every week 8 | interval: "weekly" 9 | 10 | - package-ecosystem: "pip" 11 | directory: "/" 12 | schedule: 13 | # Check for updates to Python dependencies every week 14 | interval: "weekly" 15 | -------------------------------------------------------------------------------- /.github/workflows/checkov.yml: -------------------------------------------------------------------------------- 1 | name: Checkov security test 2 | on: 3 | workflow_dispatch: 4 | push: 5 | paths: 6 | - "**/*.tf" 7 | - ".github/workflows/checkov.yml" 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | checkov_security: 14 | name: Checkov security tests 15 | runs-on: ubuntu-latest 16 | 17 | permissions: 18 | id-token: write 19 | contents: read 20 | pull-requests: read 21 | checks: write 22 | security-events: write 23 | actions: read 24 | 25 | steps: 26 | - name: Harden the runner (Audit all outbound calls) 27 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 28 | with: 29 | egress-policy: audit 30 | 31 | - name: checkout 32 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 33 | 34 | - name: prepare reports dir 35 | run: mkdir --parents ${{runner.temp}}/reports_sast_terraform/ 36 | 37 | - name: install checkov 38 | run: | 39 | pip3 install --upgrade checkov 40 | echo $PATH 41 | checkov --version 42 | which checkov 43 | 44 | - name: generate json report 45 | run: > 46 | checkov 47 | --config-file .config/sast_terraform_checkov_json.yml 48 | --directory . 49 | --output cli 50 | --output json 51 | --output sarif 52 | --output-file-path console,checkov-terraform-results.json,checkov-terraform-results.sarif 53 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - "**/*.py" 8 | - "**/*.txt" 9 | - ".github/workflows/codeql.yml" 10 | schedule: 11 | - cron: '24 0 * * 4' 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | analyze: 18 | name: Analyze 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 360 21 | permissions: 22 | security-events: write 23 | 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | language: [ 'python' ] 28 | 29 | steps: 30 | - name: Harden the runner (Audit all outbound calls) 31 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 32 | with: 33 | egress-policy: audit 34 | 35 | - name: Checkout repository 36 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 37 | 38 | - name: Initialize CodeQL 39 | uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 40 | with: 41 | languages: ${{ matrix.language }} -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, 4 | # surfacing known-vulnerable versions of the packages declared or updated in the PR. 5 | # Once installed, if the workflow run is marked as required, 6 | # PRs introducing known-vulnerable packages will be blocked from merging. 7 | # 8 | # Source repository: https://github.com/actions/dependency-review-action 9 | name: 'Dependency Review' 10 | on: [pull_request] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | dependency-review: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Harden Runner 20 | uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 21 | with: 22 | egress-policy: audit 23 | 24 | - name: 'Checkout Repository' 25 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 26 | - name: 'Dependency Review' 27 | uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0 28 | -------------------------------------------------------------------------------- /.github/workflows/python.yml: -------------------------------------------------------------------------------- 1 | name: Python tests 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | push: 6 | paths: 7 | - "**/*.py" 8 | - ".github/workflows/python.yml" 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | python_tests: 15 | name: Python tests 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Harden the runner (Audit all outbound calls) 19 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 20 | with: 21 | egress-policy: audit 22 | 23 | - name: checkout 24 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | 26 | - name: Set up Python 3.13 27 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 28 | with: 29 | python-version: "3.13" 30 | 31 | - name: Display Python version 32 | run: python -c "import sys; print(sys.version)" 33 | 34 | - name: Install dependencies 35 | run: | 36 | pip install -r requirements-dev.txt 37 | 38 | - name: Pytest 39 | run: | 40 | pytest -v modules/terraform-aws-ca-lambda 41 | 42 | - name: Black 43 | run: | 44 | black --check --line-length 120 . 45 | 46 | - name: Prospector 47 | run: | 48 | prospector 49 | 50 | - name: prepare reports dir 51 | run: mkdir --parents ${{runner.temp}}/reports_sast_python/ 52 | 53 | - name: generate json report 54 | run: > 55 | bandit -r modules/terraform-aws-ca-lambda/lambda_code modules/terraform-aws-ca-lambda/utils scripts tests utils 56 | --exit-zero 57 | --ini .config/sast_python_bandit_json.yml 58 | 1> ${{runner.temp}}/reports_sast_python/${RANDOM}.json 59 | 60 | - name: save json report 61 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 62 | with: 63 | name: sast_python 64 | if-no-files-found: error 65 | path: ${{runner.temp}}/reports_sast_python/ 66 | 67 | - name: Bandit 68 | run: > 69 | bandit -r modules/terraform-aws-ca-lambda/lambda_code modules/terraform-aws-ca-lambda/utils scripts tests utils 70 | --exit-zero 71 | --ini .config/sast_python_bandit_cli.yml 72 | -------------------------------------------------------------------------------- /.github/workflows/s3_backup.yml: -------------------------------------------------------------------------------- 1 | name: S3 Backup 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | s3_backup: 13 | name: S3 Backup 14 | runs-on: ubuntu-latest 15 | 16 | permissions: 17 | id-token: write 18 | contents: read 19 | pull-requests: read 20 | checks: write 21 | steps: 22 | - name: Harden the runner (Audit all outbound calls) 23 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 24 | with: 25 | egress-policy: audit 26 | 27 | - name: checkout 28 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 29 | 30 | - name: get-name 31 | run: | 32 | echo "REPO_NAME=$(basename ${{ github.repository }})" >> $GITHUB_ENV 33 | echo "LOWER_REPO_NAME=$(basename ${{ github.repository }} | tr [:upper:] [:lower:])" >> $GITHUB_ENV 34 | 35 | - name: Configure AWS Credentials 36 | uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1 37 | with: 38 | role-to-assume: "${{ secrets.AWS_BACKUP_ROLE_PREFIX }}${{ env.LOWER_REPO_NAME }}" 39 | aws-region: "${{ secrets.AWS_REGION }}" 40 | 41 | - name: Back up repository to S3 42 | run: | 43 | aws s3 sync . s3://"${{ secrets.BACKUP_S3_BUCKET }}"/${{ env.REPO_NAME }} --delete 44 | -------------------------------------------------------------------------------- /.github/workflows/scorecards.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '20 7 * * 2' 14 | push: 15 | branches: ["main"] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | contents: read 30 | actions: read 31 | # To allow GraphQL ListCommits to work 32 | issues: read 33 | pull-requests: read 34 | # To detect SAST tools 35 | checks: read 36 | 37 | steps: 38 | - name: Harden the runner (Audit all outbound calls) 39 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 40 | with: 41 | egress-policy: audit 42 | 43 | - name: "Checkout code" 44 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 45 | with: 46 | persist-credentials: false 47 | 48 | - name: "Run analysis" 49 | uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 50 | with: 51 | results_file: results.sarif 52 | results_format: sarif 53 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 54 | # - you want to enable the Branch-Protection check on a *public* repository, or 55 | # - you are installing Scorecards on a *private* repository 56 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 57 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 58 | 59 | # Public repositories: 60 | # - Publish results to OpenSSF REST API for easy access by consumers 61 | # - Allows the repository to include the Scorecard badge. 62 | # - See https://github.com/ossf/scorecard-action#publishing-results. 63 | # For private repositories: 64 | # - `publish_results` will always be set to `false`, regardless 65 | # of the value entered here. 66 | publish_results: true 67 | 68 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 69 | # format to the repository Actions tab. 70 | - name: "Upload artifact" 71 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 72 | with: 73 | name: SARIF file 74 | path: results.sarif 75 | retention-days: 5 76 | 77 | # Upload the results to GitHub's code scanning dashboard. 78 | - name: "Upload to code-scanning" 79 | uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 80 | with: 81 | sarif_file: results.sarif 82 | -------------------------------------------------------------------------------- /.github/workflows/secrets.yml: -------------------------------------------------------------------------------- 1 | name: Scan for secrets 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | types: [synchronize] 6 | push: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | secret_scan: 13 | name: Secret scan 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Harden the runner (Audit all outbound calls) 17 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 18 | with: 19 | egress-policy: audit 20 | 21 | - name: Checkout code 22 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 23 | - name: Install GitLeaks 24 | run: | 25 | wget https://github.com/gitleaks/gitleaks/releases/download/v8.16.1/gitleaks_8.16.1_linux_x64.tar.gz && \ 26 | tar -xf gitleaks_8.16.1_linux_x64.tar.gz 27 | sudo mv gitleaks /usr/local/bin/gitleaks && \ 28 | sudo chmod +x /usr/local/bin/gitleaks 29 | - name: Run GitLeaks Scan 30 | run: | 31 | gitleaks detect --source . -v 32 | -------------------------------------------------------------------------------- /.github/workflows/terraform.yml: -------------------------------------------------------------------------------- 1 | name: Terraform tests 2 | on: 3 | workflow_dispatch: 4 | push: 5 | paths: 6 | - "**/*.tf" 7 | - ".github/workflows/terraform.yml" 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | terraform_validate: 14 | name: Terraform validate 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Harden the runner (Audit all outbound calls) 18 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 19 | with: 20 | egress-policy: audit 21 | 22 | - name: Terraform setup 23 | uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 24 | with: 25 | terraform_version: 1.6.1 26 | 27 | - name: Checkout 28 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 29 | 30 | - name: Terraform format 31 | run: terraform fmt -check -recursive 32 | 33 | - name: Terraform init 34 | working-directory: ./examples/default 35 | run: terraform init -get -backend=false 36 | 37 | - name: Terraform validate 38 | working-directory: ./examples/default 39 | run: terraform validate 40 | -------------------------------------------------------------------------------- /.github/workflows/terraform_docs.yml: -------------------------------------------------------------------------------- 1 | name: Generate terraform docs 2 | 3 | permissions: 4 | contents: write 5 | pull-requests: write 6 | 7 | on: 8 | - pull_request 9 | jobs: 10 | docs: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Harden the runner (Audit all outbound calls) 14 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 15 | with: 16 | egress-policy: audit 17 | 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | with: 20 | ref: ${{ github.event.pull_request.head.ref }} 21 | 22 | - name: Render terraform docs inside the REFERENCE.md and push changes back to PR branch 23 | uses: terraform-docs/gh-actions@6de6da0cefcc6b4b7a5cbea4d79d97060733093c # v1.4.1 24 | with: 25 | working-dir: . 26 | output-file: docs/reference.md 27 | output-method: inject 28 | git-push: "true" -------------------------------------------------------------------------------- /.github/workflows/website.yml: -------------------------------------------------------------------------------- 1 | name: Publish website 2 | on: 3 | push: 4 | branches: 5 | - main 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | id-token: write 11 | pages: write 12 | 13 | jobs: 14 | deploy: 15 | name: Publish docs website 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Harden the runner (Audit all outbound calls) 19 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 20 | with: 21 | egress-policy: audit 22 | 23 | - name: Checkout code 24 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | 26 | - name: Configure Git Credentials 27 | run: | 28 | git config user.name github-actions[bot] 29 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 30 | 31 | - name: Setup Python 32 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 33 | with: 34 | python-version: 3.13 35 | 36 | - name: Set up build cache 37 | uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 38 | with: 39 | key: mkdocs-material-${{ hashfiles('.cache/**') }} 40 | path: .cache 41 | restore-keys: | 42 | mkdocs-material- 43 | 44 | - name: Install dependencies 45 | run: | 46 | pip install -r requirements-docs.txt 47 | 48 | - name: Deploy GitHub Pages 49 | run: mkdocs gh-deploy --force 50 | 51 | - name: Save build cache 52 | uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 53 | with: 54 | key: mkdocs-material-${{ hashfiles('.cache/**') }} 55 | path: .cache 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDEs 2 | *.code-workspace 3 | .idea 4 | .vs 5 | .vscode 6 | 7 | # Lambda 8 | modules/terraform-aws-ca-lambda/build/ 9 | 10 | # Python 11 | __pycache__ 12 | .venv 13 | .python-version 14 | **/pyvenv.cfg 15 | **/bin 16 | **/lib 17 | **/lib64 18 | 19 | # Terraform 20 | .terraform/ 21 | .terraform.lock.hcl 22 | .terraform.tfstate 23 | .terraform.tfstate.backup 24 | terraform.tfvars 25 | 26 | # MKDocs 27 | .cache/ 28 | 29 | # MacOS 30 | .DS_Store 31 | 32 | # Vim 33 | **.swp 34 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | serverlessca.com -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome contributions! 4 | 5 | Ways you can begin: 6 | - raise an Issue 7 | - contact one of the [maintainers](MAINTAINERS.md) via [Cloud Security Forum Slack](https://cloudsecurityforum.slack.com/) 8 | - get in touch with one of the [maintainers](MAINTAINERS.md) by email 9 | 10 | ## Development 11 | 12 | See [Example README](./examples/default/README.md) for information on Terraform development and testing. 13 | 14 | A guide to development and testing the Lambda function Python code is provided in the [Lambda sub-module README](/modules/terraform-aws-ca-lambda/README.MD). -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2024 Q-Solution Limited 2 | 3 | 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 4 | 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | 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. -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | Paul Schwarzenberger -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform module for Certificate Authority on AWS 2 | 3 | [![Version](https://img.shields.io/github/v/release/serverless-ca/terraform-aws-ca)](https://github.com/serverless-ca/terraform-aws-ca/releases/tag/v0.1.0) 4 | [![Build](https://img.shields.io/github/actions/workflow/status/serverless-ca/terraform-aws-ca/.github%2Fworkflows%2Fecdsa_default.yml?branch=main)](https://github.com/serverless-ca/terraform-aws-ca/actions/workflows/ecdsa_default.yml) 5 | [![Apache License](https://img.shields.io/badge/License-Apache%20v2-green.svg)](https://github.com/serverless-ca/terraform-aws-ca/blob/main/LICENSE.md) 6 | 7 | * Serverless Certificate Authority typically $50 per year 8 | * [Equivalent cost using AWS Private CA around $10,000 per year](https://serverlessca.com/faq/#how-did-you-work-out-the-cost-comparison-with-aws-private-ca) 9 | * 100% serverless 10 | * CA private keys stored in [FIPS 140-2 level 3 certified hardware](https://aws.amazon.com/about-aws/whats-new/2023/05/aws-kms-hsm-fips-security-level-3) 11 | * Wide range of [configuration options](https://serverlessca.com/options/) 12 | * Published as a public [Terraform registry module](https://registry.terraform.io/modules/serverless-ca/ca/aws/latest) 13 | 14 | 15 | 16 | > 📄 Detailed documentation is on our [Docs](https://serverlessca.com) site. If testing the Serverless CA for the first time, use the [Getting Started](https://serverlessca.com/getting-started/) guide. 17 | 18 | > 📢 We welcome contributions! See the [Contributing Guide](https://github.com/serverless-ca/terraform-aws-ca/blob/main/CONTRIBUTING.md) for how to get started. 19 | 20 | ## Open Cloud Security Conference - talk and demo 21 | 22 | Talk and demo on [YouTube](https://youtu.be/p2Cb5PSXWSE) 23 | 24 | ## SANS CloudSecNext - talk and demo 25 | 26 | Talk and demo on [YouTube](https://youtu.be/JJD2GrZxLq4) 27 | 28 | ## Blog posts 29 | > 📖 [Open-source cloud Certificate Authority](https://medium.com/@paulschwarzenberger/open-source-cloud-certificate-authority-75609439dfe7) 30 | 31 | > 📖 [AWS Application Load Balancer mTLS with open-source cloud CA](https://medium.com/@paulschwarzenberger/aws-application-load-balancer-mtls-with-open-source-cloud-ca-277cb40d60c7) 32 | 33 | > 📖 [AWS IAM Roles Anywhere with open-source private CA](https://medium.com/@paulschwarzenberger/aws-iam-roles-anywhere-with-open-source-private-ca-6c0ec5758b2b) 34 | 35 | > 📖 [API Gateway mTLS with open-source cloud CA](https://medium.com/@paulschwarzenberger/api-gateway-mtls-with-open-source-cloud-ca-3362438445de) 36 | 37 | ## Sponsors 38 | This project is supported by [Q-Solution](https://www.q-solution.co.uk) 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | serverlessca.com -------------------------------------------------------------------------------- /docs/assets/images/alb/alb-mtls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/alb/alb-mtls.png -------------------------------------------------------------------------------- /docs/assets/images/alb/alb-resource-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/alb/alb-resource-map.png -------------------------------------------------------------------------------- /docs/assets/images/alb/alb-resources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/alb/alb-resources.png -------------------------------------------------------------------------------- /docs/assets/images/alb/edit-listener.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/alb/edit-listener.png -------------------------------------------------------------------------------- /docs/assets/images/alb/firefox-cert-needed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/alb/firefox-cert-needed.png -------------------------------------------------------------------------------- /docs/assets/images/alb/hello-with-border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/alb/hello-with-border.png -------------------------------------------------------------------------------- /docs/assets/images/alb/hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/alb/hello.png -------------------------------------------------------------------------------- /docs/assets/images/alb/listener-no-mtls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/alb/listener-no-mtls.png -------------------------------------------------------------------------------- /docs/assets/images/alb/mtls-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/alb/mtls-config.png -------------------------------------------------------------------------------- /docs/assets/images/alb/postman-certs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/alb/postman-certs.png -------------------------------------------------------------------------------- /docs/assets/images/alb/postman-hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/alb/postman-hello.png -------------------------------------------------------------------------------- /docs/assets/images/alb/postman-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/alb/postman-settings.png -------------------------------------------------------------------------------- /docs/assets/images/alb/trust-store-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/alb/trust-store-config.png -------------------------------------------------------------------------------- /docs/assets/images/alb/trust-store-created.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/alb/trust-store-created.png -------------------------------------------------------------------------------- /docs/assets/images/api/add-certificate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/add-certificate.png -------------------------------------------------------------------------------- /docs/assets/images/api/add-new-mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/add-new-mapping.png -------------------------------------------------------------------------------- /docs/assets/images/api/api-ca-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/api-ca-architecture.png -------------------------------------------------------------------------------- /docs/assets/images/api/api-gateway-execution-endpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/api-gateway-execution-endpoint.png -------------------------------------------------------------------------------- /docs/assets/images/api/api-gateway-no-auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/api-gateway-no-auth.png -------------------------------------------------------------------------------- /docs/assets/images/api/api-gw-acm-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/api-gw-acm-config.png -------------------------------------------------------------------------------- /docs/assets/images/api/api-gw-truststore-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/api-gw-truststore-config.png -------------------------------------------------------------------------------- /docs/assets/images/api/certificate-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/certificate-manager.png -------------------------------------------------------------------------------- /docs/assets/images/api/client-auth-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/client-auth-success.png -------------------------------------------------------------------------------- /docs/assets/images/api/cloudwatch-logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/cloudwatch-logs.png -------------------------------------------------------------------------------- /docs/assets/images/api/custom-domain-name-configured.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/custom-domain-name-configured.png -------------------------------------------------------------------------------- /docs/assets/images/api/default-api-endpoint-inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/default-api-endpoint-inactive.png -------------------------------------------------------------------------------- /docs/assets/images/api/default-api-endpoint-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/default-api-endpoint-warning.png -------------------------------------------------------------------------------- /docs/assets/images/api/default-endpoint-failure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/default-endpoint-failure.png -------------------------------------------------------------------------------- /docs/assets/images/api/deploy-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/deploy-api.png -------------------------------------------------------------------------------- /docs/assets/images/api/disable-default-api-endpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/disable-default-api-endpoint.png -------------------------------------------------------------------------------- /docs/assets/images/api/dns-record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/dns-record.png -------------------------------------------------------------------------------- /docs/assets/images/api/lambda-function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/lambda-function.png -------------------------------------------------------------------------------- /docs/assets/images/api/postman-no-auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/api/postman-no-auth.png -------------------------------------------------------------------------------- /docs/assets/images/ca-architecture-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/ca-architecture-options.png -------------------------------------------------------------------------------- /docs/assets/images/ca-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/ca-architecture.png -------------------------------------------------------------------------------- /docs/assets/images/cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/cache.png -------------------------------------------------------------------------------- /docs/assets/images/cdp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/cdp.png -------------------------------------------------------------------------------- /docs/assets/images/cert-chain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/cert-chain.png -------------------------------------------------------------------------------- /docs/assets/images/cert-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/cert-details.png -------------------------------------------------------------------------------- /docs/assets/images/cert-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/cert-logo.png -------------------------------------------------------------------------------- /docs/assets/images/cert-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | 10 | 11 | 13 | 15 | 17 | 19 | 20 | -------------------------------------------------------------------------------- /docs/assets/images/costs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/costs.png -------------------------------------------------------------------------------- /docs/assets/images/crl-revoked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/crl-revoked.png -------------------------------------------------------------------------------- /docs/assets/images/crl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/crl.png -------------------------------------------------------------------------------- /docs/assets/images/deployment-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/deployment-workflow.png -------------------------------------------------------------------------------- /docs/assets/images/docs-development.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/docs-development.png -------------------------------------------------------------------------------- /docs/assets/images/dynamodb-test-server-cert-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/dynamodb-test-server-cert-details.png -------------------------------------------------------------------------------- /docs/assets/images/dynamodb-test-server-cert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/dynamodb-test-server-cert.png -------------------------------------------------------------------------------- /docs/assets/images/external-s3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/external-s3.png -------------------------------------------------------------------------------- /docs/assets/images/iam/add-permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/iam/add-permissions.png -------------------------------------------------------------------------------- /docs/assets/images/iam/aws-iam-roles-anywhere.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/iam/aws-iam-roles-anywhere.png -------------------------------------------------------------------------------- /docs/assets/images/iam/confidential.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/iam/confidential.png -------------------------------------------------------------------------------- /docs/assets/images/iam/create-trust-anchor-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/iam/create-trust-anchor-1.png -------------------------------------------------------------------------------- /docs/assets/images/iam/create-trust-anchor-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/iam/create-trust-anchor-2.png -------------------------------------------------------------------------------- /docs/assets/images/iam/download-bundle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/iam/download-bundle.png -------------------------------------------------------------------------------- /docs/assets/images/iam/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/iam/profile.png -------------------------------------------------------------------------------- /docs/assets/images/iam/roles-anywhere-intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/iam/roles-anywhere-intro.png -------------------------------------------------------------------------------- /docs/assets/images/iam/subject-activity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/iam/subject-activity.png -------------------------------------------------------------------------------- /docs/assets/images/iam/trust-anchor-and-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/iam/trust-anchor-and-profile.png -------------------------------------------------------------------------------- /docs/assets/images/iam/trust-anchor-created.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/iam/trust-anchor-created.png -------------------------------------------------------------------------------- /docs/assets/images/open-cloud-security.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/open-cloud-security.png -------------------------------------------------------------------------------- /docs/assets/images/q-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/q-solution.png -------------------------------------------------------------------------------- /docs/assets/images/sans-cloudsecnext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/sans-cloudsecnext.png -------------------------------------------------------------------------------- /docs/assets/images/server-cert-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/server-cert-1.png -------------------------------------------------------------------------------- /docs/assets/images/server-cert-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/server-cert-2.png -------------------------------------------------------------------------------- /docs/assets/images/sns-cert-issued.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/sns-cert-issued.png -------------------------------------------------------------------------------- /docs/assets/images/step-function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/step-function.png -------------------------------------------------------------------------------- /docs/assets/images/trusted-cert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/trusted-cert.png -------------------------------------------------------------------------------- /docs/assets/images/untrusted-cert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/docs/assets/images/untrusted-cert.png -------------------------------------------------------------------------------- /docs/automation.md: -------------------------------------------------------------------------------- 1 | # Automation 2 | 3 | The serverless CA can be deployed and updated using a CI/CD pipeline. 4 | 5 | ## GitHub Actions example 6 | 7 | For examples using GitHub Actions see one of the [test GitHub Actions workflows](https://github.com/serverless-ca/terraform-aws-ca/blob/main/.github/workflows/ecdsa_default.yml) within this repository, or the [Cloud CA deployment workflow](https://github.com/serverless-ca/cloud-ca/blob/main/.github/workflows/deploy.yml) shown below: 8 | 9 | ![GitHub Actions workflow](assets/images/deployment-workflow.png?raw=true) 10 | 11 | The principal steps are: 12 | 13 | * Terraform validate 14 | * Secret scan using GitLeaks 15 | * Terraform plan 16 | * Terraform apply 17 | * Start CA 18 | * Integration tests 19 | 20 | Further details are provided below: 21 | 22 | ## Caching Lambda zips 23 | So that Lambda functions only get rebuilt when needed, the Lambda package zip files are cached and can be viewed in the [GitHub Actions cache](https://github.com/serverless-ca/terraform-aws-ca/actions/caches): 24 | 25 | ![Lambda zip cache](assets/images/cache.png?raw=true) 26 | 27 | To ensure Lambda functions are updated when needed, check the cache name includes source code hashes and the Python version. 28 | 29 | ## Approve Terraform apply 30 | If you require a manual approval of Terraform Apply, use a GitHub Actions environment. Protect the environment with an Environment Protection rule requiring approval by an appropriate GitHub team. 31 | 32 | ## Start CA 33 | The Start CA job forces an immediate execution of the AWS Step Function. If this is omitted, the CA will only be created or updated on the next scheduled run which may take up to 24 hours. 34 | 35 | ## Integration Tests 36 | Integration tests include: 37 | 38 | * certificate issued with no passphrase 39 | * issued cert only includes client auth extension 40 | * certificate issued with passphrase 41 | * issued cert includes distinguished name specified in CSR 42 | * issued cert includes correct DNS names in SAN 43 | * issued cert with no SAN includes correct DNS name 44 | * certificate issued without SAN if common name invalid 45 | 46 | To reduce the risk of test certificates and keys being compromised and then used to access your environment, test certificates are: 47 | 48 | * not saved to disk on the GitHub Actions runner 49 | * short lifetime (1 day) 50 | 51 | ## Issue Certificates using GitOps 52 | 53 | Optionally, certificates can be issued using a GitOps process, as detailed in [Client Certificates](client-certificates.md#gitops). 54 | 55 | ## Certificate Revocation 56 | 57 | Certificate revocation is via a GitOps process, as detailed in [Revocation](./revocation.md#revoking-a-certificate). -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Python Development 4 | 5 | A guide to developing the Python code used in this project is provided in the [Lambda submodule README](https://github.com/serverless-ca/terraform-aws-ca/blob/main/modules/terraform-aws-ca-lambda/README.MD). 6 | 7 | ## Terraform Development 8 | 9 | Notes on Terraform development are included in the [Example README](https://github.com/serverless-ca/terraform-aws-ca/blob/main/examples/default/README.md). 10 | 11 | ## Documentation Development 12 | 13 | Terraform docs and MKDocs are used to produce the [Project Documentation](https://serverlessca.com). 14 | 15 | To view the site locally during development: 16 | 17 | create virtual environment 18 | ``` 19 | python -m venv .venv 20 | ``` 21 | activate virtual environment 22 | ``` 23 | source .venv/bin/activate 24 | ``` 25 | install dependencies 26 | ``` 27 | pip install -r requirements-dev.txt 28 | ``` 29 | start mkdocs server 30 | ``` 31 | mkdocs serve 32 | ``` 33 | view the web site locally at http://127.0.0.1:8000 34 | 35 | ![Alt text](assets/images/docs-development.png?raw=true "Docs development") 36 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Objectives 4 | By the end of this How-to guide you will have: 5 | 6 | * created a serverless CA in your own AWS account 7 | * viewed the Root CA, Issuing CA certificates and CRLs 8 | * issued a client certificate 9 | * issued a server certificate 10 | 11 | ## Prerequisites 12 | * AWS account 13 | * [Terraform](https://developer.hashicorp.com/terraform/install?product_intent=terraform) configured with admin credentials to your AWS account 14 | * Terraform state bucket 15 | 16 | ## Create serverless CA in your own AWS account 17 | 18 | A Root CA and Issuing CA will be deployed to your AWS account: 19 | 20 | * copy the [default example folder](https://github.com/serverless-ca/terraform-aws-ca/tree/main/examples/default) to your laptop 21 | * make sure you include the `dev` subfolder and contents 22 | * update `backend.tf` to include your own S3 Terraform state bucket in the same AWS account 23 | * update `ca.tf` with the terraform module source address and [latest version](https://registry.terraform.io/modules/serverless-ca/ca/aws/latest), e.g. 24 | ``` 25 | module "certificate_authority" { 26 | source = "serverless-ca/ca/aws" 27 | version = "1.0.0" 28 | } 29 | ``` 30 | * uncomment the other variables in `ca.tf` 31 | * uncomment `locals.tf` and enter your own company details 32 | ``` 33 | terraform init 34 | terraform apply 35 | ``` 36 | * CA lambda functions, KMS keys, S3 buckets and other resources will be created in your AWS account 37 | 38 | ## Start CA 39 | 40 | To initialise the CA, in the AWS console, select Step Furnctions, and execute the CA workflow 41 | 42 | ![Alt text](assets/images/step-function.png?raw=true "CA Step Function") 43 | 44 | Alternatively wait for the next scheduled run of the Step Function which may take up to 24 hours 45 | 46 | ## View CA certificates and CRLs 47 | CA certificates and CRLs are available in the 'external' S3 bucket created by Terraform 48 | 49 | ![Alt text](assets/images/external-s3.png?raw=true "External S3 bucket") 50 | 51 | * download the Root CA and issuing CA 52 | * import and trust both CA certificates 53 | 54 | ## Create client certificate (Linux / MacOS) 55 | * ensure Python and PIP are installed on your laptop 56 | * log in to the CA AWS account with your terminal using AWS CLI, e.g. `aws sso login` or set AWS environment variables 57 | * from the root of this repository: 58 | ``` 59 | python -m venv .venv 60 | source .venv/bin/activate 61 | pip install -r utils/requirements.txt 62 | python utils/client-cert.py 63 | ``` 64 | * you will now have a client key and certificate at `~/certs` 65 | * bundled Root CA and Issuing CA certs are also provided 66 | 67 | ## Create client certificate (Windows) 68 | * ensure Python and PIP are installed on your laptop 69 | * log in to the CA AWS account with your terminal using AWS CLI, e.g. `aws sso login` or set AWS environment variables 70 | * from the root of this repository: 71 | ``` 72 | python -m venv .venv 73 | .venv/scripts/activate 74 | pip install -r utils/requirements.txt 75 | python utils/client-cert.py 76 | ``` 77 | * you will now have a client key and certificate at `~\certs` 78 | * bundled Root CA and Issuing CA certs are also provided 79 | 80 | ## View client certificate 81 | View the client certificate `serverless-cert.crt` with your operating system cert viewer 82 | 83 | ![Alt text](assets/images/trusted-cert.png?raw=true "Client certificate") 84 | ![Alt text](assets/images/cert-details.png?raw=true "Client certificate details") 85 | ![Alt text](assets/images/cert-chain.png?raw=true "Client certificate chain") 86 | 87 | ## Create and view server certificate 88 | Create a server certificate with Subject Alternative Names 89 | ``` 90 | python utils/server-cert.py 91 | ``` 92 | ![Alt text](assets/images/server-cert-1.png?raw=true "Client certificate") 93 | ![Alt text](assets/images/server-cert-2.png?raw=true "Client certificate details") 94 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Terraform module for serverless CA on AWS 3 | description: Serverless CA in AWS with FIPS 140-2 level 3 CA key storage and cost typically under $5 per month 4 | --- 5 | # Terraform module for Certificate Authority on AWS 6 | 7 | [![Version](https://img.shields.io/github/v/release/serverless-ca/terraform-aws-ca)](https://github.com/serverless-ca/terraform-aws-ca/releases/tag/v0.1.0) 8 | [![Build](https://img.shields.io/github/actions/workflow/status/serverless-ca/terraform-aws-ca/.github%2Fworkflows%2Fecdsa_default.yml?branch=main)](https://github.com/serverless-ca/terraform-aws-ca/actions/workflows/ecdsa_default.yml) 9 | [![Apache License](https://img.shields.io/badge/License-Apache%20v2-green.svg)](https://github.com/serverless-ca/terraform-aws-ca/blob/main/LICENSE.md) 10 | 11 | * Serverless Certificate Authority typically $50 per year 12 | * [Equivalent cost using AWS Private CA around $10,000 per year](./faq.md#how-did-you-work-out-the-cost-comparison-with-aws-private-ca) 13 | * 100% serverless 14 | * CA private keys stored in [FIPS 140-2 level 3 certified hardware](https://aws.amazon.com/about-aws/whats-new/2023/05/aws-kms-hsm-fips-security-level-3) 15 | * Wide range of [configuration options](options.md) 16 | * Published as a public [Terraform registry module](https://registry.terraform.io/modules/serverless-ca/ca/aws/latest) 17 | 18 | ![Alt text](assets/images/ca-architecture-options.png?raw=true "CA architecture") 19 | 20 | ## Open Cloud Security Conference - talk and demo 21 | 22 | Talk and demo on [YouTube](https://youtu.be/p2Cb5PSXWSE) 23 | 24 | ## SANS CloudSecNext - talk and demo 25 | 26 | Talk and demo on [YouTube](https://youtu.be/JJD2GrZxLq4) 27 | 28 | ## Blog posts 29 | > 📖 [Open-source cloud Certificate Authority](https://medium.com/@paulschwarzenberger/open-source-cloud-certificate-authority-75609439dfe7) 30 | 31 | > 📖 [AWS Application Load Balancer mTLS with open-source cloud CA](https://medium.com/@paulschwarzenberger/aws-application-load-balancer-mtls-with-open-source-cloud-ca-277cb40d60c7) 32 | 33 | > 📖 [AWS IAM Roles Anywhere with open-source private CA](https://medium.com/@paulschwarzenberger/aws-iam-roles-anywhere-with-open-source-private-ca-6c0ec5758b2b) 34 | 35 | > 📖 [API Gateway mTLS with open-source cloud CA](https://medium.com/@paulschwarzenberger/api-gateway-mtls-with-open-source-cloud-ca-3362438445de) 36 | 37 | ## Sponsors 38 | This project is supported by [Q-Solution](https://www.q-solution.co.uk) 39 | 40 | ![Alt text](assets/images/q-solution.png?raw=true "Q-Solution") 41 | -------------------------------------------------------------------------------- /docs/options.md: -------------------------------------------------------------------------------- 1 | # Configuration Options 2 | 3 | The serverless CA is highly configurable by adjusting values of Terraform variables in [variables.tf](https://github.com/serverless-ca/terraform-aws-ca/blob/main/variables.tf). Principal configuration options: 4 | 5 | ## Revocation and GitOps 6 | By default, certificate revocation and GitOps are disabled. If you followed the [Getting Started](./getting-started.md) guide you'll already have enabled GitOps: 7 | 8 | * add a subdirectory to your repository with the same name as the value of the Terraform variable `env`, e.g. `dev`, `prd` 9 | add files and subdirectory following [the rsa-public-crl example](https://github.com/serverless-ca/terraform-aws-ca/blob/main/examples/rsa-public-crl/README.md) 10 | * change the value of Terraform variable `cert_info_files` to `["tls", "revoked", "revoked-root-ca"]` 11 | * apply Terraform 12 | * you can now revoke a certificate as described in [Revocation](revocation.md) 13 | 14 | ## Public CRL and CA certs 15 | 16 | See details in [Revocation](revocation.md) and [CA Cert Locations](locations.md). 17 | 18 | *Default setting: not enabled* 19 | 20 | ## CA key algorithms 21 | 22 | The following algorithms can be selected via Terraform [variables](https://github.com/serverless-ca/terraform-aws-ca/blob/main/variables.tf): 23 | `RSA_2048, RSA_3072, RSA_4096, ECC_NIST_P256, ECC_NIST_P384, ECC_NIST_P521` 24 | 25 | *Default setting: `ECC_NIST_P384` (Root CA), `ECC_NIST_P256` (Issuing CA)* 26 | 27 | ## CloudWatch log subscription filters 28 | 29 | CloudWatch log subscription filters can be used to send log events to a central S3 log bucket, from which they can be forwarded to a SIEM. 30 | 31 | *Default setting: not enabled* 32 | 33 | ## CRL publication frequency 34 | To avoid certificate validation errors, it's essential that the CRL publication interval is less than, or equal to, the CRL lifetime. This ensures there is always a valid CRL at any time. 35 | 36 | * CRLs are published once every 24 hours by default 37 | * CRLs can be published manually by executing the CA Step Function 38 | * Issuing CA and Root CA CRLs are publised at the same time 39 | * Publication frequency can be changed using the Terraform variable `schedule_expression` 40 | * Generally there should be no need to change this value from the default 41 | 42 | *Default setting: once per day at 08:15 a.m.* 43 | 44 | ## CRL lifetime 45 | To avoid certificate validation errors, it's essential that the CRL lifetime is equal to, or greater than, the publication interval. This ensures there is always a valid CRL at any time. 46 | 47 | * Issuing CA CRL lifetime can be adjusted using the Terraform variables `issuing_crl_days` and `issuing_crl_seconds` 48 | * `issuing_crl_days` should normally be identical to the interval configured in `schedule_expression` 49 | * `issuing_crl_seconds` is an additional time period used as an overlap in case of clock skew 50 | * Similarly, Root CA CRL lifetime can be adjusted using the Terraform variables `root_crl_days` and `root_crl_seconds` 51 | * Generally there should be no need to change these values from their defaults 52 | 53 | *Default setting (Issuing and Root CA CRLs): 1 day with a 600 second overlap period* 54 | 55 | ## Maximum certificate lifetime 56 | By default the maximum lifetime of an end entity certificate is set to 365 days. 57 | 58 | * If a certificate request is submitted with a greater lifetime, the issued certificate lifetime will be reduced to the maximum 59 | * This value can be configured using the `max_cert_lifetime` Terraform variable 60 | 61 | *Default setting: 365 days* 62 | -------------------------------------------------------------------------------- /docs/revocation.md: -------------------------------------------------------------------------------- 1 | # Certificate Revocation 2 | 3 | ![Certificate Revocation List](assets/images/crl.png?raw=true) 4 | 5 | * Certificates can be revoked using a Certificate Revocation List (CRL) 6 | * Online Certificate Status Protocol (OCSP) is not supported 7 | 8 | ## CRL publication 9 | CRLs are published to `external` S3 bucket, not directly accessible from public Internet 10 | 11 | To publish publicly, set `public_crl` to `true` and provide `hosted_zone_id` and `hosted_zone_name` in [Terraform variables](https://github.com/serverless-ca/terraform-aws-ca/blob/main/variables.tf). 12 | 13 | Applying Terraform will result in: 14 | 15 | * CRLs published to a public URL via CloudFront 16 | * CA certificates published to a public URL via CloudFront 17 | * CRL Distribution Point (CDP) extension added to certificates 18 | * Authority Information Access (AIA) extension added to certificates 19 | 20 | ## CRL file formats 21 | CRLs are published as: 22 | * DER file format with `.crl` extension 23 | * PEM file format with `.crl.pem` extension 24 | 25 | ## CRL location 26 | CRL locations are detailed in [CA Cert Locations](locations.md) 27 | 28 | ## Enable certificate revocation 29 | CRLs are always published, however the ability to revoke a certificate needs to be enabled. If you followed the [Getting Started](getting-started.md) guide, you'll already have done this: 30 | 31 | * add a subdirectory to your repository with the same name as the value of the Terraform variable `env`, e.g. `dev`, `prd` 32 | add files and subdirectory following the [rsa-public-crl example](https://github.com/serverless-ca/terraform-aws-ca/blob/main/examples/rsa-public-crl/README.md) 33 | * change the value of Terraform variable `cert_info_files` to `["tls", "revoked", "revoked-root-ca"]` 34 | * apply Terraform 35 | 36 | ## Revoking a certificate 37 | 38 | * identify serial number by inspecting the certificate, or looking up in DynamoDB table 39 | * add details of certificate to be revoked to the `revoked.json` list for relevant environment, e.g. `certs/dev/revoked.json` 40 | ```json 41 | [ 42 | { 43 | "common_name" : "test-tls-cert.example.com", 44 | "serial_number": "400591262296335747457420220526770623344507066427" 45 | } 46 | ] 47 | ``` 48 | * run the pipeline 49 | * wait up to 24 hours, or manually execute the CA Step Function 50 | * the revoked certificate can be viewed within the CRL: 51 | 52 | ![Revoked certificate](assets/images/crl-revoked.png?raw=true) 53 | 54 | ## CRL publication frequency 55 | If required, the default CRL publication frequency of once per day can be changed, as described in [Configuration Options](./options.md#crl-publication-frequency) 56 | 57 | ## CRL lifetime 58 | If required, the default CRL lifetime of 1 day plus a 600 seconds overlap period can be changed, as described in [Configuration Options](./options.md#crl-lifetime) -------------------------------------------------------------------------------- /docs/security.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | It's very important to implement your certificate authority (CA) in a secure way: 4 | 5 | * each CA should be in a dedicated AWS account 6 | * carefully select CA options for this module: 7 | * use ECDSA algorithms rather than RSA (default) 8 | * don't make CRL public unless needed (default) 9 | * review other options from a security perspective 10 | * very carefully control AWS IAM principals and permissions 11 | * restrict permissions allowing invocation of all Lambda functions 12 | * store user / device private keys in hardware if possible 13 | * always verify person or entity requesting certificate is authorised 14 | * limit access to CA source code repository and CI/CD pipeline 15 | * ensure updates to CA source code repository are reviewed, especially new Certificate Signing Requests (CSRs) 16 | * monitor CloudTrail for suspicious events, e.g. unauthorised signing using a CA KMS asymmetric key 17 | * export CloudTrail and CloudWatch logs to a central SIEM 18 | * create rules to alert on potential attacks, e.g. CloudTrail event showing CA KMS signing not correlated to Lambda function log in CloudWatch 19 | * update the CA regularly and ensure no vulnerable dependencies 20 | * run regular security scans on CA AWS accounts or link to a CSPM 21 | * consider an independent security review of the CA infrastructure and applications using certificates issued by the CA 22 | 23 | 24 | The security of any CA is dependent on the protection of CA private keys. AWS KMS is used to generate and store the asymmetric key pair for each CA, with no export of the private key allowed. The hardware security modules (HSMs) used by the AWS KMS service are [FIPS 140-2 level 3 certified](https://aws.amazon.com/about-aws/whats-new/2023/05/aws-kms-hsm-fips-security-level-3/) in all AWS commercial regions except China, which uses OSCCA certified HSMs. 25 | 26 | Secure operation of AWS services such as KMS rely on AWS upholding its side of the [AWS Shared Responsibility Model](https://aws.amazon.com/compliance/shared-responsibility-model/). 27 | 28 | The above information is provided to assist you in assuring the security of your CA. However, the authors accept no responsibility for your CA being implemented and operated in a secure manner, in according with the [License](https://github.com/serverless-ca/terraform-aws-ca/blob/main/LICENSE.md). 29 | -------------------------------------------------------------------------------- /examples/default/README.md: -------------------------------------------------------------------------------- 1 | # ECDSA Certificate Authority with private CRL (default) 2 | 3 | ## Local Development - Terraform 4 | from within this subdirectory: 5 | ``` 6 | terraform init -backend-config=bucket={YOUR_TERRAFORM_STATE_BUCKET} -backend-config=key=terraform-aws-ca -backend-config=region={YOUR_TERRAFORM_STATE_REGION} 7 | terraform plan 8 | terraform apply 9 | ``` 10 | 11 | ## Local Development - Python 12 | see [Lambda Submodule README](../../modules/terraform-aws-ca-lambda/README.MD) 13 | -------------------------------------------------------------------------------- /examples/default/backend.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | backend "s3" { 3 | # bucket = "YOUR S3 BUCKET NAME" 4 | # key = "terraform-aws-ca/terraform.tfstate" 5 | # region = "YOUR S3 BUCKET REGION" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/default/ca.tf: -------------------------------------------------------------------------------- 1 | module "certificate_authority" { 2 | source = "../../" 3 | # source = "serverless-ca/ca/aws" 4 | # version = "1.0.0" 5 | 6 | # cert_info_files = ["tls", "revoked", "revoked-root-ca"] 7 | # issuing_ca_info = local.issuing_ca_info 8 | # root_ca_info = local.root_ca_info 9 | 10 | providers = { 11 | aws = aws 12 | aws.us-east-1 = aws.us-east-1 # required even if CloudFront not used 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/default/certs/dev/csrs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/examples/default/certs/dev/csrs/.gitkeep -------------------------------------------------------------------------------- /examples/default/certs/dev/revoked-root-ca.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /examples/default/certs/dev/revoked.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /examples/default/certs/dev/tls.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /examples/default/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current" {} 2 | 3 | data "aws_caller_identity" "current" {} 4 | -------------------------------------------------------------------------------- /examples/default/locals.tf: -------------------------------------------------------------------------------- 1 | /* 2 | locals { 3 | issuing_ca_info = { 4 | country = "GB" 5 | locality = "London" 6 | lifetime = 3650 7 | organization = "My Company" 8 | organizationalUnit = "Security Operations" 9 | commonName = "My Company Issuing CA" 10 | pathLengthConstraint = 0 11 | } 12 | 13 | root_ca_info = { 14 | country = "GB" 15 | locality = "London" 16 | lifetime = 7300 17 | organization = "My Company" 18 | organizationalUnit = "Security Operations" 19 | commonName = "My Company Root CA" 20 | pathLengthConstraint = 1 21 | } 22 | } 23 | */ 24 | -------------------------------------------------------------------------------- /examples/default/provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "eu-west-2" 3 | } 4 | 5 | provider "aws" { 6 | alias = "us-east-1" 7 | region = "us-east-1" 8 | sts_region = "eu-west-2" 9 | } 10 | -------------------------------------------------------------------------------- /examples/default/variables.tf: -------------------------------------------------------------------------------- 1 | variable "hosted_zone_domain" { 2 | description = "Hosted Zone Domain" 3 | default = "ca.example.com" # Change to subdomain hosted zone for CRL publication within same AWS account 4 | } -------------------------------------------------------------------------------- /examples/rsa-public-crl/README.md: -------------------------------------------------------------------------------- 1 | # RSA Certificate Authority with Public CRL 2 | 3 | ## Local Development - Terraform 4 | from within this subdirectory: 5 | ``` 6 | terraform init -backend-config=bucket={YOUR_TERRAFORM_STATE_BUCKET} -backend-config=key=terraform-aws-ca -backend-config=region={YOUR_TERRAFORM_STATE_REGION} 7 | terraform plan 8 | terraform apply 9 | ``` 10 | 11 | ## Local Development - Python 12 | see [Lambda Submodule README](../../modules/terraform-aws-ca-lambda/README.MD) 13 | 14 | -------------------------------------------------------------------------------- /examples/rsa-public-crl/backend.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | backend "s3" { 3 | # bucket = "YOUR S3 BUCKET NAME" 4 | # key = "terraform-aws-ca/terraform.tfstate" 5 | # region = "YOUR S3 BUCKET REGION" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/rsa-public-crl/ca.tf: -------------------------------------------------------------------------------- 1 | module "certificate_authority" { 2 | source = "../../" 3 | # source = "serverless-ca/ca/aws" 4 | # version = "1.0.0" 5 | 6 | bucket_prefix = "my-company" 7 | env = "prod" 8 | hosted_zone_domain = var.hosted_zone_domain 9 | hosted_zone_id = data.aws_route53_zone.public.zone_id 10 | issuing_ca_info = local.issuing_ca_info 11 | root_ca_info = local.root_ca_info 12 | issuing_ca_key_spec = "RSA_4096" 13 | root_ca_key_spec = "RSA_4096" 14 | public_crl = true 15 | cert_info_files = ["tls", "revoked", "revoked-root-ca"] 16 | kms_key_alias = "custom-kms-encryption-key" 17 | xray_enabled = false 18 | 19 | custom_sns_topic_display_name = "My Company CA Notifications Production" 20 | 21 | providers = { 22 | aws = aws 23 | aws.us-east-1 = aws.us-east-1 # certificates for CloudFront must be in this region 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/rsa-public-crl/certs/prod/csrs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/examples/rsa-public-crl/certs/prod/csrs/.gitkeep -------------------------------------------------------------------------------- /examples/rsa-public-crl/certs/prod/revoked-root-ca.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /examples/rsa-public-crl/certs/prod/revoked.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /examples/rsa-public-crl/certs/prod/tls.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /examples/rsa-public-crl/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current" {} 2 | 3 | data "aws_caller_identity" "current" {} 4 | 5 | data "aws_route53_zone" "public" { 6 | name = "${var.hosted_zone_domain}." 7 | private_zone = false 8 | } 9 | -------------------------------------------------------------------------------- /examples/rsa-public-crl/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | issuing_ca_info = { 3 | country = "GB" 4 | locality = "London" 5 | lifetime = 3650 6 | organization = "My Company" 7 | organizationalUnit = "Security Operations" 8 | commonName = "My Company Issuing CA" 9 | pathLengthConstraint = 0 10 | } 11 | 12 | root_ca_info = { 13 | country = "GB" 14 | locality = "London" 15 | lifetime = 7300 16 | organization = "My Company" 17 | organizationalUnit = "Security Operations" 18 | commonName = "My Company Root CA" 19 | pathLengthConstraint = 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/rsa-public-crl/provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "eu-west-2" 3 | } 4 | 5 | provider "aws" { 6 | alias = "us-east-1" 7 | region = "us-east-1" 8 | sts_region = "eu-west-2" 9 | } 10 | -------------------------------------------------------------------------------- /examples/rsa-public-crl/variables.tf: -------------------------------------------------------------------------------- 1 | variable "hosted_zone_domain" { 2 | description = "Hosted Zone Domain" 3 | default = "ca.example.com" # Change to subdomain hosted zone for CRL publication within same AWS account 4 | } 5 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Serverless CA on AWS 2 | site_description: Terraform module for serverless Certificate Authority on AWS 3 | site_author: Paul Schwarzenberger 4 | site_url: https://serverlessca.com 5 | repo_name: serverless-ca/terraform-aws-ca 6 | repo_url: https://github.com/serverless-ca/terraform-aws-ca 7 | edit_uri: edit/main/docs/ 8 | use_directory_urls: true 9 | theme: 10 | name: material 11 | logo: assets/images/cert-logo.svg 12 | palette: 13 | - media: "(prefers-color-scheme: dark)" 14 | scheme: slate 15 | toggle: 16 | icon: material/weather-night 17 | name: Switch to system preference 18 | - scheme: default 19 | toggle: 20 | icon: material/weather-sunny 21 | name: Switch to dark mode 22 | features: 23 | - navigation.instant 24 | - navigation.instant.progress 25 | - navigation.tracking 26 | - pymdownx.snippets 27 | - navigation.footer 28 | - toc.integrate 29 | - navigation.top 30 | - search.suggest 31 | - search.highlight 32 | - content.tabs.link 33 | - content.code.annotation 34 | - content.code.copy 35 | language: en 36 | 37 | extra: 38 | status: 39 | new: Recently added 40 | deprecated: Deprecated 41 | social: 42 | - icon: fontawesome/brands/github 43 | link: https://github.com/serverless-ca/terraform-aws-ca 44 | - icon: fontawesome/brands/twitter 45 | link: https://twitter.com/paulschwarzen 46 | 47 | nav: 48 | - Home: index.md 49 | - Getting started: getting-started.md 50 | - Automation: automation.md 51 | - CA Certificate locations: locations.md 52 | - Client certificates: client-certificates.md 53 | - Configuration options: options.md 54 | - Development: development.md 55 | - FAQs: faq.md 56 | - Revocation: revocation.md 57 | - Security: security.md 58 | - How-to guides: 59 | - API Gateway: how-to-guides/api.md 60 | - Application load balancer: how-to-guides/alb.md 61 | - IAM Roles Anywhere: how-to-guides/iam.md 62 | - Terraform reference: reference.md 63 | 64 | plugins: 65 | - social 66 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-acm/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_acm_certificate" "certificate" { 2 | domain_name = var.domain_name 3 | validation_method = var.validation_method 4 | subject_alternative_names = [var.domain_name] 5 | 6 | options { 7 | certificate_transparency_logging_preference = var.certificate_transparency 8 | } 9 | 10 | lifecycle { 11 | create_before_destroy = true 12 | } 13 | } 14 | 15 | resource "aws_route53_record" "validation" { 16 | for_each = { 17 | for dvo in aws_acm_certificate.certificate.domain_validation_options : dvo.domain_name => { 18 | name = dvo.resource_record_name 19 | record = dvo.resource_record_value 20 | type = dvo.resource_record_type 21 | } 22 | } 23 | 24 | allow_overwrite = true 25 | name = each.value.name 26 | records = [each.value.record] 27 | ttl = 86400 28 | type = each.value.type 29 | zone_id = var.zone_id 30 | } 31 | 32 | resource "aws_acm_certificate_validation" "validation" { 33 | certificate_arn = aws_acm_certificate.certificate.arn 34 | validation_record_fqdns = [for record in aws_route53_record.validation : record.fqdn] 35 | } 36 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-acm/outputs.tf: -------------------------------------------------------------------------------- 1 | output "certificate_arn" { 2 | value = aws_acm_certificate_validation.validation.certificate_arn 3 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-acm/providers.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-acm/variables.tf: -------------------------------------------------------------------------------- 1 | variable "zone_id" { 2 | description = "Hosted zone ID for base domain" 3 | } 4 | 5 | variable "domain_name" { 6 | description = "Fully qualified domain name without a period at the end" 7 | } 8 | 9 | variable "certificate_transparency" { 10 | description = "Whether certificate details should be added to public certificate transparency log" 11 | default = "ENABLED" 12 | } 13 | 14 | variable "validation_method" { 15 | description = "Certificate validation method" 16 | default = "DNS" 17 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-cloudfront/README.md: -------------------------------------------------------------------------------- 1 | # Terraform submodule for AWS CloudFront 2 | * Deploys CloudFront distribution for public access to CRLs and CA certificates 3 | * Submodule of terraform-aws-ca -------------------------------------------------------------------------------- /modules/terraform-aws-ca-cloudfront/cache.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudfront_cache_policy" "policy" { 2 | 3 | name = "${local.domain_ref}-cache-policy" 4 | comment = "Cache policy for ${local.domain_name}" 5 | default_ttl = 10 6 | max_ttl = 10 7 | min_ttl = 1 8 | 9 | parameters_in_cache_key_and_forwarded_to_origin { 10 | cookies_config { 11 | cookie_behavior = "none" 12 | } 13 | headers_config { 14 | header_behavior = "none" 15 | } 16 | query_strings_config { 17 | query_string_behavior = "none" 18 | } 19 | 20 | enable_accept_encoding_brotli = true 21 | enable_accept_encoding_gzip = true 22 | } 23 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-cloudfront/headers.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudfront_response_headers_policy" "policy" { 2 | 3 | name = "${local.project_slug}-response-headers-policy-${var.environment}" 4 | 5 | cors_config { 6 | access_control_allow_credentials = false 7 | 8 | access_control_allow_methods { 9 | items = [ 10 | "GET", 11 | "HEAD" 12 | ] 13 | } 14 | 15 | access_control_allow_headers { 16 | items = ["*"] 17 | } 18 | 19 | access_control_allow_origins { 20 | items = [ 21 | "ajax.googleapis.com", 22 | "fonts.gstatic.com", 23 | "maps.gstatic.com", 24 | "maps.google.com", 25 | "maps.googleapis.com" 26 | ] 27 | } 28 | 29 | access_control_expose_headers { 30 | items = ["*"] 31 | } 32 | 33 | access_control_max_age_sec = 86400 34 | origin_override = true 35 | } 36 | 37 | security_headers_config { 38 | 39 | content_type_options { 40 | override = true 41 | } 42 | 43 | frame_options { 44 | override = true 45 | frame_option = "SAMEORIGIN" 46 | } 47 | 48 | referrer_policy { 49 | override = true 50 | referrer_policy = "strict-origin-when-cross-origin" 51 | } 52 | 53 | strict_transport_security { 54 | override = true 55 | access_control_max_age_sec = 31536000 56 | include_subdomains = true 57 | preload = true 58 | } 59 | 60 | xss_protection { 61 | override = true 62 | mode_block = true 63 | protection = true 64 | } 65 | } 66 | 67 | custom_headers_config { 68 | items { 69 | header = "permissions-policy" 70 | override = true 71 | value = "accelerometer=(none), ambient-light-sensor=(none), autoplay=(none), camera=(none), encrypted-media=(none), fullscreen=(none), geolocation=(none), gyroscope=(none), magnetometer=(none), microphone=(none), midi=(none), payment=(none), picture-in-picture=(none), speaker=(none), usb=(none), vibrate=(none), vr=(none)" 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-cloudfront/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | domain_name = var.domain_prefix == "" ? var.base_domain : "${var.domain_prefix}.${var.base_domain}" 3 | domain_ref = replace(local.domain_name, ".", "-") 4 | project_slug = lower(replace(var.project, " ", "-")) 5 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-cloudfront/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudfront_origin_access_identity" "website" { 2 | 3 | comment = "Allows access to ${var.bucket_name} S3 bucket" 4 | } 5 | 6 | resource "aws_cloudfront_distribution" "website" { 7 | 8 | origin { 9 | domain_name = var.bucket_regional_domain_name 10 | origin_id = "${local.domain_ref}-${var.environment}" 11 | s3_origin_config { 12 | origin_access_identity = aws_cloudfront_origin_access_identity.website.cloudfront_access_identity_path 13 | } 14 | } 15 | 16 | enabled = true 17 | is_ipv6_enabled = true 18 | comment = "Static web site for ${local.domain_name}" 19 | web_acl_id = var.web_acl_id 20 | default_root_object = "index.html" 21 | 22 | aliases = [var.domain_prefix == "" ? var.base_domain : "${var.domain_prefix}.${var.base_domain}"] 23 | 24 | default_cache_behavior { 25 | allowed_methods = ["GET", "HEAD"] 26 | cached_methods = ["GET", "HEAD"] 27 | target_origin_id = "${local.domain_ref}-${var.environment}" 28 | origin_request_policy_id = aws_cloudfront_origin_request_policy.policy.id 29 | response_headers_policy_id = aws_cloudfront_response_headers_policy.policy.id 30 | cache_policy_id = aws_cloudfront_cache_policy.policy.id 31 | viewer_protocol_policy = "allow-all" 32 | } 33 | 34 | price_class = "PriceClass_200" 35 | 36 | restrictions { 37 | geo_restriction { 38 | restriction_type = "blacklist" 39 | locations = ["CN", "IR", "KP", "RU"] 40 | } 41 | } 42 | 43 | viewer_certificate { 44 | acm_certificate_arn = var.certificate_arn 45 | minimum_protocol_version = "TLSv1.2_2021" 46 | ssl_support_method = "sni-only" 47 | } 48 | 49 | custom_error_response { 50 | error_caching_min_ttl = 10 51 | error_code = 403 52 | response_code = 404 53 | response_page_path = var.error_page 54 | } 55 | 56 | custom_error_response { 57 | error_caching_min_ttl = 10 58 | error_code = 404 59 | response_code = 404 60 | response_page_path = var.error_page 61 | } 62 | } 63 | 64 | resource "aws_route53_record" "cloudfront" { 65 | count = var.domain_prefix == "" ? 0 : 1 66 | zone_id = var.zone_id 67 | name = "${var.domain_prefix}.${var.base_domain}" 68 | type = "CNAME" 69 | ttl = "300" 70 | records = [aws_cloudfront_distribution.website.domain_name] 71 | } 72 | 73 | resource "aws_route53_record" "cloudfront-apex" { 74 | count = var.domain_prefix == "" ? 1 : 0 75 | zone_id = var.zone_id 76 | name = var.base_domain 77 | type = "A" 78 | 79 | alias { 80 | name = aws_cloudfront_distribution.website.domain_name 81 | zone_id = aws_cloudfront_distribution.website.hosted_zone_id 82 | evaluate_target_health = false 83 | } 84 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-cloudfront/outputs.tf: -------------------------------------------------------------------------------- 1 | output "cloudfront_origin_access_identity_arn" { 2 | value = aws_cloudfront_origin_access_identity.website.iam_arn 3 | } 4 | 5 | output "cloudfront_domain_name" { 6 | value = aws_cloudfront_distribution.website.domain_name 7 | } 8 | 9 | output "cloudfront_hosted_zone_id" { 10 | value = aws_cloudfront_distribution.website.hosted_zone_id 11 | } 12 | 13 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-cloudfront/request.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudfront_origin_request_policy" "policy" { 2 | 3 | name = "${local.project_slug}-origin-request-policy-${var.environment}" 4 | 5 | cookies_config { 6 | cookie_behavior = "whitelist" 7 | cookies { 8 | items = ["catAccCookies"] 9 | } 10 | } 11 | headers_config { 12 | header_behavior = "whitelist" 13 | headers { 14 | items = [ 15 | "sec-fetch-mode", 16 | "sec-ch-ua", 17 | "sec-ch-ua-mobile", 18 | "sec-fetch-site", 19 | "Accept", 20 | "sec-ch-ua-platform", 21 | "Referer", 22 | "User-Agent", 23 | "Accept-Language", 24 | "sec-fetch-dest" 25 | ] 26 | } 27 | } 28 | query_strings_config { 29 | query_string_behavior = "all" 30 | } 31 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-cloudfront/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project" { 2 | description = "Project name" 3 | } 4 | 5 | variable "bucket_name" { 6 | description = "Name of origin S3 bucket" 7 | } 8 | 9 | variable "bucket_regional_domain_name" { 10 | description = "Regional domain name of origin S3 bucket" 11 | } 12 | 13 | variable "certificate_arn" { 14 | description = "Certificate ARN" 15 | } 16 | 17 | variable "base_domain" { 18 | description = "Base domain, e.g. example.com" 19 | } 20 | 21 | variable "domain_prefix" { 22 | description = "Domain prefix, e.g. www" 23 | default = "" 24 | } 25 | 26 | variable "environment" { 27 | description = "Abbreviation for environment, e.g. dev, prd" 28 | } 29 | 30 | variable "zone_id" { 31 | description = "Route53 hosted zone ID" 32 | } 33 | 34 | variable "error_page" { 35 | description = "Path to custom 404 error page" 36 | default = "/page-not-found.html" 37 | } 38 | 39 | variable "web_acl_id" { 40 | description = "WAF attachment for the public CRL Cloudfront distribution, expects the WAF ARN" 41 | default = null 42 | type = string 43 | } 44 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-dynamodb/README.md: -------------------------------------------------------------------------------- 1 | # Terraform submodule for DynamoDB 2 | * Deploys DynamoDB table for CA used for details of issued certificates 3 | * Submodule of terraform-aws-ca -------------------------------------------------------------------------------- /modules/terraform-aws-ca-dynamodb/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | table_name = "${replace(title(replace(var.project, "-", " ")), " ", "")}CA${title(var.env)}" 3 | } 4 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-dynamodb/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_dynamodb_table" "ca" { 2 | name = local.table_name 3 | billing_mode = "PAY_PER_REQUEST" 4 | hash_key = "CommonName" 5 | range_key = "SerialNumber" 6 | 7 | attribute { 8 | name = "CommonName" 9 | type = "S" 10 | } 11 | 12 | attribute { 13 | name = "SerialNumber" 14 | type = "S" 15 | } 16 | 17 | point_in_time_recovery { 18 | enabled = true 19 | } 20 | 21 | server_side_encryption { 22 | enabled = true 23 | kms_key_arn = var.kms_arn_resource 24 | } 25 | 26 | tags = { 27 | Name = "${var.project}-ca-${var.env}" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-dynamodb/outputs.tf: -------------------------------------------------------------------------------- 1 | output "ddb_table_arn" { 2 | value = aws_dynamodb_table.ca.arn 3 | } 4 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-dynamodb/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project" {} 2 | 3 | variable "kms_arn_resource" { 4 | description = "KMS key ARN used for general resource encryption, different from key used for CA key protection" 5 | } 6 | 7 | variable "env" { 8 | description = "Environment name, e.g. dev" 9 | } 10 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/README.md: -------------------------------------------------------------------------------- 1 | # Terraform submodule for IAM role and policy 2 | * Deploys IAM role used by Lambda functions or Step Functions to access other resources in AWS 3 | * Submodule of terraform-aws-ca -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | account_id = data.aws_caller_identity.current.account_id 3 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "lambda" { 2 | name = "${var.project}-${var.function_name}-${var.env}" 3 | assume_role_policy = templatefile("${path.module}/templates/${var.assume_role_policy}_role.json.tpl", { 4 | aws_principals = var.aws_principals 5 | project = var.project 6 | }) 7 | } 8 | 9 | resource "aws_iam_role_policy" "lambda" { 10 | name = "${var.project}-${var.function_name}-${var.env}" 11 | role = aws_iam_role.lambda.id 12 | policy = templatefile("${path.module}/templates/${var.policy}_policy.json.tpl", { 13 | kms_arn_issuing_ca = var.kms_arn_issuing_ca, 14 | kms_arn_root_ca = var.kms_arn_root_ca, 15 | kms_arn_tls_keygen = var.kms_arn_tls_keygen, 16 | kms_arn_resource = var.kms_arn_resource, 17 | ddb_table_arn = var.ddb_table_arn, 18 | external_s3_bucket_arn = var.external_s3_bucket_arn, 19 | internal_s3_bucket_arn = var.internal_s3_bucket_arn 20 | sns_topic_arn = var.sns_topic_arn 21 | }) 22 | } 23 | 24 | resource "aws_iam_role_policy_attachment" "xray" { 25 | count = var.xray_enabled ? 1 : 0 26 | role = aws_iam_role.lambda.name 27 | policy_arn = var.xray_daemon_policy_arn 28 | } 29 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/outputs.tf: -------------------------------------------------------------------------------- 1 | output "lambda_role_arn" { 2 | value = aws_iam_role.lambda.arn 3 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/templates/db_reader_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "KMSforEncryptedResources", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "kms:Decrypt" 9 | ], 10 | "Resource": "${kms_arn_resource}" 11 | }, 12 | { 13 | "Sid": "DynamoDBReader", 14 | "Effect": "Allow", 15 | "Action": [ 16 | "dynamodb:DescribeTable", 17 | "dynamodb:GetItem", 18 | "dynamodb:Query", 19 | "dynamodb:Scan" 20 | ], 21 | "Resource": [ 22 | "${ddb_table_arn}" 23 | ] 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/templates/db_reader_role.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Action": "sts:AssumeRole", 6 | "Principal": { 7 | "AWS": ${jsonencode(aws_principals)} 8 | }, 9 | "Effect": "Allow", 10 | "Sid": "AssumeRoleFromAWSPrincipals" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/templates/issuing_ca_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "WriteToCloudWatchLogs", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "logs:CreateLogGroup", 9 | "logs:CreateLogStream", 10 | "logs:PutLogEvents" 11 | ], 12 | "Resource": [ 13 | "arn:aws:logs:*:*:*" 14 | ] 15 | }, 16 | { 17 | "Sid": "PutCloudWatchMetrics", 18 | "Effect": "Allow", 19 | "Action": "cloudwatch:PutMetricData", 20 | "Resource": "*" 21 | }, 22 | { 23 | "Sid": "KMSlist", 24 | "Effect": "Allow", 25 | "Action": [ 26 | "kms:ListAliases" 27 | ], 28 | "Resource": "*" 29 | }, 30 | { 31 | "Sid": "KMSforCAPrivateKeys", 32 | "Effect": "Allow", 33 | "Action": [ 34 | "kms:GetPublicKey", 35 | "kms:DescribeKey", 36 | "kms:Sign", 37 | "kms:Verify" 38 | ], 39 | "Resource": [ 40 | "${kms_arn_root_ca}", 41 | "${kms_arn_issuing_ca}" 42 | ] 43 | }, 44 | { 45 | "Sid": "KMSforEncryptedResources", 46 | "Effect": "Allow", 47 | "Action": [ 48 | "kms:Decrypt", 49 | "kms:Encrypt", 50 | "kms:GenerateDataKey" 51 | ], 52 | "Resource": "${kms_arn_resource}" 53 | }, 54 | { 55 | "Sid": "DynamoDB", 56 | "Effect": "Allow", 57 | "Action": [ 58 | "dynamodb:DescribeTable", 59 | "dynamodb:GetItem", 60 | "dynamodb:PutItem", 61 | "dynamodb:Query", 62 | "dynamodb:Scan", 63 | "dynamodb:UpdateItem" 64 | ], 65 | "Resource": [ 66 | "${ddb_table_arn}" 67 | ] 68 | }, 69 | { 70 | "Sid": "S3BucketLocation", 71 | "Effect": "Allow", 72 | "Action": [ 73 | "s3:GetBucketLocation", 74 | "s3:ListBucket" 75 | ], 76 | "Resource": [ 77 | "${external_s3_bucket_arn}", 78 | "${internal_s3_bucket_arn}" 79 | ] 80 | }, 81 | { 82 | "Sid": "S3BucketUpload", 83 | "Effect": "Allow", 84 | "Action": [ 85 | "s3:DeleteObject", 86 | "s3:GetObject", 87 | "s3:PutObject" 88 | ], 89 | "Resource": [ 90 | "${external_s3_bucket_arn}/*" 91 | ] 92 | }, 93 | { 94 | "Sid": "S3BucketDownload", 95 | "Effect": "Allow", 96 | "Action": [ 97 | "s3:GetObject" 98 | ], 99 | "Resource": [ 100 | "${internal_s3_bucket_arn}/*" 101 | ] 102 | } 103 | ] 104 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/templates/issuing_crl_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "WriteToCloudWatchLogs", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "logs:CreateLogGroup", 9 | "logs:CreateLogStream", 10 | "logs:PutLogEvents" 11 | ], 12 | "Resource": [ 13 | "arn:aws:logs:*:*:*" 14 | ] 15 | }, 16 | { 17 | "Sid": "PutCloudWatchMetrics", 18 | "Effect": "Allow", 19 | "Action": "cloudwatch:PutMetricData", 20 | "Resource": "*" 21 | }, 22 | { 23 | "Sid": "KMSlist", 24 | "Effect": "Allow", 25 | "Action": [ 26 | "kms:ListAliases" 27 | ], 28 | "Resource": "*" 29 | }, 30 | { 31 | "Sid": "KMSforCAPrivateKey", 32 | "Effect": "Allow", 33 | "Action": [ 34 | "kms:GetPublicKey", 35 | "kms:DescribeKey", 36 | "kms:Sign", 37 | "kms:Verify" 38 | ], 39 | "Resource": "${kms_arn_issuing_ca}" 40 | }, 41 | { 42 | "Sid": "KMSforEncryptedResources", 43 | "Effect": "Allow", 44 | "Action": [ 45 | "kms:Decrypt", 46 | "kms:Encrypt", 47 | "kms:GenerateDataKey" 48 | ], 49 | "Resource": "${kms_arn_resource}" 50 | }, 51 | { 52 | "Sid": "DynamoDB", 53 | "Effect": "Allow", 54 | "Action": [ 55 | "dynamodb:DescribeTable", 56 | "dynamodb:GetItem", 57 | "dynamodb:PutItem", 58 | "dynamodb:Query", 59 | "dynamodb:Scan", 60 | "dynamodb:UpdateItem" 61 | ], 62 | "Resource": [ 63 | "${ddb_table_arn}" 64 | ] 65 | }, 66 | { 67 | "Sid": "S3BucketLocation", 68 | "Effect": "Allow", 69 | "Action": [ 70 | "s3:GetBucketLocation", 71 | "s3:ListBucket" 72 | ], 73 | "Resource": [ 74 | "${external_s3_bucket_arn}", 75 | "${internal_s3_bucket_arn}" 76 | ] 77 | }, 78 | { 79 | "Sid": "S3BucketUpload", 80 | "Effect": "Allow", 81 | "Action": [ 82 | "s3:DeleteObject", 83 | "s3:GetObject", 84 | "s3:PutObject" 85 | ], 86 | "Resource": [ 87 | "${external_s3_bucket_arn}/*" 88 | ] 89 | }, 90 | { 91 | "Sid": "S3BucketDownload", 92 | "Effect": "Allow", 93 | "Action": [ 94 | "s3:GetObject" 95 | ], 96 | "Resource": [ 97 | "${internal_s3_bucket_arn}/*" 98 | ] 99 | } 100 | ] 101 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/templates/lambda_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "WriteToCloudWatchLogs", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "logs:CreateLogGroup", 9 | "logs:CreateLogStream", 10 | "logs:PutLogEvents" 11 | ], 12 | "Resource": [ 13 | "arn:aws:logs:*:*:*" 14 | ] 15 | }, 16 | { 17 | "Sid": "PutCloudWatchMetrics", 18 | "Effect": "Allow", 19 | "Action": "cloudwatch:PutMetricData", 20 | "Resource": "*" 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/templates/lambda_role.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Action": "sts:AssumeRole", 6 | "Principal": { 7 | "Service": "lambda.amazonaws.com" 8 | }, 9 | "Effect": "Allow", 10 | "Sid": "" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/templates/root_ca_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "WriteToCloudWatchLogs", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "logs:CreateLogGroup", 9 | "logs:CreateLogStream", 10 | "logs:PutLogEvents" 11 | ], 12 | "Resource": [ 13 | "arn:aws:logs:*:*:*" 14 | ] 15 | }, 16 | { 17 | "Sid": "PutCloudWatchMetrics", 18 | "Effect": "Allow", 19 | "Action": "cloudwatch:PutMetricData", 20 | "Resource": "*" 21 | }, 22 | { 23 | "Sid": "KMSlist", 24 | "Effect": "Allow", 25 | "Action": [ 26 | "kms:ListAliases" 27 | ], 28 | "Resource": "*" 29 | }, 30 | { 31 | "Sid": "KMSforCAPrivateKey", 32 | "Effect": "Allow", 33 | "Action": [ 34 | "kms:GetPublicKey", 35 | "kms:DescribeKey", 36 | "kms:Sign", 37 | "kms:Verify" 38 | ], 39 | "Resource": "${kms_arn_root_ca}" 40 | }, 41 | { 42 | "Sid": "KMSforEncryptedResources", 43 | "Effect": "Allow", 44 | "Action": [ 45 | "kms:Decrypt", 46 | "kms:Encrypt", 47 | "kms:GenerateDataKey" 48 | ], 49 | "Resource": "${kms_arn_resource}" 50 | }, 51 | { 52 | "Sid": "DynamoDB", 53 | "Effect": "Allow", 54 | "Action": [ 55 | "dynamodb:DescribeTable", 56 | "dynamodb:GetItem", 57 | "dynamodb:PutItem", 58 | "dynamodb:Query", 59 | "dynamodb:Scan", 60 | "dynamodb:UpdateItem" 61 | ], 62 | "Resource": [ 63 | "${ddb_table_arn}" 64 | ] 65 | }, 66 | { 67 | "Sid": "S3BucketLocation", 68 | "Effect": "Allow", 69 | "Action": [ 70 | "s3:GetBucketLocation", 71 | "s3:ListBucket" 72 | ], 73 | "Resource": [ 74 | "${external_s3_bucket_arn}", 75 | "${internal_s3_bucket_arn}" 76 | ] 77 | }, 78 | { 79 | "Sid": "S3BucketUpload", 80 | "Effect": "Allow", 81 | "Action": [ 82 | "s3:DeleteObject", 83 | "s3:GetObject", 84 | "s3:PutObject" 85 | ], 86 | "Resource": [ 87 | "${external_s3_bucket_arn}/*" 88 | ] 89 | }, 90 | { 91 | "Sid": "S3BucketDownload", 92 | "Effect": "Allow", 93 | "Action": [ 94 | "s3:GetObject" 95 | ], 96 | "Resource": [ 97 | "${internal_s3_bucket_arn}/*" 98 | ] 99 | } 100 | ] 101 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/templates/root_crl_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "WriteToCloudWatchLogs", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "logs:CreateLogGroup", 9 | "logs:CreateLogStream", 10 | "logs:PutLogEvents" 11 | ], 12 | "Resource": [ 13 | "arn:aws:logs:*:*:*" 14 | ] 15 | }, 16 | { 17 | "Sid": "PutCloudWatchMetrics", 18 | "Effect": "Allow", 19 | "Action": "cloudwatch:PutMetricData", 20 | "Resource": "*" 21 | }, 22 | { 23 | "Sid": "KMSlist", 24 | "Effect": "Allow", 25 | "Action": [ 26 | "kms:ListAliases" 27 | ], 28 | "Resource": "*" 29 | }, 30 | { 31 | "Sid": "KMSforCAPrivateKey", 32 | "Effect": "Allow", 33 | "Action": [ 34 | "kms:GetPublicKey", 35 | "kms:DescribeKey", 36 | "kms:Sign", 37 | "kms:Verify" 38 | ], 39 | "Resource": "${kms_arn_root_ca}" 40 | }, 41 | { 42 | "Sid": "KMSforEncryptedResources", 43 | "Effect": "Allow", 44 | "Action": [ 45 | "kms:Decrypt", 46 | "kms:Encrypt", 47 | "kms:GenerateDataKey" 48 | ], 49 | "Resource": "${kms_arn_resource}" 50 | }, 51 | { 52 | "Sid": "DynamoDB", 53 | "Effect": "Allow", 54 | "Action": [ 55 | "dynamodb:DescribeTable", 56 | "dynamodb:GetItem", 57 | "dynamodb:PutItem", 58 | "dynamodb:Query", 59 | "dynamodb:Scan", 60 | "dynamodb:UpdateItem" 61 | ], 62 | "Resource": [ 63 | "${ddb_table_arn}" 64 | ] 65 | }, 66 | { 67 | "Sid": "S3BucketLocation", 68 | "Effect": "Allow", 69 | "Action": [ 70 | "s3:GetBucketLocation", 71 | "s3:ListBucket" 72 | ], 73 | "Resource": [ 74 | "${external_s3_bucket_arn}", 75 | "${internal_s3_bucket_arn}" 76 | ] 77 | }, 78 | { 79 | "Sid": "S3BucketUpload", 80 | "Effect": "Allow", 81 | "Action": [ 82 | "s3:DeleteObject", 83 | "s3:GetObject", 84 | "s3:PutObject" 85 | ], 86 | "Resource": [ 87 | "${external_s3_bucket_arn}/*" 88 | ] 89 | }, 90 | { 91 | "Sid": "S3BucketDownload", 92 | "Effect": "Allow", 93 | "Action": [ 94 | "s3:GetObject" 95 | ], 96 | "Resource": [ 97 | "${internal_s3_bucket_arn}/*" 98 | ] 99 | } 100 | ] 101 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/templates/scheduler_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "StepFunction", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "states:StartExecution" 9 | ], 10 | "Resource": "*" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/templates/scheduler_role.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "Service": "scheduler.amazonaws.com" 8 | }, 9 | "Action": "sts:AssumeRole" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/templates/state_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "CloudWatchLogs", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "logs:CreateLogDelivery", 9 | "logs:CreateLogGroup", 10 | "logs:CreateLogStream", 11 | "logs:DeleteLogDelivery", 12 | "logs:DescribeLogGroups", 13 | "logs:DescribeResourcePolicies", 14 | "logs:GetLogDelivery", 15 | "logs:ListLogDeliveries", 16 | "logs:ListTagsLogGroup", 17 | "logs:PutLogEvents", 18 | "logs:PutResourcePolicy", 19 | "logs:PutRetentionPolicy", 20 | "logs:PutSubscriptionFilter", 21 | "logs:UpdateLogDelivery" 22 | ], 23 | "Resource": "*" 24 | }, 25 | { 26 | "Sid": "XRay", 27 | "Effect": "Allow", 28 | "Action": [ 29 | "xray:PutTraceSegments", 30 | "xray:PutTelemetryRecords", 31 | "xray:GetSamplingRules", 32 | "xray:GetSamplingTargets" 33 | ], 34 | "Resource": "*" 35 | }, 36 | { 37 | "Sid": "PutCloudWatchMetrics", 38 | "Effect": "Allow", 39 | "Action": "cloudwatch:PutMetricData", 40 | "Resource": "*" 41 | }, 42 | { 43 | "Sid": "Lambda", 44 | "Effect": "Allow", 45 | "Action": [ 46 | "lambda:InvokeFunction" 47 | ], 48 | "Resource": "*" 49 | }, 50 | { 51 | "Sid": "StepFunction", 52 | "Effect": "Allow", 53 | "Action": [ 54 | "states:StartExecution" 55 | ], 56 | "Resource": "*" 57 | }, 58 | { 59 | "Sid": "S3BucketLocation", 60 | "Effect": "Allow", 61 | "Action": [ 62 | "s3:GetBucketLocation", 63 | "s3:ListBucket" 64 | ], 65 | "Resource": [ 66 | "${internal_s3_bucket_arn}" 67 | ] 68 | }, 69 | { 70 | "Sid": "S3BucketDownload", 71 | "Effect": "Allow", 72 | "Action": [ 73 | "s3:GetObject" 74 | ], 75 | "Resource": [ 76 | "${internal_s3_bucket_arn}/*" 77 | ] 78 | }, 79 | { 80 | "Sid": "KMSforEncryptedResources", 81 | "Effect": "Allow", 82 | "Action": [ 83 | "kms:Decrypt", 84 | "kms:Encrypt", 85 | "kms:GenerateDataKey" 86 | ], 87 | "Resource": [ 88 | "${kms_arn_resource}" 89 | ] 90 | } 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/templates/state_role.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Action": "sts:AssumeRole", 6 | "Principal": { 7 | "Service": "states.amazonaws.com" 8 | }, 9 | "Effect": "Allow", 10 | "Sid": "" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/templates/tls_cert_policy.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "WriteToCloudWatchLogs", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "logs:CreateLogGroup", 9 | "logs:CreateLogStream", 10 | "logs:PutLogEvents" 11 | ], 12 | "Resource": [ 13 | "arn:aws:logs:*:*:*" 14 | ] 15 | }, 16 | { 17 | "Sid": "PutCloudWatchMetrics", 18 | "Effect": "Allow", 19 | "Action": "cloudwatch:PutMetricData", 20 | "Resource": "*" 21 | }, 22 | { 23 | "Sid": "KMSlist", 24 | "Effect": "Allow", 25 | "Action": [ 26 | "kms:ListAliases" 27 | ], 28 | "Resource": "*" 29 | }, 30 | { 31 | "Sid": "KMSforTLSKeyGen", 32 | "Effect": "Allow", 33 | "Action": [ 34 | "kms:Decrypt", 35 | "kms:Encrypt", 36 | "kms:GenerateDataKey", 37 | "kms:GenerateDataKeyPair", 38 | "kms:GenerateDataKeyPairWithoutPlaintext" 39 | ], 40 | "Resource": "${kms_arn_tls_keygen}" 41 | }, 42 | { 43 | "Sid": "KMSforCAPrivateKey", 44 | "Effect": "Allow", 45 | "Action": [ 46 | "kms:GetPublicKey", 47 | "kms:DescribeKey", 48 | "kms:Sign", 49 | "kms:Verify" 50 | ], 51 | "Resource": "${kms_arn_issuing_ca}" 52 | }, 53 | { 54 | "Sid": "KMSforEncryptedResources", 55 | "Effect": "Allow", 56 | "Action": [ 57 | "kms:Decrypt", 58 | "kms:Encrypt", 59 | "kms:GenerateDataKey" 60 | ], 61 | "Resource": "${kms_arn_resource}" 62 | }, 63 | { 64 | "Sid": "DynamoDB", 65 | "Effect": "Allow", 66 | "Action": [ 67 | "dynamodb:DescribeTable", 68 | "dynamodb:GetItem", 69 | "dynamodb:PutItem", 70 | "dynamodb:Query", 71 | "dynamodb:Scan", 72 | "dynamodb:UpdateItem" 73 | ], 74 | "Resource": [ 75 | "${ddb_table_arn}" 76 | ] 77 | }, 78 | { 79 | "Sid": "S3BucketLocation", 80 | "Effect": "Allow", 81 | "Action": [ 82 | "s3:GetBucketLocation", 83 | "s3:ListBucket" 84 | ], 85 | "Resource": [ 86 | "${external_s3_bucket_arn}", 87 | "${internal_s3_bucket_arn}" 88 | ] 89 | }, 90 | { 91 | "Sid": "S3BucketUpload", 92 | "Effect": "Allow", 93 | "Action": [ 94 | "s3:DeleteObject", 95 | "s3:GetObject", 96 | "s3:PutObject" 97 | ], 98 | "Resource": [ 99 | "${external_s3_bucket_arn}/*" 100 | ] 101 | }, 102 | { 103 | "Sid": "S3BucketDownload", 104 | "Effect": "Allow", 105 | "Action": [ 106 | "s3:GetObject" 107 | ], 108 | "Resource": [ 109 | "${internal_s3_bucket_arn}/*" 110 | ] 111 | }, 112 | { 113 | "Sid": "SNSPublish", 114 | "Effect": "Allow", 115 | "Action": [ 116 | "sns:Publish" 117 | ], 118 | "Resource": [ 119 | "${sns_topic_arn}" 120 | ] 121 | } 122 | ] 123 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-iam/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project" {} 2 | 3 | variable "env" { 4 | description = "Environment name, e.g. dev" 5 | } 6 | 7 | variable "assume_role_policy" { 8 | description = "Assume role policy template to use" 9 | default = "lambda" 10 | } 11 | 12 | variable "policy" { 13 | description = "policy template to use" 14 | default = "lambda" 15 | } 16 | 17 | variable "function_name" { 18 | description = "short name of the Lambda function without project or environment" 19 | } 20 | 21 | variable "kms_arn_issuing_ca" { 22 | description = "KMS key ARN for Issuing CA private key" 23 | default = "" 24 | } 25 | 26 | variable "kms_arn_root_ca" { 27 | description = "KMS key ARN for Root CA private key" 28 | default = "" 29 | } 30 | 31 | variable "kms_arn_tls_keygen" { 32 | description = "KMS key ARN for TLS certificate key generation" 33 | default = "" 34 | } 35 | 36 | variable "kms_arn_resource" { 37 | description = "KMS key ARN for general resource encryption" 38 | } 39 | 40 | variable "ddb_table_arn" { 41 | description = "DynamoDB table ARN" 42 | } 43 | 44 | variable "external_s3_bucket_arn" { 45 | description = "ARN of external S3 bucket for CRL publication" 46 | default = "" 47 | } 48 | 49 | variable "internal_s3_bucket_arn" { 50 | description = "ARN of external S3 bucket for CRL publication" 51 | default = "" 52 | } 53 | 54 | variable "aws_principals" { 55 | description = "List of ARNs for AWS principals allowed to assume role" 56 | default = [] 57 | } 58 | 59 | variable "sns_topic_arn" { 60 | description = "SNS Topic ARN" 61 | default = "" 62 | } 63 | 64 | variable "xray_enabled" { 65 | description = "Whether to add permissions to allow to send trace data to X-Ray" 66 | default = false 67 | } 68 | 69 | variable "xray_daemon_policy_arn" { 70 | description = "AWSXRayDaemonWriteAccess managed policy ARN" 71 | default = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess" 72 | } 73 | 74 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-kms/README.md: -------------------------------------------------------------------------------- 1 | # Terraform submodule for S3 2 | * Deploys Amazon S3 buckets used by serverless CA 3 | * Submodule of terraform-aws-ca -------------------------------------------------------------------------------- /modules/terraform-aws-ca-kms/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | 3 | data "aws_region" "current" {} 4 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-kms/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | algorithm = contains(["RSA_2048", "RSA_3072", "RSA_4096"], var.customer_master_key_spec) ? "RSA" : "ECDSA" 3 | ca_type = contains(split("-", var.project), "root") ? "Root CA" : "Issuing CA" 4 | 5 | asymmetric_key_description = "${var.project}-${var.env} ${local.algorithm} ${local.ca_type} key pair" 6 | symmetric_key_description = "Encryption of ${var.project}-${var.env} resources" 7 | key_description = var.customer_master_key_spec == "SYMMETRIC_DEFAULT" ? local.symmetric_key_description : local.asymmetric_key_description 8 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-kms/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_kms_key" "encryption" { 2 | description = var.description == "" ? local.key_description : var.description 3 | deletion_window_in_days = 7 4 | enable_key_rotation = var.enable_key_rotation 5 | policy = templatefile("${path.module}/templates/${var.kms_policy}.json.tpl", { 6 | account_id = data.aws_caller_identity.current.account_id, 7 | region = data.aws_region.current.name 8 | }) 9 | customer_master_key_spec = var.customer_master_key_spec 10 | key_usage = var.key_usage 11 | } 12 | 13 | resource "aws_kms_alias" "encryption" { 14 | name = contains(var.prod_envs, var.env) ? "alias/${var.project}" : "alias/${var.project}-${var.env}" 15 | target_key_id = aws_kms_key.encryption.key_id 16 | } 17 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-kms/outputs.tf: -------------------------------------------------------------------------------- 1 | output "kms_arn" { 2 | value = aws_kms_key.encryption.arn 3 | } 4 | 5 | output "kms_alias_name" { 6 | value = aws_kms_alias.encryption.name 7 | } 8 | 9 | output "kms_alias_arn" { 10 | value = aws_kms_alias.encryption.arn 11 | } 12 | 13 | output "kms_alias_target_key_arn" { 14 | value = aws_kms_alias.encryption.target_key_arn 15 | } 16 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-kms/templates/ca.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Id": "key-ca", 4 | "Statement": [ 5 | { 6 | "Sid": "Enable IAM User Permissions", 7 | "Effect": "Allow", 8 | "Principal": { 9 | "AWS": "arn:aws:iam::${account_id}:root" 10 | }, 11 | "Action": "kms:*", 12 | "Resource": "*" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-kms/templates/default.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Id": "key-default", 4 | "Statement": [ 5 | { 6 | "Sid": "Enable IAM User Permissions", 7 | "Effect": "Allow", 8 | "Principal": { 9 | "AWS": "arn:aws:iam::${account_id}:root" 10 | }, 11 | "Action": "kms:*", 12 | "Resource": "*" 13 | }, 14 | { 15 | "Effect": "Allow", 16 | "Principal": { 17 | "Service": "logs.${region}.amazonaws.com" 18 | }, 19 | "Action": [ 20 | "kms:Encrypt*", 21 | "kms:Decrypt*", 22 | "kms:ReEncrypt*", 23 | "kms:GenerateDataKey*", 24 | "kms:Describe*" 25 | ], 26 | "Resource": "*", 27 | "Condition": { 28 | "ArnEquals": { 29 | "kms:EncryptionContext:aws:logs:arn": "arn:aws:logs:${region}:${account_id}:log-group:*" 30 | } 31 | } 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-kms/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project" {} 2 | 3 | variable "description" { 4 | description = "description of KSM key overrides default" 5 | default = "" 6 | } 7 | 8 | variable "enable_key_rotation" { 9 | description = "enable key rotation" 10 | default = false # must be false for asymmetric keys, and symmetric keys used for S3 encryption with long-lived content 11 | } 12 | 13 | variable "env" { 14 | description = "Environment name, e.g. dev" 15 | } 16 | 17 | variable "prod_envs" { 18 | description = "List of production environment names. Used to define resource name suffix" 19 | default = ["prd", "prod"] 20 | } 21 | 22 | variable "kms_policy" { 23 | description = "KMS policy to use" 24 | default = "default" 25 | } 26 | 27 | variable "customer_master_key_spec" { 28 | description = "symmetric default or asymmetric algorithm" 29 | default = "SYMMETRIC_DEFAULT" 30 | 31 | validation { 32 | condition = contains([ 33 | "SYMMETRIC_DEFAULT", 34 | "RSA_2048", 35 | "RSA_3072", 36 | "RSA_4096", 37 | "HMAC_256", 38 | "ECC_NIST_P256", 39 | "ECC_NIST_P384", 40 | "ECC_NIST_P521", 41 | "ECC_SECG_P256K1", 42 | ], var.customer_master_key_spec) 43 | error_message = "Invalid customer_master_key_spec" 44 | } 45 | } 46 | 47 | variable "key_usage" { 48 | description = "intended use of the key" 49 | default = "ENCRYPT_DECRYPT" 50 | 51 | validation { 52 | condition = contains([ 53 | "ENCRYPT_DECRYPT", 54 | "SIGN_VERIFY", 55 | "GENERATE_VERIFY_MAC", 56 | ], var.key_usage) 57 | error_message = "Invalid key_usage" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/build/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/modules/terraform-aws-ca-lambda/build/.gitkeep -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/cloudwatch.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_log_group" "function_log_group" { 2 | name = "/aws/lambda/${aws_lambda_function.lambda.function_name}" 3 | retention_in_days = var.retention_in_days 4 | lifecycle { 5 | prevent_destroy = false 6 | } 7 | } 8 | 9 | resource "aws_cloudwatch_log_subscription_filter" "logs_to_s3_sentinel" { 10 | count = var.subscription_filter_destination == "" || var.logging_account_id == "" ? 0 : 1 11 | 12 | name = "${lower(replace(aws_lambda_function.lambda.function_name, " ", "-"))}-logs-to-s3-sentinel-${var.env}" 13 | log_group_name = aws_cloudwatch_log_group.function_log_group.name 14 | filter_pattern = var.filter_pattern 15 | destination_arn = "arn:aws:logs:${data.aws_region.current.name}:${var.logging_account_id}:destination:${var.subscription_filter_destination}" 16 | } 17 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current" {} 2 | 3 | data "aws_caller_identity" "current" {} -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/lambda_code/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/modules/terraform-aws-ca-lambda/lambda_code/__init__.py -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/lambda_code/create_issuing_ca/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/modules/terraform-aws-ca-lambda/lambda_code/create_issuing_ca/__init__.py -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/lambda_code/create_issuing_ca/create_issuing_ca.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import os 4 | from utils.certs.kms import kms_get_kms_key_id, kms_describe_key 5 | from utils.certs.crypto import ( 6 | crypto_kms_ca_cert_signing_request, 7 | crypto_cert_info, 8 | crypto_create_ca_bundle, 9 | ) 10 | from utils.certs.ca import ca_kms_sign_ca_certificate_request, ca_name, ca_bundle_name 11 | from utils.certs.db import db_ca_cert_issued, db_list_certificates 12 | from utils.certs.s3 import s3_upload 13 | from cryptography.x509 import load_pem_x509_certificate, load_pem_x509_csr 14 | 15 | 16 | lifetime = 3650 17 | 18 | 19 | def lambda_handler(event, context): # pylint:disable=unused-argument,too-many-locals 20 | project = os.environ["PROJECT"] 21 | env_name = os.environ["ENVIRONMENT_NAME"] 22 | external_s3_bucket_name = os.environ["EXTERNAL_S3_BUCKET"] 23 | internal_s3_bucket_name = os.environ["INTERNAL_S3_BUCKET"] 24 | domain = os.environ.get("DOMAIN") 25 | 26 | public_crl = os.environ.get("PUBLIC_CRL") 27 | enable_public_crl = False 28 | if public_crl == "enabled": 29 | enable_public_crl = True 30 | 31 | issuing_ca_info = json.loads(os.environ["ISSUING_CA_INFO"]) 32 | 33 | root_ca_name = ca_name(project, env_name, "root") 34 | ca_slug = ca_name(project, env_name, "issuing") 35 | 36 | # check Root CA exists 37 | if not db_list_certificates(project, env_name, root_ca_name): 38 | print(f"CA {root_ca_name} not found") 39 | 40 | return 41 | 42 | # check if Issuing CA already exists 43 | if db_list_certificates(project, env_name, ca_slug): 44 | print(f"CA {ca_slug} already exists. To recreate, first delete item in DynamoDB") 45 | 46 | return 47 | 48 | # get issuing CA key details from KMS 49 | kms_key_id = kms_get_kms_key_id(ca_slug) 50 | cipher = kms_describe_key(kms_key_id)["KeySpec"] 51 | 52 | print(f"using {cipher} key pair in KMS for {ca_slug}") 53 | 54 | # get root CA key details from KMS 55 | root_ca_kms_key_id = kms_get_kms_key_id(root_ca_name) 56 | 57 | # create certificate signing request 58 | csr = load_pem_x509_csr( 59 | crypto_kms_ca_cert_signing_request(ca_slug, kms_key_id, kms_describe_key(kms_key_id)["SigningAlgorithms"][0]) 60 | ) 61 | 62 | # get Root CA cert in PEM format 63 | root_ca_cert_pem = base64.b64decode(db_list_certificates(project, env_name, root_ca_name)[0]["Certificate"]["B"]) 64 | 65 | # deserialize Root CA cert 66 | root_ca_cert = load_pem_x509_certificate(root_ca_cert_pem) 67 | 68 | # sign certificate 69 | pem_certificate = ca_kms_sign_ca_certificate_request( 70 | project, 71 | env_name, 72 | domain, 73 | csr, 74 | root_ca_cert, 75 | root_ca_kms_key_id, 76 | enable_public_crl, 77 | issuing_ca_info, 78 | kms_describe_key(root_ca_kms_key_id)["SigningAlgorithms"][0], 79 | ) 80 | base64_certificate = base64.b64encode(pem_certificate) 81 | 82 | # get details to upload to DynamoDB 83 | cert = load_pem_x509_certificate(pem_certificate) 84 | info = crypto_cert_info(cert, ca_slug) 85 | 86 | # create entry in DynamoDB 87 | db_ca_cert_issued(project, env_name, info, base64_certificate) 88 | 89 | # create CA bundle 90 | cert_bundle_pem = crypto_create_ca_bundle([root_ca_cert_pem, pem_certificate]) 91 | 92 | # upload certificate and CA bundle to S3 93 | s3_upload(external_s3_bucket_name, internal_s3_bucket_name, pem_certificate, f"{ca_slug}.crt") 94 | s3_upload( 95 | external_s3_bucket_name, internal_s3_bucket_name, cert_bundle_pem, f"{ca_bundle_name(project, env_name)}.pem" 96 | ) 97 | 98 | return 99 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/lambda_code/create_issuing_ca/requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography == 45.0.3 2 | validators == 0.35.0 3 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/lambda_code/create_root_ca/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/modules/terraform-aws-ca-lambda/lambda_code/create_root_ca/__init__.py -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/lambda_code/create_root_ca/create_root_ca.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import os 4 | from utils.certs.kms import kms_get_kms_key_id, kms_get_public_key, kms_describe_key 5 | from utils.certs.crypto import crypto_cert_info 6 | from utils.certs.ca import ca_name, ca_create_kms_root_ca 7 | from utils.certs.db import db_ca_cert_issued, db_list_certificates 8 | from utils.certs.s3 import s3_upload 9 | from cryptography.x509 import load_pem_x509_certificate 10 | from cryptography.hazmat.primitives.serialization import load_der_public_key 11 | 12 | 13 | lifetime = 7300 14 | 15 | 16 | def lambda_handler(event, context): # pylint:disable=unused-argument 17 | project = os.environ["PROJECT"] 18 | env_name = os.environ["ENVIRONMENT_NAME"] 19 | external_s3_bucket_name = os.environ["EXTERNAL_S3_BUCKET"] 20 | internal_s3_bucket_name = os.environ["INTERNAL_S3_BUCKET"] 21 | root_ca_info = json.loads(os.environ["ROOT_CA_INFO"]) 22 | 23 | ca_slug = ca_name(project, env_name, "root") 24 | 25 | # check if CA already exists 26 | if db_list_certificates(project, env_name, ca_slug): 27 | print(f"CA {ca_slug} already exists. To recreate, first delete item in DynamoDB") 28 | 29 | return 30 | 31 | # get key details from KMS 32 | kms_key_id = kms_get_kms_key_id(ca_slug) 33 | cipher = kms_describe_key(kms_key_id)["KeySpec"] 34 | public_key = load_der_public_key(kms_get_public_key(kms_key_id)) 35 | 36 | print(f"using {cipher} key pair in KMS for {ca_slug}") 37 | 38 | pem_certificate = ca_create_kms_root_ca( 39 | public_key, kms_key_id, root_ca_info, kms_describe_key(kms_key_id)["SigningAlgorithms"][0] 40 | ) 41 | base64_certificate = base64.b64encode(pem_certificate) 42 | 43 | # get details to upload to DynamoDB 44 | cert = load_pem_x509_certificate(pem_certificate) 45 | info = crypto_cert_info(cert, ca_slug) 46 | 47 | # create entry in DynamoDB 48 | db_ca_cert_issued(project, env_name, info, base64_certificate) 49 | 50 | # upload CRL to S3 51 | s3_upload(external_s3_bucket_name, internal_s3_bucket_name, pem_certificate, f"{ca_slug}.crt") 52 | 53 | return 54 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/lambda_code/create_root_ca/requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography == 45.0.3 2 | validators == 0.35.0 -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/lambda_code/issuing_ca_crl/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/modules/terraform-aws-ca-lambda/lambda_code/issuing_ca_crl/__init__.py -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/lambda_code/issuing_ca_crl/issuing_ca_crl.py: -------------------------------------------------------------------------------- 1 | from cryptography.hazmat.primitives import serialization 2 | from utils.certs.kms import kms_get_kms_key_id, kms_get_public_key, kms_describe_key 3 | from utils.certs.crypto import ( 4 | crypto_ca_key_info, 5 | crypto_revoked_certificate, 6 | crypto_convert_crl_to_pem, 7 | ) 8 | from utils.certs.ca import ca_name, ca_kms_publish_crl, ca_get_ca_info 9 | from utils.certs.db import ( 10 | db_list_certificates, 11 | db_update_crl_number, 12 | db_revocation_date, 13 | ) 14 | from utils.certs.s3 import s3_download, s3_upload 15 | from cryptography.hazmat.primitives.serialization import load_der_public_key 16 | import datetime 17 | import json 18 | import os 19 | 20 | 21 | def build_list_of_revoked_certs(project, env_name, external_s3_bucket_name, internal_s3_bucket_name): 22 | """Build list of revoked certificates for CRL""" 23 | # handle certificate revocation not enabled 24 | if not s3_download(external_s3_bucket_name, internal_s3_bucket_name, "revoked.json"): 25 | print("revoked.json not found") 26 | return [] 27 | 28 | # get list of certificates to be revoked 29 | revocation_file = s3_download(external_s3_bucket_name, internal_s3_bucket_name, "revoked.json")["Body"] 30 | 31 | revocation_details = json.load(revocation_file) 32 | 33 | revoked_certs = [] 34 | for revocation_detail in revocation_details: 35 | common_name = revocation_detail["common_name"] 36 | serial_number = revocation_detail["serial_number"] 37 | revocation_date = db_revocation_date(project, env_name, common_name, serial_number) 38 | revoked_cert = crypto_revoked_certificate(serial_number, revocation_date) 39 | revoked_certs.append(revoked_cert) 40 | 41 | print(f"CA {ca_name(project, env_name, 'issuing')} has {len(revoked_certs)} revoked certificates") 42 | return revoked_certs 43 | 44 | 45 | def lambda_handler(event, context): # pylint:disable=unused-argument,too-many-locals 46 | project = os.environ["PROJECT"] 47 | env_name = os.environ["ENVIRONMENT_NAME"] 48 | external_s3_bucket_name = os.environ["EXTERNAL_S3_BUCKET"] 49 | internal_s3_bucket_name = os.environ["INTERNAL_S3_BUCKET"] 50 | 51 | issuing_ca_info = json.loads(os.environ["ISSUING_CA_INFO"]) 52 | root_ca_info = json.loads(os.environ["ROOT_CA_INFO"]) 53 | 54 | ca_slug = ca_name(project, env_name, "issuing") 55 | 56 | # check CA exists 57 | if not db_list_certificates(project, env_name, ca_slug): 58 | print(f"CA {ca_slug} not found") 59 | 60 | return 61 | 62 | # get key details from KMS 63 | kms_key_id = kms_get_kms_key_id(ca_slug) 64 | public_key = load_der_public_key(kms_get_public_key(kms_key_id)) 65 | 66 | issuing_crl_days = int(os.environ["ISSUING_CRL_DAYS"]) 67 | issuing_crl_seconds = int(os.environ["ISSUING_CRL_SECONDS"]) 68 | 69 | # issue CRL valid for one day 10 minutes 70 | timedelta = datetime.timedelta(issuing_crl_days, issuing_crl_seconds, 0) 71 | ca_key_info = crypto_ca_key_info(public_key, kms_key_id, ca_slug) 72 | ca_info = ca_get_ca_info(issuing_ca_info, root_ca_info) 73 | 74 | crl = ca_kms_publish_crl( 75 | ca_info, 76 | ca_key_info, 77 | timedelta, 78 | build_list_of_revoked_certs(project, env_name, external_s3_bucket_name, internal_s3_bucket_name), 79 | db_update_crl_number( 80 | project, env_name, ca_slug, db_list_certificates(project, env_name, ca_slug)[0]["SerialNumber"]["S"] 81 | ), 82 | kms_describe_key(kms_key_id)["SigningAlgorithms"][0], 83 | ).public_bytes(encoding=serialization.Encoding.DER) 84 | 85 | # convert CRL to PEM format 86 | crl_pem = crypto_convert_crl_to_pem(crl) 87 | 88 | # upload CRL to S3 89 | s3_upload(external_s3_bucket_name, internal_s3_bucket_name, crl, f"{ca_slug}.crl") 90 | s3_upload(external_s3_bucket_name, internal_s3_bucket_name, crl_pem, f"{ca_slug}.crl.pem") 91 | 92 | return 93 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/lambda_code/issuing_ca_crl/requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography == 45.0.3 2 | validators == 0.35.0 -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/lambda_code/root_ca_crl/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/modules/terraform-aws-ca-lambda/lambda_code/root_ca_crl/__init__.py -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/lambda_code/root_ca_crl/requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography == 45.0.3 2 | validators == 0.35.0 3 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/lambda_code/root_ca_crl/root_ca_crl.py: -------------------------------------------------------------------------------- 1 | from cryptography.hazmat.primitives import serialization 2 | from utils.certs.kms import kms_get_kms_key_id, kms_get_public_key, kms_describe_key 3 | from utils.certs.crypto import ( 4 | crypto_ca_key_info, 5 | crypto_revoked_certificate, 6 | crypto_convert_crl_to_pem, 7 | ) 8 | from utils.certs.ca import ca_name, ca_kms_publish_crl 9 | from utils.certs.db import db_list_certificates, db_update_crl_number, db_revocation_date 10 | from utils.certs.s3 import s3_download, s3_upload 11 | from cryptography.hazmat.primitives.serialization import load_der_public_key 12 | import datetime 13 | import json 14 | import os 15 | 16 | 17 | def build_list_of_revoked_certs(project, env_name, external_s3_bucket_name, internal_s3_bucket_name): 18 | """Build list of revoked certificates for CRL""" 19 | # handle certificate revocation not enabled 20 | if not s3_download(external_s3_bucket_name, internal_s3_bucket_name, "revoked-root-ca.json"): 21 | print("revoked-root-ca.json not found") 22 | return [] 23 | 24 | # get list of certificates to be revoked 25 | revocation_file = s3_download(external_s3_bucket_name, internal_s3_bucket_name, "revoked-root-ca.json")["Body"] 26 | 27 | revocation_details = json.load(revocation_file) 28 | 29 | revoked_certs = [] 30 | for revocation_detail in revocation_details: 31 | common_name = revocation_detail["common_name"] 32 | serial_number = revocation_detail["serial_number"] 33 | revocation_date = db_revocation_date(project, env_name, common_name, serial_number) 34 | revoked_cert = crypto_revoked_certificate(serial_number, revocation_date) 35 | revoked_certs.append(revoked_cert) 36 | 37 | print(f"CA {ca_name(project, env_name, 'root')} has {len(revoked_certs)} revoked certificates") 38 | return revoked_certs 39 | 40 | 41 | def lambda_handler(event, context): # pylint:disable=unused-argument,too-many-locals 42 | project = os.environ["PROJECT"] 43 | env_name = os.environ["ENVIRONMENT_NAME"] 44 | external_s3_bucket_name = os.environ["EXTERNAL_S3_BUCKET"] 45 | internal_s3_bucket_name = os.environ["INTERNAL_S3_BUCKET"] 46 | root_ca_info = json.loads(os.environ["ROOT_CA_INFO"]) 47 | 48 | ca_slug = ca_name(project, env_name, "root") 49 | 50 | # check CA exists 51 | if not db_list_certificates(project, env_name, ca_slug): 52 | print(f"CA {ca_slug} not found") 53 | 54 | return 55 | 56 | # get key details from KMS 57 | kms_key_id = kms_get_kms_key_id(ca_slug) 58 | public_key = load_der_public_key(kms_get_public_key(kms_key_id)) 59 | 60 | root_crl_days = int(os.environ["ROOT_CRL_DAYS"]) 61 | root_crl_seconds = int(os.environ["ROOT_CRL_SECONDS"]) 62 | 63 | # issue CRL valid for one day 10 minutes 64 | timedelta = datetime.timedelta(root_crl_days, root_crl_seconds, 0) 65 | ca_key_info = crypto_ca_key_info(public_key, kms_key_id, ca_slug) 66 | 67 | crl = ca_kms_publish_crl( 68 | root_ca_info, 69 | ca_key_info, 70 | timedelta, 71 | build_list_of_revoked_certs(project, env_name, external_s3_bucket_name, internal_s3_bucket_name), 72 | db_update_crl_number( 73 | project, env_name, ca_slug, db_list_certificates(project, env_name, ca_slug)[0]["SerialNumber"]["S"] 74 | ), 75 | kms_describe_key(kms_key_id)["SigningAlgorithms"][0], 76 | ).public_bytes(encoding=serialization.Encoding.DER) 77 | 78 | # convert CRL to PEM format 79 | crl_pem = crypto_convert_crl_to_pem(crl) 80 | 81 | # upload CRL to S3 82 | s3_upload(external_s3_bucket_name, internal_s3_bucket_name, crl, f"{ca_slug}.crl") 83 | s3_upload(external_s3_bucket_name, internal_s3_bucket_name, crl_pem, f"{ca_slug}.crl.pem") 84 | 85 | return 86 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/lambda_code/tls_cert/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/modules/terraform-aws-ca-lambda/lambda_code/tls_cert/__init__.py -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/lambda_code/tls_cert/requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography == 45.0.3 2 | dataclasses-json == 0.6.7 3 | validators == 0.35.0 4 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | file_name = replace(var.function_name, "-", "_") 3 | public_crl = var.public_crl ? "enabled" : "disabled" 4 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/main.tf: -------------------------------------------------------------------------------- 1 | resource "null_resource" "install_python_dependencies" { 2 | triggers = { 3 | # detect changes to Lambda code 4 | lambda_code_sha256 = sha256(join("", [for f in sort(tolist(fileset("${path.module}/lambda_code/${local.file_name}", "**"))) : filesha256("${path.module}/lambda_code/${local.file_name}/${f}")])) 5 | 6 | # detect changes to files in utils directory 7 | utils_sha256 = sha256(join("", [for f in sort(tolist(fileset("${path.module}/utils", "**"))) : filesha256("${path.module}/utils/${f}")])) 8 | 9 | # static value (true) if present, variable value (timestamp()) when not present. (so the 'false' state isn't static and forces a build by change of state whenever so. a static false value doesn't force change of state.) 10 | build_already_present = fileexists("${path.module}/build/${local.file_name}/__init__.py") ? true : timestamp() 11 | } 12 | 13 | provisioner "local-exec" { 14 | interpreter = ["/bin/sh", "-c"] 15 | command = <<-EOT 16 | chmod +x ${path.module}/scripts/lambda-build/create-package.sh 17 | ${path.module}/scripts/lambda-build/create-package.sh 18 | EOT 19 | 20 | environment = { 21 | source_code_path = "${path.module}/lambda_code" 22 | function_name = local.file_name 23 | runtime = var.runtime 24 | path_cwd = path.module 25 | platform = var.platform 26 | } 27 | } 28 | } 29 | 30 | data "archive_file" "lambda_zip" { 31 | depends_on = [null_resource.install_python_dependencies] 32 | type = "zip" 33 | source_dir = "${path.module}/build/${local.file_name}" 34 | output_path = "${path.module}/build/${local.file_name}.zip" 35 | } 36 | 37 | resource "aws_lambda_function" "lambda" { 38 | filename = data.archive_file.lambda_zip.output_path 39 | source_code_hash = data.archive_file.lambda_zip.output_base64sha256 40 | function_name = "${var.project}-${var.function_name}-${var.env}" 41 | description = "${var.project} ${var.description}" 42 | role = var.lambda_role_arn 43 | handler = "${local.file_name}.lambda_handler" 44 | runtime = var.runtime 45 | memory_size = var.memory_size 46 | timeout = var.timeout 47 | publish = true 48 | 49 | environment { 50 | variables = { 51 | DOMAIN = var.domain 52 | ENVIRONMENT_NAME = var.env 53 | PROD_ENVIRONMENTS = jsonencode(var.prod_envs) 54 | EXTERNAL_S3_BUCKET = var.external_s3_bucket 55 | INTERNAL_S3_BUCKET = var.internal_s3_bucket 56 | ISSUING_CA_INFO = jsonencode({ for k, v in var.issuing_ca_info : k => v if v != null }) 57 | ISSUING_CRL_DAYS = tostring(var.issuing_crl_days) 58 | ISSUING_CRL_SECONDS = tostring(var.issuing_crl_seconds) 59 | MAX_CERT_LIFETIME = tostring(var.max_cert_lifetime) 60 | PROJECT = var.project 61 | PUBLIC_CRL = local.public_crl 62 | ROOT_CA_INFO = jsonencode({ for k, v in var.root_ca_info : k => v if v != null }) 63 | ROOT_CRL_DAYS = tostring(var.root_crl_days) 64 | ROOT_CRL_SECONDS = tostring(var.root_crl_seconds) 65 | SNS_TOPIC_ARN = var.sns_topic_arn 66 | } 67 | } 68 | 69 | tracing_config { 70 | mode = var.xray_enabled ? "Active" : "PassThrough" 71 | } 72 | 73 | depends_on = [data.archive_file.lambda_zip] 74 | } 75 | 76 | resource "aws_lambda_alias" "lambda" { 77 | name = "${var.project}-${var.function_name}-${var.env}" 78 | description = "Alias for ${var.project}-${var.function_name}-${var.env}" 79 | function_name = aws_lambda_function.lambda.function_name 80 | function_version = "$LATEST" 81 | } 82 | 83 | resource "aws_lambda_permission" "lambda_invoke" { 84 | for_each = toset(var.allowed_invocation_principals) 85 | 86 | action = "lambda:InvokeFunction" 87 | function_name = aws_lambda_function.lambda.function_name 88 | principal = each.value 89 | } 90 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | bandit == 1.8.3 2 | black == 25.1.0 3 | boto3 == 1.38.32 4 | cryptography == 45.0.3 5 | dataclasses-json == 0.6.7 6 | prospector == 1.15.2 7 | pytest == 8.3.4 8 | requests == 2.32.3 9 | structlog == 25.4.0 10 | validators == 0.35.0 11 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/scripts/lambda-build/create-package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Executing create_package.sh..." 4 | 5 | # Check Python version matches runtime 6 | python_minor_version="$(python3 --version)" 7 | python_version="${python_minor_version%.*}" 8 | prefix="Python " 9 | local_version="${python_version#$prefix}" 10 | runtime_prefix="python" 11 | lambda_version="${runtime#$runtime_prefix}" 12 | 13 | if [ "$lambda_version" != "$local_version" ]; then 14 | echo "Error: local Python version does not match Lambda Python runtime" 15 | echo "Local Python version: $local_version" 16 | echo "Lambda Python version: $lambda_version" 17 | exit 1 18 | fi 19 | 20 | dir_name=$function_name/ 21 | mkdir -p $path_cwd/build/$dir_name 22 | 23 | # Create and activate virtual environment... 24 | if ! python3 -m venv $path_cwd/build/env_$function_name; then 25 | echo "Error: Python virtual environment creation failed" 26 | exit 1 27 | else 28 | echo "Python virtual environment created" 29 | fi 30 | if ! . $path_cwd/build/env_$function_name/bin/activate; then 31 | echo "Error: Python virtual environment activation failed" 32 | exit 1 33 | else 34 | echo "Python virtual environment activated" 35 | fi 36 | 37 | # Installing python dependencies... 38 | FILE=$path_cwd/lambda_code/$function_name/requirements.txt 39 | 40 | 41 | if [ -f "$FILE" ]; then 42 | echo "Installing dependencies..." 43 | echo "From: requirements.txt file exists..." 44 | pip install --platform $platform --target $path_cwd/build/env_$function_name/lib/$runtime/site-packages --only-binary=:all: --implementation cp -r "$FILE" 45 | # pip install --platform $platform --target $path_cwd/build/env_$function_name/lib/$runtime/site-packages --only-binary=:all: --implementation cp --python $runtime -r "$FILE" 46 | # pip install -r "$FILE" 47 | 48 | else 49 | echo "Error: requirements.txt does not exist!" 50 | fi 51 | 52 | # Deactivate virtual environment... 53 | deactivate 54 | 55 | # Create deployment package... 56 | echo "Creating deployment package..." 57 | cp -r $path_cwd/build/env_$function_name/lib/$runtime/site-packages/. $path_cwd/build/$dir_name 58 | cp -r $path_cwd/lambda_code/$function_name/. $path_cwd/build/$dir_name 59 | cp -r $path_cwd/utils $path_cwd/build/$dir_name 60 | 61 | # Removing virtual environment folder... 62 | echo "Removing virtual environment folder..." 63 | rm -rf $path_cwd/build/env_$function_name 64 | 65 | echo "Finished script execution!" 66 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/unittests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/modules/terraform-aws-ca-lambda/unittests/__init__.py -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/unittests/test_tls_cert.py: -------------------------------------------------------------------------------- 1 | from lambda_code.tls_cert.tls_cert import ( 2 | create_csr_info, 3 | create_csr_subject, 4 | CaChainResponse, 5 | CertificateResponse, 6 | Request, 7 | ) 8 | 9 | 10 | def test_create_csr_info(): 11 | event = { 12 | "common_name": "blah.example.com", 13 | } 14 | 15 | csr_info = create_csr_info(event) 16 | assert csr_info.purposes == ["client_auth"] 17 | assert csr_info.sans == ["blah.example.com"] 18 | 19 | 20 | def test_create_csr_info_with_purpose_and_sans(): 21 | event = { 22 | "common_name": "blah.example.com", 23 | "purposes": ["server_auth"], 24 | "sans": ["a.b.d.com"], 25 | } 26 | 27 | csr_info = create_csr_info(event) 28 | assert csr_info.purposes == ["server_auth"] 29 | assert csr_info.sans == ["a.b.d.com"] 30 | 31 | 32 | def test_create_csr_info_with_purpose_no_sans(): 33 | event = { 34 | "common_name": "blah.example.com", 35 | "purposes": ["server_auth"], 36 | } 37 | 38 | csr_info = create_csr_info(event) 39 | assert csr_info.purposes == ["server_auth"] 40 | assert csr_info.sans == ["blah.example.com"] 41 | assert csr_info.subject.common_name == "blah.example.com" 42 | 43 | 44 | def test_create_csr_subject(): 45 | event = { 46 | "common_name": "blah.example.com", 47 | "locality": "London", # string, location 48 | "organization": "Acme Inc", # string, organization name 49 | "organizational_unit": "Animation", # string, organizational unit name 50 | "state": "England", 51 | "email_address": "test@example.com", 52 | "country": "GB", # string, country code 53 | } 54 | 55 | subject = create_csr_subject(event) 56 | 57 | expected = ( 58 | "ST=England,OU=Animation,O=Acme Inc,L=London,1.2.840.113549.1.9.1=test@example.com,C=GB,CN=blah.example.com" 59 | ) 60 | assert subject.x509_name().rfc4514_string() == expected 61 | 62 | 63 | def test_request_deserialise_basic(): 64 | event = {"common_name": "test.example.com"} 65 | 66 | request = Request.from_dict(event) 67 | 68 | assert request.common_name == "test.example.com" 69 | assert request.lifetime == 30 70 | 71 | 72 | def test_request_deserialise_full(): 73 | event = { 74 | "common_name": "test.example.com", 75 | "locality": "London", 76 | "organization": "Example", 77 | "organizational_unit": "IT", 78 | "country": "GB", 79 | "email_address": "blah@example.com", 80 | "state": "London", 81 | "lifetime": 365, 82 | "purposes": ["server_auth"], 83 | "sans": ["test2.example.com"], 84 | "ca_chain_only": True, 85 | "force_issue": True, 86 | "csr_file": "csr.pem", 87 | "cert_bundle": True, 88 | "base64_csr_data": "base64data", 89 | } 90 | 91 | request = Request(**event) 92 | 93 | assert request.common_name == "test.example.com" 94 | assert request.lifetime == 365 95 | assert request.purposes == ["server_auth"] 96 | assert request.csr_file == "csr.pem" 97 | 98 | 99 | def test_response_serialise_as_dict(): 100 | response = CertificateResponse( 101 | certificate_info={ 102 | "CommonName": "test.example.com", 103 | "SerialNumber": "123456", 104 | "Issued": "2021-01-01 00:00:00", 105 | "Expires": "2022-01-01 00:00:00", 106 | }, 107 | base64_certificate="base64data", 108 | subject="test.example.com", 109 | base64_issuing_ca_certificate="base64data", 110 | base64_root_ca_certificate="base64data", 111 | base64_ca_chain="base64data", 112 | ) 113 | 114 | serialised = response.to_dict() 115 | 116 | assert serialised == { 117 | "CertificateInfo": { 118 | "CommonName": "test.example.com", 119 | "SerialNumber": "123456", 120 | "Issued": "2021-01-01 00:00:00", 121 | "Expires": "2022-01-01 00:00:00", 122 | }, 123 | "Base64Certificate": "base64data", 124 | "Subject": "test.example.com", 125 | "Base64IssuingCaCertificate": "base64data", 126 | "Base64RootCaCertificate": "base64data", 127 | "Base64CaChain": "base64data", 128 | } 129 | 130 | 131 | def test_ca_chain_response_serialise_as_dict(): 132 | response = CaChainResponse( 133 | base64_issuing_ca_certificate="base64data", 134 | base64_root_ca_certificate="base64data", 135 | base64_ca_chain="base64data", 136 | ) 137 | 138 | serialised = response.to_dict() 139 | 140 | assert serialised == { 141 | "Base64IssuingCaCertificate": "base64data", 142 | "Base64RootCaCertificate": "base64data", 143 | "Base64CaChain": "base64data", 144 | } 145 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/unittests/test_types.py: -------------------------------------------------------------------------------- 1 | from cryptography.x509.oid import NameOID 2 | 3 | from utils.certs.types import CsrInfo, Subject 4 | 5 | 6 | # test defaults 7 | def test_csr_info_defaults(): 8 | csr_info = CsrInfo(Subject("blah.example.com")) 9 | 10 | assert csr_info.lifetime == 30 11 | assert csr_info.purposes == ["client_auth"] 12 | assert csr_info.sans == ["blah.example.com"] 13 | 14 | 15 | def test_csr_info_with_sans(): 16 | csr_info = CsrInfo(Subject("blah.example.com"), sans=["foo.example.com"]) 17 | 18 | assert csr_info.sans == ["foo.example.com"] 19 | 20 | 21 | # If an invalid SAN is supplied we ignore it 22 | def test_csr_info_with_invalid_sans(): 23 | csr_info = CsrInfo(Subject("blah.example.com"), sans=["foo.example com"]) 24 | 25 | assert not csr_info.sans 26 | 27 | 28 | # if invalid and valid SANs are specified we filter out the invalid ones 29 | def test_csr_info_with_invalid_and_valid_sans(): 30 | csr_info = CsrInfo(Subject("blah.example.com"), sans=["foo.example com", "bar.example.com"]) 31 | 32 | assert csr_info.sans == ["bar.example.com"] 33 | 34 | 35 | def test_csr_info_with_purpose(): 36 | csr_info = CsrInfo(Subject("blah.example.com"), purposes=["server_auth"]) 37 | 38 | assert csr_info.purposes == ["server_auth"] 39 | 40 | 41 | # If an invalid purpose is specified, we default to 'client_auth' 42 | def test_csr_info_with_invalid_purpose(): 43 | csr_info = CsrInfo(Subject("blah.example.com"), purposes=["code_sign"]) 44 | 45 | assert csr_info.purposes == ["client_auth"] 46 | 47 | 48 | # If valid and invalid purposes are specified, we filter out invalid/unsupported purposes 49 | # and keep the valid / supported ones 50 | def test_csr_info_with_invalid_and_valid_purposes(): 51 | csr_info = CsrInfo(Subject("blah.example.com"), purposes=["code_sign", "server_auth"]) 52 | 53 | assert csr_info.purposes == ["server_auth"] 54 | 55 | 56 | def test_csr_info_with_invalid_cn_and_no_san(): 57 | csr_info = CsrInfo(Subject("not a valid dns name")) 58 | 59 | assert not csr_info.sans 60 | 61 | 62 | def test_subject_x509_name_simple(): 63 | subject = Subject("blah.example.com") 64 | 65 | x509_name = subject.x509_name() 66 | 67 | assert x509_name.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value == "blah.example.com" 68 | assert x509_name.rfc4514_string() == "CN=blah.example.com" 69 | 70 | 71 | def test_subject_x509_name(): 72 | subject = Subject("blah.example.com") 73 | subject.country = "GB" 74 | subject.email_address = "test@example.com" 75 | subject.locality = "London" 76 | subject.organization = "Acme" 77 | subject.organizational_unit = "Gardening" 78 | subject.state = "England" 79 | 80 | x509_name = subject.x509_name() 81 | 82 | assert x509_name.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value == "blah.example.com" 83 | assert x509_name.get_attributes_for_oid(NameOID.COUNTRY_NAME)[0].value == "GB" 84 | assert x509_name.get_attributes_for_oid(NameOID.EMAIL_ADDRESS)[0].value == "test@example.com" 85 | assert x509_name.get_attributes_for_oid(NameOID.LOCALITY_NAME)[0].value == "London" 86 | assert x509_name.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)[0].value == "Acme" 87 | assert x509_name.get_attributes_for_oid(NameOID.ORGANIZATIONAL_UNIT_NAME)[0].value == "Gardening" 88 | assert x509_name.get_attributes_for_oid(NameOID.STATE_OR_PROVINCE_NAME)[0].value == "England" 89 | 90 | expected = "ST=England,OU=Gardening,O=Acme,L=London,1.2.840.113549.1.9.1=test@example.com,C=GB,CN=blah.example.com" 91 | assert x509_name.rfc4514_string() == expected 92 | 93 | 94 | def test_subject_from_x509_name(): 95 | subject = Subject("blah.example.com") 96 | subject.country = "GB" 97 | subject.email_address = "test@example.com" 98 | subject.locality = "London" 99 | subject.organization = "Acme" 100 | subject.organizational_unit = "Gardening" 101 | subject.state = "England" 102 | 103 | x509_name = subject.x509_name() 104 | 105 | new_subject = Subject.from_x509_subject(x509_name) 106 | 107 | assert subject.common_name == new_subject.common_name 108 | assert subject.country == new_subject.country 109 | assert subject.email_address == new_subject.email_address 110 | assert subject.locality == new_subject.locality 111 | assert subject.organization == new_subject.organization 112 | assert subject.organizational_unit == new_subject.organizational_unit 113 | assert subject.state == new_subject.state 114 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/unittests/test_validate_sans.py: -------------------------------------------------------------------------------- 1 | from utils.certs.types import filter_and_validate_sans 2 | 3 | 4 | def test_filter_and_validate_sans(): 5 | sans = ["example.com", "example.org", "example.net"] 6 | output = filter_and_validate_sans("example.com", sans) 7 | expected = ["example.com", "example.org", "example.net"] 8 | 9 | assert output == expected 10 | 11 | 12 | def test_filter_and_validate_sans_invalid_domain(): 13 | sans = ["example.com", "example.org", "net"] 14 | output = filter_and_validate_sans("example.com", sans) 15 | expected = ["example.com", "example.org"] 16 | 17 | assert output == expected 18 | 19 | 20 | def test_filter_and_validate_sans_wildcard_allowed(): 21 | sans = ["example.com", "example.org", "*.example.net"] 22 | output = filter_and_validate_sans("example.com", sans) 23 | expected = ["example.com", "example.org", "*.example.net"] 24 | 25 | assert output == expected 26 | 27 | 28 | def test_filter_and_validate_sans_wildcard_disallowed_if_base_domain_invalid(): 29 | sans = ["example.com", "example.org", "*.net"] 30 | output = filter_and_validate_sans("example.com", sans) 31 | expected = ["example.com", "example.org"] 32 | 33 | assert output == expected 34 | 35 | 36 | def test_filter_and_validate_sans_mixed_domains(): 37 | sans = ["example.com", "example.org", "*.example.net", "*.net", "Invalid DNS name"] 38 | output = filter_and_validate_sans("example.com", sans) 39 | expected = ["example.com", "example.org", "*.example.net"] 40 | 41 | assert output == expected 42 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/modules/terraform-aws-ca-lambda/utils/__init__.py -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/utils/aws/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/modules/terraform-aws-ca-lambda/utils/aws/__init__.py -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/utils/aws/kms.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | 4 | def get_kms_details(key_purpose): 5 | """ 6 | Get the KMS ARN based on the key purpose 7 | """ 8 | 9 | kms_client = boto3.client("kms") 10 | 11 | key_aliases = kms_client.list_aliases()["Aliases"] 12 | key_aliases = [k for k in key_aliases if key_purpose in k["AliasName"]] 13 | 14 | key_alias = key_aliases[0]["AliasName"] 15 | 16 | return key_alias, kms_client.describe_key(KeyId=key_alias)["KeyMetadata"]["Arn"] 17 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/utils/aws/lambdas.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | 4 | 5 | def get_lambda_name(lambda_purpose): 6 | """ 7 | Get the full name of the Lambda function based on its purpose 8 | """ 9 | 10 | lambda_client = boto3.client("lambda") 11 | 12 | lambdas = lambda_client.list_functions()["Functions"] 13 | lambdas = [la for la in lambdas if lambda_purpose in la["FunctionName"]] 14 | 15 | return lambdas[0]["FunctionName"] 16 | 17 | 18 | def invoke_lambda(function_name, json_data): 19 | """ 20 | Invoke TLS certificate Lambda function 21 | """ 22 | 23 | lambda_client = boto3.client("lambda") 24 | 25 | response = lambda_client.invoke( 26 | FunctionName=function_name, 27 | Payload=json.dumps(json_data), 28 | ) 29 | 30 | return json.loads(response["Payload"].read().decode("utf-8")) 31 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/utils/aws/sns.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | 4 | 5 | def publish_to_sns(json_data, subject, sns_topic_arn, keys_to_publish="All"): 6 | # Filter out unwanted keys 7 | if keys_to_publish == "All": 8 | keys_to_publish = json_data.keys() 9 | 10 | filtered_json_data = {key: json_data[key] for key in keys_to_publish if key in json_data} 11 | 12 | client = boto3.client("sns") 13 | 14 | response = client.publish( 15 | TargetArn=sns_topic_arn, 16 | Subject=subject, 17 | Message=json.dumps({"default": json.dumps(filtered_json_data)}), 18 | MessageStructure="json", 19 | ) 20 | 21 | return response 22 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/utils/certs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/modules/terraform-aws-ca-lambda/utils/certs/__init__.py -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/utils/certs/kms.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | 4 | def kms_generate_key_pair(key_id, key_pair_spec="ECC_NIST_P256"): 5 | client = boto3.client(service_name="kms") 6 | 7 | return client.generate_data_key_pair( 8 | KeyId=key_id, 9 | KeyPairSpec=key_pair_spec, 10 | ) 11 | 12 | 13 | def kms_get_kms_key_id(alias): 14 | """returns the KMS Key ARN for a specified alias""" 15 | client = boto3.client(service_name="kms") 16 | aliases = client.list_aliases(Limit=100)["Aliases"] 17 | 18 | return [a for a in aliases if a["AliasName"] == f"alias/{alias}"][0]["TargetKeyId"] 19 | 20 | 21 | def kms_describe_key(kms_key_id): 22 | """returns details of a KMS key""" 23 | client = boto3.client(service_name="kms") 24 | 25 | return client.describe_key(KeyId=kms_key_id)["KeyMetadata"] 26 | 27 | 28 | def kms_get_public_key(kms_key_id): 29 | """returns cipher and public key of an asymmetric KMS key""" 30 | client = boto3.client(service_name="kms") 31 | 32 | response = client.get_public_key(KeyId=kms_key_id) 33 | 34 | return response["PublicKey"] 35 | 36 | 37 | def kms_sign(kms_key_id, message, signing_algorithm="RSASSA_PSS_SHA_256"): 38 | """returns digital signature""" 39 | client = boto3.client(service_name="kms") 40 | response = client.sign(KeyId=kms_key_id, SigningAlgorithm=signing_algorithm, Message=message) 41 | 42 | return response["Signature"] 43 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/utils/certs/s3.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | 4 | 5 | def s3_download_file(bucket_name, key): 6 | client = boto3.client("s3") 7 | 8 | print(f"downloading {key} from s3 bucket {bucket_name}") 9 | 10 | try: 11 | return client.get_object( 12 | Bucket=bucket_name, 13 | Key=key, 14 | ) 15 | except client.exceptions.NoSuchKey: 16 | print(f"file {key} not found in s3 bucket {bucket_name}") # noqa 17 | 18 | return None 19 | 20 | 21 | def s3_download(external_s3_bucket_name, internal_s3_bucket_name, key, internal=True): 22 | if internal: 23 | return s3_download_file(internal_s3_bucket_name, key) 24 | 25 | return s3_download_file(external_s3_bucket_name, key) 26 | 27 | 28 | def s3_upload_file(file, bucket_name, key, content_type): 29 | client = boto3.client("s3") 30 | 31 | client.put_object(Body=file, Bucket=bucket_name, Key=key, ContentType=content_type) 32 | print(f"uploaded {key} to s3 bucket {bucket_name}") 33 | 34 | 35 | # pylint:disable=too-many-arguments,too-many-positional-arguments 36 | def s3_upload( 37 | external_s3_bucket_name, internal_s3_bucket_name, file, key, content_type="application/x-pkcs7-crl", external=True 38 | ): 39 | if external: 40 | return s3_upload_file(file, external_s3_bucket_name, key, content_type) 41 | 42 | return s3_upload_file(file, internal_s3_bucket_name, key, content_type) 43 | 44 | 45 | def convert_x509_subject_str_to_dict(input_str): 46 | # split string by commas 47 | pairs = input_str.split(",") 48 | 49 | # split each pair by '=' and construct dictionary 50 | json_dictionary = {} 51 | for pair in pairs: 52 | key, value = pair.split("=") 53 | json_dictionary[key] = value 54 | 55 | return json_dictionary 56 | 57 | 58 | def cert_issued_via_gitops(internal_s3_bucket_name, subject): 59 | # get list of GitOps certificates from internal S3 bucket 60 | tls_file = s3_download_file(internal_s3_bucket_name, "tls.json") 61 | 62 | return is_cert_gitops(tls_file, subject) 63 | 64 | 65 | def is_cert_gitops(tls_file, subject): 66 | subject_json = convert_x509_subject_str_to_dict(subject) 67 | 68 | cn = subject_json["CN"] 69 | o = subject_json.get("O") 70 | ou = subject_json.get("OU") 71 | 72 | if tls_file is None: 73 | gitops_certs = [] 74 | 75 | else: 76 | # convert to json dictionary 77 | gitops_certs = json.loads(tls_file["Body"].read()) 78 | 79 | for cert in gitops_certs: 80 | common_name = cert["common_name"] 81 | organization = cert.get("organization") 82 | organizational_unit = cert.get("organizational_unit") 83 | 84 | # check if certificate is included in tls.json 85 | cn_matches = False 86 | o_matches = False 87 | ou_matches = False 88 | 89 | if cn == common_name: 90 | cn_matches = True 91 | 92 | if o is None or organization is None or o == organization: 93 | o_matches = True 94 | 95 | if ou is None or organizational_unit is None or ou == organizational_unit: 96 | ou_matches = True 97 | 98 | if cn_matches and o_matches and ou_matches: 99 | return True 100 | 101 | return False 102 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-lambda/variables.tf: -------------------------------------------------------------------------------- 1 | variable "allowed_invocation_principals" { 2 | description = "List of principals allowed to invoke this lambda" 3 | default = [] 4 | } 5 | 6 | variable "enable_subscription_filters" { 7 | description = "Enable CloudWatch logs Subscription filters for CA Lambda functions" 8 | default = false 9 | } 10 | 11 | variable "description" { 12 | description = "description of Lambda function purpose" 13 | } 14 | 15 | variable "domain" { 16 | description = "Hosted zone domain, e.g. dev.ca.example.com" 17 | } 18 | 19 | variable "env" { 20 | description = "Environment name, e.g. dev" 21 | } 22 | 23 | variable "prod_envs" { 24 | description = "List of production environment names. Used to define resource name suffix" 25 | default = ["prd", "prod"] 26 | } 27 | 28 | variable "external_s3_bucket" { 29 | description = "External S3 Bucket Name" 30 | } 31 | 32 | variable "filter_pattern" { 33 | description = "Filter pattern for CloudWatch logs subscription filter" 34 | } 35 | 36 | variable "function_name" { 37 | description = "short name of the Lambda function without project or environment" 38 | } 39 | 40 | variable "internal_s3_bucket" { 41 | description = "Internal S3 Bucket Name" 42 | } 43 | 44 | variable "issuing_ca_info" { 45 | description = "Issuing CA information" 46 | default = {} 47 | } 48 | 49 | variable "issuing_crl_days" { 50 | description = "Number of days before Issuing CA CRL expires, in addition to seconds. Must be greater than or equal to Step Function interval" 51 | default = 1 52 | } 53 | 54 | variable "issuing_crl_seconds" { 55 | description = "Number of seconds before Issuing CA CRL expires, in addition to days. Used for overlap in case of clock skew" 56 | default = 600 57 | } 58 | 59 | variable "lambda_role_arn" { 60 | description = "Lambda role ARN" 61 | } 62 | 63 | variable "logging_account_id" { 64 | description = "AWS Account ID of central logging account for CloudWatch subscription filters" 65 | default = "" 66 | } 67 | 68 | variable "max_cert_lifetime" { 69 | description = "Maximum end entity certificate lifetime in days" 70 | default = 365 71 | } 72 | 73 | variable "memory_size" { 74 | description = "Memory allocation for scanning Lambda functions" 75 | default = 128 76 | } 77 | 78 | variable "platform" { 79 | description = "ManyLinux platform version, needed to avoid glibc errors due to incompatible versions" 80 | default = "manylinux2014_x86_64" 81 | } 82 | 83 | variable "project" { 84 | description = "abbreviation for the project, forms first part of resource names" 85 | default = "secure-email" 86 | } 87 | 88 | variable "public_crl" { 89 | description = "Whether to make the CRL and CA certificates publicly available" 90 | default = false 91 | } 92 | 93 | variable "retention_in_days" { 94 | description = "CloudWatch log group retention in days" 95 | default = 30 96 | } 97 | 98 | variable "root_ca_info" { 99 | description = "Root CA information" 100 | default = {} 101 | } 102 | 103 | variable "root_crl_days" { 104 | description = "Number of days before Root CA CRL expires, in addition to seconds. Must be greater than or equal to Step Function interval" 105 | default = 1 106 | } 107 | 108 | variable "root_crl_seconds" { 109 | description = "Number of seconds before Root CA CRL expires, in addition to days. Used for overlap in case of clock skew" 110 | default = 600 111 | } 112 | 113 | variable "runtime" { 114 | description = "Lambda language runtime" 115 | } 116 | 117 | variable "sns_topic_arn" { 118 | description = "SNS Topic ARN for Lambda function to publish to" 119 | } 120 | 121 | variable "subscription_filter_destination" { 122 | description = "CloudWatch log subscription filter destination, last section of ARN" 123 | default = "" 124 | } 125 | 126 | variable "timeout" { 127 | description = "Amount of time Lambda Function has to run in seconds" 128 | default = 180 129 | } 130 | 131 | variable "xray_enabled" { 132 | description = "Whether to enable active tracing with AWS X-Ray" 133 | default = true 134 | } 135 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-s3/README.md: -------------------------------------------------------------------------------- 1 | # Terraform submodule for AWS KMS 2 | * Deploys AWS KMS keys used by serverless CA for storage of private keys and key pair generation 3 | * Submodule of terraform-aws-ca -------------------------------------------------------------------------------- /modules/terraform-aws-ca-s3/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | 3 | data "aws_region" "current" {} -------------------------------------------------------------------------------- /modules/terraform-aws-ca-s3/locals.tf: -------------------------------------------------------------------------------- 1 | resource "random_string" "suffix" { 2 | count = var.bucket_prefix == "" ? 1 : 0 3 | 4 | length = 5 5 | special = false 6 | upper = false 7 | } 8 | 9 | locals { 10 | cloudfront_policy = length(var.app_aws_principals) == 0 ? "cloudfront" : "cloudfront-with-principals" 11 | no_cloudfront_policy = length(var.app_aws_principals) == 0 ? "secure-transport" : "secure-transport-with-principals" 12 | external_policy = var.public_crl ? local.cloudfront_policy : local.no_cloudfront_policy 13 | internal_policy = "secure-transport" 14 | bucket_policy = contains(split("-", var.purpose), "external") ? local.external_policy : local.internal_policy 15 | bucket_prefix = replace(lower(var.bucket_prefix), " ", "-") 16 | standard_bucket_name = local.bucket_prefix == "" ? "${var.purpose}-${var.environment}-${random_string.suffix[0].result}" : "${local.bucket_prefix}-${var.purpose}-${var.environment}" 17 | global_bucket_name = local.bucket_prefix == "" ? "${var.purpose}-${random_string.suffix[0].result}" : "${local.bucket_prefix}-${var.purpose}" 18 | bucket_name = var.global_bucket ? local.global_bucket_name : local.standard_bucket_name 19 | kms_key_alias_arn = "arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${var.kms_key_alias}" 20 | region = data.aws_region.current.name 21 | tags = merge(var.tags, { 22 | Terraform = "true" 23 | Name = local.bucket_name, 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-s3/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_bucket" "bucket" { 2 | #checkov:skip=CKV_AWS_144: region replication not required 3 | bucket = local.bucket_name 4 | force_destroy = var.force_destroy 5 | 6 | tags = local.tags 7 | } 8 | 9 | resource "aws_s3_bucket_versioning" "bucket" { 10 | bucket = aws_s3_bucket.bucket.id 11 | 12 | versioning_configuration { 13 | status = var.versioning 14 | } 15 | } 16 | 17 | resource "aws_s3_bucket_logging" "bucket" { 18 | count = var.access_logs ? 1 : 0 19 | 20 | bucket = aws_s3_bucket.bucket.id 21 | 22 | target_bucket = var.log_bucket 23 | target_prefix = "${local.bucket_name}/" 24 | } 25 | 26 | resource "aws_s3_bucket_server_side_encryption_configuration" "kms" { 27 | count = var.server_side_encryption && var.sse_algorithm == "aws:kms" ? 1 : 0 28 | 29 | bucket = aws_s3_bucket.bucket.id 30 | 31 | rule { 32 | apply_server_side_encryption_by_default { 33 | kms_master_key_id = var.default_aws_kms_key ? null : (var.kms_encryption_key_arn != "" ? var.kms_encryption_key_arn : local.kms_key_alias_arn) 34 | sse_algorithm = var.sse_algorithm 35 | } 36 | bucket_key_enabled = var.bucket_key_enabled 37 | } 38 | } 39 | 40 | resource "aws_s3_bucket_server_side_encryption_configuration" "s3" { 41 | count = var.server_side_encryption && var.sse_algorithm == "AES256" ? 1 : 0 42 | 43 | bucket = aws_s3_bucket.bucket.id 44 | 45 | rule { 46 | apply_server_side_encryption_by_default { 47 | sse_algorithm = var.sse_algorithm 48 | } 49 | } 50 | } 51 | 52 | resource "aws_s3_bucket_policy" "bucket" { 53 | bucket = aws_s3_bucket.bucket.id 54 | policy = templatefile("${path.module}/templates/${local.bucket_policy}.json.tpl", { 55 | bucket_name = aws_s3_bucket.bucket.id, 56 | account_id = data.aws_caller_identity.current.account_id, 57 | region = data.aws_region.current.name, 58 | oai_arn = var.oai_arn, 59 | app_aws_principals = var.app_aws_principals 60 | }) 61 | depends_on = [aws_s3_bucket_public_access_block.bucket] 62 | } 63 | 64 | resource "aws_s3_bucket_public_access_block" "bucket" { 65 | bucket = aws_s3_bucket.bucket.id 66 | block_public_acls = var.block_public_acls 67 | block_public_policy = var.block_public_policy 68 | ignore_public_acls = var.ignore_public_acls 69 | restrict_public_buckets = var.restrict_public_buckets 70 | } 71 | 72 | resource "aws_s3_bucket_ownership_controls" "bucket" { 73 | bucket = aws_s3_bucket.bucket.id 74 | 75 | rule { 76 | object_ownership = var.object_ownership 77 | } 78 | } 79 | 80 | resource "aws_s3_bucket_lifecycle_configuration" "lifecycle" { 81 | count = var.lifecycle_policy ? 1 : 0 82 | 83 | bucket = aws_s3_bucket.bucket.id 84 | 85 | rule { 86 | id = "StandardRotation" 87 | 88 | transition { 89 | days = var.ia_transition 90 | storage_class = "STANDARD_IA" 91 | } 92 | 93 | transition { 94 | days = var.glacier_transition 95 | storage_class = "GLACIER" 96 | } 97 | 98 | noncurrent_version_transition { 99 | noncurrent_days = var.noncurrent_transition 100 | storage_class = "GLACIER" 101 | } 102 | 103 | abort_incomplete_multipart_upload { 104 | days_after_initiation = var.abort_uploads 105 | } 106 | 107 | status = "Enabled" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-s3/outputs.tf: -------------------------------------------------------------------------------- 1 | output "s3_bucket_name" { 2 | value = aws_s3_bucket.bucket.id 3 | } 4 | output "s3_bucket_arn" { 5 | value = aws_s3_bucket.bucket.arn 6 | } 7 | 8 | output "s3_bucket_domain_name" { 9 | value = aws_s3_bucket.bucket.bucket_domain_name 10 | } 11 | 12 | output "s3_bucket_regional_domain_name" { 13 | value = aws_s3_bucket.bucket.bucket_regional_domain_name 14 | } 15 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-s3/templates/cloudfront-with-principals.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "Allow CloudFront", 6 | "Effect": "Allow", 7 | "Principal": { 8 | "AWS": "${oai_arn}" 9 | }, 10 | "Action": "s3:GetObject", 11 | "Resource": "arn:aws:s3:::${bucket_name}/*" 12 | }, 13 | { 14 | "Sid": "Object access from app environment", 15 | "Effect": "Allow", 16 | "Principal": { 17 | "AWS": ${jsonencode(app_aws_principals)} 18 | }, 19 | "Action": [ 20 | "s3:GetObject", 21 | "s3:GetObjectAcl", 22 | "s3:GetObjectAttributes", 23 | "s3:GetObjectRetention", 24 | "s3:GetObjectTagging", 25 | "s3:GetObjectVersion", 26 | "s3:GetObjectVersionTagging" 27 | ], 28 | "Resource": "arn:aws:s3:::${bucket_name}/*" 29 | }, 30 | { 31 | "Sid": "Bucket level access from app environment", 32 | "Effect": "Allow", 33 | "Principal": { 34 | "AWS": ${jsonencode(app_aws_principals)} 35 | }, 36 | "Action": [ 37 | "s3:GetEncryptionConfiguration", 38 | "s3:GetBucketWebsite", 39 | "s3:GetBucketLocation", 40 | "s3:GetBucketObjectLockConfiguration", 41 | "s3:ListBucket" 42 | ], 43 | "Resource": "arn:aws:s3:::${bucket_name}" 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-s3/templates/cloudfront.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "AllowCloudFront", 6 | "Effect": "Allow", 7 | "Principal": { 8 | "AWS": "${oai_arn}" 9 | }, 10 | "Action": "s3:GetObject", 11 | "Resource": "arn:aws:s3:::${bucket_name}/*" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-s3/templates/secure-transport-with-principals.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Id": "secure-transport-${bucket_name}", 3 | "Version": "2012-10-17", 4 | "Statement": [ 5 | { 6 | "Sid": "AllowSSLRequestsOnly", 7 | "Action": "s3:*", 8 | "Effect": "Deny", 9 | "Resource": [ 10 | "arn:aws:s3:::${bucket_name}", 11 | "arn:aws:s3:::${bucket_name}/*" 12 | ], 13 | "Condition": { 14 | "Bool": { 15 | "aws:SecureTransport": "false" 16 | } 17 | }, 18 | "Principal": "*" 19 | }, 20 | { 21 | "Sid": "Object access from app environment", 22 | "Effect": "Allow", 23 | "Principal": { 24 | "AWS": ${jsonencode(app_aws_principals)} 25 | }, 26 | "Action": [ 27 | "s3:GetObject", 28 | "s3:GetObjectAcl", 29 | "s3:GetObjectAttributes", 30 | "s3:GetObjectRetention", 31 | "s3:GetObjectTagging", 32 | "s3:GetObjectVersion", 33 | "s3:GetObjectVersionTagging" 34 | ], 35 | "Resource": "arn:aws:s3:::${bucket_name}/*" 36 | }, 37 | { 38 | "Sid": "Bucket level access from app environment", 39 | "Effect": "Allow", 40 | "Principal": { 41 | "AWS": ${jsonencode(app_aws_principals)} 42 | }, 43 | "Action": [ 44 | "s3:GetEncryptionConfiguration", 45 | "s3:GetBucketWebsite", 46 | "s3:GetBucketLocation", 47 | "s3:GetBucketObjectLockConfiguration", 48 | "s3:ListBucket" 49 | ], 50 | "Resource": "arn:aws:s3:::${bucket_name}" 51 | } 52 | ] 53 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-s3/templates/secure-transport.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Id": "secure-transport-${bucket_name}", 3 | "Version": "2012-10-17", 4 | "Statement": [ 5 | { 6 | "Sid": "AllowSSLRequestsOnly", 7 | "Action": "s3:*", 8 | "Effect": "Deny", 9 | "Resource": [ 10 | "arn:aws:s3:::${bucket_name}", 11 | "arn:aws:s3:::${bucket_name}/*" 12 | ], 13 | "Condition": { 14 | "Bool": { 15 | "aws:SecureTransport": "false" 16 | } 17 | }, 18 | "Principal": "*" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-s3/variables.tf: -------------------------------------------------------------------------------- 1 | variable "tags" { 2 | type = map(string) 3 | default = {} 4 | } 5 | 6 | variable "bucket_prefix" { 7 | description = "first part of bucket name to ensure uniqueness, if left blank a random suffix will be used instead" 8 | default = "" 9 | } 10 | 11 | variable "purpose" { 12 | description = "second part of bucket name" 13 | } 14 | 15 | variable "global_bucket" { 16 | description = "bucket with no environment suffix" 17 | default = false 18 | } 19 | 20 | variable "environment" { 21 | description = "suffix to include in bucket name if global_bucket set to false" 22 | default = "dev" 23 | } 24 | 25 | variable "kms_key_alias" { 26 | description = "KMS key alias for bucket encryption" 27 | default = "" 28 | } 29 | 30 | variable "kms_encryption_key_arn" { 31 | description = "ARN of KMS encryption key used in some bucket policies" 32 | default = "" 33 | } 34 | 35 | variable "default_aws_kms_key" { 36 | description = "use default AWS KMS key instead of customer managed key" 37 | default = false 38 | } 39 | 40 | variable "sse_algorithm" { 41 | description = "Server side encryption algorithm, options are AES256 and aws:kms" 42 | default = "aws:kms" 43 | } 44 | 45 | variable "server_side_encryption" { 46 | description = "Enable default server side encryption" 47 | default = true 48 | } 49 | 50 | variable "bucket_key_enabled" { 51 | description = "Whether or not to use Amazon S3 Bucket Keys for SSE-KMS" 52 | default = false 53 | } 54 | 55 | variable "acl" { 56 | description = "access control list" 57 | default = "private" 58 | } 59 | 60 | variable "versioning" { 61 | description = "Enable versioning" 62 | default = "Enabled" 63 | } 64 | 65 | variable "block_public_acls" { 66 | default = true 67 | } 68 | 69 | variable "block_public_policy" { 70 | default = true 71 | } 72 | 73 | variable "ignore_public_acls" { 74 | default = true 75 | } 76 | 77 | variable "restrict_public_buckets" { 78 | default = true 79 | } 80 | 81 | variable "access_logs" { 82 | description = "Enable access logs" 83 | default = false 84 | } 85 | 86 | variable "log_bucket" { 87 | description = "name of log bucket if access_logs set to true" 88 | default = "" 89 | } 90 | 91 | variable "force_destroy" { 92 | description = "destroy S3 bucket on Terraform destroy even with objects in bucket" 93 | default = true 94 | } 95 | 96 | variable "object_ownership" { 97 | description = "manage S3 bucket ownership controls, options are BucketOwnerPreferred, ObjectWriter, BucketOwnerEnforced" 98 | default = "BucketOwnerPreferred" 99 | } 100 | 101 | variable "oai_arn" { 102 | description = "ARN of CloudFront Origin Access Identity" 103 | default = "" 104 | } 105 | 106 | variable "public_crl" { 107 | description = "Whether to make the CRL and CA certificates publicly available" 108 | default = false 109 | } 110 | 111 | variable "app_aws_principals" { 112 | description = "List of AWS principals to allow access to CA External S3 bucket" 113 | type = list(string) 114 | default = [] 115 | } 116 | 117 | variable "filter_suffix" { 118 | description = "Filter suffix for notifications" 119 | default = ".log" 120 | } 121 | 122 | variable "lifecycle_policy" { 123 | description = "Include lifecycle policy" 124 | default = false 125 | } 126 | 127 | variable "ia_transition" { 128 | description = "Days at which transition to standard IA if lifecycle policy set" 129 | default = 90 130 | } 131 | 132 | variable "glacier_transition" { 133 | description = "Days at which transition to Glacier if lifecycle policy set" 134 | default = 180 135 | } 136 | 137 | variable "noncurrent_transition" { 138 | description = "Days at which non current version to Glacier if lifecycle policy set" 139 | default = 30 140 | } 141 | 142 | variable "abort_uploads" { 143 | description = "Days at which to abort multipart uploads if lifecycle policy set" 144 | default = 2 145 | } 146 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-scheduler/README.md: -------------------------------------------------------------------------------- 1 | # Terraform submodule for Scheduler 2 | * Deploys Amazon EventBridge Scheduler used by serverless CA 3 | * Submodule of terraform-aws-ca -------------------------------------------------------------------------------- /modules/terraform-aws-ca-scheduler/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_scheduler_schedule" "schedule" { 2 | name = "${var.project}-${var.purpose}-${var.env}" 3 | group_name = var.group_name 4 | 5 | flexible_time_window { 6 | mode = "OFF" 7 | } 8 | 9 | schedule_expression = var.schedule_expression 10 | 11 | target { 12 | arn = var.target_arn 13 | role_arn = var.role_arn 14 | } 15 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-scheduler/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project" { 2 | description = "abbreviation for the project, forms first part of resource names" 3 | } 4 | 5 | variable "env" { 6 | description = "Environment name, e.g. dev" 7 | } 8 | 9 | variable "purpose" { 10 | description = "purpose of Scheduler" 11 | default = "ca" 12 | } 13 | 14 | variable "role_arn" { 15 | description = "IAM role to be assumed by scheduler" 16 | } 17 | 18 | variable "target_arn" { 19 | description = "ARN of target to be triggered" 20 | } 21 | 22 | variable "schedule_expression" { 23 | description = "Schedule in supported format" 24 | } 25 | 26 | variable "group_name" { 27 | description = "EventBridge Group name" 28 | default = "default" 29 | } 30 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-sns/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | 3 | data "aws_region" "current" {} -------------------------------------------------------------------------------- /modules/terraform-aws-ca-sns/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | sns_topic_display_name = coalesce(var.custom_sns_topic_name, replace(title(replace("${var.project}-${var.function}-${var.env}", "-", " ")), " Ca ", " CA ")) 3 | sns_topic_name = coalesce(var.custom_sns_topic_name, "${var.project}-${var.function}-${var.env}") 4 | 5 | tags = merge(var.tags, { 6 | Terraform = "true" 7 | Name = local.sns_topic_name, 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-sns/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_sns_topic" "sns_topic" { 2 | name = local.sns_topic_name 3 | display_name = local.sns_topic_display_name 4 | policy = coalesce(var.sns_policy, templatefile("${path.module}/templates/${var.sns_policy_template}.json", { region = data.aws_region.current.id, account_id = data.aws_caller_identity.current.account_id, sns_topic_name = local.sns_topic_name })) 5 | 6 | tags = merge( 7 | var.tags, 8 | tomap( 9 | { "Name" = local.sns_topic_name } 10 | ) 11 | ) 12 | kms_master_key_id = var.kms_key_arn 13 | } 14 | 15 | resource "aws_sns_topic_subscription" "email_subscriptions" { 16 | for_each = toset(var.email_subscriptions) 17 | endpoint = each.key 18 | protocol = "email" 19 | topic_arn = aws_sns_topic.sns_topic.arn 20 | raw_message_delivery = false 21 | } 22 | 23 | resource "aws_sns_topic_subscription" "lambda_subscriptions" { 24 | for_each = var.lambda_subscriptions 25 | endpoint = each.value 26 | protocol = "lambda" 27 | topic_arn = aws_sns_topic.sns_topic.arn 28 | raw_message_delivery = false 29 | } 30 | 31 | resource "aws_sns_topic_subscription" "sqs_subscriptions" { 32 | for_each = var.sqs_subscriptions 33 | endpoint = each.value 34 | protocol = "sqs" 35 | topic_arn = aws_sns_topic.sns_topic.arn 36 | raw_message_delivery = true 37 | } 38 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-sns/outputs.tf: -------------------------------------------------------------------------------- 1 | output "sns_topic_arn" { 2 | value = aws_sns_topic.sns_topic.arn 3 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-sns/templates/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Id": "default_policy", 4 | "Statement": [ 5 | { 6 | "Sid": "default_statement", 7 | "Effect": "Allow", 8 | "Principal": { 9 | "AWS": "*" 10 | }, 11 | "Action": [ 12 | "sns:GetTopicAttributes", 13 | "sns:SetTopicAttributes", 14 | "sns:AddPermission", 15 | "sns:RemovePermission", 16 | "sns:DeleteTopic", 17 | "sns:Subscribe", 18 | "sns:ListSubscriptionsByTopic", 19 | "sns:Publish", 20 | "sns:Receive" 21 | ], 22 | "Resource": "arn:aws:sns:${region}:${account_id}:${sns_topic_name}", 23 | "Condition": { 24 | "StringEquals": { 25 | "AWS:SourceOwner": "${account_id}" 26 | } 27 | } 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-sns/templates/eventbridge.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Id": "allow_account_access_to_topic_policy", 4 | "Statement": [ 5 | { 6 | "Sid": "allow_account_access_to_topic", 7 | "Effect": "Allow", 8 | "Principal": { 9 | "AWS": "*" 10 | }, 11 | "Action": [ 12 | "sns:GetTopicAttributes", 13 | "sns:SetTopicAttributes", 14 | "sns:AddPermission", 15 | "sns:RemovePermission", 16 | "sns:DeleteTopic", 17 | "sns:Subscribe", 18 | "sns:ListSubscriptionsByTopic", 19 | "sns:Publish", 20 | "sns:Receive" 21 | ], 22 | "Resource": "arn:aws:sns:${region}:${account_id}:${sns_topic_name}", 23 | "Condition": { 24 | "StringEquals": { 25 | "AWS:SourceOwner": "${account_id}" 26 | } 27 | } 28 | }, 29 | { 30 | "Sid": "allow_eventbridge_access_to_topic", 31 | "Effect": "Allow", 32 | "Principal": { 33 | "Service": "events.amazonaws.com" 34 | }, 35 | "Action": "sns:Publish", 36 | "Resource": "arn:aws:sns:${region}:${account_id}:${sns_topic_name}" 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-sns/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project" { 2 | description = "abbreviation for the project, forms the first part of the resource name" 3 | default = "" 4 | } 5 | 6 | variable "function" { 7 | description = "forms the second part of the resource name" 8 | default = "" 9 | } 10 | 11 | variable "env" { 12 | description = "suffix for environment, e.g. dev" 13 | default = "" 14 | } 15 | 16 | variable "custom_sns_topic_display_name" { 17 | description = "Customised SNS topic display name, leave empty to use standard naming convention" 18 | default = "" 19 | } 20 | 21 | 22 | variable "custom_sns_topic_name" { 23 | description = "Customised SNS topic name, leave empty to use standard naming convention" 24 | default = "" 25 | } 26 | 27 | variable "sns_policy" { 28 | description = "A string containing the SNS policy, if used" 29 | default = "" 30 | } 31 | 32 | variable "sns_policy_template" { 33 | description = "Name of SNS policy template file, if used" 34 | default = "default" 35 | } 36 | 37 | variable "kms_key_arn" { 38 | description = "A KMS key arn to be used to encrypt the queue contents at rest" 39 | default = null 40 | } 41 | 42 | variable "email_subscriptions" { 43 | type = list(string) 44 | description = "List of email addresses to subscribe to this topic" 45 | default = [] 46 | } 47 | 48 | variable "lambda_subscriptions" { 49 | type = map(string) 50 | description = "A map of lambda names to arns to subscribe to this topic" 51 | default = {} 52 | } 53 | 54 | variable "sqs_subscriptions" { 55 | type = map(string) 56 | description = "A map of SQS names to arns to subscribe to this topic" 57 | default = {} 58 | } 59 | 60 | variable "tags" { 61 | type = map(string) 62 | default = {} 63 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-step-function/README.md: -------------------------------------------------------------------------------- 1 | # Terraform submodule for Step Function 2 | * Deploys AWS Step Function used by serverless CA 3 | * Submodule of terraform-aws-ca -------------------------------------------------------------------------------- /modules/terraform-aws-ca-step-function/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | 3 | data "aws_region" "current" {} 4 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-step-function/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | template_name_prefix = contains(var.cert_info_files, "tls") ? "ca" : "ca-no-gitops" 3 | } -------------------------------------------------------------------------------- /modules/terraform-aws-ca-step-function/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_log_group" "log_group_for_sfn" { 2 | #checkov:skip=CKV_AWS_158:Agreed to not require CMK's KMS for cloudwatch 3 | name = "/aws/vendedlogs/states/${var.project}-${var.purpose}-${var.env}" 4 | retention_in_days = var.retention_in_days 5 | } 6 | 7 | resource "aws_sfn_state_machine" "state_machine" { 8 | definition = templatefile("${path.module}/templates/${local.template_name_prefix}.json.tpl", { 9 | account_id = data.aws_caller_identity.current.account_id, 10 | project = var.project, 11 | env = var.env, 12 | region = data.aws_region.current.name 13 | internal_s3_bucket = var.internal_s3_bucket 14 | }) 15 | name = "${var.project}-${var.purpose}-${var.env}" 16 | role_arn = var.role_arn 17 | 18 | logging_configuration { 19 | log_destination = "${aws_cloudwatch_log_group.log_group_for_sfn.arn}:*" 20 | include_execution_data = true 21 | level = "ALL" 22 | } 23 | 24 | tracing_configuration { 25 | enabled = true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-step-function/outputs.tf: -------------------------------------------------------------------------------- 1 | output "state_machine_arn" { 2 | value = aws_sfn_state_machine.state_machine.arn 3 | } 4 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-step-function/templates/ca-no-gitops.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "Certificate Authority operations GitOps disabled", 3 | "StartAt": "Create Root CA", 4 | "States": { 5 | "Create Root CA": { 6 | "Type": "Task", 7 | "Resource": "arn:aws:states:::lambda:invoke", 8 | "Parameters": { 9 | "Payload.$": "$", 10 | "FunctionName": "arn:aws:lambda:${region}:${account_id}:function:${project}-create-root-ca-${env}:$LATEST" 11 | }, 12 | "Retry": [ 13 | { 14 | "ErrorEquals": [ 15 | "Lambda.ServiceException", 16 | "Lambda.AWSLambdaException", 17 | "Lambda.SdkClientException", 18 | "Lambda.TooManyRequestsException" 19 | ], 20 | "IntervalSeconds": 2, 21 | "MaxAttempts": 6, 22 | "BackoffRate": 2 23 | } 24 | ], 25 | "Next": "Root CA CRL" 26 | }, 27 | "Root CA CRL": { 28 | "Type": "Task", 29 | "Resource": "arn:aws:states:::lambda:invoke", 30 | "InputPath": "$", 31 | "Parameters": { 32 | "Payload.$": "$", 33 | "FunctionName": "arn:aws:lambda:${region}:${account_id}:function:${project}-root-ca-crl-${env}:$LATEST" 34 | }, 35 | "Retry": [ 36 | { 37 | "ErrorEquals": [ 38 | "Lambda.ServiceException", 39 | "Lambda.AWSLambdaException", 40 | "Lambda.SdkClientException", 41 | "Lambda.TooManyRequestsException" 42 | ], 43 | "IntervalSeconds": 2, 44 | "MaxAttempts": 6, 45 | "BackoffRate": 2 46 | } 47 | ], 48 | "Next": "Create Issuing CA" 49 | }, 50 | "Create Issuing CA": { 51 | "Type": "Task", 52 | "Resource": "arn:aws:states:::lambda:invoke", 53 | "InputPath": "$", 54 | "Parameters": { 55 | "Payload.$": "$", 56 | "FunctionName": "arn:aws:lambda:${region}:${account_id}:function:${project}-create-issuing-ca-${env}:$LATEST" 57 | }, 58 | "Retry": [ 59 | { 60 | "ErrorEquals": [ 61 | "Lambda.ServiceException", 62 | "Lambda.AWSLambdaException", 63 | "Lambda.SdkClientException", 64 | "Lambda.TooManyRequestsException" 65 | ], 66 | "IntervalSeconds": 2, 67 | "MaxAttempts": 6, 68 | "BackoffRate": 2 69 | } 70 | ], 71 | "Next": "Issuing CA CRL" 72 | }, 73 | "Issuing CA CRL": { 74 | "Type": "Task", 75 | "Resource": "arn:aws:states:::lambda:invoke", 76 | "InputPath": "$", 77 | "Parameters": { 78 | "Payload.$": "$", 79 | "FunctionName": "arn:aws:lambda:${region}:${account_id}:function:${project}-issuing-ca-crl-${env}:$LATEST" 80 | }, 81 | "Retry": [ 82 | { 83 | "ErrorEquals": [ 84 | "Lambda.ServiceException", 85 | "Lambda.AWSLambdaException", 86 | "Lambda.SdkClientException", 87 | "Lambda.TooManyRequestsException" 88 | ], 89 | "IntervalSeconds": 2, 90 | "MaxAttempts": 6, 91 | "BackoffRate": 2 92 | } 93 | ], 94 | "End": true 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-step-function/templates/ca.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "Certificate Authority operations", 3 | "StartAt": "Create Root CA", 4 | "States": { 5 | "Create Root CA": { 6 | "Type": "Task", 7 | "Resource": "arn:aws:states:::lambda:invoke", 8 | "Parameters": { 9 | "Payload.$": "$", 10 | "FunctionName": "arn:aws:lambda:${region}:${account_id}:function:${project}-create-root-ca-${env}:$LATEST" 11 | }, 12 | "Retry": [ 13 | { 14 | "ErrorEquals": [ 15 | "Lambda.ServiceException", 16 | "Lambda.AWSLambdaException", 17 | "Lambda.SdkClientException", 18 | "Lambda.TooManyRequestsException" 19 | ], 20 | "IntervalSeconds": 2, 21 | "MaxAttempts": 6, 22 | "BackoffRate": 2 23 | } 24 | ], 25 | "Next": "Root CA CRL" 26 | }, 27 | "Root CA CRL": { 28 | "Type": "Task", 29 | "Resource": "arn:aws:states:::lambda:invoke", 30 | "InputPath": "$", 31 | "Parameters": { 32 | "Payload.$": "$", 33 | "FunctionName": "arn:aws:lambda:${region}:${account_id}:function:${project}-root-ca-crl-${env}:$LATEST" 34 | }, 35 | "Retry": [ 36 | { 37 | "ErrorEquals": [ 38 | "Lambda.ServiceException", 39 | "Lambda.AWSLambdaException", 40 | "Lambda.SdkClientException", 41 | "Lambda.TooManyRequestsException" 42 | ], 43 | "IntervalSeconds": 2, 44 | "MaxAttempts": 6, 45 | "BackoffRate": 2 46 | } 47 | ], 48 | "Next": "Create Issuing CA" 49 | }, 50 | "Create Issuing CA": { 51 | "Type": "Task", 52 | "Resource": "arn:aws:states:::lambda:invoke", 53 | "InputPath": "$", 54 | "Parameters": { 55 | "Payload.$": "$", 56 | "FunctionName": "arn:aws:lambda:${region}:${account_id}:function:${project}-create-issuing-ca-${env}:$LATEST" 57 | }, 58 | "Retry": [ 59 | { 60 | "ErrorEquals": [ 61 | "Lambda.ServiceException", 62 | "Lambda.AWSLambdaException", 63 | "Lambda.SdkClientException", 64 | "Lambda.TooManyRequestsException" 65 | ], 66 | "IntervalSeconds": 2, 67 | "MaxAttempts": 6, 68 | "BackoffRate": 2 69 | } 70 | ], 71 | "Next": "Issuing CA CRL" 72 | }, 73 | "Issuing CA CRL": { 74 | "Type": "Task", 75 | "Resource": "arn:aws:states:::lambda:invoke", 76 | "InputPath": "$", 77 | "Parameters": { 78 | "Payload.$": "$", 79 | "FunctionName": "arn:aws:lambda:${region}:${account_id}:function:${project}-issuing-ca-crl-${env}:$LATEST" 80 | }, 81 | "Retry": [ 82 | { 83 | "ErrorEquals": [ 84 | "Lambda.ServiceException", 85 | "Lambda.AWSLambdaException", 86 | "Lambda.SdkClientException", 87 | "Lambda.TooManyRequestsException" 88 | ], 89 | "IntervalSeconds": 2, 90 | "MaxAttempts": 6, 91 | "BackoffRate": 2 92 | } 93 | ], 94 | "Next": "File Analysis" 95 | }, 96 | "File Analysis": { 97 | "Type": "Map", 98 | "ItemProcessor": { 99 | "ProcessorConfig": { 100 | "Mode": "DISTRIBUTED", 101 | "ExecutionType": "STANDARD" 102 | }, 103 | "StartAt": "TLS Certificate", 104 | "States": { 105 | "TLS Certificate": { 106 | "Type": "Task", 107 | "Resource": "arn:aws:states:::lambda:invoke", 108 | "OutputPath": "$.Payload", 109 | "Parameters": { 110 | "Payload.$": "$", 111 | "FunctionName": "arn:aws:lambda:${region}:${account_id}:function:${project}-tls-cert-${env}:$LATEST" 112 | }, 113 | "Retry": [ 114 | { 115 | "ErrorEquals": [ 116 | "Lambda.ServiceException", 117 | "Lambda.AWSLambdaException", 118 | "Lambda.SdkClientException", 119 | "Lambda.TooManyRequestsException" 120 | ], 121 | "IntervalSeconds": 2, 122 | "MaxAttempts": 6, 123 | "BackoffRate": 2 124 | } 125 | ], 126 | "End": true 127 | } 128 | } 129 | }, 130 | "ItemReader": { 131 | "Resource": "arn:aws:states:::s3:getObject", 132 | "ReaderConfig": { 133 | "InputType": "JSON" 134 | }, 135 | "Parameters": { 136 | "Bucket": "${internal_s3_bucket}", 137 | "Key": "tls.json" 138 | } 139 | }, 140 | "MaxConcurrency": 1000, 141 | "Label": "FileAnalysis", 142 | "End": true 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /modules/terraform-aws-ca-step-function/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project" { 2 | description = "abbreviation for the project, forms first part of resource names" 3 | } 4 | 5 | variable "env" { 6 | description = "Environment name, e.g. dev" 7 | } 8 | 9 | variable "kms_arn" {} 10 | 11 | variable "role_arn" { 12 | description = "IAM role to be assumed by state machine" 13 | } 14 | 15 | variable "retention_in_days" { 16 | description = "specifies the number of days you want to retain log events" 17 | default = 365 18 | } 19 | 20 | variable "purpose" { 21 | description = "purpose of Step Function" 22 | default = "ca" 23 | } 24 | 25 | variable "internal_s3_bucket" { 26 | description = "Internal S3 Bucket Name" 27 | } 28 | 29 | variable "cert_info_files" { 30 | description = "List of file names to be uploaded to internal S3 bucket for processing" 31 | } 32 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "cloudfront_domain_name" { 2 | value = var.public_crl ? module.ca_cloudfront[0].cloudfront_domain_name : null 3 | description = "Domain name of CloudFront distribution used for public CRL" 4 | } 5 | 6 | output "ca_bundle_s3_key" { 7 | value = contains(var.prod_envs, var.env) ? "${var.project}-ca-bundle.pem" : "${var.project}-ca-bundle-${var.env}.pem" 8 | description = "S3 key (name) of CA bundle" 9 | } 10 | 11 | output "ca_bundle_s3_location" { 12 | value = contains(var.prod_envs, var.env) ? "${module.external_s3.s3_bucket_domain_name}/${var.project}-ca-bundle.pem" : "${module.external_s3.s3_bucket_domain_name}/${var.project}-ca-bundle-${var.env}.pem" 13 | description = "S3 location of CA bundle for use as a TrustStore" 14 | } 15 | 16 | output "external_s3_bucket_name" { 17 | value = module.external_s3.s3_bucket_name 18 | description = "External S3 bucket name" 19 | } 20 | 21 | output "internal_s3_bucket_name" { 22 | value = module.internal_s3.s3_bucket_name 23 | description = "Internal S3 bucket name" 24 | } 25 | 26 | output "issuing_ca_cert_s3_location" { 27 | value = contains(var.prod_envs, var.env) ? "${module.external_s3.s3_bucket_domain_name}/${var.project}-issuing-ca.crt" : "${module.external_s3.s3_bucket_domain_name}/${var.project}-issuing-ca-${var.env}.crt" 28 | description = "S3 location of Issuing CA certificate file" 29 | } 30 | 31 | output "issuing_ca_crl_s3_location" { 32 | value = contains(var.prod_envs, var.env) ? "${module.external_s3.s3_bucket_domain_name}/${var.project}-issuing-ca.crl" : "${module.external_s3.s3_bucket_domain_name}/${var.project}-issuing-ca-${var.env}.crl" 33 | description = "S3 location of Issuing CA CRL file" 34 | } 35 | 36 | output "root_ca_cert_s3_location" { 37 | value = contains(var.prod_envs, var.env) ? "${module.external_s3.s3_bucket_domain_name}/${var.project}-root-ca.crt" : "${module.external_s3.s3_bucket_domain_name}/${var.project}-root-ca-${var.env}.crt" 38 | description = "S3 location of Root CA certificate file" 39 | } 40 | 41 | output "root_ca_crl_s3_location" { 42 | value = contains(var.prod_envs, var.env) ? "${module.external_s3.s3_bucket_domain_name}/${var.project}-root-ca.crl" : "${module.external_s3.s3_bucket_domain_name}/${var.project}-root-ca-${var.env}.crl" 43 | description = "S3 location of Root CA CRL file" 44 | } 45 | 46 | output "sns_topic_arn" { 47 | value = module.sns_ca_notifications.sns_topic_arn 48 | description = "SNS topic ARN" 49 | } 50 | -------------------------------------------------------------------------------- /providers.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = ">= 5.0" 6 | configuration_aliases = [aws, aws.us-east-1] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements-docs.txt 2 | asn1crypto == 1.5.1 3 | assertpy == 1.1 4 | bandit == 1.8.3 5 | black == 25.1.0 6 | boto3 == 1.38.32 7 | certvalidator == 0.11.1 8 | cryptography == 45.0.3 9 | dataclasses-json == 0.6.7 10 | prospector == 1.17.1 11 | pytest == 8.4.0 12 | requests == 2.32.3 13 | structlog == 25.4.0 14 | validators == 0.35.0 15 | 16 | # TODO: Used by certvalidator - remove once latest oscrypto published to pypi 17 | oscrypto @ git+https://github.com/wbond/oscrypto.git@d5f3437ed24257895ae1edd9e503cfb352e635a8 -------------------------------------------------------------------------------- /requirements-docs.txt: -------------------------------------------------------------------------------- 1 | cairosvg == 2.8.2 2 | mkdocs-material == 9.6.14 3 | pillow == 11.2.1 4 | urllib3 == 2.4.0 -------------------------------------------------------------------------------- /scripts/delete_db_table_items.py: -------------------------------------------------------------------------------- 1 | from boto3 import client 2 | 3 | 4 | def get_dynamo_db_table(): 5 | """ 6 | ARN and name of DynamoDB table 7 | """ 8 | 9 | dynamodb_client = client("dynamodb") 10 | 11 | tables = dynamodb_client.list_tables()["TableNames"] 12 | table = [t for t in tables if "CA" in t] 13 | 14 | return table[0] 15 | 16 | 17 | def delete_dynamo_db_table_items(table): 18 | """ 19 | Delete all items from DynamoDB table 20 | """ 21 | 22 | dynamodb_client = client("dynamodb") 23 | 24 | items = dynamodb_client.scan(TableName=table, Limit=500, Select="ALL_ATTRIBUTES")["Items"] 25 | 26 | for item in items: 27 | serial_number = item["SerialNumber"]["S"] # sort key 28 | common_name = item["CommonName"]["S"] # partition key 29 | print(f"Deleting DynamoDB item certificate serial number {serial_number}") 30 | composite_key_json = {"SerialNumber": {"S": serial_number}, "CommonName": {"S": common_name}} 31 | dynamodb_client.delete_item(TableName=table, Key=composite_key_json) 32 | 33 | 34 | if __name__ == "__main__": 35 | table = get_dynamo_db_table() 36 | print(f"Deleting items from {table}...") 37 | delete_dynamo_db_table_items(table) 38 | print(f"Items from {table} deleted successfully") 39 | -------------------------------------------------------------------------------- /scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 == 1.38.32 -------------------------------------------------------------------------------- /scripts/start_ca_step_function.py: -------------------------------------------------------------------------------- 1 | from boto3 import client 2 | from time import sleep 3 | 4 | 5 | def get_ca_step_function_details(): 6 | """ 7 | Get ARN and name of CA Step function 8 | """ 9 | 10 | stepfunctions_client = client("stepfunctions") 11 | 12 | step_functions = stepfunctions_client.list_state_machines()["stateMachines"] 13 | step_function = [s for s in step_functions if "-ca-" in s["name"]] 14 | 15 | return step_function[0]["stateMachineArn"], step_function[0]["name"] 16 | 17 | 18 | def start_ca_step_function(step_function_arn): 19 | """ 20 | Start CA Step function 21 | """ 22 | 23 | stepfunctions_client = client("stepfunctions") 24 | return stepfunctions_client.start_execution(stateMachineArn=step_function_arn) 25 | 26 | 27 | def monitor_step_function_execution(execution_arn): 28 | """ 29 | Ensure CA Step Function completes without errors 30 | """ 31 | 32 | stepfunctions_client = client("stepfunctions") 33 | 34 | execution_details = stepfunctions_client.describe_execution(executionArn=execution_arn) 35 | 36 | while execution_details["status"] == "RUNNING": 37 | execution_details = stepfunctions_client.describe_execution(executionArn=execution_arn) 38 | print(f'"CA Step Function status: {execution_details["status"]}') 39 | sleep(5) 40 | 41 | return execution_details 42 | 43 | 44 | if __name__ == "__main__": 45 | step_function_arn, step_function_name = get_ca_step_function_details() 46 | print(f"Starting {step_function_name}...") 47 | execution_arn = start_ca_step_function(step_function_arn)["executionArn"] 48 | execution_details = monitor_step_function_execution(execution_arn) 49 | 50 | exec_status = execution_details["status"] 51 | if exec_status == "SUCCEEDED": 52 | print(f"Step Function {step_function_name} completed successfully") 53 | 54 | if exec_status == "FAILED": 55 | print(f"Step Function Output: {execution_details}") 56 | raise SystemExit(f"Step Function {step_function_name} failed") 57 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/tests/__init__.py -------------------------------------------------------------------------------- /utils/generate-csr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import json 4 | import argparse 5 | from modules.certs.crypto import ( 6 | create_csr_info, 7 | crypto_encode_private_key, 8 | crypto_tls_cert_signing_request, 9 | generate_key, 10 | write_key_to_disk, 11 | ) 12 | 13 | 14 | def load_variables_from_file(file_path): 15 | """ 16 | Load the variables from a JSON file. 17 | """ 18 | with open(file_path, "r", encoding="utf-8") as f: 19 | return json.load(f) 20 | 21 | 22 | def parse_arguments(): 23 | """ 24 | Parse command-line arguments. 25 | """ 26 | parser = argparse.ArgumentParser(description="Generate CSR and private key") 27 | parser.add_argument("--server", action="store_true", help="Generate server certificate (default is client)") 28 | parser.add_argument("--varfile", type=str, help="Path to JSON file containing CSR variables") 29 | parser.add_argument( 30 | "--destination", 31 | type=str, 32 | default=os.path.expanduser("~") + "/certs", 33 | help="Path to destination directory for output files (default: ~/.certs)", 34 | ) 35 | return parser.parse_args() 36 | 37 | 38 | def main(): 39 | """ 40 | Create test client or server Certificate Signing Request (CSR) for default Serverless CA environment 41 | """ 42 | 43 | # parse arguments 44 | args = parse_arguments() 45 | 46 | # set variables 47 | if args.varfile: 48 | variables = load_variables_from_file(args.varfile) 49 | else: 50 | # default values 51 | variables = { 52 | "common_name": "Cloud Engineer", 53 | "country": "GB", 54 | "locality": "London", 55 | "state": "England", 56 | "organization": "Serverless Inc", 57 | "organizational_unit": "Security Operations", 58 | } 59 | 60 | if args.server: 61 | variables["common_name"] = "server.example.com" 62 | 63 | common_name = variables["common_name"] 64 | country = variables["country"] 65 | locality = variables["locality"] 66 | state = variables["state"] 67 | organization = variables["organization"] 68 | organizational_unit = variables["organizational_unit"] 69 | 70 | if not os.path.exists(args.destination): 71 | print(f"Creating directory {args.destination}") 72 | os.makedirs(args.destination) 73 | 74 | if args.server: 75 | output_path_cert_key = f"{args.destination}/server-cert-request-key.pem" 76 | output_path_csr = f"{args.destination}/server-cert-request.csr" 77 | else: 78 | output_path_cert_key = f"{args.destination}/client-cert-request-key.pem" 79 | output_path_csr = f"{args.destination}/client-cert-request.csr" 80 | 81 | # create private key 82 | private_key = generate_key() 83 | write_key_to_disk(private_key, output_path_cert_key) 84 | 85 | # create CSR 86 | csr_info = create_csr_info(common_name, country, locality, organization, organizational_unit, state) 87 | csr_pem = crypto_tls_cert_signing_request(private_key, csr_info) 88 | 89 | # write CSR to file 90 | print(f"Certificate requested for {common_name}") 91 | 92 | if output_path_csr: 93 | with open(output_path_csr, "w", encoding="utf-8") as f: 94 | f.write(csr_pem.decode("utf-8")) 95 | print(f"Certificate request written to {output_path_csr}") 96 | 97 | # write private key to file 98 | key_data = crypto_encode_private_key(private_key) 99 | if output_path_cert_key: 100 | with open(output_path_cert_key, "w", encoding="utf-8") as f: 101 | f.write(key_data.decode("utf-8")) 102 | print(f"Private key written to {output_path_cert_key}") 103 | 104 | 105 | if __name__ == "__main__": 106 | main() 107 | -------------------------------------------------------------------------------- /utils/modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-ca/terraform-aws-ca/ed33c2be075c6db196063dcd00cc38f8834ad583/utils/modules/__init__.py -------------------------------------------------------------------------------- /utils/modules/aws/kms.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | 4 | def get_kms_details(key_purpose, session=None): 5 | """ 6 | Get the KMS ARN based on the key purpose 7 | """ 8 | if session is None: 9 | kms_client = boto3.client("kms") 10 | else: 11 | kms_client = session.client("kms") 12 | 13 | key_aliases = kms_client.list_aliases()["Aliases"] 14 | key_aliases = [k for k in key_aliases if key_purpose in k["AliasName"]] 15 | 16 | key_alias = key_aliases[0]["AliasName"] 17 | 18 | return key_alias, kms_client.describe_key(KeyId=key_alias)["KeyMetadata"]["Arn"] 19 | -------------------------------------------------------------------------------- /utils/modules/aws/lambdas.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | 4 | 5 | def get_lambda_name(lambda_purpose, env_name=None, session=None): 6 | """ 7 | Get the full name of the Lambda function based on its purpose 8 | """ 9 | if session is None: 10 | lambda_client = boto3.client("lambda") 11 | else: 12 | lambda_client = session.client("lambda") 13 | 14 | lambdas = lambda_client.list_functions()["Functions"] 15 | if env_name is None: 16 | lambdas = [la for la in lambdas if lambda_purpose in la["FunctionName"]] 17 | else: 18 | lambdas = [la for la in lambdas if la["FunctionName"].endswith(f"{lambda_purpose}-{env_name}")] 19 | 20 | return lambdas[0]["FunctionName"] 21 | 22 | 23 | def invoke_lambda(function_name, json_data, session=None): 24 | """ 25 | Invoke TLS certificate Lambda function 26 | """ 27 | 28 | if session is None: 29 | lambda_client = boto3.client("lambda") 30 | else: 31 | lambda_client = session.client("lambda") 32 | 33 | response = lambda_client.invoke( 34 | FunctionName=function_name, 35 | Payload=json.dumps(json_data), 36 | ) 37 | 38 | return json.loads(response["Payload"].read().decode("utf-8")) 39 | -------------------------------------------------------------------------------- /utils/modules/aws/s3.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | 4 | def get_s3_bucket(bucket_purpose="internal", session=None): 5 | """ 6 | Get the full name of the S3 bucket based on its purpose 7 | """ 8 | 9 | if session is None: 10 | s3_client = boto3.client("s3") 11 | else: 12 | s3_client = session.client("s3") 13 | 14 | s3_buckets = s3_client.list_buckets()["Buckets"] 15 | return [b["Name"] for b in s3_buckets if f"-{bucket_purpose}-" in b["Name"]][0] 16 | 17 | 18 | def list_s3_object_keys(bucket_name, session=None): 19 | """ 20 | List object keys in S3 bucket 21 | """ 22 | 23 | if session is None: 24 | s3_client = boto3.client("s3") 25 | else: 26 | s3_client = session.client("s3") 27 | 28 | response = s3_client.list_objects_v2(Bucket=bucket_name) 29 | 30 | s3_objects = response["Contents"] 31 | 32 | return [s3_object["Key"] for s3_object in s3_objects] 33 | 34 | 35 | def get_s3_object(bucket_name, key, session=None): 36 | """ 37 | Get object from S3 38 | """ 39 | 40 | if session is None: 41 | s3_client = boto3.client("s3") 42 | else: 43 | s3_client = session.client("s3") 44 | 45 | response = s3_client.get_object(Bucket=bucket_name, Key=key) 46 | 47 | return response["Body"].read() 48 | 49 | 50 | def put_s3_object( 51 | bucket_name, kms_arn, key, data, encryption_algorithm="aws:kms", session=None 52 | ): # pylint:disable=too-many-arguments,too-many-positional-arguments 53 | """ 54 | Put object in S3 bucket 55 | """ 56 | 57 | if session is None: 58 | s3_client = boto3.client("s3") 59 | else: 60 | s3_client = session.client("s3") 61 | 62 | s3_client.put_object( 63 | Bucket=bucket_name, SSEKMSKeyId=kms_arn, ServerSideEncryption=encryption_algorithm, Key=key, Body=data 64 | ) 65 | 66 | 67 | def delete_s3_object(bucket_name, key, session=None): 68 | """ 69 | Delete object from S3 70 | """ 71 | 72 | if session is None: 73 | s3_client = boto3.client("s3") 74 | else: 75 | s3_client = session.client("s3") 76 | 77 | s3_client.delete_object(Bucket=bucket_name, Key=key) 78 | -------------------------------------------------------------------------------- /utils/modules/certs/kms.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | 4 | def kms_generate_key_pair(key_id, key_pair_spec="ECC_NIST_P256", session=None): 5 | """Generate KMS key pair""" 6 | 7 | if session is None: 8 | client = boto3.client(service_name="kms") 9 | else: 10 | client = session.client(service_name="kms") 11 | 12 | return client.generate_data_key_pair( 13 | KeyId=key_id, 14 | KeyPairSpec=key_pair_spec, 15 | ) 16 | 17 | 18 | def kms_get_kms_key_id(alias, session=None): 19 | """returns the KMS Key ARN for a specified alias""" 20 | 21 | if session is None: 22 | client = boto3.client(service_name="kms") 23 | else: 24 | client = session.client(service_name="kms") 25 | 26 | aliases = client.list_aliases(Limit=100)["Aliases"] 27 | try: 28 | key_id_list = [a for a in aliases if a["AliasName"] == f"alias/{alias}"] 29 | key_id = key_id_list[0]["TargetKeyId"] 30 | except Exception as e: 31 | key_id = {"error": f"Failed to get KMS key ID. {e}"} 32 | 33 | return key_id 34 | -------------------------------------------------------------------------------- /utils/requirements.txt: -------------------------------------------------------------------------------- 1 | asn1crypto == 1.5.1 2 | boto3 == 1.38.32 3 | certvalidator == 0.11.1 4 | cryptography == 45.0.3 5 | validators == 0.35.0 6 | -------------------------------------------------------------------------------- /utils/variables/client.json: -------------------------------------------------------------------------------- 1 | { 2 | "lifetime": 90, 3 | "common_name": "My Test Certificate", 4 | "country": "GB", 5 | "locality": "London", 6 | "state": "England", 7 | "organization": "Serverless Inc", 8 | "organizational_unit": "Security Operations", 9 | "purposes": [ 10 | "client_auth" 11 | ], 12 | "key_alias": "serverless-tls-keygen-dev" 13 | } 14 | -------------------------------------------------------------------------------- /utils/variables/server.json: -------------------------------------------------------------------------------- 1 | { 2 | "lifetime": 90, 3 | "common_name": "test.example.com", 4 | "sans": ["test.example.com", "test2.example.com"], 5 | "country": "GB", 6 | "locality": "London", 7 | "state": "England", 8 | "organization": "Serverless Inc", 9 | "organizational_unit": "Security Operations", 10 | "purposes": [ 11 | "server_auth" 12 | ], 13 | "key_alias": "serverless-tls-keygen-dev" 14 | } 15 | --------------------------------------------------------------------------------