eDossea · delivery pipeline

How a commit becomes a HIPAA estate

A green pipeline is not evidence. This one is built to produce the proof, and every claim in this deck is a line it actually printed.

OIDC sub = repo:J2-Labz@207084436/edossea-iac@1306994950:environment:dev deploying dev into 027401589926 as edossea-dev-ci-deploy ok compliant-dev.edossea.com/api/health answered as JSON 6/6 times in a row ok email [email protected] (confirmed) no route off the VPC — the boundary holds

20 August 2026 · account 027401589926 · run 32411631180

the chain, end to end

Two repositories, one account boundary

The application builds images and says so. The infrastructure repository decides everything else. Solid means running today; dashed means written but not yet sending the trigger — the workflow is in review, and the role it assumes has not been created.

Application — J2-Labz/eDossea written · open for review
Push to main application commit
Four images api · cron · migrate · frontend
ECR, dev account edossea-dev-* · immutable tags
repository_dispatch: app-published — the app repo's only say in what happens next
Infrastructure — J2-Labz/edossea-iac running
Gate 13 checks, no credential
Deploy dev OIDC · 7 applies · schema · frontend
Deploy prod dispatch + typed confirmation · by digest

Prod is not a second build. It receives dev's manifests copied across by digest, so no CI outside the PHI account ever holds write access to its registry.

the invariant

“Deploy to prod exactly what we deployed to dev”

Three readings. Only one of them is achievable, and being precise about which one is what makes the promise real instead of reassuring.

The same commit

Achievable, and it is the mechanism. One workflow, chained with needs:, so every job in a run sees the same github.sha however far main moves meanwhile.

The same Pulumi plan

Impossible, and correctly so. A dev plan contains dev account ARNs and edossea-dev-* names. It cannot be applied to prod, because the two estates are deliberately separate resources.

The same resulting shape

Already guaranteed — by the catalogue that declares each environment's services, database and cache, not by this pipeline.

enforcedprod: Assert prod is deploying the commit dev deployed

enforcedprod: Assert prod deploys the application version dev deployed

The re-check exists because re-running prod alone is the one path where the chain cannot carry the commit for you.

the gate — before any aws call

Everything that can fail without a credential

  • typecheck
  • 362 unit tests, two estates
  • control-absence linter
  • security-group descriptions
  • phase and destroy ordering
  • OIDC trust subjects
  • CloudFormation templates
  • database seeding path
  • workflow lint
  • composite-action shell lint
  • compliance matrix staleness
  • offline pulumi preview

measured# tests 58 · pass 58 · fail 0
# tests 304 · pass 304 · fail 0

It runs on every pull request

Before that, the gate first executed after a merge — on main, where a failure is a red default branch and a pass continues straight into a deploy. A gate whose first run is post-merge has already let the change through.

Each check tests itself, both poles

Every checker plants defects into a copy of the tree and asserts each one is caught. A check that has only ever reported success is a claim about its inputs, not about the repository — three of these were false-clean when first written.

the catcha pull_request event leaves inputs.environment EMPTY — so `!= 'prod'` was true, and a PR would have deployed into the dev account

credentials

No key ever exists to be leaked

A runner asks GitHub for a short-lived assertion about itself and trades it for an AWS session. The trust policy names one repository and one GitHub environment — not a branch.

Why the environment, not the ref

A job that declares environment: presents …:environment:dev, and the ref: form never appears at all. Pinning the environment is also stronger: a ref pin says “this came from main”, an environment pin says “this came from a job that entered prod”.

The prefix is a per-repository fact

GitHub may emit an immutable prefix carrying numeric org and repo ids. Both repositories report use_default: true and emit different forms, so it cannot be inferred — only read back from the issuer.

repositorysubject prefix it sends
edossea-iacrepo:J2-Labz@207084436/edossea-iac@1306994950
eDossearepo:J2-Labz/eDossea

foundthe bootstrap composed the NAME form from $GITHUB_REPOSITORY — re-running it would have repointed the live trust at a subject GitHub never sends

nowevery deploy prints the subject it presents, one line above the assume that needs it

the applies

Seven passes over six stacks

01security-baseline CloudTrail first
02platform-dns zone · certificate
03platform-network private VPC
04app-platform pass one — publishes a security group
05platform-data RDS · cache
06app-platform pass two — needs the database endpoint back
07edge CDN · WAF
dns-delegation deliberately outside every environment

The repetition is the honest form of a cycle

platform-data needs a security group from app-platform, and app-platform needs the database endpoint back. Two passes state that plainly instead of hiding it.

One stack is kept out on purpose

dns-delegation writes into the company's real domain, where Google Workspace mail also lives. A Cloudflare token cannot be scoped to record names — only to a zone — so the credential that writes four NS records could rewrite all of them. Tearing down an environment must not be able to reach that, so the stack that can is not part of an environment, and its token never enters CI.

deriveddestroy order = the reverse of each phase's FIRST occurrence — never written out by hand

guards

Three ways a routine deploy can destroy something running

Each of these is a real incident, not a hypothetical. The fix in every case is that the guard asks AWS, because a guard whose subject is the cloud cannot have the repository as its source of truth.

A fabric-only deploy over a live service

Deploying with no application version once recomputed the first-deploy marker and deleted the ECS service, its task definitions, its role and its egress rules — 19 resources.

Now: ecs describe-services, with three outcomes — a missing cluster fails, a missing service returns an empty list, and only one of those means “fresh environment”.

A config key that names a live resource

Removing two keys would have planned a delete of the CloudFront distribution serving dev, because the code that reads the replacement key does not create the distribution when it is absent.

Now: ask CloudFront for the exact comment the brick composes, and refuse.

A seeder that rotates a live secret

“Seed secrets if absent” wrote unconditionally and irreversibly rotated three live application keys.

Now: a three-outcome check — has a value, has none, or could not be read — and only the middle one writes.

the lessonthe first version of the ECS guard grepped the repository, and dev's committed config recorded imageUri: "" — so it failed OPEN on the one live environment it existed to protect

after every deploy

Why a 200 from the API proves nothing

The distribution rewrites 403 and 404 to index.html with status 200

Correct for a single-page app, and it means a broken API is indistinguishable from a healthy one by status code. A desynchronised cache token once left the API in a reconnect loop while /api/health answered 200 with HTML, and both existing checks passed.

Six in a row, not one

With two tasks behind a load balancer, one broken task answers about half the requests — so a check that exits on its first success passes every time. A retry loop is for waiting on something starting; it must not double as the verdict.

and yethealth answered 6/6 and login returned 200 while every authenticated GET hung — the task had no egress rule to the cache, so Redis timed out forever

assertedcontent type and payload, never the status

measured6/6 consecutive JSON responses
{"success":true,"data":{"status":"ok"}}

assertedno route off the VPC — the one control with no runtime rule in any benchmark, so the pipeline reads the live route tables and treats a non-zero count as an incident

measuredok email [email protected] (confirmed)

That last one was SubscriptionArn: "Deleted" until this week — six security alarms reaching nobody. A confirmed subscription is still not proof: delivery was established by publishing a probe and reading the mailbox.

exercising it before merge

The deploy path runs from a branch, with no approval

workflow_dispatch runs the workflow file from the ref you name — so a branch's own pipeline executes against the real dev estate before anything is merged.

It works because the trust is ref-independent

A job declaring environment: dev presents the same subject whatever branch it came from, so nothing has to be widened for a test run. Pinned to ref:refs/heads/main instead, this would be impossible.

Mirroring the repository is the wrong answer

A mirror has a different repo id, so it presents a different subject — the trust policy would have to be widened to admit it, and the run would measure a relationship production does not have. It also puts the code outside the organisation's ownership and audit, which is not undoable.

runwhat it proved
fabric onlyassume from a feature branch; the live service survived the shape that once deleted it
app versionfour images by digest, schema applied, frontend published, API 6/6
pull requestgate ran; both deploy jobs reported skipped

caveata NEW workflow file must land on the default branch before its first dispatch — edits to an existing one do not

caveata dispatch deploys the REAL estate, so every destructive guard applies — which is the point

where it stands

Running today, and the next link to close

piecestate
Gate on every pull requestrunning
OIDC credential path, devrunning
Seven applies, dev estaterunning
Schema task, frontend publishrunning
Post-deploy assertionsrunning
Alarm delivery to a humanproven
Application publish workflowopen for review — eDossea #626
edossea-app-publish roletemplated, not created
AWS_PUBLISH_ROLE_ARN variablenot set
Prod account bootstrapnot started

The next link, precisely

Every piece is written. The infrastructure side listens for repository_dispatch: app-published, the AWS grant is templated, and the publish workflow itself is open for review as eDossea #626 — it assumes the push-only role by OIDC, pushes four images, then dispatches.

What is missing is two facts, not code: the role does not exist yet (NoSuchEntity), because the bootstrap has never been run, and AWS_PUBLISH_ROLE_ARN — which that run prints — is not set in the application repository. Run the bootstrap, set the variable, merge #626.

Until then the chain is still testable

Dispatching with an existing image tag exercises promote, schema, frontend and service — no application credential involved. That is how the second run above was produced.

What the first real login found

Registering an account through the application's own flow exposed a defect nothing else could: the task had no egress rule to the cache, so every request touching Redis timed out. The cache declared ingress from the application — a security group is two-sided, and only one side existed. Health checks and login never touch Redis, so the fleet read healthy for as long as nobody logged in.

livehttps://compliant-dev.edossea.com