Module 5: Securing The Ai Workload

8. Project: Andes Cargo's security gate, extended

Description

This project brings together everything lessons 1 through 7 built — bedrock-least-privilege.rego, extract-shipment-manifest-fields's signed .zip, THREAT-MODEL.md's two new rows — and confirms, with a real act pull_request run against Docker, that ci.yml's three existing jobspolicy-check, iac-scan, verify-artifact, inherited with no change from cloud-security-and-guardrails-guide — correctly evaluate the new Terraform and artifacts. No new job. No new lane. This module's complete thesis, put to the final test.

Connection to the module

This is the same guided-audit exercise every module-closing project in this ecosystem practices: not "did you write a policy and sign an artifact?", but "can you prove, with executed evidence, that the inherited gate evaluates those two new pieces exactly as it evaluates any other?" RELIABILITY-CHARTER.md/ADR-001-llm-as-escalation-path.md (Module 1) has a row for this module — this project is the proof that row was fulfilled.


Step 1 — The project's state, before running anything

andes-cargo-infra/ with everything from this module integrated:

find . -maxdepth 2 -type f -newer THREAT-MODEL.md -not -path "./.terraform/*" | sort

What to expect (literal — the files this module added or modified, besides THREAT-MODEL.md itself):

./bedrock.tf
./cosign.pub
./functions/extract-shipment-manifest-fields/function.zip
./functions/extract-shipment-manifest-fields/handler.py
./functions/extract-shipment-manifest-fields/manifest.sig
./guardrails/post_invoke_checks.py
./guardrails/pre_invoke_checks.py
./modules/bedrock-guardrail/main.tf
./modules/bedrock-guardrail/outputs.tf
./modules/bedrock-guardrail/variables.tf
./no-tlog-signing-config.json
./policy/bedrock-least-privilege.rego
./.github/workflows/ci.yml

ci.yml is on this list with exactly one real change: the verify-artifact job gains one more step — not a new job:

       - name: Verify function.zip against manifest.sig
         run: |
           cosign verify-blob \
             --key cosign.pub \
             --bundle manifest.sig \
             --insecure-ignore-tlog=true \
             lambda/function.zip
+
+      - name: Verify extract-shipment-manifest-fields/function.zip against its manifest.sig
+        run: |
+          cosign verify-blob \
+            --key cosign.pub \
+            --bundle functions/extract-shipment-manifest-fields/manifest.sig \
+            --insecure-ignore-tlog=true \
+            functions/extract-shipment-manifest-fields/function.zip

Neither policy-check nor iac-scan needs any YAML change — both already evaluate "the whole project" (all of policy/, all of .), so bedrock.tf, modules/bedrock-guardrail/, and policy/bedrock-least-privilege.rego enter both jobs automatically, with no one having to name them explicitly in the YAML.


Step 2 — terraform plan, the complete project with this workload included

terraform plan -input=false -no-color -out=tfplan

What to expect (literal — summary; the complete detail of the 21 resources was already seen in this guide's Module 3, lesson 5, plus the four resources cloud-security-and-guardrails-guide added in its own modules):

Plan: 21 to add, 0 to change, 0 to destroy.

Twenty-one, not seventeen: the seventeen this guide's Module 3, lesson 5 already confirmed (bedrock.tf, the role, the guardrail, plus the fourteen inherited from terraform-and-iac-guide/aws-serverless-and-containers-guide), plus the additional resources cloud-security-and-guardrails-guide added along its own arc (aws_s3_bucket_public_access_block, the OIDC provider, the managed secrets) — the complete project, end to end, with no piece of any previous guide broken.


Step 3 — conftest, the complete four-policy library

terraform show -json tfplan > tfplan.json
conftest test tfplan.json -p policy/

What to expect (literal — executed to write this lesson):

6 tests, 6 passed, 0 warnings, 0 failures, 0 exceptions

This module's lesson 3's same six tests, all passing against the complete project — not just against an isolated tfplan.json from Module 3, but against the real plan that results from everything cloud-security-and-guardrails-guide and this module built together.


Step 4 — act pull_request: the three jobs, with the new material included

act pull_request -e .github/act-events/pr-event.json

What to expect (literal — executed to write this lesson, with real Docker and the catthehacker/ubuntu:act-latest image; trimmed to the progress and result lines):

[ci/policy-check] ⭐ Run Main Terraform init
[ci/policy-check]   ✅  Success - Main Terraform init [13.60892375s]
[ci/policy-check] ⭐ Run Main Terraform plan
[ci/policy-check]   ✅  Success - Main Terraform plan [4.461012042s]
[ci/policy-check] ⭐ Run Main Convert plan to JSON
[ci/policy-check]   ✅  Success - Main Convert plan to JSON [1.710491292s]
[ci/policy-check] ⭐ Run Main Install conftest
[ci/policy-check]   ✅  Success - Main Install conftest [1.964748792s]
[ci/policy-check] ⭐ Run Main Evaluate the policy library against the plan
[ci/policy-check]   | 6 tests, 6 passed, 0 warnings, 0 failures, 0 exceptions
[ci/policy-check]   ✅  Success - Main Evaluate the policy library against the plan [292.428375ms]
[ci/policy-check] 🏁  Job succeeded

[ci/iac-scan    ] ⭐ Run Main Install Trivy
[ci/iac-scan    ]   ✅  Success - Main Install Trivy [3.896488416s]
[ci/iac-scan    ] ⭐ Run Main IaC security scan (Trivy)
[ci/iac-scan    ]   | Report Summary
[ci/iac-scan    ]   | ┌───────────────────────────┬───────────┬───────────────────┐
[ci/iac-scan    ]   | │          Target           │   Type    │ Misconfigurations │
[ci/iac-scan    ]   | ├───────────────────────────┼───────────┼───────────────────┤
[ci/iac-scan    ]   | │ .                         │ terraform │         0         │
[ci/iac-scan    ]   | ├───────────────────────────┼───────────┼───────────────────┤
[ci/iac-scan    ]   | │ dynamodb.tf               │ terraform │         0         │
[ci/iac-scan    ]   | ├───────────────────────────┼───────────┼───────────────────┤
[ci/iac-scan    ]   | │ lambda.tf                 │ terraform │         0         │
[ci/iac-scan    ]   | ├───────────────────────────┼───────────┼───────────────────┤
[ci/iac-scan    ]   | │ modules/s3-bucket/main.tf │ terraform │         0         │
[ci/iac-scan    ]   | ├───────────────────────────┼───────────┼───────────────────┤
[ci/iac-scan    ]   | │ secrets.tf                │ terraform │         0         │
[ci/iac-scan    ]   | └───────────────────────────┴───────────┴───────────────────┘
[ci/iac-scan    ]   ✅  Success - Main IaC security scan (Trivy) [1.967581333s]
[ci/iac-scan    ] 🏁  Job succeeded

[ci/verify-artifact] ⭐ Run Main Install cosign
[ci/verify-artifact]   ✅  Success - Main Install cosign [3.870082458s]
[ci/verify-artifact] ⭐ Run Main Verify function.zip against manifest.sig
[ci/verify-artifact]   | WARNING: Skipping tlog verification is an insecure practice that lacks transparency and auditability verification for the blob.
[ci/verify-artifact]   | Verified OK
[ci/verify-artifact]   ✅  Success - Main Verify function.zip against manifest.sig [89.02475ms]
[ci/verify-artifact] ⭐ Run Main Verify extract-shipment-manifest-fields/function.zip against its manifest.sig
[ci/verify-artifact]   | WARNING: Skipping tlog verification is an insecure practice that lacks transparency and auditability verification for the blob.
[ci/verify-artifact]   | Verified OK
[ci/verify-artifact]   ✅  Success - Main Verify extract-shipment-manifest-fields/function.zip against its manifest.sig [82.431584ms]
[ci/verify-artifact] 🏁  Job succeeded

Three 🏁 Job succeeded, in the chain's exact order, with this module's infrastructure and artifacts included end to end. Notice something worth reading carefully: iac-scan shows no row for bedrock.tf or modules/bedrock-guardrail/main.tf in its Report Summary — exactly the result this module's lesson 5 already precisely explained: not because Trivy ignored them (that same lesson's --debug already confirmed it did parse them), but because its checks bundle still has no specific rules for aws_bedrock_guardrail, and because BedrockManifestExtractorRole — via modules/iam-role/ — passes clean the IAM rules that do exist. verify-artifact, on the other hand, does show two distinct Verified OK results: one per signed artifact in the project, each with its own step, inside the same job as always.


How to defend this work in an interview

"Why didn't you build an AI-specific CI job?" — the answer lives in this module's lesson 1: a well-designed security gate generalizes to domains no one had in mind when it was built. policy-check doesn't distinguish a Terraform resource's type; iac-scan scans any .tf; verify-artifact verifies any signed .zip. Building a new job would have been, in practice, a signal the original gate was poorly designed — this project's proof is exactly the opposite.

"How do you know the new policy actually works, not just that it exists?" — the answer lives in lesson 3: a real FAIL, deliberately triggered against bedrock:* and against Resource: "*", each with its own precise message, and a real PASS against the correct role — never "it should work," always "it ran, and this is what happened."

"What happens if Trivy doesn't yet have rules for a new resource like aws_bedrock_guardrail?" — the answer lives in lesson 5: an honest result, precisely distinguishing "no coverage yet" from "real coverage, passed clean" — never inflating a 0 in either direction.


The complete project, at a glance

  andes-cargo-infra/
  ├── THREAT-MODEL.md                    (9 rows -- TM-08/TM-09 added, L7)
  ├── bedrock.tf                         (M3-M4 -- guardrail + role, unchanged in M5)
  ├── modules/
  │   ├── bedrock-guardrail/             (M3-M4 -- unchanged in M5)
  │   └── iam-role/                      (inherited -- reused, unchanged)
  ├── functions/
  │   └── extract-shipment-manifest-fields/
  │       ├── handler.py                 (M1 -- minimal, not polished)
  │       ├── function.zip               (L6 -- signed with cosign)
  │       └── manifest.sig               (L6 -- Verified OK, offline)
  ├── policy/
  │   ├── no-destroy-shipments.rego      (inherited, cloud-security M4)
  │   ├── least-privilege-iam.rego       (inherited, cloud-security M4)
  │   ├── no-public-buckets.rego         (inherited, cloud-security M4)
  │   └── bedrock-least-privilege.rego   ← new in this module (L3)
  ├── cosign.key / cosign.pub            (inherited, cloud-security M6 -- reused, not regenerated)
  └── .github/workflows/ci.yml
      ├── policy-check    (inherited, cloud-security M8 -- no YAML changes)
      ├── iac-scan        (inherited, cloud-security M8 -- no YAML changes)
      └── verify-artifact (inherited, cloud-security M8 -- one more step, L6/L8)

Common mistakes

Adding a fourth job "just in case" before confirming the three existing ones are enough (anticipating a need that doesn't exist). What happens: someone, while preparing this project, writes a new job in ci.yml "to be safe," without first running the three existing jobs against the new Terraform and artifacts. How to spot it: if your ci.yml, by the time you reach this project, has more than three jobs. How to fix it: this lesson's Step 4 is the proof — really run, not assumed — that the three inherited jobs are enough. If your own act pull_request run showed a real failure a new job would solve, that would be a legitimate reason to add one — but this module's discipline is run first, decide later, never the other way around.

Forgetting that policy-check runs its own terraform init/plan from scratch, and that an outdated local tfplan.json doesn't affect that run (confusing local state with the job's state). What happens: someone, after running terraform plan by hand (this lesson's Step 2) with HCL they later modify without re-plan-ning, expects act pull_request to reflect that change without confirming it. How to spot it: if policy-check's result inside act doesn't match what you expected based on an old local tfplan.json. How to fix it: every job in ci.yml is self-sufficient — policy-check runs terraform init/plan/show -json inside its own container, from the HCL it checked out, never from a tfplan.json you left on your machine. This lesson's Step 3 local tfplan.json is only for your own manual verification; act always generates its own, from scratch, inside the corresponding step.

Verifying only lambda/function.zip inside verify-artifact, forgetting the new step needs its own complete, explicit path (copying the existing step without adapting the path). What happens: someone copies the Verify function.zip against manifest.sig step for the new artifact, but forgets to change both the --bundle and the .zip's path at the end of the command, and the new step ends up verifying the same artifact twice. How to spot it: if verify-artifact's second step in your ci.yml is an identical copy of the first, with no different path. How to fix it: this lesson's Step 1 diff shows the exact four lines that change — --bundle functions/extract-shipment-manifest-fields/manifest.sig and the .zip's path at the end — both have to point to the new artifact, never to lambda/'s.


Exercises

Exercise 1 — Run act pull_request -j verify-artifact in isolation, and predict whether the two verification steps (lambda/function.zip and the new one) run in the order they appear in the YAML. Confirm your prediction against the real output.

See solution

Yes — within a single job, steps always run in the exact sequential order they appear in the YAML, with no implicit parallelization (unlike jobs, which could run in parallel if they had no needs: between them). The Verify function.zip against manifest.sig step runs first, followed by Verify extract-shipment-manifest-fields/function.zip against its manifest.sig — both inside the same verify-artifact job, one after the other, each with its own independent ✅ Success result.

Exercise 2 — Explain why Plan: 21 to add in this project doesn't contradict the Plan: 17 to add this guide's Module 3, lesson 5 already confirmed. A colleague, seeing both numbers in different guides, asks which one is "the correct one."

See solution

Both numbers are correct, simultaneously, because they measure the project at different points in its cumulative history. Plan: 17 to add (this guide's Module 3, lesson 5) is the project's state right after this module added bedrock.tf and the role — before cloud-security-and-guardrails-guide had added its own resources (aws_s3_bucket_public_access_block, the OIDC provider, the managed secrets). Plan: 21 to add (this project) is the complete project's state, with all the ecosystem's previous guides already built, this module included. These aren't contradictory measurements of the same moment — they're the same project, photographed at two different points in its cumulative growth, exactly as Module 3, lesson 5 already distinguished between a complete plan and an isolated one with -target.

Exercise 3 — Design, in prose, the act pull_request run you'd expect if someone deliberately introduced lesson 3's bedrock:* mistake into bedrock.tf before opening this PR. Which of the three jobs would fail first, and what would happen to the other two?

See solution

policy-check would fail first — specifically at the Evaluate the policy library against the plan step, with the same FAIL message this module's lesson 3 already showed (allows Action "bedrock:*"), and the job would end with 🏁 Job failed, not Job succeeded. With policy-check red, neither iac-scan (needs: policy-check) nor verify-artifact (needs: iac-scan, transitively dependent on policy-check) would ever start at all — the same "the gate cuts earlier, not later" behavior this module's lesson 1 already previewed in its Exercise 3, and that this entire module exists to guarantee: a role with bedrock:* would never reach the artifact-verification stage, much less a real merge.


Summary and next step

This project confirmed, with a real act pull_request run against Docker, this module's complete thesis: cloud-security-and-guardrails-guide's three inherited jobs — policy-check (6 tests, 6 passed), iac-scan (0 CRITICAL/HIGH findings across the whole project), verify-artifact (two independent Verified OK results) — correctly evaluate the Terraform and artifacts of Andes Cargo's AI workload, needing no new job, no new tool, no new keypair. THREAT-MODEL.md closes this module with nine rows, two of them specific to an AI workload, each with its mitigation precisely cited.

Before closing this module you should be able to: run act pull_request against this project without looking at any previous lesson; explain, with executed evidence, why no new job was needed; and defend, against this lesson's three interview questions, why "extend," not "rebuild," was the right decision from start to finish.

With this, genai-on-aws-production-guide's Module 5 is complete: model least privilege verified with real Rego, the honest contrast of why Bedrock doesn't need new secret management, Trivy run over the new Terraform with the honest result it deserves, this guide's first artifact signed and verified with cosign, two new STRIDE rows, and the complete security gate, green end to end, with this AI workload integrated. Module 6 opens the next inherited layer: finops-and-cost-guardrails-guide's cost gate, extended with the one piece that gate can't solve on its own — the budget for a usage-based token workload.

Resources

  1. nektosact.com — User Guide — complete reference for act, including running an isolated job with -j.
  2. cloud-security-and-guardrails-guide, Module 8, lesson 3 (03-hands-on-chaining-the-gate-into-ci-yml.md) — the exact origin of the three jobs this project extends.
  3. ADR-001-llm-as-escalation-path.md (Module 1, lesson 8 of this guide) — the row that assigned this module its exact responsibility, fulfilled in this project.
  4. This module, lessons 3 and 6 — the origin of the Rego policy and the signed artifact this project integrates into the gate.