<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Stonebridge Tech Solutions LLC</title>
    <description>The latest articles on DEV Community by Stonebridge Tech Solutions LLC (@stonebridgetechsolutions).</description>
    <link>https://dev.to/stonebridgetechsolutions</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3667756%2F6156d6ea-7b3e-4724-b0a5-f5bb907e9a62.png</url>
      <title>DEV Community: Stonebridge Tech Solutions LLC</title>
      <link>https://dev.to/stonebridgetechsolutions</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/stonebridgetechsolutions"/>
    <language>en</language>
    <item>
      <title>GitLab CI/CD parent/child pipelines for HIPAA workloads</title>
      <dc:creator>Stonebridge Tech Solutions LLC</dc:creator>
      <pubDate>Tue, 26 May 2026 15:58:39 +0000</pubDate>
      <link>https://dev.to/stonebridgetechsolutions/gitlab-cicd-parentchild-pipelines-for-hipaa-workloads-17l8</link>
      <guid>https://dev.to/stonebridgetechsolutions/gitlab-cicd-parentchild-pipelines-for-hipaa-workloads-17l8</guid>
      <description>&lt;p&gt;&lt;em&gt;What moves to the parent, what stays in the child, and why the boundary is itself a compliance control.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;GitLab gives you the strongest parent/child pipeline primitives of any major CI platform. Most HIPAA teams using GitLab still throw the advantage away.&lt;/p&gt;

&lt;p&gt;A healthcare platform team I worked with on GCP had a 1,200-line &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; at the root of their monorepo. It shipped. Tests ran. Containers built. The platform was stable across roughly 32 production VM-backed services that mixed Tomcat, Node.js, and Ubuntu in ways nobody had inventoried. They had a 3PAO assessment scheduled in five weeks. The compliance officer wanted to know how the pipeline answered the Security Rule. The pipeline file was 1,200 lines and growing.&lt;/p&gt;

&lt;p&gt;The pipeline could not, in any useful sense, answer the Security Rule. Production approval gates, scanner evaluations, evidence emission, deploy authorization, and per-service build logic all lived in the same file. Every YAML change risked dropping a compliance gate that nobody noticed was load-bearing. The auditor's question, &lt;em&gt;"show me that production deploys are gated"&lt;/em&gt;, required reading the entire file to answer. We rebuilt the pipeline before the assessment. The change that mattered most wasn't a new scanner or a tightened IAM role. It was the parent/child split.&lt;/p&gt;

&lt;p&gt;This post is the GitLab-specific deep dive on that split. What moves to the parent, what stays in the child, how the artifact contract between them works, and how the pattern handles a polyrepo with five separate repos for backend, frontend, infrastructure, networking, and security. Argo CD comes in briefly at the deploy stage; the depth on that lives in the upcoming Kubernetes platform engineering posts.&lt;/p&gt;

&lt;p&gt;If you've read the broader &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-implementation-guide" rel="noopener noreferrer"&gt;HIPAA CI/CD implementation guide&lt;/a&gt;, this is the GitLab-specific implementation of the parent/child architecture introduced there. The cloud examples here cover GCP because that is where this engagement lived, but the pattern ports directly to AWS with EKS plus IRSA in place of GKE plus Workload Identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 01 — Why GitLab is the strongest HIPAA CI primitive
&lt;/h2&gt;

&lt;p&gt;Three GitLab features make the parent/child split nearly free. Used together, they're the cleanest primitives any major CI platform offers for HIPAA compliance work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;trigger:&lt;/code&gt; keyword.&lt;/strong&gt; A job in the parent pipeline triggers a downstream pipeline (the child), waits for it to complete, and consumes its artifacts. The parent does not need to know how the child builds its service. The child does not need to know which compliance gates the parent enforces. The interface is the artifact bundle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-project triggers.&lt;/strong&gt; The same &lt;code&gt;trigger:&lt;/code&gt; primitive crosses repository boundaries with &lt;code&gt;trigger: project:&lt;/code&gt;. A parent pipeline in a compliance-platform repo can fan out to children in the backend, frontend, infrastructure, networking, and security repos. The compliance team owns the parent; service teams own their respective children. Code review, branch protection, and access controls are scoped per repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tag-based runner targeting.&lt;/strong&gt; Self-hosted runners register with one or more tags. The pipeline's &lt;code&gt;tags:&lt;/code&gt; array determines which runner picks up the job. A job that requires the &lt;code&gt;hipaa-prod-runner&lt;/code&gt; tag is only ever picked up by a runner with that tag registered. Runner registration is itself controlled by GitLab admins. Combined with scoped IAM per runner, the runner tag becomes a structural compliance control, not a procedural one.&lt;/p&gt;

&lt;p&gt;None of these is exotic. All three are built into GitLab CE and have been stable for years. The &lt;a href="https://stonebridgetechsolutions.com/services/hipaa-cicd#architecture" rel="noopener noreferrer"&gt;three architectural decisions at the pillar page&lt;/a&gt; compose them into the audit-ready pattern this post implements.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 02 — What moves to the parent pipeline
&lt;/h2&gt;

&lt;p&gt;The parent pipeline owns environment-level concerns. It changes rarely. The compliance team approves every merge to the repo that holds it. Every job in the parent is tied to a specific Security Rule control.&lt;/p&gt;

&lt;p&gt;Five responsibilities sit in the parent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identity verification.&lt;/strong&gt; Before any child runs, the parent confirms the triggering user is authorized for the target environment. § 164.308(a)(4).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Child orchestration.&lt;/strong&gt; The parent triggers each child pipeline, in order or in parallel, and waits for completion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence aggregation.&lt;/strong&gt; The parent collects signed evidence artifacts from each child into a single bundle, then writes the bundle to the evidence bucket. § 164.312(b).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy gate.&lt;/strong&gt; The parent invokes OPA against the evidence bundle. The gate returns allow or deny. § 164.308(a)(8) and § 164.312(c)(1).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy authorization.&lt;/strong&gt; If the gate allows and a named approver clicks the deploy button, the parent triggers the deploy stage on the prod-only runner. § 164.312(e)(1).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything outside that list belongs in a child. Unit tests are not the parent's job. Container builds are not the parent's job. Lint checks are not the parent's job. The parent owns the gates; the children own the code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# compliance-platform/.gitlab-ci.yml&lt;/span&gt;
&lt;span class="c1"&gt;# Parent pipeline. Owns gates, evidence, deploy authorization.&lt;/span&gt;
&lt;span class="c1"&gt;# Children live in the five service repos and are triggered below.&lt;/span&gt;

&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;authorize&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;trigger-children&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;aggregate-evidence&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;policy-gate&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;HIPAA_ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_BRANCH&lt;/span&gt;
  &lt;span class="na"&gt;EVIDENCE_BUCKET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gs://hipaa-evidence-prod"&lt;/span&gt;

&lt;span class="na"&gt;authorize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;authorize&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google/cloud-sdk:slim&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./scripts/verify-identity.sh "$GITLAB_USER_LOGIN" "$HIPAA_ENVIRONMENT"&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_BRANCH&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"main"'&lt;/span&gt;

&lt;span class="na"&gt;trigger-backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trigger-children&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hipaa-app-org/backend&lt;/span&gt;
    &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;depend&lt;/span&gt;

&lt;span class="na"&gt;trigger-frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trigger-children&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hipaa-app-org/frontend&lt;/span&gt;
    &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;depend&lt;/span&gt;

&lt;span class="na"&gt;trigger-infra&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trigger-children&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hipaa-app-org/infrastructure&lt;/span&gt;
    &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;depend&lt;/span&gt;

&lt;span class="na"&gt;trigger-networking&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trigger-children&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hipaa-app-org/networking&lt;/span&gt;
    &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;depend&lt;/span&gt;

&lt;span class="na"&gt;trigger-security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trigger-children&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hipaa-app-org/security&lt;/span&gt;
    &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;depend&lt;/span&gt;

&lt;span class="na"&gt;aggregate-evidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aggregate-evidence&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google/cloud-sdk:slim&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./scripts/collect-child-evidence.sh "$CI_PIPELINE_ID"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gsutil cp evidence-bundle.json&lt;/span&gt;
        &lt;span class="s"&gt;"$EVIDENCE_BUCKET/parent-$CI_PIPELINE_ID/bundle.json"&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;trigger-backend&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;trigger-frontend&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;trigger-infra&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;trigger-networking&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;trigger-security&lt;/span&gt;

&lt;span class="na"&gt;policy-gate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;policy-gate&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openpolicyagent/opa:0.62&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;opa eval -d policies/ -i evidence-bundle.json&lt;/span&gt;
        &lt;span class="s"&gt;"data.deploy.hipaa.allow" --format=raw | grep -q &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aggregate-evidence"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;deploy-production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hipaa-prod-runner"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
  &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manual&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./scripts/deploy-signed.sh&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy-gate"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire parent. Roughly 60 lines, every job tied to a Security Rule control, every stage doing one thing. The compliance team can read this file in two minutes and tell the auditor exactly which job satisfies which control. When something changes, the change is small and reviewable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 03 — What stays in the child pipeline
&lt;/h2&gt;

&lt;p&gt;The child pipeline owns the service. Every service repo has its own &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;. The service team owns the file. They can change it freely as long as the artifact contract back to the parent stays intact.&lt;/p&gt;

&lt;p&gt;The child's responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build.&lt;/strong&gt; Compile, package, produce a container image with a tagged digest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test.&lt;/strong&gt; Unit, integration, contract. Whatever the service team needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scan.&lt;/strong&gt; SAST, container CVE, IaC, secret, dependency. Each scan produces a signed evidence artifact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sign.&lt;/strong&gt; Cosign signs the image with a KMS-backed key. The signature is itself an artifact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emit evidence.&lt;/strong&gt; Every scan output, every signature, every build event is written into the artifact bundle the parent will collect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The child does not deploy. The child does not approve. The child does not decide whether a scan result is acceptable. Those are parent concerns. Keeping the boundary clean is what lets each child evolve independently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# backend/.gitlab-ci.yml&lt;/span&gt;
&lt;span class="c1"&gt;# Child pipeline. Service team owns this file.&lt;/span&gt;
&lt;span class="c1"&gt;# Triggered by the parent in compliance-platform.&lt;/span&gt;

&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;scan&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sign&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;emit&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;IMAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-east1-docker.pkg.dev/hipaa-app-prod/backend/backend"&lt;/span&gt;

&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hipaa-build-runner"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker buildx build -t $IMAGE:$CI_COMMIT_SHA --output type=docker .&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker push $IMAGE:$CI_COMMIT_SHA&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;reports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;dotenv&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build.env&lt;/span&gt;

&lt;span class="na"&gt;scan-container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scan&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hipaa-build-runner"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasec/trivy:latest&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;trivy image --severity HIGH,CRITICAL --format json&lt;/span&gt;
        &lt;span class="s"&gt;--output trivy.json $IMAGE@$IMAGE_DIGEST&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;trivy.json&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;reports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;container_scanning&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trivy.json&lt;/span&gt;

&lt;span class="na"&gt;scan-sast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scan&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hipaa-build-runner"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;returntocorp/semgrep:latest&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;semgrep ci --config=p/hipaa --json --output=semgrep.json&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;semgrep.json&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;reports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;sast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;semgrep.json&lt;/span&gt;

&lt;span class="na"&gt;sign&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sign&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hipaa-build-runner"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cosign sign --key gcpkms://projects/hipaa-app-prod/locations/us-east1/keyRings/hipaa/cryptoKeys/signing&lt;/span&gt;
        &lt;span class="s"&gt;$IMAGE@$IMAGE_DIGEST&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cosign verify --key gcpkms://...signing $IMAGE@$IMAGE_DIGEST &amp;gt; signature.json&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;signature.json&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;emit-evidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;emit&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hipaa-build-runner"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./scripts/bundle-evidence.sh&lt;/span&gt;
        &lt;span class="s"&gt;trivy.json semgrep.json signature.json&lt;/span&gt;
        &lt;span class="s"&gt;&amp;gt; child-evidence.json&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;child-evidence.json&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;expire_in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30 days&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's not here. No deploy job. No approval gate. No reference to a production environment. The child does not know it is part of a HIPAA pipeline; it produces evidence and exposes it as artifacts. The parent does the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 04 — The artifact contract between parent and child
&lt;/h2&gt;

&lt;p&gt;The boundary between parent and child is the artifact contract. The parent triggers the child with &lt;code&gt;strategy: depend&lt;/code&gt;; the parent's job stays in &lt;code&gt;running&lt;/code&gt; state until the child finishes and exposes its artifacts. The parent then collects those artifacts and aggregates them.&lt;/p&gt;

&lt;p&gt;Two GitLab artifact mechanisms carry the contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;artifacts:reports:dotenv&lt;/code&gt;&lt;/strong&gt; propagates structured variables (like &lt;code&gt;IMAGE_DIGEST&lt;/code&gt;) from one job to the next in the same pipeline. The parent can read these once the trigger job completes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;artifacts:reports:&amp;lt;type&amp;gt;&lt;/code&gt;&lt;/strong&gt; (container_scanning, sast, secret_detection, dependency_scanning) are typed report formats GitLab understands. The parent's evidence collection script reads them from the child's pipeline directly via the GitLab API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The parent's &lt;code&gt;collect-child-evidence.sh&lt;/code&gt; script walks the API: list child pipelines triggered by this parent, list each child's jobs, fetch the artifact archives, extract the structured reports, and write the combined JSON bundle to the evidence bucket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# scripts/collect-child-evidence.sh&lt;/span&gt;
&lt;span class="c"&gt;# Walks the GitLab API to assemble the evidence bundle from&lt;/span&gt;
&lt;span class="c"&gt;# every child pipeline triggered by this parent run.&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;PARENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://gitlab.com/api/v4"&lt;/span&gt;
&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EVIDENCE_READ_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Find every downstream pipeline this parent triggered&lt;/span&gt;
&lt;span class="nv"&gt;CHILD_IDS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API&lt;/span&gt;&lt;span class="s2"&gt;/projects/&lt;/span&gt;&lt;span class="nv"&gt;$CI_PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;/pipelines/&lt;/span&gt;&lt;span class="nv"&gt;$PARENT_ID&lt;/span&gt;&lt;span class="s2"&gt;/bridges"&lt;/span&gt; |
  jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[].downstream_pipeline.id'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;bundle&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"{}"&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;CHILD &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$CHILD_IDS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API&lt;/span&gt;&lt;span class="s2"&gt;/projects/&lt;/span&gt;&lt;span class="nv"&gt;$CI_PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;/pipelines/&lt;/span&gt;&lt;span class="nv"&gt;$CHILD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$meta&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.project_id'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  curl &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API&lt;/span&gt;&lt;span class="s2"&gt;/projects/&lt;/span&gt;&lt;span class="nv"&gt;$project&lt;/span&gt;&lt;span class="s2"&gt;/jobs/artifacts/main/raw/child-evidence.json?job=emit-evidence"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"child-&lt;/span&gt;&lt;span class="nv"&gt;$CHILD&lt;/span&gt;&lt;span class="s2"&gt;.json"&lt;/span&gt;

  &lt;span class="nv"&gt;service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.service'&lt;/span&gt; &lt;span class="s2"&gt;"child-&lt;/span&gt;&lt;span class="nv"&gt;$CHILD&lt;/span&gt;&lt;span class="s2"&gt;.json"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;bundle&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$bundle&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--slurpfile&lt;/span&gt; child &lt;span class="s2"&gt;"child-&lt;/span&gt;&lt;span class="nv"&gt;$CHILD&lt;/span&gt;&lt;span class="s2"&gt;.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--arg&lt;/span&gt; svc &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s1"&gt;'.[$svc] = $child[0]'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;done

&lt;/span&gt;&lt;span class="nv"&gt;bundle&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$bundle&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--arg&lt;/span&gt; pipeline &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PARENT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--arg&lt;/span&gt; actor &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITLAB_USER_LOGIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--arg&lt;/span&gt; &lt;span class="nb"&gt;env&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HIPAA_ENVIRONMENT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--arg&lt;/span&gt; ts &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%Y-%m-%dT%H:%M:%SZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'. + {parent_pipeline_id: $pipeline, actor: $actor, environment: $env, timestamp: $ts}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$bundle&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; evidence-bundle.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script is intentionally readable. Forty lines an auditor can walk in five minutes. Every API call uses a scoped token that can only read pipeline metadata and job artifacts; the parent cannot modify anything in the child repos. The output bundle is the input to OPA, and it's also what gets written to the evidence bucket with Object Lock. The chain of custody is queryable months later: parent pipeline ID joins to actor, joins to artifact digest, joins to scanner results, joins to deploy outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 05 — Multi-project pipelines for a five-repo polyrepo
&lt;/h2&gt;

&lt;p&gt;The team this pattern came from split their code across five repos: backend, frontend, infrastructure (Terraform), networking (VPC / firewall as code), and security (IAM, KMS, policy bundles). The split matched their team boundaries. The compliance constraint was that every deploy to production had to evaluate evidence from every repo, in the same run, before the gate opened.&lt;/p&gt;

&lt;p&gt;Multi-project pipelines handle this cleanly. The parent in &lt;code&gt;compliance-platform&lt;/code&gt; triggers a child in each of the five repos via &lt;code&gt;trigger: project:&lt;/code&gt;. With &lt;code&gt;strategy: depend&lt;/code&gt;, the parent waits for every child to complete before moving to evidence aggregation. The five children run in parallel; the slowest determines the total wait.&lt;/p&gt;

&lt;p&gt;Three behaviors of multi-project triggers matter for HIPAA:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Token scope.&lt;/strong&gt; The trigger uses a CI job token from the parent. The downstream repo's CI/CD settings explicitly list which repos can trigger pipelines via job token. A repo outside that allow-list cannot trigger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branch pinning.&lt;/strong&gt; The parent triggers &lt;code&gt;branch: main&lt;/code&gt; on each child. A feature branch on a child cannot be triggered from the production parent pipeline. The child can be tested independently on feature branches; production deploys always come from main.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artifact passthrough.&lt;/strong&gt; Artifacts from child pipelines are reachable via the GitLab API using the parent's scoped read token. The parent does not need write access to any child repo; it only needs to read artifacts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A small note on the monorepo-vs-polyrepo question. For healthcare teams under 30 engineers, my default is monorepo plus &lt;code&gt;include:&lt;/code&gt;. The polyrepo split makes sense when the team boundaries are firm, code review ownership is genuinely separable, and each repo is large enough on its own that combining them slows everyone down. The team in this engagement was past that threshold; their five repos already existed for organizational reasons unrelated to CI. The parent/child pattern works either way. Don't split repos to support the pattern; the pattern supports whichever split you already have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 06 — Compliance runner tags as a deploy boundary
&lt;/h2&gt;

&lt;p&gt;GitLab self-hosted runners register with one or more tags. The pipeline declares which tags it needs. GitLab routes the job to a runner whose tags match. The tag becomes a structural compliance control because the alternative requires an admin to register a new runner.&lt;/p&gt;

&lt;p&gt;Three runner pools in this engagement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hipaa-build-runner&lt;/code&gt;&lt;/strong&gt; handles every child pipeline job: build, test, scan, sign, emit. Read-only access to the artifact registry; no production IAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hipaa-evidence-runner&lt;/code&gt;&lt;/strong&gt; handles the parent's evidence aggregation and policy gate. Read access to the GitLab API and write access to the evidence bucket. No deploy permissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hipaa-prod-runner&lt;/code&gt;&lt;/strong&gt; handles only the deploy stage. Has the production IAM role bound via Workload Identity. Used only when the policy gate has already allowed and a named approver has clicked.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/gitlab-runner/config.toml on the prod-only runner host&lt;/span&gt;

&lt;span class="nn"&gt;[[runners]]&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"hipaa-prod-runner-01"&lt;/span&gt;
  &lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://gitlab.com/"&lt;/span&gt;
  &lt;span class="py"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"REDACTED"&lt;/span&gt;
  &lt;span class="py"&gt;executor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"kubernetes"&lt;/span&gt;

  &lt;span class="nn"&gt;[runners.kubernetes]&lt;/span&gt;
    &lt;span class="py"&gt;namespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gitlab-runners-prod"&lt;/span&gt;
    &lt;span class="py"&gt;service_account&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"hipaa-prod-runner-sa"&lt;/span&gt;
    &lt;span class="py"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"registry.example.com/runner-images/hipaa:v1.4.2"&lt;/span&gt;
    &lt;span class="py"&gt;pull_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"always"&lt;/span&gt;
    &lt;span class="nn"&gt;[runners.kubernetes.node_selector]&lt;/span&gt;
      &lt;span class="py"&gt;"stonebridge.io/runner-pool"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"hipaa-prod"&lt;/span&gt;
    &lt;span class="nn"&gt;[[runners.kubernetes.volumes.secret]]&lt;/span&gt;
      &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"hipaa-prod-kms-rolebinding"&lt;/span&gt;
      &lt;span class="py"&gt;mount_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/etc/runner/kms"&lt;/span&gt;
      &lt;span class="py"&gt;read_only&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runner image is signed and version-pinned. The pod runs on a tainted node pool that only accepts pods tolerating the runner taint. The runner's pod service account is bound to a GCP service account via Workload Identity; the GCP SA can deploy to the prod GKE cluster and nothing else.&lt;/p&gt;

&lt;p&gt;The combined effect: a job in the parent that declares &lt;code&gt;tags: ["hipaa-prod-runner"]&lt;/code&gt; can only land on this runner, which can only deploy to one cluster, which can only happen after the policy gate allowed. The pipeline YAML cannot lift its own privileges.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 07 — Where Argo CD comes in
&lt;/h2&gt;

&lt;p&gt;The parent pipeline above triggers a deploy by calling &lt;code&gt;kubectl&lt;/code&gt; directly. For most HIPAA engagements past a certain size, I move that deploy step to Argo CD with GitOps semantics. The parent pipeline writes a signed manifest update to a config repo; Argo CD reconciles the cluster against the config repo; the deploy event lands in Argo CD's audit log as well as the parent's evidence bundle.&lt;/p&gt;

&lt;p&gt;The HIPAA-relevant property is that the running cluster state is continuously verified against the config repo, not just at deploy time. Drift detection becomes a control rather than a periodic check.&lt;/p&gt;

&lt;p&gt;I'll cover the Argo CD side in depth in upcoming Kubernetes platform engineering posts. For the parent/child architecture, what matters is that the deploy stage's interface stays the same: the parent emits a signed deploy event, the evidence bundle records it, and downstream consumers can reconcile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 08 — What the rebuild looked like at the 32-host platform
&lt;/h2&gt;

&lt;p&gt;The team I opened with had 32 production VM-backed services on GCP, a 1,200-line monorepo pipeline, and five weeks before their 3PAO assessment. Their compliance officer wanted to know which hosts complied with the required baseline (Tomcat 9.0.62+, Node.js 18+, Ubuntu 20.04+). They had not had the cycles to walk every host.&lt;/p&gt;

&lt;p&gt;We split the engagement in two. Track one was the host inventory: an Ansible plus Python tool that connected to each VM through GCP Identity-Aware Proxy, scraped the running versions, and produced a compliance matrix. The compliance team had the inventory inside a week. Track two was the pipeline rebuild.&lt;/p&gt;

&lt;p&gt;The pipeline rebuild took four weeks. We split the 1,200-line file into a 60-line parent in a new &lt;code&gt;compliance-platform&lt;/code&gt; repo and five children in their existing service repos (backend, frontend, infrastructure, networking, security). The polyrepo split predated us; the team had organized along those lines a year earlier for code review reasons. The parent/child pattern took advantage of the split instead of fighting it.&lt;/p&gt;

&lt;p&gt;Three structural changes carried the audit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The parent owns the gates. Identity verification, evidence aggregation, OPA policy, deploy authorization. The compliance team approves merges to this repo. Service teams have no write access.&lt;/li&gt;
&lt;li&gt;Children own their service. Build, test, scan, sign, emit. Service teams have full control. Changes to the child pipeline don't risk dropping a compliance gate because the gates live in the parent.&lt;/li&gt;
&lt;li&gt;Three runner pools with scoped IAM. The build runner has no production IAM. The evidence runner has no deploy IAM. The prod runner has no read access to other clusters. A dev pipeline cannot reach production via any path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The audit cleared on first-party review. The next quarterly internal review passed without remediation work because the architecture made future regressions structurally difficult. The 32-host inventory tool became part of the compliance team's continuous monitoring practice. The pipeline rebuild was the lasting change.&lt;/p&gt;

&lt;p&gt;The same pattern is what we recommend on every GitLab-based HIPAA engagement now, regardless of cloud. The shape stays constant; the runner infrastructure and the deploy target swap by cloud.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 09 — Tooling recommendations
&lt;/h2&gt;

&lt;p&gt;Opinionated picks for GitLab on HIPAA. The architecture matters more than the tool.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Recommended&lt;/th&gt;
&lt;th&gt;Acceptable&lt;/th&gt;
&lt;th&gt;Avoid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline structure&lt;/td&gt;
&lt;td&gt;Parent in compliance repo, children in service repos&lt;/td&gt;
&lt;td&gt;Parent + children in same monorepo via &lt;code&gt;include:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Single thousand-line &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-repo orchestration&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;trigger: project:&lt;/code&gt; with &lt;code&gt;strategy: depend&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Pipeline scheduling via API&lt;/td&gt;
&lt;td&gt;Cron jobs that hope children are fresh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artifact contract&lt;/td&gt;
&lt;td&gt;Structured &lt;code&gt;reports:&lt;/code&gt; (container_scanning, sast, dotenv)&lt;/td&gt;
&lt;td&gt;JSON artifacts collected via API&lt;/td&gt;
&lt;td&gt;Logs scraped from job traces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runner isolation&lt;/td&gt;
&lt;td&gt;Three runner pools per environment, tag-enforced&lt;/td&gt;
&lt;td&gt;Two pools (build, deploy) with scoped IAM&lt;/td&gt;
&lt;td&gt;One shared runner pool with broad IAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy gate&lt;/td&gt;
&lt;td&gt;OPA in the parent, evidence bundle as input&lt;/td&gt;
&lt;td&gt;Conftest in a required job&lt;/td&gt;
&lt;td&gt;Slack notification, advisory only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy&lt;/td&gt;
&lt;td&gt;Argo CD pulling from config repo&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kubectl&lt;/code&gt; in the prod runner&lt;/td&gt;
&lt;td&gt;SSH and shell scripts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evidence storage&lt;/td&gt;
&lt;td&gt;GCS Bucket Lock or S3 Object Lock, 6-year retention&lt;/td&gt;
&lt;td&gt;Versioned bucket with retention policy&lt;/td&gt;
&lt;td&gt;GitLab artifact storage alone&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Section 10 — Common mistakes to avoid
&lt;/h2&gt;

&lt;p&gt;Five quick callouts. Each one shows up in real GitLab pipelines I'm called in to fix.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Thousand-line &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;.&lt;/strong&gt; Refactor to parent/child before the file passes 500 lines. Past 1,000 it's an audit finding waiting to surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Children pinned to &lt;code&gt;branch: main&lt;/code&gt; from the parent but no protection on main.&lt;/strong&gt; If anyone can push to &lt;code&gt;main&lt;/code&gt; on the child, the parent can be tricked into running unreviewed code. Branch protection on every child's &lt;code&gt;main&lt;/code&gt; is non-negotiable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared runner pool with broad IAM.&lt;/strong&gt; Three pools per environment, scoped IAM, no exceptions. A build runner with prod IAM is the most common audit finding I see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artifact bundles assembled inline in YAML.&lt;/strong&gt; Move the assembly to a versioned script in the compliance repo. The script is reviewable; inline YAML rotting over months is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The parent triggering children by branch name from the YAML.&lt;/strong&gt; Hardcode &lt;code&gt;main&lt;/code&gt; in the parent. A misconfigured pipeline shouldn't be able to point at an unreviewed branch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The longer-form versions of these failure modes live in &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-5-mistakes" rel="noopener noreferrer"&gt;five patterns that fail HIPAA audits&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 11 — Conclusion
&lt;/h2&gt;

&lt;p&gt;GitLab's parent/child primitives are the strongest any major CI platform offers for HIPAA work. The teams that use them well treat the boundary between parent and child as a compliance control, not a stylistic preference.&lt;/p&gt;

&lt;p&gt;The parent owns the gates: identity verification, evidence aggregation, policy gating, deploy authorization. The compliance team owns the parent. The children own the services: build, test, scan, sign, emit. Service teams own each child. The artifact contract between them is the only interface. Tag-scoped runners make the deploy boundary structural; multi-project triggers handle polyrepo splits without compromising the gates.&lt;/p&gt;

&lt;p&gt;Build the pipeline this way and the auditor's questions become queries against the evidence bundle. Build it the other way and you spend the next assessment cycle rebuilding what you should have built once.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you're running GitLab in a HIPAA environment:&lt;/strong&gt; Stonebridge runs &lt;a href="https://stonebridgetechsolutions.com/services/hipaa-cicd#engagements" rel="noopener noreferrer"&gt;two-week HIPAA CI/CD audits&lt;/a&gt; that map your existing GitLab pipeline against the Security Rule and produce a written remediation roadmap. Fixed fee, founder-led, the report holds up under first-party review by your auditor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep reading:&lt;/strong&gt; the broader architecture pattern across CI platforms is in the &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-implementation-guide" rel="noopener noreferrer"&gt;HIPAA CI/CD implementation guide&lt;/a&gt;. The parallel post for teams on GitHub Actions is &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-github-actions" rel="noopener noreferrer"&gt;GitHub Actions for HIPAA-compliant deployments&lt;/a&gt;. The pre-audit walkthrough of every Security Rule control is in &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-audit-checklist" rel="noopener noreferrer"&gt;the HIPAA CI/CD audit checklist&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;Lucas Jones is the Founder and Principal Platform Engineer at &lt;a href="https://stonebridgetechsolutions.com/about" rel="noopener noreferrer"&gt;Stonebridge Tech Solutions&lt;/a&gt;. Six years building cloud infrastructure and CI/CD pipelines in regulated environments, including HIPAA, FedRAMP, and SOC 2 work for healthcare and defense engineering teams across AWS, GCP, Azure, and OCI. Based in Sacramento, California.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post originally appeared on &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-gitlab-parent-child" rel="noopener noreferrer"&gt;Stonebridge Tech Solutions&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hipaa</category>
      <category>gitlab</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>GitHub Actions for HIPAA-compliant deployments</title>
      <dc:creator>Stonebridge Tech Solutions LLC</dc:creator>
      <pubDate>Thu, 21 May 2026 20:39:25 +0000</pubDate>
      <link>https://dev.to/stonebridgetechsolutions/github-actions-for-hipaa-compliant-deployments-4kp8</link>
      <guid>https://dev.to/stonebridgetechsolutions/github-actions-for-hipaa-compliant-deployments-4kp8</guid>
      <description>&lt;p&gt;&lt;em&gt;OIDC trust scope, self-hosted runner discipline, reusable workflows as the compliance contract.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most "GitHub Actions for HIPAA" content reads like generic CI security with HIPAA labels pasted on top. This one is platform-specific.&lt;/p&gt;

&lt;p&gt;A healthcare SaaS team I worked with earlier this year had six weeks to make their GitHub Actions pipeline audit-ready. They were a clinical workflow platform on AWS, four squads, roughly 90 active workflow files spread across a primary application repo and three sibling repos for infrastructure, data, and integrations. They had passed SOC 2 Type II the prior year. They had just closed their first hospital system contract and the BAA addendum had landed on engineering with a familiar one-line note from legal: "should be fine."&lt;/p&gt;

&lt;p&gt;It wasn't fine. Their GitHub Actions pipeline was clean by SOC 2 standards. By HIPAA standards the auditor could ask three questions that the pipeline couldn't answer in seconds. Which named human approved this production deploy? Which signing key produced the artifact running in the prod cluster? What happens if a critical CVE shows up during a deploy? None of those answers were structurally encoded in the workflows. They were tribal knowledge spread across Slack and a shared Notion page.&lt;/p&gt;

&lt;p&gt;Three GitHub-specific decisions separate a HIPAA-aligned GitHub Actions pipeline from a SOC 2 one. The OIDC trust scope. The runner labeling discipline. The reusable workflow boundary as the compliance contract. The rest of this post is each of those, with the AWS code that makes them work, plus the policy gate that ties them together.&lt;/p&gt;

&lt;p&gt;If you've read the broader &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-implementation-guide" rel="noopener noreferrer"&gt;HIPAA CI/CD implementation guide&lt;/a&gt;, this is the GitHub Actions-specific deep dive on the same architectural pattern. The parent/child concept ports cleanly; GitHub calls it reusable workflows. The runner isolation pattern ports cleanly; GitHub calls it self-hosted runner labels. The cloud examples here cover AWS specifically because that is where most US healthcare SaaS GitHub Actions deployments live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 01 — What HIPAA actually needs from a GitHub Actions pipeline
&lt;/h2&gt;

&lt;p&gt;HIPAA's Security Rule doesn't mention GitHub Actions. It also doesn't mention pipelines. It specifies safeguards. A correctly built GitHub Actions pipeline satisfies those safeguards continuously, instead of producing them as quarterly evidence runs before audit windows.&lt;/p&gt;

&lt;p&gt;Five controls touch the pipeline most directly. The wording matters; auditors quote the regulation back at you, not the vendor checklist.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;§ 164.308(a)(5)(ii)(C) Log-in monitoring.&lt;/strong&gt; Every deployment is attributable to an authenticated identity with audited access. GitHub Actions translates this to OIDC tokens issued to specific workflow paths, not long-lived secrets in repository settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;§ 164.308(a)(8) Periodic evaluation.&lt;/strong&gt; Security evaluations happen on every change. Scanners and policy evaluations run on every push, not on a cron the platform team forgets to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;§ 164.312(b) Audit controls.&lt;/strong&gt; The pipeline records who deployed what, when, against which approval chain. GitHub's &lt;code&gt;workflow_run&lt;/code&gt; history is not the audit log; it's a UI on top of one. The HIPAA audit log lives in S3 with Object Lock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;§ 164.312(c)(1) Integrity controls.&lt;/strong&gt; Artifacts are signed, signatures are verified before deployment, and tampering is structurally detectable. Cosign signing inside a reusable workflow that the application repo cannot override.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;§ 164.312(e)(1) Transmission security.&lt;/strong&gt; Deploys to PHI-bearing environments use mutually authenticated, encrypted channels. No bearer tokens to production, no plain HTTP anywhere on the path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are exotic. All of them are routinely missed in pipelines built without HIPAA in mind from day one. The framing that helps most: HIPAA tells you what evidence the pipeline must produce. Once you accept that, the architecture follows. The &lt;a href="https://stonebridgetechsolutions.com/services/hipaa-cicd#architecture" rel="noopener noreferrer"&gt;full control mapping at the pillar page&lt;/a&gt; covers each Security Rule section against a specific pipeline touchpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 02 — Three GitHub-specific gaps
&lt;/h2&gt;

&lt;p&gt;The architecture maps cleanly across CI platforms. The gaps don't. Three places GitHub Actions makes HIPAA harder than GitLab or Argo CD, and what to do about each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OIDC trust scope.&lt;/strong&gt; The default GitHub OIDC subject claim is &lt;code&gt;repo:org/name:ref:refs/heads/main&lt;/code&gt; or similar. The default sample IAM trust policy that everyone copies trusts the entire org. A misconfigured trust policy gives any workflow in your org a credential path to production. Scope the trust to a specific repo, a specific workflow file, and a specific environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runner labels as the only deploy boundary.&lt;/strong&gt; Self-hosted runners are addressed by label. There is no platform-enforced separation between a runner labeled &lt;code&gt;prod&lt;/code&gt; and a runner labeled &lt;code&gt;prod-staging&lt;/code&gt; if both are registered to the same org. The compliance boundary is the label, plus the runner's IAM trust, plus the workflow's &lt;code&gt;runs-on:&lt;/code&gt; clause. Get any one of those wrong and a dev pipeline can target a prod runner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reusable workflow drift.&lt;/strong&gt; Reusable workflows (&lt;code&gt;workflow_call&lt;/code&gt;) are the GitHub-native parent/child pattern. They work well. The drift mode is teams write reusable workflows, then let application repos copy the YAML inline "just this once" because the reusable workflow doesn't yet support some new step. After three of those, the compliance contract has rotted. Lock the reusable workflow into a repo the application teams cannot write to.&lt;/p&gt;

&lt;p&gt;Each of these has a fix. The next three sections walk them with code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 03 — OIDC federation, scoped to one workflow
&lt;/h2&gt;

&lt;p&gt;OIDC federation between GitHub Actions and AWS removes the entire category of long-lived credentials in repository secrets. No more &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; rotated quarterly by a developer who remembers. The runner exchanges its GitHub-issued JWT for an STS session every time the workflow runs. The session is short-lived, attributable, and audit-logged in CloudTrail with the workflow identity intact.&lt;/p&gt;

&lt;p&gt;The trust policy is the load-bearing piece. Most sample policies you'll find trust the entire org, which is too broad for HIPAA. The auditor will look at the trust policy and ask "what stops a dev branch from assuming this role?" The honest answer needs to be a &lt;code&gt;StringEquals&lt;/code&gt; on the subject claim, scoped to one repo, one workflow file, and one environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Terraform: HIPAA-aligned GitHub OIDC trust for AWS&lt;/span&gt;

&lt;span class="c1"&gt;# One OIDC provider per AWS account. Hosted by GitHub; the&lt;/span&gt;
&lt;span class="c1"&gt;# thumbprint changes rarely. Keep this in the security account.&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_openid_connect_provider"&lt;/span&gt; &lt;span class="s2"&gt;"github"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://token.actions.githubusercontent.com"&lt;/span&gt;
  &lt;span class="nx"&gt;client_id_list&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sts.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;thumbprint_list&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"6938fd4d98bab03faadb97b34396831e3780aea1"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Production deploy role. Scoped to ONE repo, ONE workflow file,&lt;/span&gt;
&lt;span class="c1"&gt;# ONE environment. A dev branch cannot assume this role no matter&lt;/span&gt;
&lt;span class="c1"&gt;# what YAML it writes.&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"hipaa_prod_deploy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hipaa-prod-deploy"&lt;/span&gt;

  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Federated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_openid_connect_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRoleWithWebIdentity"&lt;/span&gt;
      &lt;span class="nx"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;StringEquals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="s2"&gt;"token.actions.githubusercontent.com:aud"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts.amazonaws.com"&lt;/span&gt;
          &lt;span class="c1"&gt;# Subject is the load-bearing scope. Pin to:&lt;/span&gt;
          &lt;span class="c1"&gt;#   - exact repo&lt;/span&gt;
          &lt;span class="c1"&gt;#   - exact environment (GitHub environment, not branch)&lt;/span&gt;
          &lt;span class="c1"&gt;# No wildcards. A wildcard here is the audit finding.&lt;/span&gt;
          &lt;span class="s2"&gt;"token.actions.githubusercontent.com:sub"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="s2"&gt;"repo:hipaa-app-org/clinical-platform:environment:production"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;StringLike&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;# Belt and suspenders: explicit job_workflow_ref check&lt;/span&gt;
          &lt;span class="s2"&gt;"token.actions.githubusercontent.com:job_workflow_ref"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="s2"&gt;"hipaa-app-org/compliance-workflows/.github/workflows/deploy-prod.yml@refs/tags/v*"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two pieces that matter. First, the subject claim pins the role to a single repo and a single GitHub environment (which is itself behind required reviewers; we'll get to that). Second, the &lt;code&gt;job_workflow_ref&lt;/code&gt; condition pins to a specific tagged version of a reusable workflow living in a separate &lt;code&gt;compliance-workflows&lt;/code&gt; repo. That second condition is what stops a developer from writing inline deploy steps in the application repo and bypassing the reusable workflow.&lt;/p&gt;

&lt;p&gt;The workflow side is short. The role assumption happens once per job, with no static credentials anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/deploy-prod.yml (excerpt)&lt;/span&gt;
&lt;span class="c1"&gt;# Lives in the compliance-workflows repo, pinned by tag in the&lt;/span&gt;
&lt;span class="c1"&gt;# application repo's caller workflow. Application teams cannot&lt;/span&gt;
&lt;span class="c1"&gt;# modify this file.&lt;/span&gt;

&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HIPAA production deploy&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_call&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image_digest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;target_cluster&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;self-hosted&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;hipaa-prod&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;linux&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;x64&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Assume scoped prod role&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::111122223333:role/hipaa-prod-deploy&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
          &lt;span class="na"&gt;role-session-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gha-${{ github.run_id }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify image signature&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;cosign verify \&lt;/span&gt;
            &lt;span class="s"&gt;--certificate-identity-regexp \&lt;/span&gt;
              &lt;span class="s"&gt;"https://github.com/hipaa-app-org/compliance-workflows/.github/workflows/build-sign.yml@.*" \&lt;/span&gt;
            &lt;span class="s"&gt;--certificate-oidc-issuer https://token.actions.githubusercontent.com \&lt;/span&gt;
            &lt;span class="s"&gt;${{ inputs.image_digest }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy by digest, not tag&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;aws eks update-kubeconfig --name ${{ inputs.target_cluster }}&lt;/span&gt;
          &lt;span class="s"&gt;kubectl set image deployment/hipaa-app \&lt;/span&gt;
            &lt;span class="s"&gt;hipaa-app=${{ inputs.image_digest }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deploy uses the image digest, not the tag, so a race between tag-and-deploy can't substitute a different image. The cosign verification is keyless, using Sigstore's Fulcio identity tied to the building workflow's OIDC subject. The certificate identity regex restricts trusted signers to the &lt;code&gt;compliance-workflows&lt;/code&gt; repo's build workflow. A signature from any other workflow fails verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 04 — Self-hosted runners on hardened infrastructure
&lt;/h2&gt;

&lt;p&gt;Take the strong position here: GitHub-hosted runners are not appropriate for the deploy stage of a HIPAA pipeline. They're acceptable for build and scan, where no production credentials are in play. They are not acceptable for the job that holds the production OIDC role.&lt;/p&gt;

&lt;p&gt;The reasoning isn't about GitHub's security posture. It's about evidence shape. A GitHub-hosted runner is shared infrastructure outside your BAA scope. CloudTrail records the AWS API calls. CloudTrail does not record what else ran on that runner in the same job. The host is ephemeral, the logs are GitHub's, the network egress is GitHub's. If an auditor asks "show me that the runner that performed this deploy was within your BAA-covered infrastructure," you have no answer for GitHub-hosted.&lt;/p&gt;

&lt;p&gt;For PHI-touching deploys, run on self-hosted runners in your own AWS account, on EKS, with IRSA scoping the runner pods to specific IAM roles. The runner labels become the deploy boundary. The &lt;code&gt;runs-on:&lt;/code&gt; clause in the workflow becomes the contract that says "this job runs only on a runner labeled hipaa-prod, which only exists in the prod runner pool."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Terraform: HIPAA prod runner pool on EKS&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_eks_node_group"&lt;/span&gt; &lt;span class="s2"&gt;"hipaa_prod_runners"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cluster_name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_eks_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hipaa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;node_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hipaa-prod-runners"&lt;/span&gt;
  &lt;span class="nx"&gt;node_role_arn&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runner_node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_ids&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_ids&lt;/span&gt;

  &lt;span class="nx"&gt;scaling_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;desired_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="nx"&gt;min_size&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="nx"&gt;max_size&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# Taints keep general workloads off the runner pool&lt;/span&gt;
  &lt;span class="nx"&gt;taint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"workload"&lt;/span&gt;
    &lt;span class="nx"&gt;value&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hipaa-runner"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NO_SCHEDULE"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;labels&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"stonebridge.io/runner-pool"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hipaa-prod"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;ami_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"BOTTLEROCKET_x86_64"&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Compliance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HIPAA"&lt;/span&gt;
    &lt;span class="nx"&gt;Boundary&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"production"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# IRSA: the runner pod's SA can assume a runner role.&lt;/span&gt;
&lt;span class="c1"&gt;# The runner role can in turn assume the deploy role,&lt;/span&gt;
&lt;span class="c1"&gt;# but only from this specific namespace + SA.&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"runner_irsa"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hipaa-prod-runner-irsa"&lt;/span&gt;

  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Federated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_openid_connect_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRoleWithWebIdentity"&lt;/span&gt;
      &lt;span class="nx"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;StringEquals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="s2"&gt;"${replace(aws_iam_openid_connect_provider.eks.url, "&lt;/span&gt;&lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//", "")}:sub" =&lt;/span&gt;
            &lt;span class="s2"&gt;"system:serviceaccount:gha-runners:hipaa-prod-runner"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter. The taint keeps general workloads off the runner pool, so the prod-runner host isn't co-tenanted with dev jobs. The IRSA trust policy scopes the runner pod's IAM to one namespace and one service account. A dev workflow that tries to schedule onto these nodes via a forged &lt;code&gt;runs-on:&lt;/code&gt; value fails because the dev runner pool has a different label and a different IRSA chain.&lt;/p&gt;

&lt;p&gt;The runner registration itself is gated at the GitHub org level: in &lt;code&gt;Org Settings → Actions → Runner groups&lt;/code&gt;, the &lt;code&gt;hipaa-prod&lt;/code&gt; runner group is restricted to a specific GitHub team's repos. A repo outside the team cannot target the label even if a developer reads it from a Slack screenshot and tries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 05 — Reusable workflows as the compliance contract
&lt;/h2&gt;

&lt;p&gt;Reusable workflows are GitHub's native parent/child pattern. The application repo's caller workflow handles build-time decisions; the reusable workflow in &lt;code&gt;compliance-workflows&lt;/code&gt; handles every compliance-relevant step: signing, scanning, evidence emission, deploy.&lt;/p&gt;

&lt;p&gt;The compliance contract is the input shape. The reusable workflow defines exactly which inputs it accepts. The caller workflow provides them. Anything else, including any inline shell that wants to bypass a gate, is structurally invisible to the reusable workflow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Caller in the application repo: .github/workflows/release.yml&lt;/span&gt;
&lt;span class="c1"&gt;# Application teams own this file. They cannot bypass the gates;&lt;/span&gt;
&lt;span class="c1"&gt;# the gates live in the reusable workflow they invoke.&lt;/span&gt;

&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Release to prod&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v*.*.*'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build_and_sign&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hipaa-app-org/compliance-workflows/.github/workflows/build-sign.yml@v3.2.1&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inherit&lt;/span&gt;

  &lt;span class="na"&gt;deploy_prod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build_and_sign&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hipaa-app-org/compliance-workflows/.github/workflows/deploy-prod.yml@v3.2.1&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image_digest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.build_and_sign.outputs.image_digest }}&lt;/span&gt;
      &lt;span class="na"&gt;target_cluster&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hipaa-prod-east&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
    &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inherit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reusable workflow is in a separate repo with branch protection: the compliance team approves every merge, the application teams have no write access. Tagged versions are immutable. The application repo pins by tag (&lt;code&gt;@v3.2.1&lt;/code&gt;), not by branch (&lt;code&gt;@main&lt;/code&gt;). Pinning by tag is the audit-grade choice; pinning by branch is how reusable workflows drift out of compliance after they were originally written correctly.&lt;/p&gt;

&lt;p&gt;Two organization-level controls hold this together. First, an organization ruleset that requires all production deploys to use workflows from the &lt;code&gt;compliance-workflows&lt;/code&gt; repo, enforced by the &lt;code&gt;required_workflows&lt;/code&gt; policy. Second, the OIDC trust policy from Section 03 pins to the &lt;code&gt;job_workflow_ref&lt;/code&gt; of the compliance-workflows file. Even if a developer copies the workflow inline and removes the &lt;code&gt;uses:&lt;/code&gt; reference, the role assumption fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 06 — Environment protection and required reviewers
&lt;/h2&gt;

&lt;p&gt;GitHub environments are the platform-native approval gate. They're underused outside enterprise accounts, but they're the cleanest way to satisfy § 164.308(a)(1)(ii)(B) sanction enforcement and § 164.308(a)(4) information access management.&lt;/p&gt;

&lt;p&gt;Three settings on the &lt;code&gt;production&lt;/code&gt; environment carry the compliance weight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Required reviewers from a separate team.&lt;/strong&gt; The approver cannot be the deploy author. The HIPAA Security Rule wants a separation of duties; a self-approved deploy fails audit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait timer of 5 to 15 minutes.&lt;/strong&gt; A minimum delay between approval and execution gives the platform team a chance to catch a click that shouldn't have happened. For PHI-bearing deploys, the cost of the delay is trivial; the cost of a misclick is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment branch restrictions.&lt;/strong&gt; Production deploys allowed only from &lt;code&gt;refs/tags/v*&lt;/code&gt;. A &lt;code&gt;main&lt;/code&gt; branch push, even one that built successfully, cannot deploy to production without a tag. The tag is a positive action with provenance.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Terraform: GitHub environment protection for production&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"github_repository_environment"&lt;/span&gt; &lt;span class="s2"&gt;"production"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;repository&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"clinical-platform"&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"production"&lt;/span&gt;

  &lt;span class="nx"&gt;reviewers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;teams&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github_team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hipaa_approvers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# Important: empty users list. Approvers via team only,&lt;/span&gt;
    &lt;span class="c1"&gt;# so adds/removes flow through team membership audit.&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;wait_timer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

  &lt;span class="nx"&gt;deployment_branch_policy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;protected_branches&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="nx"&gt;custom_branch_policies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"github_repository_environment_deployment_policy"&lt;/span&gt; &lt;span class="s2"&gt;"prod_tags"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;repository&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"clinical-platform"&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;github_repository_environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;production&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="nx"&gt;tag_pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"v*.*.*"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Belt-and-suspenders: the reviewer is a team, not a list of named users. Team membership is itself an audit artifact in GitHub's audit log, with adds and removals attributed to whoever performed them. The quarterly access review walks the team membership history and confirms it matches the current authorized list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 07 — The policy gate that ties it together
&lt;/h2&gt;

&lt;p&gt;Everything above is GitHub-native. The last piece is platform-agnostic: an OPA policy that evaluates the evidence bundle and returns allow or deny before the deploy job runs.&lt;/p&gt;

&lt;p&gt;The policy receives a JSON document containing the signature attestation, the scanner outputs, the approver identity, and the target environment. It returns a boolean. The deploy job's first step is the policy check; if it returns deny, the job exits non-zero and the deploy never happens. The signature, the scans, and the approval all have to be valid for the policy to allow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rego"&gt;&lt;code&gt;&lt;span class="c1"&gt;# policies/github_deploy.rego&lt;/span&gt;
&lt;span class="c1"&gt;# Policy gate for HIPAA GitHub Actions production deploys.&lt;/span&gt;

&lt;span class="ow"&gt;package&lt;/span&gt; &lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hipaa&lt;/span&gt;

&lt;span class="ow"&gt;import&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;if&lt;/span&gt;
&lt;span class="ow"&gt;import&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;

&lt;span class="ow"&gt;default&lt;/span&gt; &lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;scans_passed&lt;/span&gt;
    &lt;span class="n"&gt;signature_valid&lt;/span&gt;
    &lt;span class="n"&gt;approver_authorized&lt;/span&gt;
    &lt;span class="n"&gt;workflow_ref_pinned&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;scans_passed&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp_ns&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now_ns&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1e9&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp_ns&lt;/span&gt;      &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now_ns&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1e9&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;critical&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;critical&lt;/span&gt;      &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findings&lt;/span&gt;   &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;signature_valid&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;artifact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cosign_verified&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;artifact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signed_by&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"https://github.com/hipaa-app-org/compliance-workflows/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;approver_authorized&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;approver&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deploy_author&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;approver&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;approvers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;production&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;workflow_ref_pinned&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="n"&gt;compliance&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;workflows&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;workflows&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yml&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;refs&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="err"&gt;$`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;workflow_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;workflow_ref_pinned&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"workflow_ref %v is not pinned to a semver tag (HIPAA § 164.312(c)(1))"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;workflow_ref&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The policy is small on purpose. Forty lines that a third-party auditor can read in one sitting and a junior engineer can debug at 2am. Each condition is checkable, each is auditable, each fails closed. When the auditor asks "how do you stop a deploy if a scanner finds a critical CVE," the answer is a thirty-line Rego file plus the run log showing the deny message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 08 — What this looked like at the 90-workflow team
&lt;/h2&gt;

&lt;p&gt;Back to the clinical workflow platform from the lede. Six weeks, four engineers half-time on the engagement, 90 workflows down to the same compliance footprint via three changes.&lt;/p&gt;

&lt;p&gt;Week one and two: stand up the &lt;code&gt;compliance-workflows&lt;/code&gt; repo. Move the production deploy logic out of the application repo into a tagged reusable workflow. Pin the application repo's caller workflow at tag v1.0.0. Confirm OIDC federation works end-to-end against a temporary IAM role with broad permissions.&lt;/p&gt;

&lt;p&gt;Week three: tighten the OIDC trust policy. Add the &lt;code&gt;job_workflow_ref&lt;/code&gt; condition. Rotate the previous broad role out. The first time we tried this we broke a staging deploy because the staging caller workflow was pinned to &lt;code&gt;@main&lt;/code&gt; instead of a tag; that surfaced the drift mode in real time and reinforced why pinning by tag is the discipline.&lt;/p&gt;

&lt;p&gt;Week four: stand up the self-hosted runner pool on EKS. Move the deploy job's &lt;code&gt;runs-on:&lt;/code&gt; from &lt;code&gt;ubuntu-latest&lt;/code&gt; to &lt;code&gt;[self-hosted, hipaa-prod]&lt;/code&gt;. Confirm the runner taints, the IRSA trust, and the org-level runner group restrictions all hold under attempts to deploy from a non-approved repo.&lt;/p&gt;

&lt;p&gt;Week five: wire the OPA policy gate into the deploy workflow. Migrate the existing Slack-notification scanners to evidence-emitting scanners that write JSON into an S3 bucket with Object Lock. Add the cosign verification step. Add the environment protection rules in Terraform.&lt;/p&gt;

&lt;p&gt;Week six: dry-run the 3PAO assessment internally. The platform lead walked the auditor's expected questions and answered each with a query. The audit cleared on first-party review two weeks later. More importantly, the same pipeline kept passing through the next quarterly internal review without remediation work.&lt;/p&gt;

&lt;p&gt;The pattern is repeatable. Most healthcare SaaS teams I work with on GitHub Actions can hit this footprint in 4 to 8 weeks of focused effort, depending on how much existing inline workflow logic has to be migrated into the reusable workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 09 — Tooling recommendations
&lt;/h2&gt;

&lt;p&gt;Opinionated picks for GitHub Actions on HIPAA. Substitutions are fine; the architecture matters more than the tool.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Recommended&lt;/th&gt;
&lt;th&gt;Acceptable&lt;/th&gt;
&lt;th&gt;Avoid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Identity&lt;/td&gt;
&lt;td&gt;OIDC federation, scoped to repo + environment&lt;/td&gt;
&lt;td&gt;OIDC scoped to repo only&lt;/td&gt;
&lt;td&gt;Long-lived access keys in repo secrets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runner (build/scan)&lt;/td&gt;
&lt;td&gt;GitHub-hosted (acceptable for non-PHI stages)&lt;/td&gt;
&lt;td&gt;Self-hosted on EKS&lt;/td&gt;
&lt;td&gt;Self-hosted on a developer laptop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runner (deploy)&lt;/td&gt;
&lt;td&gt;Self-hosted on EKS with IRSA, labeled per env&lt;/td&gt;
&lt;td&gt;Self-hosted on EC2 with instance profile&lt;/td&gt;
&lt;td&gt;GitHub-hosted for PHI-touching deploys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workflow boundary&lt;/td&gt;
&lt;td&gt;Reusable workflows in a separate repo, tag-pinned&lt;/td&gt;
&lt;td&gt;Reusable workflows in the same repo, tag-pinned&lt;/td&gt;
&lt;td&gt;Inline workflow logic, branch-pinned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signing&lt;/td&gt;
&lt;td&gt;Cosign keyless, Fulcio identity from compliance repo&lt;/td&gt;
&lt;td&gt;Cosign with KMS-backed keys&lt;/td&gt;
&lt;td&gt;Docker Content Trust (Notary v1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy gate&lt;/td&gt;
&lt;td&gt;OPA evaluated in a deploy job step, fails closed&lt;/td&gt;
&lt;td&gt;Conftest in a separate job, required check&lt;/td&gt;
&lt;td&gt;Slack notification, advisory only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit logs&lt;/td&gt;
&lt;td&gt;S3 Object Lock in compliance mode, 6-year retention&lt;/td&gt;
&lt;td&gt;CloudWatch Logs to a logging account, retention locked&lt;/td&gt;
&lt;td&gt;GitHub Actions run history alone&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Section 10 — Common mistakes to avoid
&lt;/h2&gt;

&lt;p&gt;Five quick callouts from the field. Each fails audits more often than it should.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Org-wide OIDC trust policy.&lt;/strong&gt; The default sample IAM trust trusts the entire org. Scope to repo + environment + workflow ref. A wildcard in the subject claim is an audit finding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub-hosted runners for the deploy job.&lt;/strong&gt; Acceptable for build and scan. Not acceptable for the job that holds production credentials. Move PHI-touching deploys onto self-hosted runners in your BAA-covered infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reusable workflows pinned to &lt;code&gt;@main&lt;/code&gt;.&lt;/strong&gt; Tag-pin everything that touches production. Branch pinning is how compliance contracts rot over time without anyone noticing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub environment with &lt;code&gt;Required reviewers: anyone with write access&lt;/code&gt;.&lt;/strong&gt; The approver must be a separate team. Self-approval satisfies nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI run history as the audit log.&lt;/strong&gt; GitHub rotates run history aggressively. The audit log lives in S3 with Object Lock, written by the workflow at the time the event occurs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For longer-form versions of these failure modes against GitLab as well, the broader writeup on &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-5-mistakes" rel="noopener noreferrer"&gt;five patterns that fail HIPAA audits&lt;/a&gt; walks each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 11 — Conclusion
&lt;/h2&gt;

&lt;p&gt;The team that ships well on GitHub Actions in regulated environments isn't the one with the most YAML. It's the one whose workflows make compliance violations structurally difficult.&lt;/p&gt;

&lt;p&gt;OIDC federation scoped to a single workflow file removes the credential-rotation problem and pins the trust to a verifiable subject. Self-hosted runners on hardened EKS infrastructure keep the deploy job inside your BAA scope and make the audit story crisp. Reusable workflows in a separate repo, tag-pinned and protected, are the compliance contract that application teams cannot bypass. An OPA policy gate evaluating evidence at the moment of deploy turns checklists into enforcement.&lt;/p&gt;

&lt;p&gt;Build the GitHub Actions architecture this way and the platform's primitives carry the weight of the Security Rule. Build it the other way and you spend the next audit cycle rebuilding what you should have built once.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you're working through this on a healthcare SaaS team:&lt;/strong&gt; Stonebridge runs &lt;a href="https://stonebridgetechsolutions.com/services/hipaa-cicd#engagements" rel="noopener noreferrer"&gt;two-week HIPAA CI/CD audits&lt;/a&gt; that map your existing GitHub Actions setup against the Security Rule and produce a written remediation roadmap. Fixed fee, founder-led, the report holds up under first-party review by your auditor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep reading:&lt;/strong&gt; the broader architecture pattern across CI platforms is in the &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-implementation-guide" rel="noopener noreferrer"&gt;HIPAA CI/CD implementation guide&lt;/a&gt;. The pre-audit walkthrough of every Security Rule control is in &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-audit-checklist" rel="noopener noreferrer"&gt;the HIPAA CI/CD audit checklist&lt;/a&gt;. Where this pattern diverges for teams coming from SOC 2 is mapped in &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-vs-soc2" rel="noopener noreferrer"&gt;HIPAA CI/CD vs SOC 2 CI/CD&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;Lucas Jones is the Founder and Principal Platform Engineer at &lt;a href="https://stonebridgetechsolutions.com/about" rel="noopener noreferrer"&gt;Stonebridge Tech Solutions&lt;/a&gt;. Six years building cloud infrastructure and CI/CD pipelines in regulated environments, including HIPAA, FedRAMP, and SOC 2 work for healthcare and defense engineering teams across AWS, GCP, Azure, and OCI. Based in Sacramento, California.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post originally appeared on &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-github-actions" rel="noopener noreferrer"&gt;Stonebridge Tech Solutions&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hipaa</category>
      <category>githubactions</category>
      <category>devops</category>
      <category>healthcare</category>
    </item>
    <item>
      <title>The HIPAA CI/CD audit checklist for engineering teams</title>
      <dc:creator>Stonebridge Tech Solutions LLC</dc:creator>
      <pubDate>Tue, 19 May 2026 14:54:10 +0000</pubDate>
      <link>https://dev.to/stonebridgetechsolutions/the-hipaa-cicd-audit-checklist-for-engineering-teams-b9m</link>
      <guid>https://dev.to/stonebridgetechsolutions/the-hipaa-cicd-audit-checklist-for-engineering-teams-b9m</guid>
      <description>&lt;p&gt;&lt;em&gt;The practitioner's control map I use during 2-week audit engagements, with the CFR section, the auditor's question, and the architectural fix for each one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A HIPAA CI/CD audit is not a paperwork exercise. The auditor will examine the pipeline and ask specific questions, and the architecture has to be able to answer them in seconds, not weeks.&lt;/p&gt;

&lt;p&gt;A healthcare engineering team called me four weeks before their first 3PAO assessment. They had a CI/CD pipeline that shipped well. They had no idea whether it would survive scrutiny. Their compliance officer had sent them a checklist. Their auditor had sent a different checklist. The procurement reviewer at their largest customer had sent a third. None of the three agreed on what specifically the auditor would examine in the pipeline itself.&lt;/p&gt;

&lt;p&gt;We pulled the latest production deploy log together. The pipeline ran. The deploy succeeded. But the deploy authorization was attributable to a CI service account, not a human. The signed artifact lived in a registry the engineering team could write to. The scanner results were stored as pipeline run artifacts that expired after 90 days. The HIPAA Security Rule asks for audit controls (§ 164.312(b)), integrity controls (§ 164.312(c)(1)), and access management (§ 164.308(a)(4)). The pipeline could not answer any of them.&lt;/p&gt;

&lt;p&gt;The fix was architectural, not procedural. Over six weeks we rebuilt the pipeline so the auditor's questions could be answered in seconds. Signed artifacts with retention-locked evidence. Identity-attributable deploys. Continuous scanning with policy gates. Evidence stored separately from CI infrastructure. The assessment cleared on first-party review.&lt;/p&gt;

&lt;p&gt;That engagement is also where this checklist comes from. It is the same control map I use during &lt;a href="https://stonebridgetechsolutions.com/services/hipaa-cicd#engagements" rel="noopener noreferrer"&gt;2-week HIPAA CI/CD audits&lt;/a&gt; at Stonebridge, organized in the order I would actually walk a pipeline through it. Each control maps to a specific CFR section, the auditor's actual question, what passes, what fails, and the architectural fix. Use this before an audit. Use it during scoping when you do not know whether you have a problem. Use it as the basis for the remediation roadmap your compliance officer asks for.&lt;/p&gt;

&lt;h2&gt;
  
  
  01 — What this checklist is, and what it is not
&lt;/h2&gt;

&lt;p&gt;This checklist covers the technical safeguards of the HIPAA Security Rule (45 CFR Part 164 Subpart C) as they apply specifically to the build, test, and deploy pipeline. The scope is the pipeline itself: source repositories, CI runners, registries, signing infrastructure, deployment targets, and the evidence the pipeline produces along the way.&lt;/p&gt;

&lt;p&gt;It does not cover Business Associate Agreement management. It does not cover PHI access controls inside the running application. It does not cover workforce training, sanction policy, or the administrative safeguards required by § 164.308(a)(1) and § 164.308(a)(5). Those are real obligations, but they are not what your auditor will examine when they ask to see the pipeline.&lt;/p&gt;

&lt;p&gt;It is also not a substitute for a real audit. A 2-week Stonebridge engagement produces a written control mapping, a prioritized remediation roadmap, and effort estimates. This checklist gets you to the point where you know what the engagement would surface. For deeper architectural context on the patterns referenced below, the &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-implementation-guide" rel="noopener noreferrer"&gt;HIPAA CI/CD implementation guide&lt;/a&gt; covers the parent/child pipeline architecture, isolated runners, and policy gates that satisfy most of these controls structurally.&lt;/p&gt;

&lt;h2&gt;
  
  
  02 — Pull evidence first. Then walk the controls.
&lt;/h2&gt;

&lt;p&gt;The most common mistake teams make before a HIPAA CI/CD audit is trying to fix gaps before they know what the gaps are. I have watched engineering teams spend two weeks rebuilding their signing chain only to discover their bigger exposure was a logging account with read-only access for the wrong people.&lt;/p&gt;

&lt;p&gt;Run the checklist in this order. Spend the first two hours pulling evidence onto a single shared document, then walk the controls with the evidence visible. Each control either has supporting evidence or it does not; trying to remember from memory whether something is configured correctly is how teams sign off on findings that fail under scrutiny.&lt;/p&gt;

&lt;p&gt;The evidence pull at minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pipeline configuration files for every active branch (the entire &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; or &lt;code&gt;.github/workflows/&lt;/code&gt; tree)&lt;/li&gt;
&lt;li&gt;The latest 30 days of pipeline run logs for production deploys&lt;/li&gt;
&lt;li&gt;The most recent scanner outputs (SAST, SCA, container CVE, IaC, secrets) for the production branch&lt;/li&gt;
&lt;li&gt;The signing chain: signing keys, key rotation history, signature verification logs&lt;/li&gt;
&lt;li&gt;The current contents of the evidence bucket, including retention configuration&lt;/li&gt;
&lt;li&gt;The IAM, RBAC, or equivalent identity grants for every account or principal that can deploy to a PHI-bearing environment&lt;/li&gt;
&lt;li&gt;The CloudTrail, audit log, or equivalent showing the last 90 days of admin actions on production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With that on the table, the rest of the checklist becomes a query, not a memory test. The patterns below are organized by control family, not in the order the Security Rule presents them, because the practitioner walk is faster when you group by where the evidence lives. The &lt;a href="https://stonebridgetechsolutions.com/services/hipaa-cicd#architecture" rel="noopener noreferrer"&gt;architecture pattern I use&lt;/a&gt; emits this evidence as a property of how the pipeline runs, so the pull becomes a one-line query instead of a forensic exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  03 — Identity and access: who can deploy to PHI-bearing environments
&lt;/h2&gt;

&lt;p&gt;This is the first thing auditors actually look at, and it is the most commonly broken in pipelines designed without compliance posture in mind. The relevant Security Rule sections are § 164.308(a)(3) (workforce security), § 164.308(a)(4) (information access management), and § 164.312(a)(1) (access control). All three reduce to the same engineering question: who, by named identity, can change production state.&lt;/p&gt;




&lt;h3&gt;
  
  
  § 164.308(a)(4) · Information access management
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Deploy authorization is attributable to a named human identity.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Show me, by named identity, every human who has deployed to production in the last 90 days."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Passes:&lt;/strong&gt; Federated identity from corporate IdP (Okta, Entra ID, IAM Identity Center) to CI. Every deploy runs under a role assumed by a named human via SSO with phishing-resistant MFA. The deploy event log shows the human, the timestamp, and the role assumed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails:&lt;/strong&gt; Long-lived CI service account credentials. Shared "deploy" tokens passed in environment variables. Deploy keys checked into a vault any developer can read. "The pipeline did it" is not a name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; OIDC federation between CI and cloud (GitHub OIDC to AWS, Workload Identity Federation for GCP). Short-lived role assumption per pipeline run. Named-human approval required at the production gate, recorded with identity and timestamp.&lt;/p&gt;




&lt;h3&gt;
  
  
  § 164.308(a)(3) · Workforce security
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Access grants and revocations are auditable and time-bounded.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Show me when this developer was granted production access and when it was revoked."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Passes:&lt;/strong&gt; Access is granted through the IdP with a documented approval workflow. Time-bounded role assumption (hours, not days). Quarterly access reviews logged. Revocation propagates from IdP through CI within minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails:&lt;/strong&gt; Permissions held directly in CI (GitHub repo collaborator, GitLab project member) without IdP gating. No record of when someone was granted access. Revocation requires a person to remember to remove the user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; IdP groups gate all CI access. Group membership is the source of truth. Access reviews run quarterly, output flows to the central log account.&lt;/p&gt;




&lt;h3&gt;
  
  
  § 164.312(a)(1) · Access control
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Phishing-resistant MFA is enforced at the deploy gate.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Show me the MFA challenge for this production deploy."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Passes:&lt;/strong&gt; WebAuthn or FIDO2 enforced at the IdP for any session that can trigger a production deploy. SMS-based or app-based MFA is not sufficient for production-PHI paths. The MFA event is logged with the same identity that appears on the deploy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails:&lt;/strong&gt; SMS MFA only. MFA at IdP login but no re-challenge for high-risk actions. Service accounts bypassing MFA via static tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Phishing-resistant MFA enforced at IdP. Step-up authentication required for any role that can write to a PHI-bearing environment. Log the auth event in the same store that holds deploy events so the auditor can join them on identity and time.&lt;/p&gt;




&lt;p&gt;The structural pattern underneath all three: no human credential survives longer than the pipeline run that uses it. Every action is attributable to a named identity that exists in your corporate IdP. The CI system is downstream of identity, not a parallel identity store.&lt;/p&gt;

&lt;h2&gt;
  
  
  04 — Audit logging: every deploy emits tamper-evident evidence
&lt;/h2&gt;

&lt;p&gt;HIPAA § 164.312(b) is one of the few Security Rule sections that names the requirement directly: "implement hardware, software, and/or procedural mechanisms that record and examine activity in information systems that contain or use electronic protected health information." For a pipeline, that means every deploy event, every signature verification, every policy decision, and every access grant lands in a log that engineers cannot edit.&lt;/p&gt;




&lt;h3&gt;
  
  
  § 164.312(b) · Audit controls
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Logs are centralized in an account engineers cannot write to.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Show me the deploy event for this production release, with the approver identity and timestamp."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Passes:&lt;/strong&gt; A dedicated logging account (AWS), project (GCP), or subscription (Azure) with S3 Object Lock or equivalent retention lock. Production accounts write logs; nobody in production can read or delete them. CloudTrail organization trail captures every admin action with the assumed-role chain intact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails:&lt;/strong&gt; Pipeline logs only in the CI run output, expiring after 30-90 days. Logs in the same account where deploys happen. CloudTrail enabled but writable by the same role that performs deploys. "We ship to Datadog" with no retention policy mapped to the regulation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Centralized logging account with S3 Object Lock in compliance mode, retention configured to your HIPAA retention requirement (typically 6 years for documents under § 164.316(b)(2), longer in practice). Cross-account replication is one-way: production writes, logging stores, nobody deletes.&lt;/p&gt;




&lt;h3&gt;
  
  
  § 164.312(b) · Audit controls
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Every deploy is queryable by identity, environment, and artifact hash.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Show me every production deploy this user made in Q1, and the SHA-256 of the artifact deployed."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Passes:&lt;/strong&gt; Pipeline emits a structured deploy event on every run: who, when, what (artifact digest), where (environment), why (change ticket reference). The event lands in the logging account with the same retention as access logs. The auditor can answer the query above with one SQL or Athena query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails:&lt;/strong&gt; Deploy history lives in the CI tool only (GitLab pipelines, GitHub Actions runs). Artifact hashes are not recorded against the deploy event. Answering the query requires correlating five systems and a Slack search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Pipeline writes a structured deploy event to the logging account at the moment of cutover. Schema: &lt;code&gt;deploy_id, timestamp, actor_identity, environment, artifact_digest, change_ticket, approval_chain&lt;/code&gt;. Treat it as a first-class output of the pipeline, not telemetry.&lt;/p&gt;




&lt;h2&gt;
  
  
  05 — Integrity controls: every artifact deployed has a signature you can verify
&lt;/h2&gt;

&lt;p&gt;§ 164.312(c)(1) requires "policies and procedures to protect electronic protected health information from improper alteration or destruction." For pipelines, that is the supply chain: from the source commit to the running container, you have to be able to demonstrate that what is in production is what was reviewed and approved.&lt;/p&gt;

&lt;p&gt;I recommend Cosign for signing container images. The Sigstore ecosystem has matured to the point where Cosign integrates with KMS-backed signing keys natively, and the verification policy lives in admission control where it can fail closed. &lt;strong&gt;Do not use Docker Content Trust for new HIPAA pipelines.&lt;/strong&gt; Notary v1 is effectively unmaintained, and the verification story does not survive auditor questioning.&lt;/p&gt;




&lt;h3&gt;
  
  
  § 164.312(c)(1) · Integrity
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Container images are signed before deploy and verified on admission.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Show me the signature for this container, the key used to sign it, and the admission decision that allowed it into production."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Passes:&lt;/strong&gt; Cosign or equivalent signs every image at build time using a KMS-backed key. The cluster admission controller (Kyverno or OPA Gatekeeper) verifies the signature against the trusted public key. Unsigned or improperly-signed images are rejected at admission, the rejection logged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails:&lt;/strong&gt; Images deployed without signature verification. Signing keys held in a developer's keyring or in plaintext in the CI environment. Signature checks performed in CI but not enforced at the cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Cosign signing at build with a KMS-stored key. Kyverno admission policy enforcing verification at the cluster. Drift detection comparing running images against the signed-and-approved list.&lt;/p&gt;




&lt;p&gt;The admission policy itself is short and worth showing. This is the Kyverno-equivalent pattern in OPA Rego, derivative from public Sigstore examples and Stonebridge's reference patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rego"&gt;&lt;code&gt;&lt;span class="c1"&gt;# File: policies/hipaa/admission/signed_images.rego&lt;/span&gt;
&lt;span class="c1"&gt;# Enforces signed-image admission for any pod entering a PHI-bearing namespace.&lt;/span&gt;
&lt;span class="c1"&gt;# Maps to 45 CFR § 164.312(c)(1) integrity controls and § 164.308(a)(4)&lt;/span&gt;
&lt;span class="c1"&gt;# information access management for production-PHI workloads.&lt;/span&gt;

&lt;span class="ow"&gt;package&lt;/span&gt; &lt;span class="n"&gt;kubernetes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admission&lt;/span&gt;

&lt;span class="ow"&gt;import&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;if&lt;/span&gt;
&lt;span class="ow"&gt;import&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;

&lt;span class="c1"&gt;# Namespaces that hold PHI workloads. The deny list is explicit so a&lt;/span&gt;
&lt;span class="c1"&gt;# misconfigured namespace cannot accidentally accept unsigned images.&lt;/span&gt;
&lt;span class="n"&gt;phi_namespaces&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"prod-phi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"staging-phi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"validation-phi"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Public keys we trust. In production these live in a ConfigMap mounted&lt;/span&gt;
&lt;span class="c1"&gt;# at runtime, sourced from KMS public key endpoints.&lt;/span&gt;
&lt;span class="n"&gt;trusted_keys&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"cosign-prod-key-2026"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"-----BEGIN PUBLIC KEY-----..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"cosign-prod-key-2025-rotation"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"-----BEGIN PUBLIC KEY-----..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Hard deny: any image in a PHI namespace without a verified signature&lt;/span&gt;
&lt;span class="c1"&gt;# from a trusted key fails admission. The reason string lands in the&lt;/span&gt;
&lt;span class="c1"&gt;# audit log so the rejection is queryable.&lt;/span&gt;
&lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;phi_namespaces&lt;/span&gt;
  &lt;span class="ow"&gt;some&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;containers&lt;/span&gt;
  &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;image_signed_by_trusted_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"HIPAA integrity violation: image %v not signed by trusted key in namespace %v (45 CFR 164.312(c)(1))"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Soft warning: even non-PHI namespaces should reject unsigned images.&lt;/span&gt;
&lt;span class="c1"&gt;# A warning lets the platform team see drift without breaking dev.&lt;/span&gt;
&lt;span class="n"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;phi_namespaces&lt;/span&gt;
  &lt;span class="ow"&gt;some&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;containers&lt;/span&gt;
  &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;image_signed_by_trusted_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Unsigned image %v in non-PHI namespace %v. Cluster baseline requires signatures."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Helper: returns true if any trusted key verified the image signature.&lt;/span&gt;
&lt;span class="c1"&gt;# The actual verification happens in a sidecar that pre-populates the&lt;/span&gt;
&lt;span class="c1"&gt;# image annotations with verification results. This keeps Rego pure.&lt;/span&gt;
&lt;span class="n"&gt;image_signed_by_trusted_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;annotation&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;annotations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"cosign.verified/%v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;])]&lt;/span&gt;
  &lt;span class="n"&gt;annotation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verified&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;annotation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_id&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trusted_keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The policy fails closed. A PHI-bearing namespace will not accept an unsigned image, period. The denial message includes the CFR citation so when the auditor pulls the admission log, the regulatory mapping is already in the record. The longer write-up of the parent-child pipeline architecture that produces signed artifacts in the first place is in the &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-implementation-guide" rel="noopener noreferrer"&gt;HIPAA CI/CD implementation guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  06 — Transmission security: modern TLS, mutually authenticated where it matters
&lt;/h2&gt;

&lt;p&gt;§ 164.312(e)(1) requires safeguards against unauthorized access during transmission. For pipelines, the relevant transmission paths are the CI-to-registry path, the registry-to-cluster path, the cluster-to-PHI-database path, and the deploy-evidence-to-logging-account path. Each needs encryption in transit; the PHI-bearing paths need mutual authentication.&lt;/p&gt;




&lt;h3&gt;
  
  
  § 164.312(e)(1) · Transmission security
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Encrypted transit for every PHI-bearing path, with mutual auth where the threat model warrants.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Show me the TLS configuration for the path between the pipeline and the production database. Which ciphers are accepted? Is the client authenticated?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Passes:&lt;/strong&gt; TLS 1.2 minimum, TLS 1.3 preferred. Modern cipher suites only. Mutual TLS (mTLS) on internal PHI-bearing service-to-service calls, typically via a service mesh (Istio, Linkerd) or platform-native (AWS App Mesh, GCP Cloud Service Mesh). Private connectivity (PrivateLink, Private Service Connect) where cross-network paths exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails:&lt;/strong&gt; TLS 1.0 or 1.1 still accepted on any PHI-touching endpoint. Plain-HTTP internal calls. Service-to-service auth via bearer tokens with no transport-level identity. Network ACLs treated as the encryption layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Enforce TLS 1.2+ at every load balancer. Adopt a service mesh for internal mTLS. Use private connectivity for cross-VPC PHI paths. Run a scheduled scanner against your endpoints (sslyze, testssl.sh) and ship the report to the evidence bucket.&lt;/p&gt;




&lt;h2&gt;
  
  
  07 — Continuous evaluation: scanners as policy gates, not as advisory notifications
&lt;/h2&gt;

&lt;p&gt;§ 164.308(a)(8) requires periodic technical evaluation of the security posture. The way that maps to pipelines is straightforward: scanners run on every change, the results gate deploys, and the findings flow into the evidence trail. Auditors will look for five scanner types in a HIPAA pipeline. Each maps to a category of risk; missing any one is a finding.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scanner type&lt;/th&gt;
&lt;th&gt;Recommended tool&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;th&gt;Maps to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SAST&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Semgrep, CodeQL&lt;/td&gt;
&lt;td&gt;Source-code vulnerabilities in your application&lt;/td&gt;
&lt;td&gt;§ 164.308(a)(8)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SCA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OSV-Scanner, Snyk&lt;/td&gt;
&lt;td&gt;Known CVEs in third-party dependencies&lt;/td&gt;
&lt;td&gt;§ 164.308(a)(8), § 164.312(c)(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Container CVE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Trivy, Grype&lt;/td&gt;
&lt;td&gt;Vulnerable OS packages and libraries in container images&lt;/td&gt;
&lt;td&gt;§ 164.308(a)(8)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IaC&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;tfsec, Checkov&lt;/td&gt;
&lt;td&gt;Misconfigured cloud resources, public buckets, open security groups&lt;/td&gt;
&lt;td&gt;§ 164.312(a)(1), § 164.312(e)(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Secrets&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gitleaks, TruffleHog&lt;/td&gt;
&lt;td&gt;Hardcoded credentials, API keys, tokens in source&lt;/td&gt;
&lt;td&gt;§ 164.308(a)(4)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The crucial structural detail: scanners are policy gates, not advisory notifications. A CVE result that sends a Slack message and lets the deploy proceed is not a control. The gate has to fail closed. The pattern looks like this in GitHub Actions, derivative from public open-source patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# File: .github/workflows/hipaa-build.yml&lt;/span&gt;
&lt;span class="c1"&gt;# Build, scan, and gate. Every scanner is a policy gate, not a notification.&lt;/span&gt;
&lt;span class="c1"&gt;# Maps to 45 CFR § 164.308(a)(8) continuous evaluation and § 164.312(c)(1)&lt;/span&gt;
&lt;span class="c1"&gt;# integrity. Failed gates block deploys; results emit to the evidence bucket.&lt;/span&gt;

&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HIPAA pipeline&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;self-hosted-hipaa-baseline&lt;/span&gt;   &lt;span class="c1"&gt;# Hardened, network-isolated runner&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SAST (Semgrep)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;semgrep --config p/hipaa --error --json &amp;gt; sast.json&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SCA (OSV-Scanner)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;osv-scanner --format json --output sca.json ./&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Container CVE (Trivy)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trivy image --severity HIGH,CRITICAL --exit-code 1 --format json --output trivy.json ${{ env.IMAGE }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IaC (Checkov)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkov -d ./terraform --output json --output-file iac.json --soft-fail &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret scan (Gitleaks)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitleaks detect --no-git -v --report-format json --report-path secrets.json&lt;/span&gt;

      &lt;span class="c1"&gt;# Policy gate. Any HIGH/CRITICAL finding from any scanner blocks the&lt;/span&gt;
      &lt;span class="c1"&gt;# pipeline. The gate runs after every scanner so partial failures are&lt;/span&gt;
      &lt;span class="c1"&gt;# visible in the run log.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HIPAA policy gate&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;python3 .ci/policy_gate.py \&lt;/span&gt;
            &lt;span class="s"&gt;--sast sast.json \&lt;/span&gt;
            &lt;span class="s"&gt;--sca sca.json \&lt;/span&gt;
            &lt;span class="s"&gt;--trivy trivy.json \&lt;/span&gt;
            &lt;span class="s"&gt;--iac iac.json \&lt;/span&gt;
            &lt;span class="s"&gt;--secrets secrets.json \&lt;/span&gt;
            &lt;span class="s"&gt;--baseline hipaa-2026&lt;/span&gt;

      &lt;span class="c1"&gt;# Evidence emit. Every scanner output is signed and pushed to the&lt;/span&gt;
      &lt;span class="c1"&gt;# retention-locked evidence bucket. This is the artifact the auditor&lt;/span&gt;
      &lt;span class="c1"&gt;# will read, so the schema is stable and queryable.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Emit signed evidence&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;cosign attest --predicate sast.json     --type vuln $IMAGE&lt;/span&gt;
          &lt;span class="s"&gt;cosign attest --predicate sca.json      --type vuln $IMAGE&lt;/span&gt;
          &lt;span class="s"&gt;cosign attest --predicate trivy.json    --type vuln $IMAGE&lt;/span&gt;
          &lt;span class="s"&gt;aws s3 cp ./*.json s3://hipaa-evidence-prod/pipeline-runs/${{ github.run_id }}/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to notice. First, the runner itself is a hardened, network-isolated self-hosted runner. Public hosted runners reaching production PHI infrastructure is a finding on its own. Second, every scanner output is signed (Cosign attest) before being stored, so the evidence has its own integrity chain. Third, the policy gate is a script, not a YAML flag. The script encodes the baseline in version control, can be unit tested, and the failure conditions are auditable.&lt;/p&gt;

&lt;h2&gt;
  
  
  08 — Environment isolation: the runner that builds dev cannot reach prod
&lt;/h2&gt;

&lt;p&gt;The single biggest pipeline-level finding I see in HIPAA-environment audits is runners that can reach environments they should not. A pipeline runner with credentials for production, used to build a dev branch, is the textbook example of why the network and identity layers have to be scoped per environment.&lt;/p&gt;




&lt;h3&gt;
  
  
  § 164.308(a)(4), § 164.312(a)(1)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Per-environment runners with scoped IAM and explicit egress.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Show me which runner deployed this artifact, and demonstrate the runner cannot reach any other environment."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Passes:&lt;/strong&gt; Separate runner pool per environment (dev, staging, prod-PHI). Runners run on dedicated nodes with environment-scoped IAM roles. Egress is controlled by VPC Service Controls (GCP), AWS PrivateLink, or equivalent. A dev runner trying to reach prod-PHI gets a network denial, not an IAM denial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails:&lt;/strong&gt; Shared runner pool. Runner credentials with permissions across environments. "We use namespaces" as the only isolation. CI runner with internet egress to anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Dedicated runner pools per environment. IAM scoped to the single account the runner serves. Egress allowlist per runner. Network policy at the cluster level if runners run on Kubernetes.&lt;/p&gt;




&lt;h2&gt;
  
  
  09 — Evidence collection and retention: the audit trail has to outlive the pipeline that created it
&lt;/h2&gt;

&lt;p&gt;Evidence retention is where most pipelines fail the audit even when the pipeline itself is well-built. CI tools rotate their run logs aggressively (30 to 90 days by default). HIPAA documentation retention is six years under § 164.316(b)(2). The arithmetic does not work, so the evidence has to live outside the CI tool from the moment it is produced.&lt;/p&gt;

&lt;p&gt;The pattern: every pipeline run emits a structured set of artifacts (deploy event, scanner outputs, signature attestations, policy decisions) to an evidence bucket in a separate account, with retention locks configured. The Terraform for that bucket is short, and worth getting right:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# File: terraform/hipaa-evidence/main.tf&lt;/span&gt;
&lt;span class="c1"&gt;# Retention-locked evidence bucket for HIPAA CI/CD pipeline artifacts.&lt;/span&gt;
&lt;span class="c1"&gt;# Maps to 45 CFR § 164.312(b) audit controls and § 164.316(b)(2)&lt;/span&gt;
&lt;span class="c1"&gt;# documentation retention. The bucket lives in a dedicated logging&lt;/span&gt;
&lt;span class="c1"&gt;# account; production accounts have write-only access via a scoped role.&lt;/span&gt;

&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/aws"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 5.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Evidence bucket. Object Lock in COMPLIANCE mode means even the root&lt;/span&gt;
&lt;span class="c1"&gt;# account cannot delete objects within the retention window. This is&lt;/span&gt;
&lt;span class="c1"&gt;# the property that satisfies the auditor's "can engineers tamper with&lt;/span&gt;
&lt;span class="c1"&gt;# the evidence?" question.&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"hipaa_evidence"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hipaa-evidence-${var.env}-${var.account_suffix}"&lt;/span&gt;
  &lt;span class="nx"&gt;object_lock_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Purpose&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HIPAA pipeline evidence"&lt;/span&gt;
    &lt;span class="nx"&gt;CFRSection&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"164.312(b),164.316(b)(2)"&lt;/span&gt;
    &lt;span class="nx"&gt;Retention&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"6y"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Retention configuration. COMPLIANCE mode is non-negotiable for HIPAA&lt;/span&gt;
&lt;span class="c1"&gt;# evidence. GOVERNANCE mode lets privileged users override; that is a&lt;/span&gt;
&lt;span class="c1"&gt;# finding waiting to happen.&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_object_lock_configuration"&lt;/span&gt; &lt;span class="s2"&gt;"hipaa_evidence"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hipaa_evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;default_retention&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"COMPLIANCE"&lt;/span&gt;
      &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2190&lt;/span&gt;  &lt;span class="c1"&gt;# 6 years; matches § 164.316(b)(2) documentation retention&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Versioning is a prerequisite for Object Lock. Without it the lock&lt;/span&gt;
&lt;span class="c1"&gt;# applies to a single object that can be overwritten with a new version.&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_versioning"&lt;/span&gt; &lt;span class="s2"&gt;"hipaa_evidence"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hipaa_evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;versioning_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Enabled"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Encryption at rest with a customer-managed key. The key policy denies&lt;/span&gt;
&lt;span class="c1"&gt;# deletion by anyone but a designated break-glass role with MFA.&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_server_side_encryption_configuration"&lt;/span&gt; &lt;span class="s2"&gt;"hipaa_evidence"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hipaa_evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;apply_server_side_encryption_by_default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;sse_algorithm&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws:kms"&lt;/span&gt;
      &lt;span class="nx"&gt;kms_master_key_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_kms_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hipaa_evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;bucket_key_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Public access is blocked at the bucket level. There is no scenario in&lt;/span&gt;
&lt;span class="c1"&gt;# which HIPAA evidence should be public, so the block is total.&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_public_access_block"&lt;/span&gt; &lt;span class="s2"&gt;"hipaa_evidence"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hipaa_evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;block_public_acls&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;block_public_policy&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;ignore_public_acls&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;restrict_public_buckets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key property is Object Lock in COMPLIANCE mode. Once an object lands in this bucket, no one can delete it inside the retention window. Not the engineering team, not the platform team, not the root account. That is the architectural property that turns "we keep our evidence" into a verifiable statement. The same pattern translates to GCS Bucket Lock and Azure Immutable Blob Storage.&lt;/p&gt;

&lt;p&gt;The cross-cutting principle here is one of the architectural decisions in the &lt;a href="https://stonebridgetechsolutions.com/services/hipaa-cicd#architecture" rel="noopener noreferrer"&gt;pipeline architecture pattern I use&lt;/a&gt;: the evidence outlives the pipeline that produced it, and lives where the pipeline cannot reach back to modify it. That is what makes the audit trail trustworthy under scrutiny.&lt;/p&gt;

&lt;h2&gt;
  
  
  10 — Change management: approval chains that survive auditor questioning
&lt;/h2&gt;

&lt;p&gt;Every production deploy needs an approval chain that ties a named human to the artifact being deployed. The relevant question from § 164.308(a)(1)(ii)(B) is whether your sanction policy can be enforced: if a developer deploys an unreviewed change to a PHI environment, can you identify who, what, and when after the fact. If the answer is "we would have to dig," the control fails.&lt;/p&gt;




&lt;h3&gt;
  
  
  § 164.308(a)(1)(ii)(B), § 164.312(b)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Production deploys require named-human approval, recorded against the artifact.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"For this production deploy, show me who approved, what they approved, and the change ticket they referenced."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Passes:&lt;/strong&gt; Production environment is a protected environment in GitHub or a protected branch in GitLab. Approval is required from a named human in a defined approver group, not the deploy author. The approval event records the approver identity, the artifact digest, and the linked change ticket. The chain lands in the evidence bucket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails:&lt;/strong&gt; Auto-deploy from a green build. Approval clicks with no identity ("approved by user 'admin'"). Approver can be the same person who authored the change. The approval lives in the CI tool only and expires with the run log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Protected environment with required reviewers from a separate approver group. The approval webhook emits a structured event into the evidence bucket, joining the approver identity, the artifact digest, and the change ticket reference. Treat the approval chain as a first-class deploy output.&lt;/p&gt;




&lt;h2&gt;
  
  
  11 — A representative engagement: what I found at a 32-host healthcare platform on GCP
&lt;/h2&gt;

&lt;p&gt;I worked with a healthcare platform team running production workloads on GCP across roughly 32 VM-backed services that mixed Tomcat, Node.js, and OS versions in ways nobody had inventoried. Their compliance officer had a required baseline (Tomcat 9.0.62+, Node.js 18+, Ubuntu 20.04+). Their auditor was scheduled in five weeks. They did not know which hosts complied.&lt;/p&gt;

&lt;p&gt;The first day we pulled their pipeline configurations and ran the evidence pull. The pipeline shipped well. Tests ran. Containers were built. The platform was stable. But: every deploy ran under a CI service account with full project-level IAM. The signing chain did not exist. Scanner output lived in pipeline artifacts that aged out after 90 days. The deploy event was not stored anywhere outside the CI tool. They had every individual capability needed to pass the audit, just not connected in the way the Security Rule expected.&lt;/p&gt;

&lt;p&gt;We split the remediation into two parallel tracks. Track one: write an Ansible and Python tool that connected to each VM through GCP Identity-Aware Proxy, scraped the running versions of Tomcat, Node.js, and OS, and produced a compliance matrix mapping each host to the required baseline. That gave the compliance team the inventory they needed inside the first week instead of the four weeks a manual walk would have taken. Track two: rebuild the pipeline so the same baseline could not regress. We added Cosign signing with a KMS-backed key, Trivy and OSV-Scanner as policy gates that failed closed, Workload Identity Federation to eliminate the long-lived service account, a centralized logging account with Object Lock at 6-year retention, and a deploy-event emitter that wrote structured records to the evidence bucket on every cutover.&lt;/p&gt;

&lt;p&gt;The audit cleared on first-party review. The compliance team kept the inventory tool as part of their continuous monitoring practice. The pipeline architecture is what the &lt;a href="https://stonebridgetechsolutions.com/services/hipaa-cicd#architecture" rel="noopener noreferrer"&gt;HIPAA CI/CD service page&lt;/a&gt; describes generically, but the engagement-specific detail that mattered: the baseline enforcement happens at &lt;em&gt;plan&lt;/em&gt; time, not at deploy time. By the time the pipeline tries to ship a non-compliant container, the policy gate has already rejected the build. Future regressions are structurally impossible.&lt;/p&gt;

&lt;p&gt;The pattern translates directly to AWS (CloudTrail organization trail + S3 Object Lock + IAM Identity Center) and Azure (Activity Log + Immutable Blob Storage + Entra ID federation). Where the integration patterns differ across SOC 2 and HITRUST is covered in &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-vs-soc2" rel="noopener noreferrer"&gt;HIPAA CI/CD vs SOC 2 CI/CD&lt;/a&gt;; the structural pattern is the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  12 — The five questions auditors always ask
&lt;/h2&gt;

&lt;p&gt;Across roughly a dozen HIPAA-environment engagements over the last few years, these five questions come up in some form every single time. Pre-answering them in your runbook is the single highest-leverage prep activity for an audit. Each maps to a control family already covered above; the value is having the answer in one paragraph that a non-engineer can read aloud to an assessor.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Show me, by named human, who can deploy to a production-PHI environment, and when each person was last reviewed for that access."&lt;/strong&gt; The answer should be one query against the IdP plus one against the access-review log. If the answer involves grepping CI runners, the architecture is wrong.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"For this specific production deploy from last Tuesday, show me the approval chain, the artifact signature, and the scanner results."&lt;/strong&gt; The answer should be one query against the evidence bucket using the deploy ID. Three artifacts come back: the approval event, the signature attestation, and the scanner outputs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Can engineers in the production account read or modify the audit log?"&lt;/strong&gt; The answer should be "no, the logs live in a separate account, retention-locked, write-only from production." If you have to qualify the answer, the architecture is wrong.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"What happens if a scanner finds a CRITICAL CVE during a production deploy?"&lt;/strong&gt; The answer should be "the deploy blocks. The policy gate fails closed. We have a documented exception process that requires named-human approval and is itself logged as evidence."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Show me a deploy from six months ago, and prove the running container today matches what was approved then."&lt;/strong&gt; The answer should be a signature verification against the image currently running, plus the historical signing event from the evidence bucket. Drift detection is the supporting control.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your team cannot answer all five in under five minutes per question with documentation, the audit will surface that. Better to surface it in your own dry-run two weeks ahead.&lt;/p&gt;

&lt;h2&gt;
  
  
  13 — Common gaps we see every engagement
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick gap list · cross-reference&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long-lived service account credentials.&lt;/strong&gt; Any credential that lives longer than the pipeline run that uses it is a finding. OIDC federation removes the entire category.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logs in the same account as production.&lt;/strong&gt; The most common single finding. Move them to a logging account with retention locks the day you start the audit prep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scanners as Slack notifications.&lt;/strong&gt; Advisory scanners are not controls. The gate has to fail closed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No deploy event outside CI.&lt;/strong&gt; CI run logs expire. The deploy event has to live in the evidence bucket from the moment of cutover.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approval workflow that lets the author approve.&lt;/strong&gt; The approver and the author must be different humans. Required-reviewer configuration handles this in GitHub and GitLab.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unsigned images in production.&lt;/strong&gt; Cosign-signed images verified at admission. Anything less invites the question "how do you know what is running."&lt;/p&gt;

&lt;p&gt;Most of these are structural choices made early that compound into hard-to-fix gaps. The longer write-up of the patterns most often missed lives in &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-5-mistakes" rel="noopener noreferrer"&gt;5 mistakes healthcare teams make on HIPAA CI/CD&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  14 — Closing: the architecture has to answer the question
&lt;/h2&gt;

&lt;p&gt;Every control in this checklist reduces to the same property: the pipeline architecture makes the auditor's question answerable in seconds. Identity, integrity, audit trail, evidence retention, scanner gates, approval chains. Each is a touchpoint where the pipeline either has the answer or it does not.&lt;/p&gt;

&lt;p&gt;The teams that ship well in regulated environments are not the ones with the most paperwork. They are the ones whose architecture makes compliance violations structurally difficult and whose evidence emits as a property of how the pipeline runs. That is what an auditor recognizes as a mature compliance posture, and it is also what lets engineering keep shipping while compliance reviews happen in parallel instead of in series.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you are walking into a 3PAO assessment, a customer security review, or a HITRUST readiness pass:&lt;/strong&gt; Stonebridge runs &lt;a href="https://stonebridgetechsolutions.com/services/hipaa-cicd#engagements" rel="noopener noreferrer"&gt;2-week HIPAA CI/CD audits&lt;/a&gt; built around this exact checklist. Fixed fee, founder-led, the deliverable is a written control map plus a prioritized remediation roadmap your team can act on. We sign BAAs as part of the engagement and the report holds up under first-party review by your auditor.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;Lucas Jones is the Founder and Principal Platform Engineer at &lt;a href="https://stonebridgetechsolutions.com/about" rel="noopener noreferrer"&gt;Stonebridge Tech Solutions&lt;/a&gt;. Six years building cloud infrastructure and CI/CD pipelines in regulated environments, including HIPAA, FedRAMP, and SOC 2 work for healthcare and defense engineering teams across AWS, GCP, Azure, and OCI. Based in Sacramento, California.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post originally appeared on &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-audit-checklist" rel="noopener noreferrer"&gt;Stonebridge Tech Solutions&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hipaa</category>
      <category>cicd</category>
      <category>devops</category>
      <category>healthcare</category>
    </item>
    <item>
      <title>HIPAA CI/CD vs SOC 2 CI/CD: where the controls differ</title>
      <dc:creator>Stonebridge Tech Solutions LLC</dc:creator>
      <pubDate>Mon, 18 May 2026 16:21:56 +0000</pubDate>
      <link>https://dev.to/stonebridgetechsolutions/hipaa-cicd-vs-soc-2-cicd-where-the-controls-differ-32ag</link>
      <guid>https://dev.to/stonebridgetechsolutions/hipaa-cicd-vs-soc-2-cicd-where-the-controls-differ-32ag</guid>
      <description>&lt;p&gt;&lt;em&gt;If you have SOC 2 and assume HIPAA is incremental, your pipeline disagrees.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;SOC 2 audits the policies you chose. HIPAA audits the system you built. At the CI/CD layer, that distinction stops being abstract and starts producing engineering work.&lt;/p&gt;

&lt;p&gt;A healthcare engineering team I worked with had Type II SOC 2 in hand. They had passed it the year before on the second attempt. Their pipeline was clean, their evidence binder tidy, their GRC lead had stopped flinching at the word "scope."&lt;/p&gt;

&lt;p&gt;Then they closed their first hospital system. The contract came with a Business Associate Agreement and a HIPAA addendum. Legal forwarded it to engineering with a one-line note: "Should be fine, we have SOC 2."&lt;/p&gt;

&lt;p&gt;It was not fine. Inside two weeks they had fourteen control gaps. Their pipeline runners sat on a shared pool that handled both staging and a non-PHI demo environment. Their audit logs rolled off at 90 days. Their cosign signature verification was a soft warning in their Helm chart, not a deploy-blocker. Their incident response runbook said the right things about breach notification, but their pipeline emitted nothing the legal team could query against the 60-day clock in 45 CFR § 164.404. They had SOC 2. They did not have the system HIPAA assumes.&lt;/p&gt;

&lt;p&gt;Six weeks later, after three pipeline rewrites and a round of running the BAA through outside counsel, they shipped. The 20% gap between SOC 2 and HIPAA is not 20% of the cost. It is the part of the pipeline that has to change to emit evidence on its own.&lt;/p&gt;

&lt;p&gt;SOC 2 lets you scope your controls and have your auditor read a narrative. HIPAA hands you the Security Rule, removes most of your scoping latitude, and asks the system itself to produce the evidence. At the pipeline layer, that distinction produces changes you cannot policy your way out of. The rest of this post is a control-by-control map of where those changes live. If you are coming from the earlier &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-implementation-guide" rel="noopener noreferrer"&gt;HIPAA CI/CD implementation guide&lt;/a&gt;, this is the comparison piece beside it. The &lt;a href="https://stonebridgetechsolutions.com/services/hipaa-cicd" rel="noopener noreferrer"&gt;pillar page&lt;/a&gt; covers positioning and control mapping at the architecture level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 01 — The framework difference in one sentence
&lt;/h2&gt;

&lt;p&gt;SOC 2 is a trust services framework. You choose which of the five criteria (security, availability, processing integrity, confidentiality, privacy) you attest against, and which systems are in scope. Your auditor reads your description, looks at evidence for the controls you committed to, and writes a narrative report. If your framework is reasonable and your evidence consistent with what you claimed, you pass.&lt;/p&gt;

&lt;p&gt;HIPAA's Security Rule is not a framework you scope. It is a federal regulation. 45 CFR § 164 sets administrative, physical, and technical safeguards. Some are "required"; some are "addressable" (implement or document a defensible alternative). You do not scope out of the Security Rule for an environment that handles PHI. You choose how to implement, not whether.&lt;/p&gt;

&lt;p&gt;At the pipeline level, this is the difference that drives everything else. SOC 2 lets you write "production deploys are reviewed by an authorized engineer per documented procedure" and have your auditor confirm the procedure is followed. HIPAA expects the same control implemented in a way that is observable and queryable, with the system itself producing the evidence at the time the control runs. SOC 2 audits the policy. HIPAA audits the artifact.&lt;/p&gt;

&lt;p&gt;Once you accept that framing, the rest of the comparison is a series of consequences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 02 — Where HIPAA and SOC 2 controls overlap
&lt;/h2&gt;

&lt;p&gt;Three control areas are roughly equivalent between SOC 2 and HIPAA at the pipeline layer. If you implemented them honestly for SOC 2 Type II, you are most of the way to the HIPAA equivalent. This is the overlap the comparison vendors latch onto.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pipeline access control.&lt;/strong&gt; SOC 2's CC6.1 requires logical access controls over systems in scope. HIPAA's § 164.312(a)(1) requires unique user identification, emergency access procedure, automatic logoff, and encryption controls. Both want the same pipeline-layer thing: MFA-protected CI/CD access, no shared service accounts triggering deploys, identity attribution on every job. A SOC 2 implementation with SSO + MFA and no long-lived bearer tokens covers HIPAA's unique user identification cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encryption in transit.&lt;/strong&gt; SOC 2's CC6.7 requires data in transit between components is protected. HIPAA's § 164.312(e)(1) requires transmission security for PHI. Both expect TLS 1.2+ on every channel touching sensitive data: pipeline to registry, pipeline to cloud API, pipeline to deploy target. Same configuration, same evidence shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit logging baseline.&lt;/strong&gt; SOC 2's CC7.2 requires monitoring system components for anomalies and security events. HIPAA's § 164.312(b) requires audit controls that record activity on systems containing PHI. Both expect pipeline-level logs capturing who did what, when, on which artifact. If your SOC 2 evidence already streams pipeline logs to a SIEM, the HIPAA baseline is covered.&lt;/p&gt;

&lt;p&gt;These three overlap honestly. They are why "we have SOC 2 so we're 80% there" sounds plausible. The problem is what's left after the easy 60%. The &lt;a href="https://stonebridgetechsolutions.com/services/hipaa-cicd#architecture" rel="noopener noreferrer"&gt;pillar page walks the architecture&lt;/a&gt; control by control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 03 — Where HIPAA and SOC 2 controls diverge
&lt;/h2&gt;

&lt;p&gt;Five control areas break the SOC 2-to-HIPAA equivalence story. Each is a place where the SOC 2 implementation is acceptable as policy and inadequate as system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Business Associate Agreement scope.&lt;/strong&gt; HIPAA requires that every entity touching PHI on your behalf, including downstream services in the pipeline, is covered by a BAA. SOC 2 has no equivalent concept. A pipeline that uses a managed KMS service for cosign signatures, a container registry for PHI-bound images, and a secrets manager for deploy credentials is fine for SOC 2 if your vendor list is current. For HIPAA, every one of those services has to be BAA-covered and the runner orchestrating them has to be covered too. § 164.308(a)(4) sets the requirement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Six-year retention with immutability.&lt;/strong&gt; SOC 2 does not specify a minimum audit log retention. Your auditor will accept whatever you wrote. HIPAA § 164.316(b)(2)(i) is specific: documentation must be retained for six years from creation or the date it was last in effect, whichever is later. And it must be defensible, which in practice means immutable. A pipeline that writes audit logs to a default CloudWatch group with 90-day retention is a SOC 2 yes and a HIPAA no.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encryption verification, not attestation.&lt;/strong&gt; SOC 2 accepts "TLS 1.2 enforced via load balancer policy, verified during the annual review." HIPAA's § 164.312(a)(2)(iv) and § 164.312(e)(1) expect ongoing assurance that encryption is happening, not that it was once configured. At the pipeline layer, that means deploy-time tests that fail the build when TLS verification breaks, not a once-a-year posture review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workforce sanction enforcement.&lt;/strong&gt; HIPAA § 164.308(a)(1)(ii)(C) requires sanctions applied against workforce members who fail to comply with security policies. SOC 2 expects you to have a sanction policy. HIPAA expects it enforced, which in pipeline terms means a deploy by a workforce member whose access has been revoked fails at the runner, not at the code review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breach notification telemetry.&lt;/strong&gt; HIPAA § 164.404 gives covered entities 60 days from breach discovery to notify affected individuals. SOC 2 has no equivalent regulatory clock. HIPAA expects the system to surface enough provenance that a security event can be scoped against the 60-day window; SOC 2 lets you reconstruct the same evidence over weeks.&lt;/p&gt;

&lt;p&gt;These five gaps are where the engineering work lives. They are also where the comparison vendors stop writing. The next four sections walk what each requires from the pipeline, with code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision 01 — Which framework owns your evidence model
&lt;/h2&gt;

&lt;p&gt;Every architectural decision that follows comes down to one question: which framework owns your evidence model? Build the pipeline for SOC 2 evidence and you build something an auditor can read. Build it for HIPAA evidence and you build something an auditor can query.&lt;/p&gt;

&lt;p&gt;SOC 2 evidence is built for narrative: screenshots, control descriptions, sample-based attestations, policy documents reviewed annually. The model assumes a human assembles evidence on a schedule, an auditor reads it, and the narrative carries the weight. HIPAA evidence is built for query: signed manifests, append-only audit logs, immutable artifact registries, deploy events with attribution timestamps. The system itself is the source of truth, producing the workflow and the evidence at the same time.&lt;/p&gt;

&lt;p&gt;My opinionated stance: if you are building a pipeline that will support both frameworks, architect it for HIPAA evidence. The reverse path does not work. A SOC 2-shaped evidence pipeline will not pass HIPAA's evidence expectations without a rebuild. A HIPAA-shaped evidence pipeline produces SOC 2 evidence as a side effect of operating correctly. Build for the stricter framework; the looser one comes free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 02 — Pipeline access and BAA scope
&lt;/h2&gt;

&lt;p&gt;SOC 2 lets you document which engineers can deploy. HIPAA expects the runner to enforce it, and extends the requirement to every downstream service the runner touches.&lt;/p&gt;

&lt;p&gt;§ 164.308(a)(4) requires that workforce access to PHI is granted only to those whose role justifies it. The Security Rule extends that obligation to systems accessing PHI, including CI/CD systems and the downstream services they call. A pipeline runner with IAM credentials reaching PHI-bearing workloads is itself in BAA scope. So is every service it touches. That produces three pipeline-level requirements SOC 2 does not check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The runner is on infrastructure covered by a BAA, your own or a cloud provider's BAA-covered service.&lt;/li&gt;
&lt;li&gt;Every downstream service the runner calls (KMS, registry, secrets manager, deploy target) is BAA-covered.&lt;/li&gt;
&lt;li&gt;The deploy attempt fails if any service in the chain falls outside that scope.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The third requirement is where policy-as-code earns its place. An OPA policy at the deploy gate inspects the deploy context, verifies every service touched is in the BAA-covered set, and blocks the deploy if any is not. The pipeline does not depend on a human knowing which services are covered. The policy does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rego"&gt;&lt;code&gt;&lt;span class="c1"&gt;# policies/baa_scope.rego&lt;/span&gt;
&lt;span class="c1"&gt;# Block HIPAA pipeline deploys that touch a service outside BAA scope.&lt;/span&gt;
&lt;span class="c1"&gt;# Evaluated by the parent pipeline before any deploy stage runs.&lt;/span&gt;

&lt;span class="ow"&gt;package&lt;/span&gt; &lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hipaa&lt;/span&gt;

&lt;span class="ow"&gt;import&lt;/span&gt; &lt;span class="n"&gt;rego&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;

&lt;span class="c1"&gt;# Services covered under an active BAA, with effective date.&lt;/span&gt;
&lt;span class="c1"&gt;# Sourced from a maintained registry, not hand-edited per build.&lt;/span&gt;
&lt;span class="n"&gt;baa_covered&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"kms.googleapis.com"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;              &lt;span class="s2"&gt;"2025-01-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"artifactregistry.googleapis.com"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2025-01-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"secretmanager.googleapis.com"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s2"&gt;"2025-01-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"container.googleapis.com"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="s2"&gt;"2025-01-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"logging.googleapis.com"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;          &lt;span class="s2"&gt;"2025-01-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Workforce members with current PHI access entitlement.&lt;/span&gt;
&lt;span class="c1"&gt;# Sourced from IdP group membership at evaluation time.&lt;/span&gt;
&lt;span class="n"&gt;authorized_actors&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"bob@example.com"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="ow"&gt;default&lt;/span&gt; &lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;actor_authorized&lt;/span&gt;
    &lt;span class="n"&gt;every_service_covered&lt;/span&gt;
    &lt;span class="n"&gt;runner_in_scope&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;actor_authorized&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;authorized_actors&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;every_service_covered&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;unscoped_service_exists&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;unscoped_service_exists&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ow"&gt;some&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;services_touched&lt;/span&gt;
    &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;baa_covered&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;runner_in_scope&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"hipaa-prod-runner"&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signed&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Deny messages surface in the pipeline log for the auditor.&lt;/span&gt;
&lt;span class="n"&gt;deny&lt;/span&gt; &lt;span class="n"&gt;contains&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ow"&gt;some&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;services_touched&lt;/span&gt;
    &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;baa_covered&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"service %s is outside BAA scope; deploy blocked"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;deny&lt;/span&gt; &lt;span class="n"&gt;contains&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;authorized_actors&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"actor %s lacks current PHI entitlement"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The policy is small on purpose. It runs against the same evidence bundle the parent pipeline aggregates, the same one referenced in the &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-implementation-guide" rel="noopener noreferrer"&gt;parent/child architecture in the implementation guide&lt;/a&gt;. The BAA registry is its own resource, kept in code with PRs gated on legal review. When a new service is introduced, the deploy fails until legal confirms coverage. Engineering does not have to keep the BAA list in their head.&lt;/p&gt;

&lt;p&gt;For SOC 2, the policy is overkill. For HIPAA, this is the minimum that survives an evidence pull.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 03 — Audit log retention and immutability
&lt;/h2&gt;

&lt;p&gt;SOC 2 retention is whatever you wrote in your policy. HIPAA retention is six years, immutable, recoverable. § 164.316(b)(2)(i) is direct about this, and it is one of the harder controls to retrofit later because retroactively making logs immutable is not a thing you can do.&lt;/p&gt;

&lt;p&gt;Three pipeline implications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Audit events go to a write-once destination at the time the event happens, not at end of pipeline.&lt;/li&gt;
&lt;li&gt;The destination supports object retention locks for the full six-year window, with no path to early deletion.&lt;/li&gt;
&lt;li&gt;The pipeline emits a signed manifest of every audit event, so tampering is detectable at audit time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On AWS, the cleanest implementation is S3 Object Lock in compliance mode with a six-year retention. On GCP, Bucket Lock with equivalent duration. Both work; neither is set up by default.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/audit-emit.yml&lt;/span&gt;
&lt;span class="c1"&gt;# Reusable workflow: emit a HIPAA-aligned audit event to an Object Lock bucket.&lt;/span&gt;
&lt;span class="c1"&gt;# Caller workflows invoke this on every pipeline state change worth recording.&lt;/span&gt;

&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;emit-audit-event&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_call&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;event_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="na"&gt;artifact_digest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;emit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hipaa-prod-runner&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Assume audit writer role&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::123456789012:role/hipaa-audit-writer&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-west-2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Compose audit event&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;# Event schema mirrors the SIEM ingestion contract.&lt;/span&gt;
          &lt;span class="s"&gt;# Hash the inputs so any field tampering invalidates the manifest.&lt;/span&gt;
          &lt;span class="s"&gt;cat &amp;gt; event.json &amp;lt;&amp;lt;EOF&lt;/span&gt;
          &lt;span class="s"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"event_type": "${{ inputs.event_type }}",&lt;/span&gt;
            &lt;span class="s"&gt;"artifact_digest": "${{ inputs.artifact_digest }}",&lt;/span&gt;
            &lt;span class="s"&gt;"actor": "${{ github.actor }}",&lt;/span&gt;
            &lt;span class="s"&gt;"pipeline_id": "${{ github.run_id }}",&lt;/span&gt;
            &lt;span class="s"&gt;"occurred_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"&lt;/span&gt;
          &lt;span class="s"&gt;}&lt;/span&gt;
          &lt;span class="s"&gt;EOF&lt;/span&gt;
          &lt;span class="s"&gt;sha256sum event.json | awk '{print $1}' &amp;gt; event.sha256&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Write to Object Lock bucket&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;# Compliance mode: no override path, even for the root account.&lt;/span&gt;
          &lt;span class="s"&gt;# Retention set on the object itself, not a bucket policy that can be relaxed.&lt;/span&gt;
          &lt;span class="s"&gt;aws s3api put-object \&lt;/span&gt;
            &lt;span class="s"&gt;--bucket hipaa-audit-logs \&lt;/span&gt;
            &lt;span class="s"&gt;--key "events/${{ github.run_id }}/$(date -u +%s).json" \&lt;/span&gt;
            &lt;span class="s"&gt;--body event.json \&lt;/span&gt;
            &lt;span class="s"&gt;--object-lock-mode COMPLIANCE \&lt;/span&gt;
            &lt;span class="s"&gt;--object-lock-retain-until-date \&lt;/span&gt;
              &lt;span class="s"&gt;"$(date -u -d '+6 years' +%Y-%m-%dT%H:%M:%SZ)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Retention is set on the object itself, not on a bucket policy that can be relaxed later. Compliance mode means the retention cannot be shortened even by the AWS root account. The manifest hash is computed before write, so tampering is detectable. A query "who deployed artifact X" returns the workforce member, the runner, the artifact digest, and the timestamp in one row.&lt;/p&gt;

&lt;p&gt;For SOC 2, this is more than your auditor will ask for. For HIPAA, this is the floor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 04 — Encryption verification, not attestation
&lt;/h2&gt;

&lt;p&gt;SOC 2 lets you say "TLS 1.2 enforced via load balancer policy." HIPAA expects the pipeline to verify it on every deploy.&lt;/p&gt;

&lt;p&gt;§ 164.312(a)(2)(iv) on encryption and § 164.312(e)(1) on transmission security both map to SOC 2's CC6.7, which accepts annual attestation. HIPAA does not. The expectation is that encryption posture is verified at deploy time, with failure modes that block the deploy. Two verifications matter at the pipeline layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Artifact signature verification.&lt;/strong&gt; Every container deployed to a PHI-bearing environment carries a cosign signature; the deploy fails if signature verification fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transit verification.&lt;/strong&gt; Every PHI-touching endpoint the pipeline reaches negotiates TLS 1.2+ with a current cipher; the deploy fails if any endpoint downgrades or refuses.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# scripts/verify-encryption.sh&lt;/span&gt;
&lt;span class="c"&gt;# Run at the end of the deploy stage. Blocks the deploy on any verification miss.&lt;/span&gt;
&lt;span class="c"&gt;# HIPAA-aligned: § 164.312(a)(2)(iv) and § 164.312(e)(1).&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;IMAGE_DIGEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;:?image&lt;span class="p"&gt; digest required&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;PHI_ENDPOINTS&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;
    &lt;span class="s2"&gt;"https://api.hipaa-app.internal/v1/healthz"&lt;/span&gt;
    &lt;span class="s2"&gt;"https://audit.hipaa-app.internal/ingest"&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;

verify_signature&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;digest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="c"&gt;# Verify against the production-only Fulcio identity.&lt;/span&gt;
    &lt;span class="c"&gt;# Any other signer fails the build.&lt;/span&gt;
    cosign verify &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;digest&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--certificate-identity&lt;/span&gt; &lt;span class="s2"&gt;"platform-team@example.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--certificate-oidc-issuer&lt;/span&gt; &lt;span class="s2"&gt;"https://accounts.google.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;span class="o"&gt;}&lt;/span&gt;

verify_transit&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="c"&gt;# Require TLS 1.2+, refuse on downgrade.&lt;/span&gt;
    &lt;span class="c"&gt;# Surface the negotiated protocol for the audit log.&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;proto
    &lt;span class="nv"&gt;proto&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--tls-max&lt;/span&gt; 1.3 &lt;span class="nt"&gt;--tlsv1&lt;/span&gt;.2 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--write-out&lt;/span&gt; &lt;span class="s2"&gt;"%{ssl_version}"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; /dev/null &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;proto&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
        &lt;/span&gt;TLSv1.2|TLSv1.3&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ok &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;proto&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
        &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"fail &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;proto&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;esac&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== verifying artifact signature"&lt;/span&gt;
verify_signature &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;IMAGE_DIGEST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== verifying transit posture"&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;endpoint &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PHI_ENDPOINTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;verify_transit &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;endpoint&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== encryption verification ok"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter. First, it is a deploy gate, not a post-deploy check. A failed signature or a TLS downgrade blocks the deploy before any PHI-touching code reaches a runtime handling a request. Second, the verification output is itself an audit event, written to the Object Lock bucket from Decision 03. The auditor's question, "show me that encryption was verified on the deploy that touched record X," has a one-query answer.&lt;/p&gt;

&lt;p&gt;SOC 2 tells you the load balancer config is correct. HIPAA tells you the deploy on May 13 at 09:14 UTC verified TLS 1.3 to the audit endpoint and TLS 1.2 to the API endpoint with this cipher.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 04 — HIPAA SOC 2 control mapping, opinionated
&lt;/h2&gt;

&lt;p&gt;The table below is the version of "HIPAA vs SOC 2" I would hand a platform team on day one of an engagement. It maps the control areas where the frameworks diverge against the pipeline pattern we would build, the pattern we would accept under time pressure, and the pattern we would refuse to ship.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control area&lt;/th&gt;
&lt;th&gt;HIPAA&lt;/th&gt;
&lt;th&gt;SOC 2&lt;/th&gt;
&lt;th&gt;Recommended&lt;/th&gt;
&lt;th&gt;Acceptable&lt;/th&gt;
&lt;th&gt;Avoid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BAA scope on deploy path&lt;/td&gt;
&lt;td&gt;§ 164.308(a)(4)&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;OPA gate against a versioned BAA registry&lt;/td&gt;
&lt;td&gt;Quarterly BAA review with deploy-path inventory&lt;/td&gt;
&lt;td&gt;Trust the cloud provider default without verifying covered services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit log retention&lt;/td&gt;
&lt;td&gt;§ 164.316(b)(2)(i)&lt;/td&gt;
&lt;td&gt;CC7.2&lt;/td&gt;
&lt;td&gt;S3/GCS Object Lock at 6-year retention in compliance mode&lt;/td&gt;
&lt;td&gt;Append-only SIEM with documented 6-year archive&lt;/td&gt;
&lt;td&gt;Default CloudWatch retention; "we never delete logs" as the policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encryption verification&lt;/td&gt;
&lt;td&gt;§ 164.312(a)(2)(iv), § 164.312(e)(1)&lt;/td&gt;
&lt;td&gt;CC6.7&lt;/td&gt;
&lt;td&gt;Deploy-time signature and TLS verification; fail the build on miss&lt;/td&gt;
&lt;td&gt;Daily synthetic transaction with alerting&lt;/td&gt;
&lt;td&gt;Annual TLS posture review; soft warning in Helm charts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workforce sanction enforcement&lt;/td&gt;
&lt;td&gt;§ 164.308(a)(1)(ii)(C)&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;IdP-backed deploy entitlement evaluated at runner invocation&lt;/td&gt;
&lt;td&gt;Pre-deploy human check against current entitlement list&lt;/td&gt;
&lt;td&gt;Documented sanction policy with no system-level enforcement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breach notification telemetry&lt;/td&gt;
&lt;td&gt;§ 164.404&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;Signed deploy manifest queryable by digest, actor, and time&lt;/td&gt;
&lt;td&gt;Centralized log search with deploy-event tagging&lt;/td&gt;
&lt;td&gt;Reconstruct deploy provenance from Slack and CI run history&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Recommended" passes both audits without ambiguity. "Acceptable" is what you sit in under time pressure, with a written remediation date. "Avoid" produces audit findings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 05 — What SOC 2 to HIPAA migration looked like in production
&lt;/h2&gt;

&lt;p&gt;Six weeks. Eight engineers across two squads. Roughly 5,200 lines of new pipeline code, 1,800 lines retired. That was the shape of the engagement that triggered this post.&lt;/p&gt;

&lt;p&gt;The team came in with passing Type II SOC 2, a closed contract with a regional hospital system, and a BAA addendum that assumed evidence the pipeline could not yet produce. We mapped their existing SOC 2 evidence against the Security Rule. The overlap was real: pipeline access was already SSO with MFA, transit was already TLS 1.2+, audit logs already streamed to a SIEM. The gaps were the ones in Section 03. None were unfamiliar; all required engineering work.&lt;/p&gt;

&lt;p&gt;The work split four weeks pipeline and four weeks the runtime and IAM context the pipeline depended on. Pipeline rewrites cost less time than expected; parent/child separation was already partially in place, and the OPA gate for BAA scope fit cleanly into the structure they had. The expensive work was outside the pipeline: an IAM redesign to scope the audit writer role away from any operator who could shorten retention, legal review of every cloud service in the deploy path (which surfaced three uncovered services no one had noticed), and the cosign deploy gate catching a staging cluster pulling unsigned dev images through a cached registry mirror. None of those was a pipeline bug. All were pipeline-adjacent.&lt;/p&gt;

&lt;p&gt;What shipped at week six was a pipeline that emitted evidence on its own, against the Security Rule, with the same control set the SOC 2 auditor had read about in narrative. The first internal HIPAA audit took the platform lead 90 minutes instead of the four days the prior SOC 2 evidence pull had taken. That is the difference between a system that produces evidence and a human who produces evidence about a system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 06 — Common mistakes
&lt;/h2&gt;

&lt;p&gt;Three calls show up in nearly every "we have SOC 2, adding HIPAA" conversation. Each one is wrong in a specific way.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"We're 80% there because we have SOC 2."&lt;/strong&gt; The overlap is 60% on control inventory and much smaller on implementation work. The 40% gap is where the architecture has to change. SOC 2 was built for narrative; HIPAA was built for query. Most of the engineering cost lives in the conversion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"We can use the same auditor."&lt;/strong&gt; Sometimes true, often beside the point. Your SOC 2 auditor reads your control framework against criteria you chose. Your HIPAA controls have to satisfy a regulation you did not choose. The evidence shape has to change regardless of who reads it. The earlier post on &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-5-mistakes" rel="noopener noreferrer"&gt;five things healthcare engineering teams get wrong about HIPAA CI/CD&lt;/a&gt; walks five places where SOC 2-style evidence passes and HIPAA-style evidence does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Our pipeline doesn't really touch PHI."&lt;/strong&gt; It almost always does, once you trace artifact provenance. The artifact you build runs against PHI in production. The cosign key signs it. The deploy credential places it. The audit log records the placement. Every one of those is a HIPAA touchpoint. The pipeline does not have to read PHI to be in scope.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each call is reasonable on its face. Each underbudgets the engineering work by a factor of three. The vendors making the call get paid for the conclusion; the platform team pays for the rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 07 — Frequently asked
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I need HIPAA compliance if I already have SOC 2?
&lt;/h3&gt;

&lt;p&gt;Yes, if your environment touches PHI. SOC 2 is a voluntary trust services framework where you scope your own controls. HIPAA is a federal regulation that does not let you scope out of the Security Rule for any environment handling PHI. SOC 2 compliance covers about 60% of HIPAA's control inventory at the pipeline layer, but does not satisfy HIPAA's evidence requirements. The remaining 40% is engineering work.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does it take to add HIPAA to a SOC 2 pipeline?
&lt;/h3&gt;

&lt;p&gt;Three months is a defensible estimate if the engineering team is honest about the work involved. The expensive parts are BAA scope mapping across every service the pipeline touches, audit log retention architecture with six-year immutability, and encryption verification gates at deploy time. Thirty days is the number comparison vendors quote and the platform team regrets accepting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is SOC 2 enough for selling into healthcare?
&lt;/h3&gt;

&lt;p&gt;No. SOC 2 Type II is table stakes for B2B SaaS but does not satisfy HIPAA requirements for handling Protected Health Information. Hospital systems and other covered entities require a Business Associate Agreement and HIPAA-compliant controls, which SOC 2 alone does not provide. SOC 2 plus HIPAA is the combination most healthcare engineering teams need.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between HIPAA and SOC 2 audit log requirements?
&lt;/h3&gt;

&lt;p&gt;SOC 2 does not specify a minimum audit log retention; your auditor accepts whatever you wrote in your policy. HIPAA § 164.316(b)(2)(i) requires six years of retention from creation or last effective date, whichever is later, with practical immutability requirements. A pipeline writing audit logs to default CloudWatch retention with 90-day rollover passes SOC 2 but fails HIPAA.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does SOC 2 compliance satisfy a Business Associate Agreement?
&lt;/h3&gt;

&lt;p&gt;No. BAA is a HIPAA construct with no SOC 2 equivalent. Every entity that touches PHI on your behalf, including downstream services in your CI/CD pipeline, must be covered by a BAA. SOC 2 has no mechanism for this requirement. The runner orchestrating deploys, the KMS service signing artifacts, the registry holding PHI-bound images, and the secrets manager providing deploy credentials all need BAA coverage under HIPAA.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use the same auditor for HIPAA and SOC 2?
&lt;/h3&gt;

&lt;p&gt;Sometimes, but it's often beside the point. Your SOC 2 auditor reads your control framework against criteria you chose. Your HIPAA controls have to satisfy a federal regulation you did not choose. The evidence shape has to change regardless of who reads it. A SOC 2-shaped evidence pipeline will not pass HIPAA expectations without a rebuild; the opposite is not true.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does it cost to upgrade from SOC 2 to HIPAA-compliant CI/CD?
&lt;/h3&gt;

&lt;p&gt;Pricing depends on the size of your engineering team, the number of pipelines in scope, and the maturity of your existing SOC 2 controls. A two-week fixed-fee HIPAA CI/CD audit produces a written remediation roadmap with effort estimates sized to your specific environment. Stonebridge runs &lt;a href="https://stonebridgetechsolutions.com/services/hipaa-cicd#engagements" rel="noopener noreferrer"&gt;audit and build engagements as fixed-fee&lt;/a&gt;, so you know the scope, the deliverables, and the price before any work starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 08 — Closing
&lt;/h2&gt;

&lt;p&gt;SOC 2 audits the policies you chose. HIPAA audits the system you built. The overlap is real and worth using. The divergence is real and worth budgeting for. Three months is a defensible estimate if the team is honest about the work; thirty days is the number the comparison vendors quote and the platform team regrets accepting.&lt;/p&gt;

&lt;p&gt;Compliance is a property of the system, not a process humans run. Build the pipeline to emit HIPAA evidence on its own, and SOC 2 evidence falls out of normal operation. Build it the other way and the gap closes by rewrite, not retrofit.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you are in the SOC 2-to-HIPAA conversation right now:&lt;/strong&gt; Stonebridge runs &lt;a href="https://stonebridgetechsolutions.com/services/hipaa-cicd#engagements" rel="noopener noreferrer"&gt;two-week HIPAA CI/CD audits&lt;/a&gt; that map your existing SOC 2 controls against the Security Rule and produce a written remediation roadmap. Fixed fee, founder-led, the report holds up under first-party review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep reading:&lt;/strong&gt; the practitioner's walkthrough of every Security Rule control before an audit lives in &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-audit-checklist" rel="noopener noreferrer"&gt;the HIPAA CI/CD audit checklist for engineering teams&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;Lucas Jones is the Founder and Principal Platform Engineer at &lt;a href="https://stonebridgetechsolutions.com/about" rel="noopener noreferrer"&gt;Stonebridge Tech Solutions&lt;/a&gt;. Six years building cloud infrastructure and CI/CD pipelines in regulated environments, including HIPAA, FedRAMP, and SOC 2 work for healthcare and defense engineering teams across AWS, GCP, Azure, and OCI. Based in Sacramento, California.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post originally appeared on &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-vs-soc2" rel="noopener noreferrer"&gt;Stonebridge Tech Solutions&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hipaa</category>
      <category>soc2</category>
      <category>cicd</category>
      <category>compliance</category>
    </item>
    <item>
      <title>How to build a HIPAA-compliant CI/CD pipeline: a 2026 implementation guide</title>
      <dc:creator>Stonebridge Tech Solutions LLC</dc:creator>
      <pubDate>Sun, 17 May 2026 20:39:47 +0000</pubDate>
      <link>https://dev.to/stonebridgetechsolutions/how-to-build-a-hipaa-compliant-cicd-pipeline-a-2026-implementation-guide-aka</link>
      <guid>https://dev.to/stonebridgetechsolutions/how-to-build-a-hipaa-compliant-cicd-pipeline-a-2026-implementation-guide-aka</guid>
      <description>&lt;p&gt;&lt;em&gt;The architecture, the code, and the parts auditors actually inspect.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most HIPAA CI/CD content describes the controls. This one describes the architecture.&lt;/p&gt;

&lt;p&gt;A healthcare engineering team I worked with had six weeks to make their CI/CD pipeline audit-ready. They had GitLab. They had AWS. They had a smart team that had already read 45 CFR § 164. They knew what HIPAA required.&lt;/p&gt;

&lt;p&gt;They were stuck anyway. Every guide they could find described which controls HIPAA mandates and which scanners to run. None of them described how to actually build the pipeline that emits the controls.&lt;/p&gt;

&lt;p&gt;Three architectural decisions separate HIPAA-compliant pipelines from generic CI/CD: parent/child pipeline separation, isolated runners per environment, and security scanners as policy gates rather than advisory output. Everything else flows from these three.&lt;/p&gt;

&lt;p&gt;If you've already read the &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-5-mistakes" rel="noopener noreferrer"&gt;five patterns that fail HIPAA audits&lt;/a&gt;, this is the implementation side of the same coin. The earlier post was about what goes wrong. This one is about what to build instead. The code examples assume GitLab CI/CD as the primary platform. The patterns port cleanly to GitHub Actions and Argo CD; I'll call out the translations where they matter. The cloud examples cover GCP and AWS specifically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 01 — What HIPAA actually requires from a pipeline
&lt;/h2&gt;

&lt;p&gt;HIPAA's Security Rule doesn't specify pipelines. It specifies safeguards. A correctly-built CI/CD pipeline is one of the most efficient places to satisfy those safeguards continuously, instead of producing them as quarterly evidence runs before audit windows.&lt;/p&gt;

&lt;p&gt;Five controls touch the pipeline most directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;§ 164.308(a)(5)(ii)(C) Log-in monitoring.&lt;/strong&gt; Every deployment must be attributable to an authenticated identity with audited access. In pipeline terms: who triggered this deploy, with what credentials, and is that identity authorized for this environment?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;§ 164.308(a)(8) Periodic evaluation.&lt;/strong&gt; Security evaluations must happen on every change, not on a schedule. Vulnerability scans, dependency checks, and policy evaluations run on every push.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;§ 164.312(b) Audit controls.&lt;/strong&gt; The pipeline records who deployed what, when, against which approval chain. Every deploy is queryable months later without forensic reconstruction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;§ 164.312(c)(1) Integrity controls.&lt;/strong&gt; Artifacts are signed, signatures are verified before deployment, and tampering is structurally detectable. The artifact you deploy is provably the one that passed scanning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;§ 164.312(e)(1) Transmission security.&lt;/strong&gt; Deployments to PHI-bearing environments use mutually-authenticated, encrypted channels. No deploys over unencrypted protocols, no bearer-token-only authentication to production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are exotic. All of them are routinely missed in pipelines built without the controls in mind from day one. The framing that helps most: HIPAA doesn't tell you how to build a pipeline. It tells you what evidence the pipeline must produce. Once you accept that, the architecture follows.&lt;/p&gt;

&lt;p&gt;The pillar page covers the &lt;a href="https://stonebridgetechsolutions.com/services/hipaa-cicd#architecture" rel="noopener noreferrer"&gt;full control mapping in more detail&lt;/a&gt;. The five above are enough to anchor every architectural decision that follows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 02 — Three architectural decisions
&lt;/h2&gt;

&lt;p&gt;Most HIPAA pipeline guides are checklists. Run SAST. Run container scanning. Sign your artifacts. Encrypt your transmissions. Store your audit logs. The checklists are correct, and almost completely useless.&lt;/p&gt;

&lt;p&gt;They're useless because they describe outputs without describing the architecture that produces them. A team can implement every item on the checklist and still build a pipeline that fails audit. I've seen it happen four times.&lt;/p&gt;

&lt;p&gt;Three decisions, made early, determine whether a HIPAA pipeline actually works. The next three sections walk through each one: what goes wrong without it, what the architecture looks like, and what the code looks like.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision 01 — Parent/child pipeline separation
&lt;/h2&gt;

&lt;p&gt;Most HIPAA pipelines start as a single &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; file. It works fine for the first three months. By month six it's 800 lines. By month twelve it's 1,500 lines and nobody touches it without flinching.&lt;/p&gt;

&lt;p&gt;The problem isn't just maintainability. It's auditability. A 1,500-line pipeline file mixes environment-level concerns (production approval gates, evidence aggregation, signing) with service-level concerns (unit tests, container builds, lint checks). The auditor's question, &lt;em&gt;"show me that production deploys are gated"&lt;/em&gt;, requires reading the entire file to answer. And every YAML change risks dropping a compliance gate that nobody noticed was load-bearing.&lt;/p&gt;

&lt;p&gt;The fix is structural separation. A &lt;strong&gt;parent pipeline&lt;/strong&gt; owns environment-level concerns: compliance gates, approvals, evidence aggregation, deployment authorization. It changes rarely and is the system of record for "what happened." &lt;strong&gt;Child pipelines&lt;/strong&gt; own service-level concerns: build, test, scan, container packaging. They evolve independently per service.&lt;/p&gt;

&lt;p&gt;The pattern is platform-independent. In GitLab CI/CD it's implemented with &lt;code&gt;trigger:&lt;/code&gt; jobs and downstream artifact propagation. In GitHub Actions, with reusable workflows (&lt;code&gt;workflow_call&lt;/code&gt;). In Argo CD, with ApplicationSets and progressive delivery patterns. The platform changes; the architecture doesn't.&lt;/p&gt;

&lt;p&gt;A simplified parent pipeline looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .gitlab-ci.yml (parent)&lt;/span&gt;
&lt;span class="c1"&gt;# Owns: compliance gates, evidence, deploy authorization&lt;/span&gt;

&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;authorize&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;aggregate-evidence&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;policy-gate&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;HIPAA_ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${CI_COMMIT_BRANCH}&lt;/span&gt;
  &lt;span class="na"&gt;EVIDENCE_BUCKET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gs://hipaa-evidence-${ENV}"&lt;/span&gt;

&lt;span class="na"&gt;authorize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;authorize&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./scripts/verify-identity.sh "$GITLAB_USER_ID" "$HIPAA_ENVIRONMENT"&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_BRANCH&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"main"'&lt;/span&gt;

&lt;span class="na"&gt;trigger-build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.gitlab/child-build.yml&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;depend&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;PARENT_PIPELINE_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_PIPELINE_ID&lt;/span&gt;

&lt;span class="na"&gt;aggregate-evidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aggregate-evidence&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./scripts/collect-evidence.sh "$PARENT_PIPELINE_ID"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gsutil cp evidence-bundle.json "$EVIDENCE_BUCKET/$CI_PIPELINE_ID/"&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trigger-build"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;policy-gate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;policy-gate&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openpolicyagent/opa:latest&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;opa eval -d policies/ -i evidence-bundle.json "data.deploy.hipaa.allow"&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aggregate-evidence"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;deploy-production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hipaa-prod-runner"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
  &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manual&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./scripts/deploy-signed.sh&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy-gate"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire parent pipeline: under 40 lines, every stage doing one thing, every job tied to an explicit compliance concern. The child pipeline handles build/test/scan in a separate file that the service team owns. The parent owns the gates; the child owns the code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://stonebridgetechsolutions.com/services/hipaa-cicd#architecture" rel="noopener noreferrer"&gt;More on the three architecture principles I apply on every HIPAA pipeline build →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 02 — Isolated runners per environment
&lt;/h2&gt;

&lt;p&gt;The most underappreciated HIPAA control isn't in the pipeline file. It's in the runner.&lt;/p&gt;

&lt;p&gt;Most teams use shared GitLab or GitHub Actions runners with IAM credentials broad enough to reach multiple environments. A misconfigured &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; from a dev branch can deploy to production, because the runner has the credentials and nothing stops the YAML from using them. Worse: the runner itself becomes an attack surface. A compromised runner with production IAM access reaches PHI directly, and your audit logs don't show that as a privileged access path because the runner is "just CI."&lt;/p&gt;

&lt;p&gt;Auditors want a specific story: &lt;em&gt;this deployment to production went through this specific runner, with this specific identity, at this specific time, signed by this specific key.&lt;/em&gt; If you can't tell that story crisply, the runner is your audit finding.&lt;/p&gt;

&lt;p&gt;Four patterns isolate runners properly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dedicated runners per environment.&lt;/strong&gt; Production deploys run on production-only runners. Dev branches cannot trigger production runners through any path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoped IAM per runner.&lt;/strong&gt; Dev runner has dev IAM; prod runner has prod IAM. Neither can deploy to the other. The pipeline file cannot lift its own privileges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signed, version-pinned runner images.&lt;/strong&gt; No &lt;code&gt;gitlab-runner:latest&lt;/code&gt;. Image signature verified before runner starts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HIPAA-aligned runner infrastructure.&lt;/strong&gt; Encrypted at rest, audit-logged, ephemeral where possible, isolated network egress.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On GCP, the cleanest implementation is a dedicated GKE node pool per environment, with Workload Identity binding runner service accounts to environment-scoped IAM roles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Terraform: GCP HIPAA runner infrastructure&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_container_node_pool"&lt;/span&gt; &lt;span class="s2"&gt;"hipaa_prod_runners"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hipaa-prod-runners"&lt;/span&gt;
  &lt;span class="nx"&gt;cluster&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google_container_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hipaa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;node_count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

  &lt;span class="nx"&gt;node_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;machine_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"n2-standard-4"&lt;/span&gt;
    &lt;span class="nx"&gt;image_type&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"COS_CONTAINERD"&lt;/span&gt;

    &lt;span class="c1"&gt;# Workload Identity: runner SA cannot escape its scope&lt;/span&gt;
    &lt;span class="nx"&gt;workload_metadata_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GKE_METADATA"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;service_account&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google_service_account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runner_prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;
    &lt;span class="nx"&gt;oauth_scopes&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://www.googleapis.com/auth/cloud-platform"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Encrypted boot disk&lt;/span&gt;
    &lt;span class="nx"&gt;disk_size_gb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
    &lt;span class="nx"&gt;disk_type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pd-ssd"&lt;/span&gt;

    &lt;span class="c1"&gt;# Network isolation: only egress to allowed targets&lt;/span&gt;
    &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"hipaa-prod-runner"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# Auto-upgrade for CIS-aligned base images&lt;/span&gt;
  &lt;span class="nx"&gt;management&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;auto_upgrade&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;auto_repair&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_service_account"&lt;/span&gt; &lt;span class="s2"&gt;"runner_prod"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;account_id&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hipaa-prod-runner"&lt;/span&gt;
  &lt;span class="nx"&gt;display_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HIPAA Production CI Runner"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Runner SA can deploy to prod GKE only — not dev, not staging&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_project_iam_member"&lt;/span&gt; &lt;span class="s2"&gt;"runner_prod_deploy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"roles/container.developer"&lt;/span&gt;
  &lt;span class="nx"&gt;member&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"serviceAccount:${google_service_account.runner_prod.email}"&lt;/span&gt;

  &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;title&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Production cluster only"&lt;/span&gt;
    &lt;span class="nx"&gt;expression&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"resource.name.startsWith('projects/${var.project_id}/zones/${var.zone}/clusters/hipaa-prod')"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The IAM condition is the load-bearing part. Even if a developer writes a pipeline that tries to deploy to staging from a production runner, the IAM denies the action at the GCP API layer. The pipeline file cannot grant itself privileges the runner doesn't have.&lt;/p&gt;

&lt;p&gt;The 5 mistakes post covers &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-5-mistakes" rel="noopener noreferrer"&gt;why pipeline-runner isolation matters in more depth&lt;/a&gt;. The TL;DR: shared runners with broad IAM are the most common HIPAA pipeline finding I see, and the cheapest to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 03 — Security scanners as policy gates
&lt;/h2&gt;

&lt;p&gt;Almost every HIPAA pipeline guide includes security scanners. SAST, DAST, container CVE scanning, dependency checking, IaC scanning. The tools are right. The configuration is usually wrong.&lt;/p&gt;

&lt;p&gt;Most teams run scanners as advisory: scans execute, results are saved to a dashboard, the deploy proceeds regardless. Auditor asks: &lt;em&gt;"what happens when a critical vulnerability is found?"&lt;/em&gt; The honest answer is usually "the developer gets notified and decides what to do." That answer fails audit.&lt;/p&gt;

&lt;p&gt;The right answer: &lt;em&gt;the deploy is blocked by policy. The human approver only sees the deploy button if every scanner produced evidence that passed threshold.&lt;/em&gt; Scanners produce inputs to a policy engine. The policy engine decides whether the deploy proceeds. Humans provide judgment on top of that decision, not validation that the pipeline ran.&lt;/p&gt;

&lt;p&gt;Five scanners earn their place in a HIPAA pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SAST.&lt;/strong&gt; Semgrep with custom rules for healthcare-specific patterns. CodeQL is a strong alternative if you're already on GitHub Advanced Security.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container CVE scanning.&lt;/strong&gt; Trivy. Grype is acceptable. Don't rely on cloud-provider-only scanners (ECR scan, Artifact Registry scan) as your only line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IaC scanning.&lt;/strong&gt; tfsec plus Checkov. They catch different things; run both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secret scanning.&lt;/strong&gt; Gitleaks in pre-commit and in CI. TruffleHog also works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency scanning.&lt;/strong&gt; OSV-Scanner for transitive vulnerabilities. Dependabot for routine updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture is the same regardless of which tools you pick. Scanner runs, produces structured output (JSON or SARIF), evidence is signed and stored, policy engine evaluates evidence, gate opens or closes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rego"&gt;&lt;code&gt;&lt;span class="c1"&gt;# policies/hipaa_deploy.rego&lt;/span&gt;
&lt;span class="c1"&gt;# OPA policy gate for HIPAA production deploys&lt;/span&gt;

&lt;span class="ow"&gt;package&lt;/span&gt; &lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hipaa&lt;/span&gt;

&lt;span class="ow"&gt;default&lt;/span&gt; &lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="c1"&gt;# Deploy allowed if all four conditions hold&lt;/span&gt;
&lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;scan_evidence_valid&lt;/span&gt;
    &lt;span class="n"&gt;signature_valid&lt;/span&gt;
    &lt;span class="n"&gt;approver_authorized&lt;/span&gt;
    &lt;span class="n"&gt;target_environment_matches&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# All scanners produced evidence in last 24 hours&lt;/span&gt;
&lt;span class="n"&gt;scan_evidence_valid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now_ns&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1e9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;     &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now_ns&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1e9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;       &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now_ns&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1e9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Zero critical findings across all scanners&lt;/span&gt;
&lt;span class="n"&gt;scan_evidence_valid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;critical&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;critical&lt;/span&gt;      &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;critical&lt;/span&gt;       &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findings&lt;/span&gt;   &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Artifact signature verified by Cosign&lt;/span&gt;
&lt;span class="n"&gt;signature_valid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;artifact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cosign_verified&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;artifact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signed_by&lt;/span&gt;       &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expected_signer&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Approver is on the authorized list for this environment&lt;/span&gt;
&lt;span class="n"&gt;approver_authorized&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;authorized&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;approvers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target_environment&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;approver_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;authorized&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Deploy target matches the artifact's intended environment&lt;/span&gt;
&lt;span class="n"&gt;target_environment_matches&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;artifact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target_env&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target_environment&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every condition in that policy is checkable, auditable, and structurally enforced. When the auditor asks &lt;em&gt;"how do you guarantee scans pass before deploy?"&lt;/em&gt;, the answer is a 35-line Rego file you can hand them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 03 — Putting it together: the reference architecture
&lt;/h2&gt;

&lt;p&gt;The three decisions compose into a six-stage pipeline. Each stage emits evidence. Each evidence artifact is signed and stored separately from code. The policy gate evaluates the full evidence bundle before any deploy proceeds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────────── PARENT PIPELINE ─────────────────────────┐
│ Compliance gates · Approvals · Evidence aggregation · Authorization│
└────┬───────────────┬─────────────────┬─────────────────┬──────────┘
     ▼               ▼                 ▼                 ▼
┌─────────┐    ┌─────────┐       ┌─────────┐       ┌─────────┐
│ CHILD 01│    │ CHILD 02│       │ CHILD 03│       │ CHILD 04│
│  Build  │    │  Test   │       │  Scan   │       │  Sign   │
└────┬────┘    └────┬────┘       └────┬────┘       └────┬────┘
     │              │                 │                 │
     └──────────────┴─────────┬───────┴─────────────────┘
                              ▼
       ┌──── IMMUTABLE EVIDENCE BUCKET (retention-locked) ────┐
       │ SBOMs · Scan results · Signatures · Approval records │
       └──────────────────────┬───────────────────────────────┘
                              ▼
                    ┌───── POLICY GATE ─────┐
                    │ OPA evaluates evidence│
                    │   allow or deny       │
                    └──────────┬────────────┘
                               ▼
              ┌──────────────────────────────────┐
              │ Deploy via isolated prod runner  │
              └──────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diagram makes one thing clear that the prose can hide: &lt;strong&gt;evidence flows in one direction only&lt;/strong&gt;. Child pipelines write evidence to the bucket; nothing reads from it except the policy gate. The bucket is write-once from CI's perspective, and read-only from the gate's perspective. Engineers cannot tamper with evidence after the fact, because the bucket's IAM policy doesn't allow CI to modify or delete existing objects.&lt;/p&gt;

&lt;p&gt;This is what auditors mean by "audit controls." Not a logfile somewhere. A separate, immutable, queryable record of everything the pipeline did, signed by the pipeline's identity, retained per your compliance policy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Adjacent frameworks · CMMC and FedRAMP&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The same architecture satisfies most of the technical controls in CMMC 2.0 (Levels 2 and 3) and FedRAMP Moderate. The control mappings differ. CMMC inherits NIST 800-171 control families; FedRAMP uses NIST 800-53. The underlying engineering is identical. Parent/child pipelines satisfy CM-3 (Configuration Change Control) and AU-2 (Audit Events). Isolated runners satisfy AC-3 (Access Enforcement) and SC-7 (Boundary Protection). Policy gates satisfy SI-2 (Flaw Remediation) and CA-7 (Continuous Monitoring).&lt;/p&gt;

&lt;p&gt;For defense workloads, the only material differences are runner placement (CI runners must operate inside the GovCloud or Azure Government boundary) and KMS configuration (FIPS 140-2 validated). Build the pipeline correctly for HIPAA and the FedRAMP version is mostly a deployment configuration change.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Section 04 — GCP-specific implementation
&lt;/h2&gt;

&lt;p&gt;On GCP, the HIPAA pipeline architecture maps to a specific set of native services. The translations matter because cloud-specific service quirks determine whether the pattern actually satisfies the underlying control.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Evidence storage.&lt;/strong&gt; GCS bucket with versioning, retention locks, and Bucket Lock for write-once semantics. Use a separate project for the evidence bucket so its IAM is independently managed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signing keys.&lt;/strong&gt; Cloud KMS with automatic rotation. Cosign integrates natively. Use HSM-backed keys for production signing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runner infrastructure.&lt;/strong&gt; GKE Autopilot or Standard with environment-specific node pools. Workload Identity binds runner pods to GCP service accounts. Per-environment service accounts with IAM conditions enforce boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artifact storage.&lt;/strong&gt; Artifact Registry, not the deprecated Container Registry. Turn on vulnerability scanning at the registry layer as a second line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPC-SC perimeters.&lt;/strong&gt; Restrict service-to-service access at the network layer. CI runners outside the perimeter cannot reach PHI-bearing services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity-Aware Proxy.&lt;/strong&gt; When the pipeline must reach VMs (rare but real, especially for legacy environments), IAP provides the audited, encrypted, identity-aware channel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simplified GCP deploy job looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .gitlab/child-deploy-gcp.yml&lt;/span&gt;
&lt;span class="c1"&gt;# HIPAA-aligned GKE deploy, GCP-specific&lt;/span&gt;

&lt;span class="na"&gt;deploy-gcp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google/cloud-sdk:slim&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hipaa-prod-runner"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;before_script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Workload Identity authentication (no static keys)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gcloud auth print-access-token &amp;gt; /tmp/token&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gcloud config set project $GCP_PROJECT_ID&lt;/span&gt;

  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Verify artifact signature before deploy&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cosign verify&lt;/span&gt;
        &lt;span class="s"&gt;--key gcpkms://projects/$GCP_PROJECT_ID/locations/$GCP_REGION/keyRings/hipaa/cryptoKeys/signing&lt;/span&gt;
        &lt;span class="s"&gt;$ARTIFACT_REGISTRY_URL/hipaa-app:$CI_COMMIT_SHA&lt;/span&gt;

    &lt;span class="c1"&gt;# Deploy to GKE with image digest (not tag)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;kubectl set image deployment/hipaa-app&lt;/span&gt;
        &lt;span class="s"&gt;hipaa-app=$ARTIFACT_REGISTRY_URL/hipaa-app@sha256:$ARTIFACT_DIGEST&lt;/span&gt;

    &lt;span class="c1"&gt;# Emit deployment evidence&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./scripts/emit-evidence.sh deploy-complete $CI_PIPELINE_ID&lt;/span&gt;

  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production-gcp&lt;/span&gt;
    &lt;span class="na"&gt;deployment_tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;

  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_TAG&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_TAG&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;=~&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/^v\d+\.\d+\.\d+$/'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to notice. First, no JSON service account key is ever written to disk; Workload Identity issues short-lived tokens. Second, the deploy uses the image digest, not the tag, so a race condition between tag-and-deploy can't substitute a different image. Third, evidence emission is part of the deploy job itself; if the deploy succeeds, evidence is written, and the parent pipeline can see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 05 — AWS-specific implementation
&lt;/h2&gt;

&lt;p&gt;On AWS the architecture is the same; the services change names. For teams operating in both clouds (increasingly common in healthcare), the parent pipeline can dispatch to either cloud's child pipeline based on a variable. Both clouds emit evidence to a centralized bucket, both clouds use OPA for policy gating, both clouds use Cosign for signing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Evidence storage.&lt;/strong&gt; S3 with Object Lock in Compliance Mode, versioning enabled, cross-region replication for redundancy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signing keys.&lt;/strong&gt; AWS KMS with customer-managed keys (CMKs), automatic rotation, CloudTrail logging on every key use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runner infrastructure.&lt;/strong&gt; EKS managed node groups with IRSA (IAM Roles for Service Accounts). Separate node groups per environment with taints and tolerations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artifact storage.&lt;/strong&gt; ECR with image scanning enabled and immutable tags. Lifecycle policies for evidence retention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network isolation.&lt;/strong&gt; Transit Gateway for environment-level isolation. Security groups operate on principles (referencing tags or names) rather than IP allowlists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GovCloud variant.&lt;/strong&gt; Same architecture, runners placed inside the GovCloud boundary for FedRAMP-aligned workloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;IRSA is the AWS analog to GCP's Workload Identity. The Terraform looks similar but the trust policy is what does the work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Terraform: AWS HIPAA runner IAM role (IRSA)&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"hipaa_prod_runner"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hipaa-prod-runner"&lt;/span&gt;

  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Federated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_openid_connect_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRoleWithWebIdentity"&lt;/span&gt;
      &lt;span class="nx"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;StringEquals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="s2"&gt;"${replace(aws_iam_openid_connect_provider.eks.url, "&lt;/span&gt;&lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//", "")}:sub" =&lt;/span&gt;
            &lt;span class="s2"&gt;"system:serviceaccount:gitlab-runner:hipaa-prod-runner"&lt;/span&gt;
          &lt;span class="c1"&gt;# Scope to one specific namespace + one service account&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Scoped policy: deploy to prod cluster only&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"hipaa_prod_deploy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hipaa_prod_runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"eks:DescribeCluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"eks:ListClusters"&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_eks_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hipaa_prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
      &lt;span class="nx"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;StringEquals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="s2"&gt;"aws:RequestedRegion"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The IRSA trust policy is the boundary enforcement. The runner pod can only assume this role if it runs in the specific namespace with the specific service account. A dev pipeline cannot bypass it; an attacker who compromises a dev runner cannot pivot to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 06 — What this looks like in production
&lt;/h2&gt;

&lt;p&gt;One of our healthcare engagements involved auditing a GCP-based platform running production workloads with PHI in flight. The internal team had been told to be ready for a third-party HIPAA assessment in six weeks. They had GitLab CI/CD, a working pipeline, and a compliance team that knew what HIPAA wanted. What they didn't have was visibility into VM-level inventory or evidence that their pipeline controls were enforced rather than recommended.&lt;/p&gt;

&lt;p&gt;We started with the architectural audit: was the pipeline parent/child or monolithic (monolithic, 1,200 lines), were runners isolated per environment (no, one shared runner with broad IAM), were scanners producing policy gate inputs (no, scanners ran as advisory). Three findings, all structural, all addressable.&lt;/p&gt;

&lt;p&gt;The remediation was the architecture above. Parent pipeline owns gates; child pipelines own builds. Dedicated runner per environment with scoped IAM. Scanners producing signed evidence into an immutable bucket; OPA policy evaluating before deploy. Six weeks of work, fixed scope, fixed fee.&lt;/p&gt;

&lt;p&gt;The team passed the third-party audit on first-party review. More importantly, the pipeline kept passing through subsequent quarterly audits without remediation work, because the architecture made the controls structural instead of procedural.&lt;/p&gt;

&lt;p&gt;That's the test of a HIPAA pipeline: does it pass audit when the auditor changes, the team changes, and the code changes? An architecturally correct pipeline does. A checklist-compliant pipeline doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 07 — Tooling recommendations
&lt;/h2&gt;

&lt;p&gt;Opinionated picks, based on what actually holds up in regulated environments. Substitutions are fine; the architecture matters more than the specific tool.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Recommended&lt;/th&gt;
&lt;th&gt;Acceptable alternative&lt;/th&gt;
&lt;th&gt;Avoid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build&lt;/td&gt;
&lt;td&gt;Buildah, Kaniko (rootless)&lt;/td&gt;
&lt;td&gt;docker:dind (with caveats)&lt;/td&gt;
&lt;td&gt;Privileged Docker on shared runners&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SAST&lt;/td&gt;
&lt;td&gt;Semgrep with custom rules&lt;/td&gt;
&lt;td&gt;CodeQL (if on GitHub)&lt;/td&gt;
&lt;td&gt;Cloud-vendor scanners alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container scanning&lt;/td&gt;
&lt;td&gt;Trivy&lt;/td&gt;
&lt;td&gt;Grype&lt;/td&gt;
&lt;td&gt;ECR / Artifact Registry scan as only line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IaC scanning&lt;/td&gt;
&lt;td&gt;tfsec + Checkov (both)&lt;/td&gt;
&lt;td&gt;KICS&lt;/td&gt;
&lt;td&gt;Manual review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret scanning&lt;/td&gt;
&lt;td&gt;Gitleaks (pre-commit + CI)&lt;/td&gt;
&lt;td&gt;TruffleHog&lt;/td&gt;
&lt;td&gt;Regex-only homebrew checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signing&lt;/td&gt;
&lt;td&gt;Cosign with KMS-backed keys&lt;/td&gt;
&lt;td&gt;Sigstore (notary v2)&lt;/td&gt;
&lt;td&gt;Manual signing, local keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy gates&lt;/td&gt;
&lt;td&gt;OPA / Rego&lt;/td&gt;
&lt;td&gt;Kyverno (k8s-specific)&lt;/td&gt;
&lt;td&gt;Manual approval only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evidence storage&lt;/td&gt;
&lt;td&gt;S3 Object Lock / GCS Bucket Lock&lt;/td&gt;
&lt;td&gt;Versioned bucket only&lt;/td&gt;
&lt;td&gt;Same Git repo as code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Section 08 — The CI/CD platform question
&lt;/h2&gt;

&lt;p&gt;Three platforms cover roughly 90% of HIPAA CI/CD work I see: GitLab CI/CD, GitHub Actions, and Argo CD. None of them are wrong choices for HIPAA work. Each makes the architecture above easier or harder in specific ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitLab CI/CD&lt;/strong&gt; is the cleanest fit for parent/child pipelines. Native &lt;code&gt;trigger:&lt;/code&gt; jobs, downstream artifact propagation, and self-hosted runners with custom tags make the runner isolation pattern straightforward. The compliance gates ride naturally on the parent pipeline structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions&lt;/strong&gt; can do the same with reusable workflows (&lt;code&gt;workflow_call&lt;/code&gt;), but the model is more constrained. Runner isolation requires GitHub Enterprise or self-hosted runners with custom labels. Workable, not as clean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Argo CD&lt;/strong&gt; is excellent for the deploy stage. It's not a complete CI tool. Pair it with GitLab CI/CD or GitHub Actions for build/test/scan, then use Argo CD for the actual deploy with GitOps semantics. ApplicationSets are an underappreciated mechanism for environment isolation.&lt;/p&gt;

&lt;p&gt;For new HIPAA pipelines, my opinionated default is GitLab CI/CD plus Argo CD. Stronger primitives for parent/child separation, cleaner runner isolation, and Argo CD's progressive delivery patterns reduce blast radius. Don't try to use Jenkins for new HIPAA pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 09 — Common mistakes to avoid
&lt;/h2&gt;

&lt;p&gt;Five quick callouts from the field. Each one fails audits more often than it should.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single thousand-line pipeline file.&lt;/strong&gt; Refactor to parent/child before the file passes 500 lines. Past 1,000 it's an audit finding waiting to surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared runners with broad IAM.&lt;/strong&gt; Isolated runners per environment, scoped IAM, no exceptions for "the deploy job."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scanners running as advisory.&lt;/strong&gt; Scanners produce evidence. Evidence feeds policy. Policy decides. The human approves with judgment, not by validating the pipeline ran.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit evidence in Git.&lt;/strong&gt; Evidence lives in a separate immutable bucket. The same Git repo as code is the wrong answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual approval as the only control.&lt;/strong&gt; A human clicking a button satisfies nothing on its own. The button only appears when the policy gate passes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For longer-form examples of these failure modes, the earlier post on &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-5-mistakes" rel="noopener noreferrer"&gt;five patterns that fail HIPAA audits&lt;/a&gt; walks through each with healthcare engagement examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 10 — Conclusion
&lt;/h2&gt;

&lt;p&gt;The team that ships well in regulated environments isn't the one with the most paperwork. It's the one whose architecture makes compliance violations structurally difficult.&lt;/p&gt;

&lt;p&gt;"Audit-ready" isn't a state you achieve. It's a property of how the pipeline operates. Parent/child separation isolates compliance from delivery so both can evolve. Isolated runners per environment stop pipeline files from lifting their own privileges. Security scanners as policy gates turn checklists into enforcement.&lt;/p&gt;

&lt;p&gt;Build the architecture correctly and audits become queries. Build it incorrectly and audits become fire drills, no matter how many tools you bolt on.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you're working through this at your team:&lt;/strong&gt; Stonebridge runs &lt;a href="https://stonebridgetechsolutions.com/services/hipaa-cicd#engagements" rel="noopener noreferrer"&gt;two-week HIPAA CI/CD audits&lt;/a&gt; that map your existing pipeline against the Security Rule and produce a written remediation roadmap. Fixed fee, founder-led, the report holds up under first-party review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep reading:&lt;/strong&gt; the structural mistakes most often blocking this architecture live in &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-5-mistakes" rel="noopener noreferrer"&gt;5 mistakes healthcare teams make on HIPAA CI/CD&lt;/a&gt;. Where the same pattern diverges for SOC 2 procurement is mapped in &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-vs-soc2" rel="noopener noreferrer"&gt;HIPAA CI/CD vs SOC 2 CI/CD&lt;/a&gt;. The pre-audit walkthrough of every Security Rule control is in &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-audit-checklist" rel="noopener noreferrer"&gt;the HIPAA CI/CD audit checklist&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;Lucas Jones is the Founder and Principal Platform Engineer at &lt;a href="https://stonebridgetechsolutions.com/about" rel="noopener noreferrer"&gt;Stonebridge Tech Solutions&lt;/a&gt;. Six years building cloud infrastructure and CI/CD pipelines in regulated environments, including HIPAA, FedRAMP, and SOC 2 work for healthcare and defense engineering teams across AWS, GCP, Azure, and OCI. Based in Sacramento, California.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post originally appeared on &lt;a href="https://stonebridgetechsolutions.com/blog/hipaa-cicd-implementation-guide" rel="noopener noreferrer"&gt;Stonebridge Tech Solutions&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hipaa</category>
      <category>cicd</category>
      <category>devops</category>
      <category>healthcare</category>
    </item>
    <item>
      <title>5 things healthcare engineering teams get wrong about HIPAA CI/CD</title>
      <dc:creator>Stonebridge Tech Solutions LLC</dc:creator>
      <pubDate>Tue, 05 May 2026 23:26:00 +0000</pubDate>
      <link>https://dev.to/stonebridgetechsolutions/5-things-healthcare-engineering-teams-get-wrong-about-hipaa-cicd-5ao</link>
      <guid>https://dev.to/stonebridgetechsolutions/5-things-healthcare-engineering-teams-get-wrong-about-hipaa-cicd-5ao</guid>
      <description>&lt;p&gt;I've spent the last six years building cloud infrastructure and CI/CD pipelines for healthcare and defense engineering teams. The same five mistakes keep showing up across every HIPAA engagement I take on, and none of them are about not knowing what HIPAA requires.&lt;/p&gt;

&lt;p&gt;Engineers in healthcare aren't dumb. They've read 45 CFR § 164. They know what audit logs are. They've sat through compliance training that lasted longer than their last on-call rotation.&lt;/p&gt;

&lt;p&gt;The problem is structural. Most CI/CD pipelines were designed for unregulated software, then bolted with compliance controls afterward. The result is pipelines that satisfy neither engineers nor auditors. Slow, brittle, and somehow still failing audits.&lt;/p&gt;

&lt;p&gt;Here are the five patterns I see most often, what goes wrong with each, and what actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Treating compliance as a final gate
&lt;/h2&gt;

&lt;p&gt;The most common pattern: your pipeline runs build, test, and deploy as normal. Then somewhere near the end, a "compliance check" stage runs that produces a report. Audit time rolls around and someone has to dig through six months of build artifacts to assemble evidence.&lt;/p&gt;

&lt;p&gt;This fails for two reasons.&lt;/p&gt;

&lt;p&gt;First, you can't debug what failed. When the auditor asks "show me the security review for build #4,827 from last March," nobody knows where that evidence lives. It's somewhere in CI logs that have probably rolled off retention.&lt;/p&gt;

&lt;p&gt;Second, the late-stage gate creates a false sense of security. Engineers learn that compliance is "the thing that happens at the end," so they stop thinking about it during development. Vulnerabilities ship to staging, get caught at the gate, and the pipeline gets blocked. Now you have an angry developer, a bottlenecked release, and a control that's optimized for catching mistakes rather than preventing them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What works instead:&lt;/strong&gt; emit compliance evidence as a &lt;em&gt;property&lt;/em&gt; of every stage. Build stage produces an SBOM. Test stage produces signed test results. Scan stage produces vulnerability data. Sign stage produces a signature chain. All of it gets pushed to immutable storage with retention locks the moment it's generated.&lt;/p&gt;

&lt;p&gt;When the auditor asks for evidence, the answer is a query, not a project.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A single thousand-line &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;I've inherited too many pipelines that look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .gitlab-ci.yml, 1,847 lines&lt;/span&gt;
&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;scan&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;notify&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;more-things&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;even-more-things&lt;/span&gt;

&lt;span class="na"&gt;build:dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# 200 lines&lt;/span&gt;

&lt;span class="na"&gt;build:staging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# 200 lines, mostly copy-pasted from above&lt;/span&gt;

&lt;span class="na"&gt;build:prod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# 200 more lines&lt;/span&gt;

&lt;span class="na"&gt;test:dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You cannot review this file. You cannot test it. You cannot meaningfully change it without breaking something else. And every time you push to fix a typo, the entire pipeline runs.&lt;/p&gt;

&lt;p&gt;For a HIPAA workload, this is doubly bad. Auditors specifically ask whether you can demonstrate that controls are applied consistently across environments. With a monolithic file, the answer is "trust us." With auditors, "trust us" is the wrong answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What works instead:&lt;/strong&gt; parent/child pipeline architecture.&lt;/p&gt;

&lt;p&gt;Your &lt;strong&gt;parent pipeline&lt;/strong&gt; handles environment-level concerns: compliance gates, deployment authorization, evidence aggregation. It's stable, rarely changes, and is the system of record for "what happened."&lt;/p&gt;

&lt;p&gt;Your &lt;strong&gt;child pipelines&lt;/strong&gt; handle service-level concerns: build, test, scan, deploy. They're triggered by the parent and can evolve independently per service.&lt;/p&gt;

&lt;p&gt;In GitLab, this is implemented with &lt;code&gt;trigger:&lt;/code&gt; jobs and &lt;code&gt;include:&lt;/code&gt; directives. In GitHub Actions, with &lt;code&gt;workflow_call&lt;/code&gt;. In Argo CD, with ApplicationSets.&lt;/p&gt;

&lt;p&gt;The pattern matters more than the platform. Once you separate "what runs" from "what's allowed to run," your pipeline becomes auditable instead of incomprehensible.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Manual approval as the only meaningful control
&lt;/h2&gt;

&lt;p&gt;You've seen this in every regulated environment: a deployment job that requires a human to click "approve" before production. Sometimes there's even a Slack notification asking three people to thumbs-up before it proceeds.&lt;/p&gt;

&lt;p&gt;Auditors love seeing this in pipeline diagrams. Until you ask: what is the human actually approving?&lt;/p&gt;

&lt;p&gt;If the answer is "they're confirming the build looks good," that's not a control. That's theater.&lt;/p&gt;

&lt;p&gt;A meaningful approval is one backed by &lt;em&gt;something&lt;/em&gt;. The signed artifacts checked out clean. The vulnerability scan came back below threshold. The compliance baseline was verified. The deployment target is in the right environment. The approver is providing judgment on top of those things, not validating that the pipeline ran.&lt;/p&gt;

&lt;p&gt;If your approver is just looking at a green checkmark and clicking yes, you have a process that &lt;em&gt;looks&lt;/em&gt; like a control but doesn't actually gate anything. Auditors who know what they're doing catch this immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What works instead:&lt;/strong&gt; policy-as-code gates &lt;em&gt;before&lt;/em&gt; the human approval. We use Open Policy Agent (OPA) with Rego policies that verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Container signature chain validates against trusted keys&lt;/li&gt;
&lt;li&gt;SBOM exists and matches the deployed image&lt;/li&gt;
&lt;li&gt;Vulnerability scan is &amp;lt; 24 hours old and below threshold&lt;/li&gt;
&lt;li&gt;Approver identity matches an authorized list&lt;/li&gt;
&lt;li&gt;Target environment matches what was scanned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The human approver only sees the deploy button if all of those pass. Their job is judgment ("should we ship this on a Friday?"), not validation ("is this safe?"). That's automated.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. No environment isolation in the CI runners themselves
&lt;/h2&gt;

&lt;p&gt;This one slipped past me on a project a few years ago, and I see it almost everywhere. You're running CI on shared runners. The runners have IAM credentials with access to multiple AWS accounts, including production. A misconfigured &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; from a developer branch could, in theory, deploy to production.&lt;/p&gt;

&lt;p&gt;Worse: the runner itself becomes a giant attack surface. If anyone compromises a CI job (through a malicious dependency, a typosquatted image, anything), they have whatever access the runner has.&lt;/p&gt;

&lt;p&gt;For a HIPAA workload, this is catastrophic. Your &lt;em&gt;runner&lt;/em&gt; now has access to PHI-bearing environments, and your audit logs don't show that as a privileged access path because the runner is "just CI."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What works instead:&lt;/strong&gt; runner isolation at the network and IAM level.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Production deployments run on &lt;strong&gt;dedicated runners&lt;/strong&gt; in a network that can only reach production. Dev branches can't trigger them.&lt;/li&gt;
&lt;li&gt;Runner IAM roles are &lt;strong&gt;scoped per-environment&lt;/strong&gt;. The dev runner can deploy to dev. The prod runner can deploy to prod. Neither can deploy to the other.&lt;/li&gt;
&lt;li&gt;Runner images are &lt;strong&gt;signed and version-pinned&lt;/strong&gt;. We don't pull &lt;code&gt;gitlab-runner:latest&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Runners themselves are &lt;strong&gt;HIPAA-aligned&lt;/strong&gt;: encrypted at rest, audit-logged, ephemeral where possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern auditors want to see is: "the deployment to production went through &lt;em&gt;this specific runner&lt;/em&gt;, with &lt;em&gt;this specific identity&lt;/em&gt;, at &lt;em&gt;this specific time&lt;/em&gt;, signed by &lt;em&gt;this specific key&lt;/em&gt;." If you can't tell that story, your pipeline is the audit finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Audit evidence stored alongside the code
&lt;/h2&gt;

&lt;p&gt;Here's the one I see catch teams off guard the most. They've done everything else right. Pipeline emits evidence, signed artifacts, policy gates, the works. Then the auditor asks: where does the evidence live?&lt;/p&gt;

&lt;p&gt;"In our Git repo. We commit pipeline logs and scan results."&lt;/p&gt;

&lt;p&gt;This fails immediately. The control says "the evidence must be tamper-evident." If engineers have write access to the repository (and they do, that's the whole point of the repo), then engineers have write access to the evidence. The control is broken.&lt;/p&gt;

&lt;p&gt;I had this exact conversation with a healthcare team a couple of years back. Smart engineers, modern pipeline, decent security posture. They'd been storing artifacts and audit logs in the same repo as their Terraform code. Worse: the team had GCP service account credentials committed to the Terraform code base. Not by malice. Just because someone had been moving fast on a Friday and pushed credentials to make a CI test work.&lt;/p&gt;

&lt;p&gt;The credentials were in the repo. The audit logs were in the repo. The Terraform that provisioned the audit logging &lt;em&gt;bucket&lt;/em&gt; was in the repo. None of that survived an honest auditor's first questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What works instead:&lt;/strong&gt; evidence storage is its own thing, separate from CI infrastructure entirely.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Object storage with &lt;strong&gt;versioning + retention locks&lt;/strong&gt; (S3 Object Lock, GCS retention policies, Azure Blob immutable storage). Once written, can't be modified or deleted within the retention window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write-only access&lt;/strong&gt; from CI. The pipeline can push evidence. Nobody (including the people who run the pipeline) can modify or delete it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read access&lt;/strong&gt; is logged separately. Auditors can read; reads are themselves audit-logged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encryption keys&lt;/strong&gt; are managed in a different account / project than where the pipeline runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The control to demonstrate is: "even if every engineer on the team colluded, they couldn't tamper with the audit trail." That's what the auditor wants to verify. If you can't structurally guarantee it, you don't have a control. You have a hope.&lt;/p&gt;

&lt;p&gt;If you have credentials in your Terraform code base, rotate them today, then move them into a secrets manager (GCP Secret Manager, AWS Secrets Manager, HashiCorp Vault) referenced by Terraform but never stored in plaintext. Treat any committed credential as compromised regardless of whether the repo is private.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern under all five
&lt;/h2&gt;

&lt;p&gt;If you read these carefully, the same principle keeps showing up: &lt;strong&gt;the controls have to be properties of the system, not activities humans remember to perform.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Human-dependent controls fail because humans forget, get busy, get tired, or move on to other teams. System-dependent controls fail only when someone changes the system, and changing the system is itself an audit-loggable event.&lt;/p&gt;

&lt;p&gt;Healthcare engineering teams that ship well in regulated environments aren't the ones with the most paperwork. They're the ones whose pipeline architecture makes compliance violations structurally difficult.&lt;/p&gt;

&lt;p&gt;That's what auditors actually want to see, and it's also what lets your engineering team ship without fighting compliance the whole time.&lt;/p&gt;

&lt;p&gt;If this resonates with what your team is dealing with, or if you're staring down an audit and your pipeline looks more like #2 than what I described, this is what we work on at &lt;a href="https://stonebridgetechsolutions.com/services/hipaa-cicd" rel="noopener noreferrer"&gt;Stonebridge Tech Solutions&lt;/a&gt;. Happy to talk shop either way.&lt;/p&gt;

&lt;p&gt;What patterns am I missing? Drop them in the comments. I'd genuinely like to know what other people are seeing in the field.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>healthcare</category>
      <category>cicd</category>
      <category>security</category>
    </item>
  </channel>
</rss>
