Here is a sentence I have heard, in some form, from three different engineers and one vendor sales deck: "We're fine, we only use HIPAA-eligible AWS services."
I build healthcare infrastructure for a living, and every time I hear it I do the same mental translation: "We're fine, our data center signed a contract." It's true, it's necessary, and it answers roughly none of the questions a HIPAA auditor will ask you.
So I decided to make the gap measurable. I took six AWS services that show up in practically every PHI-handling stack - S3, RDS for PostgreSQL, ECS on Fargate, KMS, the CloudTrail/CloudWatch audit pair, and the ALB - and audited each against the Security Rule's technical safeguards (45 CFR 164.312): what the regulation demands, what the service does by default, and what you must configure yourself.
Spoiler: of the 35 concrete controls I mapped, the defaults covered 6.
What "HIPAA-eligible" actually means
Start with what AWS itself says. The HIPAA-eligible services reference defines eligible services as those that may "create, receive, process, maintain, or transmit" electronic protected health information (ePHI) - provided you have signed AWS's Business Associate Addendum (BAA) first. And then, in plain sight, the sentence everyone skips: "Customers still must configure these services consistent with HIPAA requirements."
The fast primer:
- A covered entity (provider, health plan, clearinghouse) or a business associate (anyone handling PHI for them - your SaaS included, if hospitals are your customers) must comply with HIPAA. AWS becomes your business associate when you sign the BAA - today a self-service click in AWS Artifact.
- The BAA covers AWS's side of the shared responsibility model: data centers, hypervisors, service internals - security of the cloud. Everything you configure - security in the cloud - stays yours.
- The Security Rule is the part of HIPAA that talks to engineers. Its technical safeguards (164.312) name five standards: access control, audit controls, integrity, person or entity authentication, and transmission security.
- Implementation specifications are required or addressable. Addressable does not mean optional - it means implement it, implement a documented equivalent, or document a defensible reason you did neither. Encryption at rest and in transit are both addressable, and every healthcare shop I know treats them as required: "we decided not to encrypt the PHI" is not a paragraph anyone wants to defend in a breach investigation.
- And the kicker, straight from AWS's own HIPAA compliance page: "There is no HIPAA certification for a cloud service provider (CSP) such as AWS." There is no certification for you either. Only your configuration, your documentation, and eventually someone's audit.
So "HIPAA-eligible" means exactly one thing: the service is covered by AWS's BAA, so AWS's side of the split is contractually handled. Whether your side is handled is the rest of this article.
Last year I distilled a few years of building this into a set of internal Terraform modules for my team - one hardened block per service, each control traced to its Security Rule citation. The most useful artifact of that project was the diff between "what the module enforces" and "what AWS gave us out of the box." That diff is essentially this article.
The audit
Per service: what 164.312 asks, what you get by default, what you must add. Every default claim is sourced; where a default changed recently, I say when.
S3 - the poster child of improved-but-insufficient
Credit where due: S3's defaults have genuinely improved. Since January 2023, every new object is encrypted with SSE-S3, and you cannot turn that off. Since April 2023, new buckets get all four Block Public Access settings enabled and ACLs disabled. Two real 164.312(a) wins, for free. Now the rest:
-
Transmission security (164.312(e), required standard): S3 happily serves plaintext HTTP unless you attach a bucket policy denying
aws:SecureTransport = false. Nothing does this for you. - Integrity (164.312(c)): versioning - your recovery path when an object is improperly altered or deleted - is off by default.
- Audit controls (164.312(b)): server access logging is off by default, and object-level API logging (who read which object) doesn't come from S3 at all - it needs CloudTrail data events, also off by default (see CloudTrail below).
- Encryption, but accountably: SSE-S3 encrypts with a key you can't scope, rotate on your terms, or audit the use of. For PHI you want SSE-KMS with a customer-managed key, plus a policy denying uploads that request the wrong key - so a misconfigured client fails loudly instead of writing PHI under the wrong crypto.
One non-obvious lesson from building the S3 module: keep patient identifiers out of object keys. Keys leak into access logs, CloudTrail events, error messages, and URLs - surfaces your PHI inventory forgot about.
RDS for PostgreSQL - encryption is opt-in, still, in 2026
-
Encryption at rest (164.312(a)(2)(iv)): storage encryption is not enabled by default at the API level -
StorageEncrypteddefaults to false - and can only be set at creation time. Forget it in your Terraform, and the fix is a snapshot-copy-restore migration, not a flag flip. -
Transmission security (164.312(e)): a moving target. For RDS PostgreSQL 15 and later,
rds.force_ssldefaults to 1; for 14 and older it defaults to 0. It also lives in a parameter group, one console edit away from silently becoming 0 again. In my modules it's pinned at the engine, so a client that forgetssslmode=requiregets an error instead of a plaintext session. -
Access control (164.312(a)(1)): nothing stops you from making the instance publicly accessible or dropping it in a public subnet. Private placement, security-group-to-security-group ingress (no CIDR allowlists - identities beat address ranges), and
publicly_accessible = falseare all decisions you must make and enforce. - Integrity (164.312(c)): deletion protection is disabled by default in the API and IaC (the console pre-checks it for production templates - your safety net depends on which tool created the database).
- Authentication (164.312(d)) and audit (164.312(b)): RDS can generate and rotate the master password in Secrets Manager so it never touches Terraform state; it can export connection logs to CloudWatch. Both are opt-in. By default the master password is wherever you put it, and connection history is a shrug.
ECS on Fargate - the good defaults are the invisible ones
Fargate's best HIPAA property doesn't appear in any console checkbox: there are no instances. No AMIs to patch, no SSH, no node agents - a whole slab of the malicious-software surface moves to AWS's side of the BAA. That and IAM's deny-by-default (a fresh task role can do nothing) are real defaults in your favor. Everything else is on you:
- Access control (164.312(a)(1)): nothing prevents tasks in public subnets with public IPs. Private placement is a choice you codify.
-
Authentication (164.312(d)): plain environment variables appear in
DescribeTaskDefinitionoutput and the console. Database credentials must go through the task definition'ssecretsintegration with Secrets Manager - injected at container start, never written down. - Unique identity (164.312(a)(2)(i)): the execution role (ECS pulling images, injecting secrets) and the task role (your app) are separate identities - if you bother creating them separately instead of reusing one fat role.
-
Audit controls (164.312(b)): my favorite gap in this audit. ECS Exec - an interactive shell inside a container that may hold PHI in memory - records, by default, only the
ExecuteCommandAPI call in CloudTrail. What happened inside the session goes unrecorded unless you configure session I/O logging to an encrypted log group. A shell in a PHI container is an access event; out of the box, it's one with no transcript.
KMS - the service that only helps if you invite it
KMS is HIPAA-eligible, excellent, and entirely opt-in. Every encryption integration on this page uses KMS if you wire it in; the eligible list has no opinion about whether you did. Even once you create customer-managed keys:
- Automatic key rotation is disabled by default for customer-managed keys (AWS-managed keys rotate yearly; the keys you control don't, until you say so).
- The default key policy hands the account root full access and calls it a day. Separating key administration (policy, rotation, deletion - no decrypt) from key use (decrypt, no administration) is a policy you write yourself. Worth writing: no single identity can then both reconfigure the crypto and read the data - exactly what 164.312(a)(1) wants from an access control story.
- One default that genuinely protects you: key deletion requires a 7-30 day waiting period. Deleting a key that ever encrypted PHI destroys the data with it, so in my modules the window is pinned to the 30-day maximum and
ScheduleKeyDeletionevents page a human.
One design decision I'd defend anywhere: separate keys per purpose - data, logs, backups. Revoking access to PHI data should never break log delivery, and CloudTrail can hold the logs key without gaining any path to the data key.
CloudTrail and CloudWatch - the audit trail you think you have
164.312(b) is a required standard with no addressable escape hatch: "implement... mechanisms that record and examine activity in information systems that contain or use ePHI." What you have by default is 90 days of management-event history in the console. That's it. Specifically:
- No durable trail. For records past 90 days you must create a trail delivering to S3. HIPAA's documentation retention requirement (164.316(b)(2)(i)) is six years; most programs apply the same horizon to audit artifacts. 90 days is not in the same universe.
-
Data events are not logged by default. Management events tell you someone changed a bucket policy; they do not tell you someone downloaded 10,000 patient records via
GetObject. Object-level audit is a separate, per-event-billed opt-in - in my modules it's a flag with a cost warning, but a flag someone consciously declines, not a silence nobody noticed. - Log file validation (the SHA-256 digest chain that makes trail tampering detectable - the 164.312(c) integrity story for the audit trail itself) is a setting, not a given.
- On the CloudWatch side: log groups never expire by default - retention is a policy you set deliberately - and while log data is SSE-encrypted at rest, customer-managed KMS encryption is optional, attached per log group.
- And the quiet half of 164.312(b): "record and examine." Metric filters, alarms, anything that turns the trail into a signal a human reviews - none of it exists until you build it. An unread audit trail satisfies nothing.
ALB - TLS quality depends on which tool you clicked
The load balancer is your transmission security (164.312(e)) chokepoint, with the most path-dependent defaults of the lot:
- Create an HTTPS listener in the console today and you get a modern TLS 1.3/1.2 policy. Create it through the CLI or CloudFormation and the default is
ELBSecurityPolicy-2016-08, which still accepts TLS 1.0 and 1.1. Your cipher floor depends on which tool provisioned the listener. Pin the policy explicitly and this stops being interesting. - Nothing forces HTTPS to exist at all. A port-80 listener forwarding plaintext is perfectly deployable; the 80-to-443 redirect is a choice.
- Access logs are disabled by default - there goes 164.312(b) at the edge until you wire up the log bucket.
- Deletion protection: off by default. Deleting the front door of a PHI service should take two steps.
- And one my team debated for a week: by default TLS terminates at the ALB, and the hop to your tasks is HTTP inside the private network. A common, defensible posture - but a documented risk decision your analysis has to own, not something the eligible list settled for you.
The scoreboard
Counting controls involves judgment calls, so here's mine - each row counts the specific 164.312-relevant controls discussed above:
| Service | Controls mapped | Covered by default | You configure |
|---|---|---|---|
| S3 | 7 | 2 (SSE-S3 since 2023, Block Public Access since 2023) | 5 |
| RDS Postgres | 8 | 1 (forced TLS, PG15+ only) | 7 |
| ECS Fargate | 6 | 2 (no node surface, IAM deny-by-default) | 4 |
| KMS | 3 | 1 (deletion waiting period) | 2 |
| CloudTrail / CloudWatch | 6 | 0 (90-day history is partial credit at best) | 6 |
| ALB | 5 | 0 | 5 |
| Total | 35 | 6 | 29 |
Roughly one control in six arrives configured. The other 29 are your job, on services that are all on the HIPAA-eligible list. And notice which six: the freebies cluster in encryption at rest, where AWS has spent a decade raising defaults. Audit controls and transmission security - both required standards - score near zero.
The floor is genuinely rising: S3 in 2023, PostgreSQL 15's force_ssl, console TLS policies. But that's also the trap - your actual defaults depend on the year the feature shipped, the engine version, and whether the resource was born in the console or in CloudFormation. A compliance posture made of remembered defaults is a compliance posture made of trivia.
What to do about it: treat the list as a floor
The eligible list answers one question: may PHI touch this service under our BAA? Everything after that yes is configuration, and configuration you rely on should be code:
- Codify the invariants, and don't make them variables. The rule that survived every review of our modules: if the Security Rule requires it, there is no toggle. Storage encryption, TLS enforcement, log validation, private placement - hardcoded. Variables exist only for things a compliant deployment may legitimately vary, like retention above the floor. A toggle someone can forget is a finding someone will write.
-
Let policy-as-code catch the drift. checkov and tflint in CI flag most of the gaps in this article - unencrypted RDS, missing bucket policies, permissive TLS policies - before they exist. Native
terraform testcan pin the invariants so a refactor can't quietly reintroduce a toggle. Where you do deviate, suppress with a written justification inline; that comment is your addressable-specification documentation. - Map controls to citations, in the repo. Every module of ours carries a table: citation, what the code enforces, and - the column that earns its keep - what remains yours. No amount of Terraform covers the administrative safeguards: risk analysis, a log review procedure someone actually follows, restore testing, access reviews, BAAs with every other PHI-touching vendor. The code can only make the technical floor solid enough for your people to stand on.
"HIPAA-eligible" was never a lie. It's a contract term that got promoted, somewhere between the sales deck and the standup, into a security property. The list tells you where PHI may go. The Security Rule tells you what must be true when it gets there. The distance between the two is not covered by anyone's BAA - it's covered by you, ideally in version control.
Disclaimer: I'm an infrastructure engineer, not a lawyer; this is engineering analysis of published regulations and AWS documentation, not legal advice. Defaults cited were verified in August 2026 and do change (sometimes for the better). Run the details past your compliance officer, who will find at least one thing here that your specific situation makes wrong.
What's the widest eligible-vs-compliant gap you've hit in the wild? I collect these.
Top comments (0)