04 — Audit loggingEvery deploy emits tamper-evident evidence.
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.
§ 164.312(b) · Audit controls
Logs are centralized in an account engineers cannot write to.
"Show me the deploy event for this production release, with the approver identity and timestamp."
Passes
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.
Fails
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.
Fix
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.
§ 164.312(b) · Audit controls
Every deploy is queryable by identity, environment, and artifact hash.
"Show me every production deploy this user made in Q1, and the SHA-256 of the artifact deployed."
Passes
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.
Fails
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.
Fix
Pipeline writes a structured deploy event to the logging account at the moment of cutover. Schema: deploy_id, timestamp, actor_identity, environment, artifact_digest, change_ticket, approval_chain. Treat it as a first-class output of the pipeline, not telemetry.
05 — Integrity controlsEvery artifact deployed has a signature you can verify.
§ 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.
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. Do not use Docker Content Trust for new HIPAA pipelines. Notary v1 is effectively unmaintained, and the verification story does not survive auditor questioning.
§ 164.312(c)(1) · Integrity
Container images are signed before deploy and verified on admission.
"Show me the signature for this container, the key used to sign it, and the admission decision that allowed it into production."
Passes
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.
Fails
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.
Fix
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.
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:
# File: policies/hipaa/admission/signed_images.rego
# Enforces signed-image admission for any pod entering a PHI-bearing namespace.
# Maps to 45 CFR § 164.312(c)(1) integrity controls and § 164.308(a)(4)
# information access management for production-PHI workloads.
package kubernetes.admission
import future.keywords.if
import future.keywords.in
# Namespaces that hold PHI workloads. The deny list is explicit so a
# misconfigured namespace cannot accidentally accept unsigned images.
phi_namespaces := {"prod-phi", "staging-phi", "validation-phi"}
# Public keys we trust. In production these live in a ConfigMap mounted
# at runtime, sourced from KMS public key endpoints.
trusted_keys := {
"cosign-prod-key-2026": "-----BEGIN PUBLIC KEY-----...",
"cosign-prod-key-2025-rotation": "-----BEGIN PUBLIC KEY-----...",
}
# Hard deny: any image in a PHI namespace without a verified signature
# from a trusted key fails admission. The reason string lands in the
# audit log so the rejection is queryable.
deny[msg] if {
input.request.namespace in phi_namespaces
some container in input.request.object.spec.containers
not image_signed_by_trusted_key(container.image)
msg := sprintf("HIPAA integrity violation: image %v not signed by trusted key in namespace %v (45 CFR 164.312(c)(1))", [container.image, input.request.namespace])
}
# Soft warning: even non-PHI namespaces should reject unsigned images.
# A warning lets the platform team see drift without breaking dev.
warn[msg] if {
not input.request.namespace in phi_namespaces
some container in input.request.object.spec.containers
not image_signed_by_trusted_key(container.image)
msg := sprintf("Unsigned image %v in non-PHI namespace %v. Cluster baseline requires signatures.", [container.image, input.request.namespace])
}
# Helper: returns true if any trusted key verified the image signature.
# The actual verification happens in a sidecar that pre-populates the
# image annotations with verification results. This keeps Rego pure.
image_signed_by_trusted_key(image) if {
annotation := input.request.object.metadata.annotations[sprintf("cosign.verified/%v", [image])]
annotation.verified == true
annotation.key_id in object.keys(trusted_keys)
}
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 HIPAA CI/CD implementation guide.
06 — Transmission securityModern TLS, mutually authenticated where it matters.
§ 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.
§ 164.312(e)(1) · Transmission security
Encrypted transit for every PHI-bearing path, with mutual auth where the threat model warrants.
"Show me the TLS configuration for the path between the pipeline and the production database. Which ciphers are accepted? Is the client authenticated?"
Passes
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.
Fails
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.
Fix
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.
07 — Continuous evaluationScanners as policy gates, not as advisory notifications.
§ 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.
| Scanner type |
Recommended tool |
What it catches |
Maps to |
| SAST |
Semgrep, CodeQL |
Source-code vulnerabilities in your application |
§ 164.308(a)(8) |
| SCA |
OSV-Scanner, Snyk |
Known CVEs in third-party dependencies |
§ 164.308(a)(8), § 164.312(c)(1) |
| Container CVE |
Trivy, Grype |
Vulnerable OS packages and libraries in container images |
§ 164.308(a)(8) |
| IaC |
tfsec, Checkov |
Misconfigured cloud resources, public buckets, open security groups |
§ 164.312(a)(1), § 164.312(e)(1) |
| Secrets |
Gitleaks, TruffleHog |
Hardcoded credentials, API keys, tokens in source |
§ 164.308(a)(4) |
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:
# File: .github/workflows/hipaa-build.yml
# Build, scan, and gate. Every scanner is a policy gate, not a notification.
# Maps to 45 CFR § 164.308(a)(8) continuous evaluation and § 164.312(c)(1)
# integrity. Failed gates block deploys; results emit to the evidence bucket.
name: HIPAA pipeline
on:
push:
branches: [main]
jobs:
scan:
runs-on: self-hosted-hipaa-baseline # Hardened, network-isolated runner
steps:
- uses: actions/checkout@v4
- name: SAST (Semgrep)
run: semgrep --config p/hipaa --error --json > sast.json
- name: SCA (OSV-Scanner)
run: osv-scanner --format json --output sca.json ./
- name: Container CVE (Trivy)
run: trivy image --severity HIGH,CRITICAL --exit-code 1 --format json --output trivy.json ${{ env.IMAGE }}
- name: IaC (Checkov)
run: checkov -d ./terraform --output json --output-file iac.json --soft-fail false
- name: Secret scan (Gitleaks)
run: gitleaks detect --no-git -v --report-format json --report-path secrets.json
# Policy gate. Any HIGH/CRITICAL finding from any scanner blocks the
# pipeline. The gate runs after every scanner so partial failures are
# visible in the run log.
- name: HIPAA policy gate
run: |
python3 .ci/policy_gate.py \
--sast sast.json \
--sca sca.json \
--trivy trivy.json \
--iac iac.json \
--secrets secrets.json \
--baseline hipaa-2026
# Evidence emit. Every scanner output is signed and pushed to the
# retention-locked evidence bucket. This is the artifact the auditor
# will read, so the schema is stable and queryable.
- name: Emit signed evidence
run: |
cosign attest --predicate sast.json --type vuln $IMAGE
cosign attest --predicate sca.json --type vuln $IMAGE
cosign attest --predicate trivy.json --type vuln $IMAGE
aws s3 cp ./*.json s3://hipaa-evidence-prod/pipeline-runs/${{ github.run_id }}/
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.
08 — Environment isolationThe runner that builds dev cannot reach prod.
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.
§ 164.308(a)(4), § 164.312(a)(1)
Per-environment runners with scoped IAM and explicit egress.
"Show me which runner deployed this artifact, and demonstrate the runner cannot reach any other environment."
Passes
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.
Fails
Shared runner pool. Runner credentials with permissions across environments. "We use namespaces" as the only isolation. CI runner with internet egress to anywhere.
Fix
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.
09 — Evidence collection and retentionThe audit trail has to outlive the pipeline that created it.
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.
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:
# File: terraform/hipaa-evidence/main.tf
# Retention-locked evidence bucket for HIPAA CI/CD pipeline artifacts.
# Maps to 45 CFR § 164.312(b) audit controls and § 164.316(b)(2)
# documentation retention. The bucket lives in a dedicated logging
# account; production accounts have write-only access via a scoped role.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
# Evidence bucket. Object Lock in COMPLIANCE mode means even the root
# account cannot delete objects within the retention window. This is
# the property that satisfies the auditor's "can engineers tamper with
# the evidence?" question.
resource "aws_s3_bucket" "hipaa_evidence" {
bucket = "hipaa-evidence-${var.env}-${var.account_suffix}"
object_lock_enabled = true
tags = {
Purpose = "HIPAA pipeline evidence"
CFRSection = "164.312(b),164.316(b)(2)"
Retention = "6y"
}
}
# Retention configuration. COMPLIANCE mode is non-negotiable for HIPAA
# evidence. GOVERNANCE mode lets privileged users override; that is a
# finding waiting to happen.
resource "aws_s3_bucket_object_lock_configuration" "hipaa_evidence" {
bucket = aws_s3_bucket.hipaa_evidence.id
rule {
default_retention {
mode = "COMPLIANCE"
days = 2190 # 6 years; matches § 164.316(b)(2) documentation retention
}
}
}
# Versioning is a prerequisite for Object Lock. Without it the lock
# applies to a single object that can be overwritten with a new version.
resource "aws_s3_bucket_versioning" "hipaa_evidence" {
bucket = aws_s3_bucket.hipaa_evidence.id
versioning_configuration {
status = "Enabled"
}
}
# Encryption at rest with a customer-managed key. The key policy denies
# deletion by anyone but a designated break-glass role with MFA.
resource "aws_s3_bucket_server_side_encryption_configuration" "hipaa_evidence" {
bucket = aws_s3_bucket.hipaa_evidence.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.hipaa_evidence.arn
}
bucket_key_enabled = true
}
}
# Public access is blocked at the bucket level. There is no scenario in
# which HIPAA evidence should be public, so the block is total.
resource "aws_s3_bucket_public_access_block" "hipaa_evidence" {
bucket = aws_s3_bucket.hipaa_evidence.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
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.
The cross-cutting principle here is one of the architectural decisions in the pipeline architecture pattern Stonebridge uses: 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.