惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
G
GRAHAM CLULEY
P
Privacy & Cybersecurity Law Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
宝玉的分享
宝玉的分享
P
Proofpoint News Feed
H
Help Net Security
V
Visual Studio Blog
阮一峰的网络日志
阮一峰的网络日志
C
Cisco Blogs
人人都是产品经理
人人都是产品经理
Know Your Adversary
Know Your Adversary
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Recorded Future
Recorded Future
I
Intezer
罗磊的独立博客
T
The Exploit Database - CXSecurity.com
Blog — PlanetScale
Blog — PlanetScale
Malwarebytes
Malwarebytes
Spread Privacy
Spread Privacy
T
Tor Project blog
V
Vulnerabilities – Threatpost
云风的 BLOG
云风的 BLOG
腾讯CDC
B
Blog RSS Feed
Stack Overflow Blog
Stack Overflow Blog
F
Future of Privacy Forum
MyScale Blog
MyScale Blog
Latest news
Latest news
IT之家
IT之家
MongoDB | Blog
MongoDB | Blog
The Hacker News
The Hacker News
S
Securelist
博客园 - 【当耐特】
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
Threat Research - Cisco Blogs
Jina AI
Jina AI
Cisco Talos Blog
Cisco Talos Blog
B
Blog
博客园 - 三生石上(FineUI控件)
Last Week in AI
Last Week in AI
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
M
MIT News - Artificial intelligence
V
V2EX
D
Darknet – Hacking Tools, Hacker News & Cyber Security
The Cloudflare Blog
The GitHub Blog
The GitHub Blog
博客园 - 聂微东
F
Full Disclosure
C
CERT Recently Published Vulnerability Notes

DEV Community

Selling Without Stripe in a Country That Stripe Can't Reach: When Compliance Becomes a Technical Problem Solana's Biggest Consensus Overhaul Is Live for Testing. Here's What Builders Need to do right now. Your agent keeps using that word ... OpenSparrow v2.3 – visual admin panel, zero dependencies, now with ERD and M2M support Why AI Engineering Is Becoming More Like Distributed Systems Engineering How I Cut My LLM Costs by 90% Without Changing My App Logic Security Is Important. Automate It I killed my SaaS after 17 days and rebuilt it into something else How to Stop Your LLM Agent From Looping Itself Into Oblivion Apache Kafka for Beginners: Building Real-Time Streaming Systems with Python Dating the Crawler AI-Assisted Frontend Reviews Using Gemma 4 Building Secure Multi-Agent Systems: My Takeaways from Google I/O 2026 The Most Underrated Announcement from Google I/O 2026 Was Buried in a 90-Second Demo How to Fix CUDA Out of Memory Errors in Stable Diffusion WebUI My Experience Building My First Token And Having it Exist On-Chain. African Creators Deserve Better: How I Built a Payment Gateway for Every Corner of the Continent React CRUD basics Should Websites Allow AI Search Crawlers? Chunking Strategies for AI Code Review on Large Repos Beyond the Prompt: How to Build Stateful AI Agents with Persistent Memory and Self-Learning Loops What 10 University Visits in Cameroon Taught Me About Building AI for the Real World, and Why Gemma 4 Was the Answer The Universal Remote for AI: A Deep Dive into the Model Context Protocol (MCP) AgentGuard 0.3.0 — macOS menu bar app, Telegram rollback, and more Antigravity CLI: A Hands-On Guide to Google's Terminal Coding Agent Shopify Functions vs Shopify Scripts: A Migration Walkthrough What Actually Survives a Chicago-Area Winter on Your Deck Rethinking Geo-Blocking and Stripe's Failures in Global Access: A Cautionary Tale of Misoptimization I Built a Free Brat Generator - Here's What I Learned About Next.js Performance published Found a Second Layer to a GitHub Follow Botnet? AI Daily Digest: May 22, 2026 — Agentic Workflows, Coding Agents & Embodied AI How I Secured Internal Microservice Calls Without Passing JWTs Stop Mixing Them Up: SLI vs SLO vs SLA Explained Rebuilding My Engineering Mind Building a Music Production Ecosystem Instead of Just Releasing Plugins The Vonage Dev Discussion: How AI is transforming software development I Gave Our Enterprise AI a Memory. It Started Citing Last Quarter's Incidents. 𝐓𝐡𝐞 𝐂𝐨𝐦𝐦𝐮𝐧𝐢𝐜𝐚𝐭𝐢𝐨𝐧 𝐒𝐭𝐲𝐥𝐞 𝐂𝐫𝐢𝐬𝐢𝐬 Hermes Agent in the Wild: How I Turned It Into an AI Ops Employee Navigating the Hazy Jungle of Global E-commerce: How We Built a Reliable System for Digital Creators in Tanzania The Cost of Cross-Platform Development: Native Module Integration AI-Native Apps Will Swallow the Web I switched my Gemma 4 model three times in 72 hours. Here's the decision tree I wish I'd had. Inside #100DaysofSolana: A Guided Path into Web3 I Built and Shipped TinyHab: an ADHD-Friendly Habit Tracker for iOS I'm an ECE Student Who Vibe Codes Hardware Projects — Here's What Google I/O 2026 Actually Changed for Me From Fragmented Pipelines to Coherent Intelligence — Why Gemma 4 Actually Changes How I Work Our AI Inference Bill Dropped 65% After We Stopped Treating Every Query the Same Why P95 Latency Is the Only Metric That Matters at 3 AM Recycling made easy: a Polish recycling assistant powered by Gemma 4 The Complete Guide to Running a Midnight Node: Setup, Sync & Monitoring De CSRF a RCE: una visita web cuesta una shell en OpenYak Why We Built a Faster Wiki Building a Browser-Based Inkarnate Alternative for D&D Battle Maps Apache Kafka How to Build a FinTech Platform as a Solo Developer (By Any Means Necessary) Your LLM Logs Deserve Better — Send Claude Code Events to Bronto I built a free tool to track subscriptions and stop getting surprised by charges Building the TEYZIX CORE Internship Portal — My Full-Stack Development Journey PocketCFO: a private personal-finance brain that runs entirely in your browser Go Idioms I Wish I Knew Earlier Hey how are you guys I'm newbie web developer , learning wordpress+elementor Right now I don't know what to make I don't know what to write or use what color can you tell me about it ? Google I/O 2026 Blew My Mind — Here's What It Means for the Family App I'm Building 5 Things I Learned in My First Month as a Dev Intern EU AI Sovereignty Belongs in the Workflow Layer Why AI Coding Agents Need Business Context, Not Just Code Context How I Built 9 Claude AI Features into a Production SaaS Expo SDK 56 HashiCorp built an MCP server for writing Terraform. I built one for reviewing it Why Enterprise AI Agent Deployments Keep Failing Date Shear: A New Term for a Common Programming Pain Point Compass v1.1.0 · we shipped a memory plugin that catches its own consumption drift Zod Validation: Type-Safe APIs & Forms in TypeScript (Complete Guide) GitHub Actions CI/CD: Build a Complete Node.js Pipeline (2026) MCP in 2026: The numbers behind the ecosystem explosion working with an ai model mirror Learnt new things Four Metrics That Actually Tell You Whether Your Enterprise RAG Is Working Beyond the Stateless Prompt: Building an Auditable Product Intelligence Pipeline with Cascadeflow and Hindsight Most Creators Are Building in Pieces. I’m Building the Entire System. The Hidden Privacy Problem in Every AI App CVE-2026-26007: Subgroup Confinement Attack in pyca/cryptography The One Thing I See in Every Developer Who Gets Unstuck AI Memory Governance for Legal Tech: How Contract AI Agents Handle Privileged Data Two tables, zero migrations, full LINQ — a .NET data engine that's been running our production for 3 months Join the GitHub Finish-Up-A-Thon Challenge: $3,000 Prize Pool! I Replaced a $50/Month OCR API with Gemma 4’s Native Vision (And You Can Too) Building a Data-Driven Medical Image Enhancement Pipeline with Differential Evolution 🔥🩻 Why I Like Small Software Beyond the Model: Why the Gemini Ecosystem and Google AI Studio Are Redefining Enterprise AI Architecture in 2026 Complete set of Claude Skills for Solo Developer I read 50 years of network science, then built a CRM that runs entirely in the browser The New AI Workflow Is Not “More Agents” How to Make Large Time-Series Charts Smooth in Vue.js + ApexCharts (and fix Zoom & Scroll behavior issues) I Built a Cross-Platform Port Intelligence Tool to Stop Accidental Process Kills During Local Dev AI is heading toward a wall, and most people still don’t see it... Python String Methods Explained Simply (Common Operations) Why We Built a Zero-Knowledge Clipboard Manager for Developers (And Dropped Native Mobile Apps) Add Your Own Component to Bombie in 5 Edits Why Your OSS Advocacy Strategy Probably Doesn't Fit
GitHub Actions for HIPAA-compliant deployments
Stonebridge · 2026-05-22 · via DEV Community

OIDC trust scope, self-hosted runner discipline, reusable workflows as the compliance contract.

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

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."

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.

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.

If you've read the broader HIPAA CI/CD implementation guide, 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.

Section 01 — What HIPAA actually needs from a GitHub Actions pipeline

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.

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

  • § 164.308(a)(5)(ii)(C) Log-in monitoring. 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.
  • § 164.308(a)(8) Periodic evaluation. Security evaluations happen on every change. Scanners and policy evaluations run on every push, not on a cron the platform team forgets to maintain.
  • § 164.312(b) Audit controls. The pipeline records who deployed what, when, against which approval chain. GitHub's workflow_run history is not the audit log; it's a UI on top of one. The HIPAA audit log lives in S3 with Object Lock.
  • § 164.312(c)(1) Integrity controls. 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.
  • § 164.312(e)(1) Transmission security. Deploys to PHI-bearing environments use mutually authenticated, encrypted channels. No bearer tokens to production, no plain HTTP anywhere on the path.

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 full control mapping at the pillar page covers each Security Rule section against a specific pipeline touchpoint.

Section 02 — Three GitHub-specific gaps

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.

OIDC trust scope. The default GitHub OIDC subject claim is repo:org/name:ref:refs/heads/main 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.

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

Reusable workflow drift. Reusable workflows (workflow_call) 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.

Each of these has a fix. The next three sections walk them with code.


Section 03 — OIDC federation, scoped to one workflow

OIDC federation between GitHub Actions and AWS removes the entire category of long-lived credentials in repository secrets. No more AWS_ACCESS_KEY_ID 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.

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 StringEquals on the subject claim, scoped to one repo, one workflow file, and one environment.

# Terraform: HIPAA-aligned GitHub OIDC trust for AWS

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

# Production deploy role. Scoped to ONE repo, ONE workflow file,
# ONE environment. A dev branch cannot assume this role no matter
# what YAML it writes.
resource "aws_iam_role" "hipaa_prod_deploy" {
  name = "hipaa-prod-deploy"

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

Enter fullscreen mode Exit fullscreen mode

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 job_workflow_ref condition pins to a specific tagged version of a reusable workflow living in a separate compliance-workflows repo. That second condition is what stops a developer from writing inline deploy steps in the application repo and bypassing the reusable workflow.

The workflow side is short. The role assumption happens once per job, with no static credentials anywhere:

# .github/workflows/deploy-prod.yml (excerpt)
# Lives in the compliance-workflows repo, pinned by tag in the
# application repo's caller workflow. Application teams cannot
# modify this file.

name: HIPAA production deploy
on:
  workflow_call:
    inputs:
      image_digest: { type: string, required: true }
      target_cluster: { type: string, required: true }

permissions:
  id-token: write
  contents: read

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

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

      - name: Deploy by digest, not tag
        run: |
          aws eks update-kubeconfig --name ${{ inputs.target_cluster }}
          kubectl set image deployment/hipaa-app \
            hipaa-app=${{ inputs.image_digest }}

Enter fullscreen mode Exit fullscreen mode

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 compliance-workflows repo's build workflow. A signature from any other workflow fails verification.

Section 04 — Self-hosted runners on hardened infrastructure

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.

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.

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 runs-on: 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."

# Terraform: HIPAA prod runner pool on EKS

resource "aws_eks_node_group" "hipaa_prod_runners" {
  cluster_name    = aws_eks_cluster.hipaa.name
  node_group_name = "hipaa-prod-runners"
  node_role_arn   = aws_iam_role.runner_node.arn
  subnet_ids      = var.private_subnet_ids

  scaling_config {
    desired_size = 2
    min_size     = 2
    max_size     = 6
  }

  # Taints keep general workloads off the runner pool
  taint {
    key    = "workload"
    value  = "hipaa-runner"
    effect = "NO_SCHEDULE"
  }

  labels = {
    "stonebridge.io/runner-pool" = "hipaa-prod"
  }

  ami_type = "BOTTLEROCKET_x86_64"

  tags = {
    Compliance = "HIPAA"
    Boundary   = "production"
  }
}

# IRSA: the runner pod's SA can assume a runner role.
# The runner role can in turn assume the deploy role,
# but only from this specific namespace + SA.
resource "aws_iam_role" "runner_irsa" {
  name = "hipaa-prod-runner-irsa"

  assume_role_policy = jsonencode({
    Statement = [{
      Effect = "Allow"
      Principal = {
        Federated = aws_iam_openid_connect_provider.eks.arn
      }
      Action = "sts:AssumeRoleWithWebIdentity"
      Condition = {
        StringEquals = {
          "${replace(aws_iam_openid_connect_provider.eks.url, "https://", "")}:sub" =
            "system:serviceaccount:gha-runners:hipaa-prod-runner"
        }
      }
    }]
  })
}

Enter fullscreen mode Exit fullscreen mode

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 runs-on: value fails because the dev runner pool has a different label and a different IRSA chain.

The runner registration itself is gated at the GitHub org level: in Org Settings → Actions → Runner groups, the hipaa-prod 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.

Section 05 — Reusable workflows as the compliance contract

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

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.

# Caller in the application repo: .github/workflows/release.yml
# Application teams own this file. They cannot bypass the gates;
# the gates live in the reusable workflow they invoke.

name: Release to prod

on:
  push:
    tags: ['v*.*.*']

jobs:
  build_and_sign:
    uses: hipaa-app-org/compliance-workflows/.github/workflows/build-sign.yml@v3.2.1
    permissions:
      id-token: write
      contents: read
      packages: write
    secrets: inherit

  deploy_prod:
    needs: build_and_sign
    uses: hipaa-app-org/compliance-workflows/.github/workflows/deploy-prod.yml@v3.2.1
    with:
      image_digest: ${{ needs.build_and_sign.outputs.image_digest }}
      target_cluster: hipaa-prod-east
    permissions:
      id-token: write
      contents: read
    secrets: inherit

Enter fullscreen mode Exit fullscreen mode

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 (@v3.2.1), not by branch (@main). 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.

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

Section 06 — Environment protection and required reviewers

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.

Three settings on the production environment carry the compliance weight:

  • Required reviewers from a separate team. The approver cannot be the deploy author. The HIPAA Security Rule wants a separation of duties; a self-approved deploy fails audit.
  • Wait timer of 5 to 15 minutes. 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.
  • Deployment branch restrictions. Production deploys allowed only from refs/tags/v*. A main branch push, even one that built successfully, cannot deploy to production without a tag. The tag is a positive action with provenance.
# Terraform: GitHub environment protection for production

resource "github_repository_environment" "production" {
  repository  = "clinical-platform"
  environment = "production"

  reviewers {
    teams = [data.github_team.hipaa_approvers.id]
    # Important: empty users list. Approvers via team only,
    # so adds/removes flow through team membership audit.
  }

  wait_timer = 10

  deployment_branch_policy {
    protected_branches     = false
    custom_branch_policies = true
  }
}

resource "github_repository_environment_deployment_policy" "prod_tags" {
  repository  = "clinical-platform"
  environment = github_repository_environment.production.environment
  tag_pattern = "v*.*.*"
}

Enter fullscreen mode Exit fullscreen mode

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.

Section 07 — The policy gate that ties it together

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.

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.

# policies/github_deploy.rego
# Policy gate for HIPAA GitHub Actions production deploys.

package deploy.hipaa

import future.keywords.if
import future.keywords.in

default allow := false

allow if {
    scans_passed
    signature_valid
    approver_authorized
    workflow_ref_pinned
}

scans_passed if {
    input.scans.container.timestamp_ns > time.now_ns() - 3600 * 1e9
    input.scans.sast.timestamp_ns      > time.now_ns() - 3600 * 1e9
    input.scans.container.critical == 0
    input.scans.sast.critical      == 0
    input.scans.secrets.findings   == 0
}

signature_valid if {
    input.artifact.cosign_verified == true
    startswith(
        input.artifact.signed_by,
        "https://github.com/hipaa-app-org/compliance-workflows/",
    )
}

approver_authorized if {
    input.approver != input.deploy_author
    input.approver in data.approvers.production
}

workflow_ref_pinned if {
    regex.match(
        `compliance-workflows/.github/workflows/.*\.yml@refs/tags/v\d+\.\d+\.\d+$`,
        input.workflow_ref,
    )
}

deny[msg] if {
    not workflow_ref_pinned
    msg := sprintf(
        "workflow_ref %v is not pinned to a semver tag (HIPAA § 164.312(c)(1))",
        [input.workflow_ref],
    )
}

Enter fullscreen mode Exit fullscreen mode

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.

Section 08 — What this looked like at the 90-workflow team

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.

Week one and two: stand up the compliance-workflows 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.

Week three: tighten the OIDC trust policy. Add the job_workflow_ref 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 @main instead of a tag; that surfaced the drift mode in real time and reinforced why pinning by tag is the discipline.

Week four: stand up the self-hosted runner pool on EKS. Move the deploy job's runs-on: from ubuntu-latest to [self-hosted, hipaa-prod]. 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.

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.

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.

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.

Section 09 — Tooling recommendations

Opinionated picks for GitHub Actions on HIPAA. Substitutions are fine; the architecture matters more than the tool.

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

Section 10 — Common mistakes to avoid

Five quick callouts from the field. Each fails audits more often than it should.

  • Org-wide OIDC trust policy. 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.
  • GitHub-hosted runners for the deploy job. 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.
  • Reusable workflows pinned to @main. Tag-pin everything that touches production. Branch pinning is how compliance contracts rot over time without anyone noticing.
  • GitHub environment with Required reviewers: anyone with write access. The approver must be a separate team. Self-approval satisfies nothing.
  • CI run history as the audit log. GitHub rotates run history aggressively. The audit log lives in S3 with Object Lock, written by the workflow at the time the event occurs.

For longer-form versions of these failure modes against GitLab as well, the broader writeup on five patterns that fail HIPAA audits walks each.

Section 11 — Conclusion

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.

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.

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.

If you're working through this on a healthcare SaaS team: Stonebridge runs two-week HIPAA CI/CD audits 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.

Keep reading: the broader architecture pattern across CI platforms is in the HIPAA CI/CD implementation guide. The pre-audit walkthrough of every Security Rule control is in the HIPAA CI/CD audit checklist. Where this pattern diverges for teams coming from SOC 2 is mapped in HIPAA CI/CD vs SOC 2 CI/CD.


About the author

Lucas Jones is the Founder and Principal Platform Engineer at Stonebridge Tech Solutions. 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.

This post originally appeared on Stonebridge Tech Solutions.