diff --git a/.github/workflows/attested-ci.yaml b/.github/workflows/attested-ci.yaml new file mode 100644 index 0000000..462142d --- /dev/null +++ b/.github/workflows/attested-ci.yaml @@ -0,0 +1,358 @@ +name: Attested CI Pipeline + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + id-token: write + contents: read + +jobs: + attested-ci: + name: CI Pipeline (Attested) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.24" + check-latest: false + + # ---- SA-11: lint + secretscan ---- + # secretscan attestor scans sibling attestors' outputs (stdout, + # env vars, products) for leaked secrets before the DSSE ships. + - name: Lint + Secrets + uses: aflock-ai/cilock-action@main + with: + step: lint + command: echo "lint passed" + attestations: environment git github secretscan + + # ---- SA-11: SAST (gosec → SARIF) ---- + - name: Install gosec + run: go install github.com/securego/gosec/v2/cmd/gosec@latest + + - name: SAST + uses: aflock-ai/cilock-action@main + with: + step: sast + command: bash -c "gosec -fmt=sarif -out=gosec.sarif ./... || true" + attestations: environment git github sarif + product-include-glob: "gosec.sarif" + + # ---- SA-11 / RA-5: SAST (semgrep → SARIF, unique glob) ---- + - name: Install semgrep + continue-on-error: true + run: pipx install semgrep || pip3 install --quiet semgrep || python3 -m pip install --quiet semgrep + + - name: SAST (semgrep) + continue-on-error: true + uses: aflock-ai/cilock-action@main + with: + step: sast-semgrep + command: bash -c "semgrep --config=auto --sarif -o semgrep.sarif . || true" + attestations: environment git github sarif + product-include-glob: "semgrep.sarif" + + # ---- IA-5: secret scan (gitleaks → SARIF, unique glob) ---- + - name: Install gitleaks + continue-on-error: true + run: | + case "$(uname -m)" in x86_64) GA=x64;; aarch64|arm64) GA=arm64;; *) GA=x64;; esac + curl -sSL https://github.com/gitleaks/gitleaks/releases/download/v8.18.4/gitleaks_8.18.4_linux_${GA}.tar.gz -o /tmp/gitleaks.tgz + tar -xzf /tmp/gitleaks.tgz -C /usr/local/bin gitleaks + chmod +x /usr/local/bin/gitleaks + + - name: Secret Scan (gitleaks) + continue-on-error: true + uses: aflock-ai/cilock-action@main + with: + step: secretscan-gitleaks + command: bash -c "gitleaks detect --no-git -f sarif -r gitleaks.sarif --source . || gitleaks dir . -f sarif -r gitleaks.sarif || true" + attestations: environment git github sarif + product-include-glob: "gitleaks.sarif" + + # ---- Test ---- + - name: Test + uses: aflock-ai/cilock-action@main + with: + step: test + command: go test -count=1 ./... + attestations: environment git github + + # ---- SA-11 / CM-4: Unit tests → test-results (JUnit) ---- + - name: Install gotestsum + continue-on-error: true + run: go install gotest.tools/gotestsum@latest + + - name: Test Results + continue-on-error: true + uses: aflock-ai/cilock-action@main + with: + step: test-results + command: bash -c "gotestsum --junitfile=junit.xml -- -count=1 ./... || true" + attestations: environment git github test-results + product-include-glob: "junit.xml" + + # ---- RA-5 / SI-2: SCA vuln (govulncheck native) ---- + - name: Install govulncheck + continue-on-error: true + run: go install golang.org/x/vuln/cmd/govulncheck@latest + + - name: Govulncheck + continue-on-error: true + uses: aflock-ai/cilock-action@main + with: + step: govulncheck + command: bash -c "govulncheck -json ./... > govulncheck.json || true" + attestations: environment git github govulncheck + product-include-glob: "govulncheck.json" + + # ---- SR-11 / SI-7 / CM-8: Build + SBOM (cyclonedx-gomod → CycloneDX) ---- + - name: Install cyclonedx-gomod + run: go install github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@v1.9.0 + + - name: Build + SBOM + uses: aflock-ai/cilock-action@main + with: + step: build + command: bash -c "CGO_ENABLED=0 go build -o bin/api ./cmd/api && cyclonedx-gomod app -licenses -json -output bin/bom.cdx.json -main ./cmd/api ." + attestations: environment git github sbom + product-include-glob: "bin/*" + + # ---- SR-4 / SA-15 / CM-2: go-build provenance + SLSA export ---- + # Bare `go build` (no bash -c) so the go-build attestor observes the argv. + - name: Go Build Provenance + continue-on-error: true + uses: aflock-ai/cilock-action@main + with: + step: go-build + command: go build -o bin/api-provenance ./cmd/api + attestations: environment git github go-build + attestor-slsa-export: "true" + product-include-glob: "bin/api-provenance" + + # ---- SI-2 / RA-5: Vuln scan (trivy → SARIF) ---- + - name: Install trivy + run: | + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin + + - name: Vuln Scan + uses: aflock-ai/cilock-action@main + with: + step: vuln-scan + command: bash -c "trivy fs --format sarif -o trivy.sarif . || true" + attestations: environment git github sarif + product-include-glob: "trivy.sarif" + + # ---- RA-5 / SI-2: Vuln scan (trivy native predicate) ---- + # Bare `trivy` argv so the trivy attestor emits the native trivy predicate. + - name: Vuln Scan (trivy native) + continue-on-error: true + uses: aflock-ai/cilock-action@main + with: + step: vuln-trivy + command: trivy fs --format json --output trivy.json . + attestations: environment git github trivy + product-include-glob: "trivy.json" + + # ---- RA-5: Vuln scan (grype → SARIF, unique glob) ---- + - name: Install grype + continue-on-error: true + run: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin + + - name: Vuln Scan (grype) + continue-on-error: true + uses: aflock-ai/cilock-action@main + with: + step: vuln-grype + command: bash -c "grype dir:. -o sarif > grype.sarif || true" + attestations: environment git github sarif + product-include-glob: "grype.sarif" + + # ---- RA-5: Vuln scan (osv-scanner → SARIF, unique glob) ---- + - name: Install osv-scanner + continue-on-error: true + run: | + case "$(uname -m)" in x86_64) OA=amd64;; aarch64|arm64) OA=arm64;; *) OA=amd64;; esac + curl -sSL https://github.com/google/osv-scanner/releases/download/v1.9.2/osv-scanner_linux_${OA} -o /usr/local/bin/osv-scanner + chmod +x /usr/local/bin/osv-scanner + + - name: Vuln Scan (osv-scanner) + continue-on-error: true + uses: aflock-ai/cilock-action@main + with: + step: vuln-osv + command: bash -c "osv-scanner --format sarif -r . > osv.sarif || true" + attestations: environment git github sarif + product-include-glob: "osv.sarif" + + # ---- SR-3 / CM-2: dependency lockfiles ---- + - name: Prepare lockfile + continue-on-error: true + run: | + go mod download 2>/dev/null || true + [ -f go.sum ] || touch go.sum + + - name: Lockfiles + continue-on-error: true + uses: aflock-ai/cilock-action@main + with: + step: lockfiles + command: bash -c "go mod download 2>/dev/null || true" + attestations: environment git github lockfiles + + # ---- CM-8: OS component inventory (system-packages) ---- + - name: System Packages + continue-on-error: true + uses: aflock-ai/cilock-action@main + with: + step: system-packages + command: echo "capturing OS package inventory" + attestations: environment git github system-packages + + # ---- Docker build ---- + - name: Docker Build + uses: aflock-ai/cilock-action@main + with: + step: docker-build + command: docker buildx build --metadata-file docker-metadata.json -t dropbox-clone:test --load . + attestations: environment git github docker + product-include-glob: "docker-metadata.json" + + # ---- CM-6: Dockerfile config scan (hadolint → SARIF, unique glob) ---- + - name: Install hadolint + continue-on-error: true + run: | + case "$(uname -m)" in x86_64) HA=x86_64;; aarch64|arm64) HA=arm64;; *) HA=x86_64;; esac + curl -sSL https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-Linux-${HA} -o /usr/local/bin/hadolint + chmod +x /usr/local/bin/hadolint + + - name: Config Scan (hadolint) + continue-on-error: true + uses: aflock-ai/cilock-action@main + with: + step: config-hadolint + command: bash -c "hadolint --format sarif Dockerfile > hadolint.sarif || true" + attestations: environment git github sarif + product-include-glob: "hadolint.sarif" + + # ---- SR-11 / CM-8: image SBOM (syft → CycloneDX, unique glob) ---- + - name: Install syft + continue-on-error: true + run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin + + - name: Image SBOM (syft) + continue-on-error: true + uses: aflock-ai/cilock-action@main + with: + step: image-sbom + command: bash -c "syft dropbox-clone:test -o cyclonedx-json=syft.cdx.json || true" + attestations: environment git github sbom + product-include-glob: "syft.cdx.json" + + # ---- SR-11 / SR-4: OCI image export → oci ---- + - name: OCI Export + continue-on-error: true + uses: aflock-ai/cilock-action@main + with: + step: oci-export + command: bash -c "docker save dropbox-clone:test -o image.oci.tar || true" + attestations: environment git github oci + product-include-glob: "image.oci.tar" + + # ---- RA-5 / SI-2 / SR-8: VEX (OpenVEX not_affected assertion) ---- + # Written in a prep step, then (re)created inside the cilock command so it + # is captured as a PRODUCT of the vex step (not just a material). + - name: Prepare VEX document + continue-on-error: true + run: | + cat > app.openvex.src.json <<'JSON' + { + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://cloudvault.dev/vex/cloudvault-api-2026-07-01", + "author": "CloudVault Security ", + "timestamp": "2026-07-01T00:00:00Z", + "version": 1, + "statements": [ + { + "vulnerability": { "name": "CVE-2024-24790" }, + "products": [ { "@id": "pkg:golang/github.com/cloudvault-dev/cloudvault-api" } ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path" + } + ] + } + JSON + + - name: VEX + continue-on-error: true + uses: aflock-ai/cilock-action@main + with: + step: vex + command: bash -c "cp app.openvex.src.json app.openvex.json || true" + attestations: environment git github vex + product-include-glob: "app.openvex.json" + + # ---- Install cloud tooling (aws-cli + kubectl) for deploy steps ---- + - name: Install AWS + kubectl + if: env.AWS_ACCESS_KEY_ID != '' + run: | + # aws-cli v2 + curl -sSL "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o /tmp/awscliv2.zip || \ + curl -sSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip + unzip -q /tmp/awscliv2.zip -d /tmp && /tmp/aws/install --update 2>/dev/null || true + aws --version + # kubectl + ARCH=$(uname -m | sed 's/aarch64/arm64/' | sed 's/x86_64/amd64/') + curl -sSLo /usr/local/bin/kubectl "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/${ARCH}/kubectl" + chmod +x /usr/local/bin/kubectl + kubectl version --client 2>/dev/null || true + + # ---- Deploy to EKS: RETIRED 2026-07-12 ---- + # The dropbox-clone-dev EKS cluster (testifysec-demo account) was torn + # down 2026-07-08. This step is disabled (if: false) rather than left + # gated on AWS_ACCESS_KEY_ID alone, so it can never silently attempt to + # deploy to a cluster that no longer exists. + - name: Deploy + if: false + uses: aflock-ai/cilock-action@main + with: + step: deploy + command: | + aws eks update-kubeconfig --name dropbox-clone-dev --region us-east-1 + kubectl set image deployment/dropbox-clone-api dropbox-clone-api=dropbox-clone:test -n dropbox-clone || echo "deployment not found — first deploy needs full kustomize apply" + kubectl rollout status deployment/dropbox-clone-api -n dropbox-clone --timeout=120s || true + attestations: environment git github k8smanifest + + # ---- Smoke test against deployed endpoint: RETIRED 2026-07-12 (depends on Deploy above) ---- + - name: Smoke Test + if: false + uses: aflock-ai/cilock-action@main + with: + step: smoke-test + command: | + curl -sf --retry 5 --retry-delay 5 https://dev.dropbox-clone.example.com/health || echo "smoke test endpoint unreachable (expected in local sim)" + attestations: environment git github + + # ---- Cloud posture: prowler against the AWS account ---- + - name: Install prowler + if: env.AWS_ACCESS_KEY_ID != '' + run: | + python3 -m venv /tmp/prowler-venv + /tmp/prowler-venv/bin/pip install --quiet prowler + + - name: Cloud Posture (Prowler) + if: env.AWS_ACCESS_KEY_ID != '' + uses: aflock-ai/cilock-action@main + with: + step: cloud-posture + command: bash -c "/tmp/prowler-venv/bin/prowler aws --severity critical high --compliance cis_1.5_aws -M json-ocsf -o prowler-results || true" + attestations: environment git github + product-include-glob: "prowler-results/*" diff --git a/.github/workflows/attested-infra.yaml b/.github/workflows/attested-infra.yaml new file mode 100644 index 0000000..b8330dd --- /dev/null +++ b/.github/workflows/attested-infra.yaml @@ -0,0 +1,107 @@ +name: Attested Infrastructure + +# From-scratch cloud footprint in the throwaway demo account, every step signed by cilock: +# iac-scan checkov over terraform/demo -> sarif attestor (CM-6, SA-11) +# terraform-plan plan against the live account -> aws + git + github (CM-3, CM-4) +# terraform-apply apply the plan -> aws + git + github (CM-2, CM-8) +# cloud-posture prowler over the account, ASFF out -> asff attestor (CA-7, RA-5, SI-4) + +on: + push: + branches: [main] + paths: + - "terraform/**" + - ".github/workflows/attested-infra.yaml" + workflow_dispatch: + inputs: + action: + description: "apply or destroy" + required: true + default: "apply" + type: choice + options: [apply, destroy] + +permissions: + id-token: write + contents: read + +env: + AWS_REGION: us-east-1 + TF_VERSION: "1.9.8" + TF_DIR: terraform/demo + +jobs: + infra: + name: Terraform + posture (attested) + runs-on: ubuntu-latest + concurrency: cloudvault-demo-infra + steps: + - uses: actions/checkout@v4 + + - name: Configure AWS credentials (GitHub OIDC) + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_GITHUB_ACTIONS_ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} + + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + + - name: Install checkov + run: pipx install checkov || pip3 install --quiet checkov + + # ---- CM-6 / SA-11: IaC misconfiguration scan ---- + - name: IaC Scan (checkov) + uses: aflock-ai/cilock-action@main + with: + step: iac-scan + command: bash -c "checkov -d ${TF_DIR} -o sarif --output-file-path . --quiet || true; test -f results_sarif.sarif && mv results_sarif.sarif checkov.sarif || true" + attestations: environment git github sarif + product-include-glob: "checkov.sarif" + + - name: Terraform Init + run: | + terraform -chdir=${TF_DIR} init -input=false \ + -backend-config="bucket=${{ vars.TF_STATE_BUCKET }}" \ + -backend-config="key=cloudvault/demo/terraform.tfstate" \ + -backend-config="region=${AWS_REGION}" + + # ---- CM-3 / CM-4: what will change, recorded before it changes ---- + - name: Terraform Plan + if: ${{ github.event.inputs.action != 'destroy' }} + uses: aflock-ai/cilock-action@main + with: + step: terraform-plan + command: bash -c "terraform -chdir=${TF_DIR} plan -input=false -out=tfplan.bin && terraform -chdir=${TF_DIR} show -json tfplan.bin > ${TF_DIR}/tfplan.json" + attestations: environment git github aws + product-include-glob: "terraform/demo/tfplan.json" + + # ---- CM-2 / CM-8: the change itself ---- + - name: Terraform Apply + if: ${{ github.event.inputs.action != 'destroy' }} + uses: aflock-ai/cilock-action@main + with: + step: terraform-apply + command: bash -c "terraform -chdir=${TF_DIR} apply -input=false -auto-approve tfplan.bin && terraform -chdir=${TF_DIR} output -json > ${TF_DIR}/outputs.json" + attestations: environment git github aws + product-include-glob: "terraform/demo/outputs.json" + + - name: Terraform Destroy + if: ${{ github.event.inputs.action == 'destroy' }} + run: terraform -chdir=${TF_DIR} destroy -input=false -auto-approve + + - name: Install prowler + if: ${{ github.event.inputs.action != 'destroy' }} + run: pipx install prowler==4.6.2 || pip3 install --quiet prowler==4.6.2 + + # ---- CA-7 / RA-5 / SI-4: posture of the account as built ---- + - name: Cloud Posture (prowler) + if: ${{ github.event.inputs.action != 'destroy' }} + uses: aflock-ai/cilock-action@main + with: + step: cloud-posture + command: bash -c "prowler aws --region ${AWS_REGION} -M json-asff -o prowler-out -F prowler --ignore-exit-code-3 --no-banner || true; cp prowler-out/prowler.asff.json prowler.asff.json" + attestations: environment git github aws asff + product-include-glob: "prowler.asff.json" diff --git a/.gitignore b/.gitignore index d56cc65..955a5c8 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,7 @@ temp/ # Coverage reports coverage.html coverage.txt + +**/.terraform/ +*.tfplan +tfplan.bin diff --git a/scripts/demo-account-bootstrap.sh b/scripts/demo-account-bootstrap.sh new file mode 100755 index 0000000..fd7d1c2 --- /dev/null +++ b/scripts/demo-account-bootstrap.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# One-shot bootstrap for the CloudVault demo AWS account. +# Run from the management account (profile testifysec-prod) after `aws sso login`. +# Creates: member account, Terraform state bucket, GitHub OIDC provider, CI role. +# Idempotent where AWS lets it be. Tear-down is scripts/demo-account-teardown.sh. +set -euo pipefail + +MGMT_PROFILE="${MGMT_PROFILE:-testifysec-prod}" +ACCOUNT_NAME="${ACCOUNT_NAME:-cloudvault-demo}" +ACCOUNT_EMAIL="${ACCOUNT_EMAIL:-aws+cloudvault-demo@testifysec.com}" +REGION="${REGION:-us-east-1}" +GITHUB_REPO="${GITHUB_REPO:-cloudvault-dev/cloudvault-api}" +ROLE_NAME="cloudvault-demo-github-actions" + +say() { printf '\n== %s\n' "$*"; } + +say "Looking for an existing account named ${ACCOUNT_NAME}" +ACCOUNT_ID="$(aws organizations list-accounts --profile "$MGMT_PROFILE" \ + --query "Accounts[?Name=='${ACCOUNT_NAME}' && Status=='ACTIVE'].Id | [0]" --output text)" + +if [[ -z "$ACCOUNT_ID" || "$ACCOUNT_ID" == "None" ]]; then + say "Creating member account ${ACCOUNT_NAME} <${ACCOUNT_EMAIL}>" + REQ="$(aws organizations create-account --profile "$MGMT_PROFILE" \ + --email "$ACCOUNT_EMAIL" --account-name "$ACCOUNT_NAME" \ + --query 'CreateAccountStatus.Id' --output text)" + for _ in $(seq 1 40); do + STATE="$(aws organizations describe-create-account-status --profile "$MGMT_PROFILE" \ + --create-account-request-id "$REQ" --query 'CreateAccountStatus.State' --output text)" + [[ "$STATE" == "SUCCEEDED" ]] && break + [[ "$STATE" == "FAILED" ]] && { aws organizations describe-create-account-status --profile "$MGMT_PROFILE" --create-account-request-id "$REQ"; exit 1; } + sleep 10 + done + ACCOUNT_ID="$(aws organizations describe-create-account-status --profile "$MGMT_PROFILE" \ + --create-account-request-id "$REQ" --query 'CreateAccountStatus.AccountId' --output text)" +fi +say "Account: ${ACCOUNT_ID}" + +say "Assuming OrganizationAccountAccessRole in the new account" +CREDS="$(aws sts assume-role --profile "$MGMT_PROFILE" \ + --role-arn "arn:aws:iam::${ACCOUNT_ID}:role/OrganizationAccountAccessRole" \ + --role-session-name cloudvault-bootstrap --duration-seconds 3600 \ + --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text)" +export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN +read -r AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN <<<"$CREDS" +unset AWS_PROFILE +aws sts get-caller-identity --region "$REGION" --output text + +STATE_BUCKET="cloudvault-demo-tfstate-${ACCOUNT_ID}" +say "Terraform state bucket ${STATE_BUCKET}" +if ! aws s3api head-bucket --bucket "$STATE_BUCKET" --region "$REGION" 2>/dev/null; then + aws s3api create-bucket --bucket "$STATE_BUCKET" --region "$REGION" + aws s3api put-bucket-versioning --bucket "$STATE_BUCKET" --versioning-configuration Status=Enabled + aws s3api put-bucket-encryption --bucket "$STATE_BUCKET" \ + --server-side-encryption-configuration '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}' + aws s3api put-public-access-block --bucket "$STATE_BUCKET" \ + --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true +fi + +say "GitHub OIDC provider" +OIDC_ARN="arn:aws:iam::${ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com" +if ! aws iam get-open-id-connect-provider --open-id-connect-provider-arn "$OIDC_ARN" >/dev/null 2>&1; then + aws iam create-open-id-connect-provider \ + --url https://token.actions.githubusercontent.com \ + --client-id-list sts.amazonaws.com \ + --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1 1c58a3a8518e8759bf075b76b750d4f2df264fcd >/dev/null +fi + +say "CI role ${ROLE_NAME} (AdministratorAccess — throwaway demo account)" +TRUST="$(cat </dev/null 2>&1; then + aws iam create-role --role-name "$ROLE_NAME" --assume-role-policy-document "$TRUST" >/dev/null +else + aws iam update-assume-role-policy --role-name "$ROLE_NAME" --policy-document "$TRUST" +fi +aws iam attach-role-policy --role-name "$ROLE_NAME" --policy-arn arn:aws:iam::aws:policy/AdministratorAccess +ROLE_ARN="arn:aws:iam::${ACCOUNT_ID}:role/${ROLE_NAME}" + +say "Wiring the GitHub repo" +gh secret set AWS_GITHUB_ACTIONS_ROLE_ARN -R "$GITHUB_REPO" --body "$ROLE_ARN" +gh variable set TF_STATE_BUCKET -R "$GITHUB_REPO" --body "$STATE_BUCKET" +gh variable set AWS_ACCOUNT_ID -R "$GITHUB_REPO" --body "$ACCOUNT_ID" + +say "Done" +echo "ACCOUNT_ID=${ACCOUNT_ID}" +echo "ROLE_ARN=${ROLE_ARN}" +echo "STATE_BUCKET=${STATE_BUCKET}" diff --git a/terraform/demo/.terraform.lock.hcl b/terraform/demo/.terraform.lock.hcl new file mode 100644 index 0000000..cdc1668 --- /dev/null +++ b/terraform/demo/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/terraform/demo/main.tf b/terraform/demo/main.tf new file mode 100644 index 0000000..65d8fb2 --- /dev/null +++ b/terraform/demo/main.tf @@ -0,0 +1,141 @@ +# CloudVault demo environment — a from-scratch cloud footprint in a throwaway AWS account. +# Deliberately small: network, object storage, container registry, and the app role. +# No EKS, no RDS. Every apply is attested by cilock in .github/workflows/attested-infra.yaml. + +terraform { + required_version = ">= 1.5.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + backend "s3" { + # bucket/key/region come from -backend-config in CI (see attested-infra.yaml) + } +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = var.project_name + Environment = var.environment + ManagedBy = "terraform" + Attested = "cilock" + } + } +} + +data "aws_caller_identity" "current" {} + +locals { + name = "${var.project_name}-${var.environment}" +} + +module "vpc" { + source = "../modules/vpc" + + project_name = var.project_name + environment = var.environment + vpc_cidr = var.vpc_cidr + availability_zones = var.availability_zones + cluster_name = local.name +} + +# Application role: what the API would run as. Exists so the bucket policy has a principal. +resource "aws_iam_role" "app" { + name = "${local.name}-app" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = { Service = "ec2.amazonaws.com" } + Action = "sts:AssumeRole" + }] + }) +} + +module "s3" { + source = "../modules/s3" + + bucket_name = "${local.name}-files-${data.aws_caller_identity.current.account_id}" + versioning_enabled = true + allowed_role_arns = [aws_iam_role.app.arn] +} + +module "ecr" { + source = "../modules/ecr" + + repository_name = local.name + scan_on_push = true + max_image_count = 10 +} + +# Audit trail for the account (AU-2 / AU-12): one multi-region trail to an encrypted bucket. +resource "aws_s3_bucket" "trail" { + bucket = "${local.name}-trail-${data.aws_caller_identity.current.account_id}" + force_destroy = true +} + +resource "aws_s3_bucket_public_access_block" "trail" { + bucket = aws_s3_bucket.trail.id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "trail" { + bucket = aws_s3_bucket.trail.id + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +data "aws_iam_policy_document" "trail" { + statement { + sid = "AWSCloudTrailAclCheck" + actions = ["s3:GetBucketAcl"] + resources = [aws_s3_bucket.trail.arn] + principals { + type = "Service" + identifiers = ["cloudtrail.amazonaws.com"] + } + } + statement { + sid = "AWSCloudTrailWrite" + actions = ["s3:PutObject"] + resources = ["${aws_s3_bucket.trail.arn}/AWSLogs/${data.aws_caller_identity.current.account_id}/*"] + principals { + type = "Service" + identifiers = ["cloudtrail.amazonaws.com"] + } + condition { + test = "StringEquals" + variable = "s3:x-amz-acl" + values = ["bucket-owner-full-control"] + } + } +} + +resource "aws_s3_bucket_policy" "trail" { + bucket = aws_s3_bucket.trail.id + policy = data.aws_iam_policy_document.trail.json +} + +resource "aws_cloudtrail" "main" { + name = local.name + s3_bucket_name = aws_s3_bucket.trail.id + is_multi_region_trail = true + include_global_service_events = true + enable_log_file_validation = true + + depends_on = [aws_s3_bucket_policy.trail] +} diff --git a/terraform/demo/outputs.tf b/terraform/demo/outputs.tf new file mode 100644 index 0000000..fd6f54f --- /dev/null +++ b/terraform/demo/outputs.tf @@ -0,0 +1,19 @@ +output "account_id" { + value = data.aws_caller_identity.current.account_id +} + +output "vpc_id" { + value = module.vpc.vpc_id +} + +output "files_bucket" { + value = module.s3.bucket_id +} + +output "ecr_repository_url" { + value = module.ecr.repository_url +} + +output "cloudtrail_arn" { + value = aws_cloudtrail.main.arn +} diff --git a/terraform/demo/variables.tf b/terraform/demo/variables.tf new file mode 100644 index 0000000..1b5b465 --- /dev/null +++ b/terraform/demo/variables.tf @@ -0,0 +1,24 @@ +variable "project_name" { + type = string + default = "cloudvault" +} + +variable "environment" { + type = string + default = "demo" +} + +variable "aws_region" { + type = string + default = "us-east-1" +} + +variable "vpc_cidr" { + type = string + default = "10.42.0.0/16" +} + +variable "availability_zones" { + type = list(string) + default = ["us-east-1a", "us-east-1b"] +}