Module 4: Secrets Environments And Identity
8. Project: Andes Cargo's secrets and environments plan
Description
This is the project that closes Module 4. It has two parts of different nature, and this lesson is honest about which is which from the start. The first is executed: you confirm, with act really running, that ci.yml —already migrated in lesson 7— keeps working as the complete LocalStack pipeline it's been since Module 3. The second is a document, in technical prose, not new YAML nor a built IAM role: what secret, what environment, and what OIDC role Andes Cargo would use if this pipeline ran against a real AWS account —without building any of that here, exactly as lesson 5 declared about the complete OIDC pattern.
Connection to the module
Lessons 2 and 3 gave the why and the mechanism of Secrets; lessons 4 and 5, OIDC's complete (though representative) mechanism; lesson 6, the (also representative) mechanism of Environments with approval; lesson 7 did the real executed work: migrating ci.yml. This project integrates all of that into two deliverables: the migrated ci.yml, confirmed once more end to end, and a new document, SECRETS-AND-ENVIRONMENTS.md, this guide's first portfolio artifact that isn't code — it's the kind of document a real engineer writes before touching a production account, so the security design gets reviewed before implementation, not after.
Part 1 — Executed: confirm the complete pipeline, with migrated secrets
On andes-cargo-infra/, with the migrated ci.yml you left in lesson 7, run the complete cycle once more, this time listing the available jobs first, to confirm the entire project stays coherent:
act -l
What to expect (literal excerpt, executed to write this lesson — the accumulated workflows from all four modules so far):
Stage Job ID Job name Workflow name Workflow file Events
0 terraform-checks terraform-checks ci ci.yml pull_request
0 say-hello-to-localstack say-hello-to-localstack hello-andes-cargo hello-andes-cargo.yml push
0 print-payload print-payload print-event-payload print-event.yml pull_request
0 check-secrets check-secrets secrets-test secrets-test.yml workflow_dispatch
0 check-one-secret check-one-secret single-secret-test single-secret-test.yml workflow_dispatch
act pull_request -e .github/act-events/pr-event.json -j terraform-checks --secret-file .secrets
What to expect (literal — the same end-to-end result you confirmed in lesson 7):
[ci/terraform-checks] ✅ Success - Main Terraform format check [...]
[ci/terraform-checks] ✅ Success - Main Terraform init [...]
[ci/terraform-checks] ✅ Success - Main Terraform validate [...]
[ci/terraform-checks] ✅ Success - Main Install awslocal [...]
[ci/terraform-checks] ❌ Failure - Main Confirm the runner can reach LocalStack on the host [...]
[ci/terraform-checks] ✅ Success - Main Install tflocal [...]
[ci/terraform-checks] | Plan: 12 to add, 0 to change, 0 to destroy.
[ci/terraform-checks] ✅ Success - Main Terraform plan [...]
[ci/terraform-checks] ✅ Success - Main Publish the plan to the job summary [...]
[ci/terraform-checks] 🏁 Job succeeded
Plan: 12 to add, 0 to change, 0 to destroy is, again, the signature of Andes Cargo's four canonical resources (bucket, table, two roles, function) deploying with their modules and supporting resources — nothing changed from Module 3, and nothing should have: lesson 7's migration exclusively touched where two credential values come from, never what the pipeline does with them.
This confirms the part this module can actually execute: the complete LocalStack pipeline, with .secrets as the only source of credentials, with no credential line written directly in a committed file.
Part 2 — The document: SECRETS-AND-ENVIRONMENTS.md
Create this file at andes-cargo-infra/'s root, alongside the rest of the project — it's this guide's first technical-prose document, the direct predecessor to the pipeline README.md Module 8 is going to build as the final portfolio deliverable:
# Secrets, environments and identity — Andes Cargo's real deployment plan
> This document describes the security design Andes Cargo would use if this pipeline
> ran against a real AWS account. Nothing described here is built in this repository:
> it's the plan, not the implementation. The complete implementation lives in
> `cloud-security-and-guardrails-guide`.
## Secrets
**In this repository (LocalStack, executed):** a single pair of dummy credentials
(`AWS_ACCESS_KEY_ID=test`, `AWS_SECRET_ACCESS_KEY=test`), as repository `Secrets`,
used the same way in any branch and any environment.
**In a real deployment:** zero long-lived credentials stored as a `Secret`, at all.
With OIDC (Module 4, lesson 5) correctly configured, there's no need to store any
`AWS_ACCESS_KEY_ID` — the pipeline receives temporary credentials on every run. The
only thing a real workflow would need on the configuration side is the ARN of the
role to assume (`role-to-assume`), and that value **is not a secret**: knowing a
role's ARN gives nobody access without also being able to present a valid JWT signed
by GitHub for the correct repository and branch. That's why, in a real deployment,
that ARN would live as a GitHub Actions `variable` (`vars.`, not `secrets.`) —
visible, not protected, because it doesn't need to be.
## Environments
| Environment | Allowed branch | Required reviewers | IAM role it would assume |
|---|---|---|---|
| `dev` | any feature branch, via `pull_request` | No | `AndesCargoPlanOnlyRole` — read-only/plan-only permissions, never `apply` |
| `production` | only `main`, via `push` | Yes, at least one person other than the change's author | `AndesCargoDeployRole` — `apply` permissions on the four canonical resources |
The separation between `AndesCargoPlanOnlyRole` (read-only, for `ci.yml`) and
`AndesCargoDeployRole` (with `apply` permission, for `apply.yml`, Module 5) is
deliberate: no `pull_request` from a feature branch —where the code hasn't been
reviewed yet— should have, even in theory, permissions to modify real
infrastructure. Only a `push` to `main`, after an already-reviewed PR merges,
would assume the role with write permissions.
## The `production` role: `trust policy` and permissions, in prose
**Trust policy:** trusts only GitHub OIDC tokens whose `sub` claim is exactly
`repo:andes-cargo/andes-cargo-infra:ref:refs/heads/main` — no other branch,
no fork, no Pull Request can assume this role, regardless of who triggers it.
**Permissions:** limited exactly to the four canonical resources you already know
from `terraform-and-iac-guide` — the `andes-cargo-shipment-docs` bucket, the
`Shipments` table, the `LambdaManifestProcessorRole`/`AppServerRole` roles, and
the `process-shipment-manifest` function — plus the Terraform permissions needed
to read/write the remote `state` (outside LocalStack's scope, which uses a local
`state`). No administrator permission, no `*:*` — the same least-privilege
principle already applied, on the application side, to those same roles during
`terraform-and-iac-guide`.
## What this document does NOT solve
It doesn't include the `Identity Provider`'s exact JSON, the complete `trust
policy`, or the complete permissions policy — those are already shown, in full,
in Module 4, lesson 5, and built end to end in `cloud-security-and-guardrails-guide`.
This document is the **plan**, written before building anything, exactly as a real
engineer would write it before a security design review.
Commit the document:
git add SECRETS-AND-ENVIRONMENTS.md
git commit -m "Add SECRETS-AND-ENVIRONMENTS.md: the real-deployment plan for secrets, environments, and OIDC roles"
git log --oneline -5
What to expect (representative for the hashes, literal for the structure):
9f2c8a1 Add SECRETS-AND-ENVIRONMENTS.md: the real-deployment plan for secrets, environments, and OIDC roles
a1b2c3d Migrate ci.yml credentials from hardcoded values to secrets.AWS_ACCESS_KEY_ID / secrets.AWS_SECRET_ACCESS_KEY
7b6421e ci.yml: publish the plan to the job summary
dfcbfce ci.yml: install tflocal and run terraform plan
ba47438 Add ci.yml: terraform fmt and validate as CI steps
Closing Module 4
You completed the module answering head-on the market audit's most-cited gap. Review what you're taking with you:
- The antipattern, precisely named, and seen with your own eyes: a long-lived access key written directly in a committed file — not in the abstract, but located in your own
ci.yml— and why Git's history, not the current file, is the real problem with a leaked credential (lesson 2, with a real recovery experiment). - The intermediate mechanism, really executed: GitHub
Secrets, repo-scoped and environment-scoped, referenced withsecrets.<NAME>, really migrated in Andes Cargo's realci.yml, with identical output before and after (lessons 3 and 7). - The complete mechanism, shown and with a cited honesty reason: OIDC federation —short-lived JWT,
permissions: id-token: write, IAM trust policy— in real production YAML, not executed for two cited technical reasons, with a direct pointer tocloud-security-and-guardrails-guide(lessons 4 and 5). - The human-approval control, described and tested at its limit: Environments with
required reviewers, with a realactrun confirming —in 1.6 seconds, no pause— that the local tool completely ignores this protection (lesson 6). - The complete plan, in technical prose, for a real deployment: what role, what trust policy, what separation between
devandproductionAndes Cargo would use, documented without building it (this lesson).
What comes next
Module 5 takes this same ci.yml —already migrated, already with correct Secrets— and builds half of the pipeline's CD: apply.yml, triggered by push to main, chained with needs: to the plan that preceded it, so apply uses exactly the same plan that was reviewed, not one recalculated. You're going to see environment: production —this lesson's mechanism— declared for the first time in a real workflow in this guide, and you're going to build concurrency control and scheduled drift detection.
Common mistakes
Building the real Identity Provider or IAM role inside andes-cargo-infra/, "since the document describes them" (flow-based, the most important one in this project). What happens: someone, motivated by how concrete SECRETS-AND-ENVIRONMENTS.md sounds, adds a new .tf file with aws_iam_openid_connect_provider and aws_iam_role, trying to apply it against LocalStack. Why it happens: the document describes AWS resources in enough detail to feel "almost ready to write in HCL." How to spot it: if your andes-cargo-infra/ has a new file declaring an aws_iam_openid_connect_provider. How to fix it: delete it — this guide's DISENO.md is explicit that no new business HCL resource gets declared except Module 6's minimal guardrail; LocalStack, additionally, doesn't implement OIDC validation, so that resource, even if applied, would do nothing real. This lesson's document is deliberately a plan, not an HCL draft.
Treating SECRETS-AND-ENVIRONMENTS.md as optional or filler (expectation-based). What happens: someone writes the document hastily, with one or two lines, without the environments table or the explicit separation between AndesCargoPlanOnlyRole and AndesCargoDeployRole. Why it happens: compared to a ci.yml that runs and fails or succeeds, a prose document feels less "verifiable," and it's tempting to treat it as less important. How to spot it: if your version of the document doesn't clearly explain why dev and production need different roles, not just different Secrets. How to fix it: this document is exactly the kind of deliverable reviewed in a real technical interview or a security design review — the quality of your reasoning about permission separation matters as much as the YAML that actually runs. Rewrite it with the complete detail from this lesson's example.
Exercises
Exercise 1 — Justify the role separation between dev and production. Without looking at this lesson's document, explain in your own words why AndesCargoPlanOnlyRole and AndesCargoDeployRole should be two distinct IAM roles, instead of a single shared role with apply permissions used in both dev and production.
See solution
A pull_request from a feature branch runs code that hasn't been reviewed yet —that's precisely ci.yml's purpose: generating a plan for someone to review before merging. If that pull_request assumed a role with apply permissions, any malicious code or a serious bug on a feature branch could, in theory, apply real changes against the infrastructure before anyone reviewed it — exactly the opposite of the plan-before-apply pattern's purpose that Module 3 built. Separating the roles ensures the worst possible case in dev (a compromised or buggy feature branch) is limited to generating a plan nobody needs to apply, never to modifying real infrastructure on its own.
Exercise 2 — Explain why the role's ARN doesn't need to be a Secret. A colleague, writing their own SECRETS-AND-ENVIRONMENTS.md, stores AndesCargoDeployRole's ARN as a GitHub Secret, "just in case." Is it necessary? Justify your answer in two sentences.
See solution
It's not necessary, and it doesn't hurt anyone for it to be one, but it also adds no real protection: knowing an IAM role's ARN doesn't let anyone assume it — the real security lives entirely in that role's trust policy (which repository, which branch, which OIDC claim can assume it), not in keeping the ARN secret. Storing it as vars.AWS_ROLE_ARN (a normal, unprotected variable) instead of secrets.AWS_ROLE_ARN is more honest about what the system actually protects, and makes the YAML more readable for anyone reviewing it.
Exercise 3 — Walk through the complete module from memory, one sentence per lesson. Without looking back, summarize each of this module's 8 lessons in a single sentence, in the order they appeared.
See solution
1. The module's map and why it's the competition's most-cited security gap. 2. Why a credential leaked in a commit lives in Git's history, not in the current file. 3. GitHub Secrets, repo-scoped and environment-scoped, and the secrets.<NAME> syntax. 4. What OIDC federation is, conceptually: short-lived JWT, permissions: id-token: write, trust policy. 5. The complete OIDC pattern in production YAML, representative, with the cited technical reason act can't execute it. 6. Environments with required reviewers, representative, confirmed with a real act run that ignores it. 7. ci.yml's real executed migration from written credentials to Secrets, with identical output before and after. 8. The secrets, environments, and OIDC roles plan Andes Cargo would use in a real deployment, documented without building it.
Summary and next step
In this project you closed Module 4 with two deliverables of different nature, both honest about their own nature: ci.yml, confirmed once more end to end with act really running, and SECRETS-AND-ENVIRONMENTS.md, a technical-prose document describing —without building— the complete secrets, environments, and OIDC roles design Andes Cargo would use against a real AWS account.
Before moving on you should be able to: explain the difference between AndesCargoPlanOnlyRole and AndesCargoDeployRole, and why they exist as two separate roles; confirm, running act yourself, that ci.yml still works the same after the credentials migration; and name, without hesitation, this module's three security pieces in order of maturity: credential written in the YAML (the antipattern) → GitHub Secret (better, but still a long-lived key) → OIDC (no long-lived key to store at all).
With this, Module 4 is closed. You have this guide's complete identity-security vocabulary, the real pipeline running with correct Secrets, and a design document for the real deployment you haven't built yet — on purpose.
Next module: half of the pipeline's CD — apply.yml triggered by push to main, chained to the plan with needs:, with environment: production declared for real for the first time, concurrency control, and scheduled drift detection.
Resources
src/paths/aws-cloud-ecosystem/VALIDACION.md(NIEVA) — the source for the market gap this entire module solves, cited in lesson 1.- GitHub Docs — Configuring OpenID Connect in Amazon Web Services — OIDC's complete pattern, the basis for lesson 5 and this lesson's document.
terraform-and-iac-guide, capstone (NIEVA) — the origin ofLambdaManifestProcessorRoleandAppServerRole, the two roles this lesson's document uses as a least-privilege reference.cloud-security-and-guardrails-guide(NIEVA) — the guide that builds, end to end, exactly what this document plans without implementing.