DevOps ·

HIPAA Engineering for Startups: PHI Encryption, Access Controls, and Audit Architecture Without Enterprise Tooling

A practical engineering guide for startup teams that need HIPAA compliance without dedicated compliance staff or enterprise budgets. Covers PHI classification, encryption patterns, RBAC, audit logging, and the 2026 Security Rule update.

HIPAA Engineering for Startups: PHI Encryption, Access Controls, and Audit Architecture Without Enterprise Tooling

Most HIPAA guides are written for compliance officers. This one is written for the engineering team that has to implement what the compliance officer signed off on. If you are building a healthtech product at seed or Series A stage, without a dedicated security team and without a $50K/year enterprise compliance platform, this is the guide you need.

HIPAA does not tell you which database to use, which cloud provider to choose, or which encryption library to import. It defines requirements. How you satisfy those requirements is your call. That flexibility is actually useful, but it also means you have to understand the underlying requirements well enough to make defensible decisions.

What HIPAA Actually Requires from Engineering

The HIPAA Security Rule governs electronic protected health information (ePHI). The Privacy Rule covers policy and patient rights. As an engineer, the Security Rule is your domain.

The Security Rule has three categories of safeguards: administrative, physical, and technical. Physical safeguards are largely handled by your cloud provider (AWS, GCP, Azure all have HIPAA-eligible services and will sign a Business Associate Agreement). Administrative safeguards are largely policy. Technical safeguards are what you build.

The mid-2026 HIPAA update (effective April 2026) removed the historic “required vs. addressable” distinction that allowed covered entities to treat certain controls as optional based on “reasonableness.” Under the updated rule, all technical safeguards are now required. This matters in practice: encryption at rest, which was previously addressable and therefore arguable, is now required. There is no longer a documented exception path.

The key technical safeguard requirements:

  • Access controls: unique user identification, automatic logoff, encryption and decryption
  • Audit controls: hardware, software, and procedural mechanisms that record and examine activity
  • Integrity: mechanisms to authenticate ePHI and detect unauthorized alteration
  • Person or entity authentication: verify that persons or entities seeking access are who they claim
  • Transmission security: guard against unauthorized access to ePHI being transmitted over a network

PHI Data Classification

Before you can protect PHI, you need to know what it is. HIPAA defines 18 identifiers that, when combined with health information, create PHI:

Names, geographic data smaller than state, dates (except year) related to an individual, phone numbers, fax numbers, email addresses, social security numbers, medical record numbers, health plan beneficiary numbers, account numbers, certificate/license numbers, vehicle identifiers, device identifiers, web URLs, IP addresses, biometric identifiers, full-face photographs, and any other unique identifying number or code.

In practical terms: a row containing { userId: "abc123", bloodPressure: 120, timestamp: "2026-03-15" } is PHI if userId is traceable to a real person. A row containing { anonymizedId: "x7f2", bloodPressure: 120 } where the anonymization key is stored in a separate system is not PHI, but only if the separation is real and the key access is controlled.

The classification decision you make at schema design time determines your compliance surface area. Keep PHI in as few tables, buckets, and services as possible. Every place PHI lives is a place you need to audit, encrypt, and control.

What does NOT count as PHI: de-identified data under the Safe Harbor method (all 18 identifiers removed), aggregate statistics, research data that has gone through proper de-identification, and metadata that cannot be traced to an individual.

Encryption at Rest: Envelope Encryption with KMS

The pattern for encrypting PHI at rest is envelope encryption. You generate a data encryption key (DEK) for each record or each tenant, encrypt the data with the DEK, then encrypt the DEK with a key encryption key (KEK) managed by a key management service. The DEK travels with the data, but is only useful if the caller can also decrypt it via the KMS.

AWS KMS is the practical choice for most startups. It is HIPAA-eligible, integrates with S3, RDS, DynamoDB, and Secrets Manager, and has a clear audit trail through CloudTrail.

import {
  KMSClient,
  GenerateDataKeyCommand,
  DecryptCommand,
} from "@aws-sdk/client-kms";
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";

const kms = new KMSClient({ region: process.env.AWS_REGION });
const PHI_CMK_ARN = process.env.PHI_KMS_KEY_ARN!;

interface EncryptedPayload {
  encryptedDek: string; // base64, encrypted by KMS CMK
  iv: string;           // base64, initialization vector
  ciphertext: string;   // base64, AES-256-GCM ciphertext
  authTag: string;      // base64, GCM authentication tag
}

export async function encryptPhi(plaintext: string): Promise<EncryptedPayload> {
  // Request a 256-bit DEK from KMS; KMS returns both plaintext and encrypted versions
  const { Plaintext: dek, CiphertextBlob: encryptedDek } = await kms.send(
    new GenerateDataKeyCommand({
      KeyId: PHI_CMK_ARN,
      KeySpec: "AES_256",
    })
  );

  if (!dek || !encryptedDek) {
    throw new Error("KMS failed to generate data key");
  }

  const iv = randomBytes(12); // 96-bit IV for GCM
  const cipher = createCipheriv("aes-256-gcm", Buffer.from(dek), iv);

  const encrypted = Buffer.concat([
    cipher.update(plaintext, "utf8"),
    cipher.final(),
  ]);

  const authTag = cipher.getAuthTag();

  // Zero out the plaintext DEK from memory immediately
  (dek as Buffer).fill(0);

  return {
    encryptedDek: Buffer.from(encryptedDek).toString("base64"),
    iv: iv.toString("base64"),
    ciphertext: encrypted.toString("base64"),
    authTag: authTag.toString("base64"),
  };
}

export async function decryptPhi(payload: EncryptedPayload): Promise<string> {
  const { Plaintext: dek } = await kms.send(
    new DecryptCommand({
      CiphertextBlob: Buffer.from(payload.encryptedDek, "base64"),
      KeyId: PHI_CMK_ARN,
    })
  );

  if (!dek) {
    throw new Error("KMS decryption failed");
  }

  const decipher = createDecipheriv(
    "aes-256-gcm",
    Buffer.from(dek),
    Buffer.from(payload.iv, "base64")
  );

  decipher.setAuthTag(Buffer.from(payload.authTag, "base64"));

  const decrypted = Buffer.concat([
    decipher.update(Buffer.from(payload.ciphertext, "base64")),
    decipher.final(),
  ]);

  (dek as Buffer).fill(0);

  return decrypted.toString("utf8");
}

A few tradeoffs worth noting: per-record encryption is the most granular but introduces KMS API call overhead for every read. Per-tenant encryption reduces KMS calls but means a compromised tenant key exposes all that tenant’s data. For most startups, per-tenant encryption with key rotation on a schedule (quarterly or on suspected breach) is the right balance.

KMS GenerateDataKey calls are not free. At scale (millions of records), you want to cache the plaintext DEK in memory for the duration of a request or a short window (no longer than 5 minutes), then discard it. Never write the plaintext DEK to disk or logs.

Encryption in Transit: TLS Enforcement

TLS 1.2 is the minimum required for HIPAA. TLS 1.3 is preferred. The practical steps:

At your load balancer or CDN layer, enforce a minimum TLS version and reject older negotiation. On AWS ALB, set the security policy to ELBSecurityPolicy-TLS13-1-2-2021-06 or newer. On Cloudflare, set minimum TLS version to 1.2 in your SSL/TLS settings.

For internal service-to-service communication, do not assume your VPC is safe. Use mutual TLS (mTLS) for services that handle PHI. This is not paranoia; it is defense in depth against a compromised internal service. Tools like AWS Certificate Manager Private CA make this manageable without standing up your own PKI.

Certificate rotation should be automated. Expired certificates that cause outages are a common reason teams disable TLS checks in test environments, which then leaks into production patterns. Use ACM or cert-manager (on Kubernetes), not manually-managed certificates.

Access Controls: RBAC and Minimum Necessary

The minimum necessary principle in HIPAA means users should access only the PHI required for their specific task. In engineering terms: your RBAC model needs to be granular enough to enforce this, and your application layer needs to respect it.

The common failure mode is a role: "admin" check that gives full read access to all PHI. Instead, model permissions around specific PHI categories and workflows.

// PHI access is scoped to specific data categories, not just "admin" vs "user"
type PhiCategory =
  | "demographics"
  | "diagnoses"
  | "medications"
  | "lab_results"
  | "mental_health" // extra sensitivity, often requires explicit consent
  | "substance_use"; // extra sensitivity

type PhiPermission = `phi:${PhiCategory}:${"read" | "write"}`;

interface UserContext {
  userId: string;
  role: string;
  permissions: PhiPermission[];
  patientRelationship?: "treating" | "billing" | "admin"; // for relationship-based access
}

// Middleware for PHI access — runs before any handler that returns PHI
export function requirePhiAccess(
  requiredPermissions: PhiPermission[]
) {
  return (
    req: Request & { user: UserContext },
    res: Response,
    next: NextFunction
  ) => {
    const missing = requiredPermissions.filter(
      (p) => !req.user.permissions.includes(p)
    );

    if (missing.length > 0) {
      // Log the access denial for audit purposes
      writeAuditEvent({
        eventType: "phi_access_denied",
        actorId: req.user.userId,
        requiredPermissions: missing,
        resourcePath: req.path,
        ipAddress: req.ip,
      });

      return res.status(403).json({
        error: "Insufficient permissions for PHI access",
      });
    }

    next();
  };
}

// Example route: only billing staff can see patient demographic info
app.get(
  "/patients/:id/demographics",
  requirePhiAccess(["phi:demographics:read"]),
  getPatientDemographics
);

// Mental health notes require an additional explicit permission
app.get(
  "/patients/:id/mental-health-notes",
  requirePhiAccess(["phi:mental_health:read"]),
  getMentalHealthNotes
);

Break-glass procedures are for emergencies where a provider needs access to a patient’s record outside their normal permission scope (an ER physician accessing records of a patient they are not the treating provider for, for example). The pattern: allow the access, but require an explicit justification string, immediately notify a supervisor or compliance officer, and flag every subsequent action taken on that record in that session with the break-glass marker. Break-glass is an audit event, not an audit bypass.

export async function breakGlassAccess(
  actorId: string,
  patientId: string,
  justification: string
): Promise<BreakGlassSession> {
  if (!justification || justification.length < 20) {
    throw new Error("Break-glass justification must be substantive");
  }

  const session = await db.breakGlassSessions.create({
    actorId,
    patientId,
    justification,
    createdAt: new Date(),
    expiresAt: new Date(Date.now() + 4 * 60 * 60 * 1000), // 4-hour max
  });

  await notifyComplianceOfficer({ actorId, patientId, justification });

  await writeAuditEvent({
    eventType: "break_glass_initiated",
    actorId,
    patientId,
    justification,
    sessionId: session.id,
  });

  return session;
}

Audit Logging: Immutable Trails

HIPAA requires audit logs covering: who accessed what PHI, when, and from where. It also requires you to retain those logs for 6 years from creation or last effective date.

The immutability requirement is the part that catches teams off-guard. Your application database is not an appropriate audit log store if the same application can delete from it. Audit logs must be written to a system where records cannot be modified or deleted by the application.

Practical options in order of cost:

  1. AWS CloudWatch Logs with a log group retention policy and no delete permissions for the application role
  2. S3 with Object Lock (WORM mode) for long-term retention
  3. DynamoDB with a dedicated audit table where the IAM policy allows PutItem but not DeleteItem or UpdateItem

For write throughput and reliability, write audit events asynchronously to a queue (SQS) and have a separate consumer write to your immutable store. This decouples the audit write from the request path.

import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";

const sqs = new SQSClient({ region: process.env.AWS_REGION });

interface AuditEvent {
  eventType: string;
  actorId: string;
  patientId?: string;
  resourcePath?: string;
  requiredPermissions?: string[];
  ipAddress?: string;
  sessionId?: string;
  justification?: string;
  timestamp?: string;
}

export async function writeAuditEvent(event: AuditEvent): Promise<void> {
  const enriched = {
    ...event,
    timestamp: event.timestamp ?? new Date().toISOString(),
    environment: process.env.NODE_ENV,
    serviceVersion: process.env.APP_VERSION,
  };

  await sqs.send(
    new SendMessageCommand({
      QueueUrl: process.env.AUDIT_QUEUE_URL!,
      MessageBody: JSON.stringify(enriched),
      // Use patient ID as message group ID for ordered delivery per patient
      MessageGroupId: event.patientId ?? "system",
      MessageDeduplicationId: `${enriched.eventType}-${enriched.actorId}-${enriched.timestamp}`,
    })
  );
}

Events that must be logged: all PHI reads (even failed permission checks), all PHI writes and deletions, all authentication events (login, logout, failed login, MFA), all break-glass access initiations, all permission changes, and all bulk exports or reports containing PHI.

Events people forget to log: automated jobs that read PHI (nightly reports, ML training data exports), third-party webhooks that receive PHI, and API calls from your own mobile or frontend client.

For retention, S3 Object Lock with Compliance mode and a 6-year retention period is the most straightforward implementation. Governance mode allows administrators to delete; Compliance mode does not. Use Compliance mode.

BAA Management: The Technical Side

A Business Associate Agreement (BAA) is a contract where your vendor agrees to handle PHI according to HIPAA requirements. The technical implication: any service that stores, processes, or transmits PHI on your behalf must have a signed BAA with you.

AWS offers BAAs for HIPAA-eligible services. Not all AWS services are HIPAA-eligible. Services that are not eligible include: Lambda@Edge (standard Lambda is eligible), some preview/beta services, and several analytics services. Review the AWS HIPAA-eligible services page before routing PHI through any service.

The common mistake: PHI appears in a log line, that log line gets shipped to a third-party logging service (Datadog, Papertrail, Loggly), and suddenly you have PHI in a system without a BAA. Scrub PHI from logs before they leave your environment. At minimum, never log raw request bodies for PHI endpoints, and never log decrypted PHI values.

Other services that need BAAs if they touch PHI: email providers (for appointment notifications), SMS providers, analytics platforms, and error tracking services.

Infrastructure Patterns: VPC Isolation

PHI workloads should run in isolated network segments. The practical pattern for AWS:

A dedicated VPC (or dedicated subnets within a shared VPC) for PHI services. Security groups that allow inbound connections only from known application servers, not from the public internet. PHI databases in private subnets with no internet gateway route. NAT gateway for outbound-only internet access from PHI subnets. VPC endpoints for AWS services (KMS, S3, SQS) to keep traffic off the public internet.

At startup scale, a single VPC with public and private subnets, a NAT gateway, and strict security groups is sufficient. You do not need a separate AWS account for PHI unless you hit enterprise compliance requirements or your threat model specifically calls for account-level isolation.

Tag every PHI-touching resource in your infrastructure with a phi: "true" tag. This makes it easy to generate the asset inventory required for a HIPAA risk analysis, and it feeds into cost attribution for your compliance-related infrastructure.

Compliance Engineering Checklist

Technical controls to verify before you launch or before an audit:

Encryption

  • AES-256 encryption at rest for all PHI (RDS storage encryption, S3 server-side encryption, DynamoDB encryption)
  • Envelope encryption with KMS for application-level PHI fields
  • KMS key rotation enabled (annual minimum)
  • TLS 1.2+ enforced at load balancer, API gateway, and internal service boundaries
  • No PHI in plaintext in environment variables or config files

Access Controls

  • Unique user IDs, no shared credentials for PHI access
  • RBAC with minimum necessary permissions implemented at the application layer
  • Automatic session timeout for PHI access (15-30 minutes of inactivity is common)
  • MFA required for all users with PHI access
  • Break-glass procedures documented and tested
  • IAM roles for services, not IAM users with static keys

Audit Logging

  • All PHI access events logged (read, write, delete, denied)
  • All authentication events logged
  • Logs written to an immutable store (S3 Object Lock or equivalent)
  • 6-year log retention configured
  • Automated jobs that access PHI are logged with a service identity
  • PHI scrubbed from application logs before they leave your environment

Network

  • PHI databases in private subnets, no public endpoint
  • VPC endpoints for AWS services that handle PHI
  • Security groups restrict inbound access to PHI services
  • Network flow logs enabled for PHI VPC/subnets

Vendor and BAA

  • AWS BAA signed
  • BAA signed with every third-party service that touches PHI
  • List of HIPAA-eligible vs. non-eligible services documented
  • PHI never routed to non-BAA services (logging, analytics, error tracking)

Common Mistakes That Fail Audits

PHI in application logs. The most common finding. A request handler logs the incoming payload for debugging, the payload contains a patient name or date of birth, and that log goes to a third-party aggregator without a BAA.

Shared service accounts accessing PHI. A nightly job runs as service-account@company.com with no human identity attached. Audit trail shows PHI access but no individual accountability. Use role-based service identities with names that map to specific jobs or workflows.

No access termination procedure. An employee who left six months ago still has database access. Access reviews are an administrative safeguard, but they depend on technical controls: automated deprovisioning tied to your IdP, not a manual checklist someone runs quarterly.

Encryption at rest on the database, nothing at the field level. Full disk encryption does not protect against a compromised application that has database credentials. Field-level or application-level encryption provides defense in depth.

Test data contains real PHI. Engineers copy a production database to a development environment for debugging. That environment has no PHI controls. This is a breach. Use synthetic data generation or properly de-identified snapshots for non-production environments.

Audit logs in the same database as the application. The application can modify them. A compromised application layer can cover tracks. Audit logs must be in a separate system with write-only access from the application.

Ignoring the 2026 Security Rule update. If you built your HIPAA controls before 2026 and relied on the “addressable” carve-out to skip encryption at rest, you need to remediate. The updated rule treats all technical safeguards as required, and auditors will check.

HIPAA compliance at the engineering level is not a product you buy. It is a set of architecture decisions made consistently across your data model, your access control layer, your logging infrastructure, and your vendor relationships. Most of what it requires is good engineering practice anyway: encrypt sensitive data, log access to sensitive resources, limit permissions to the minimum needed, and keep an immutable record that you did all of it. The compliance overhead is real but manageable. The alternative (a breach that involves unprotected PHI) is not.

More in DevOps

How Terraform Works Internally: HCL Parsing, Dependency Graph Execution, and the Provider Protocol Behind Infrastructure as Code
DevOps ·

How Terraform Works Internally: HCL Parsing, Dependency Graph Execution, and the Provider Protocol Behind Infrastructure as Code

A deep dive into Terraform's internal architecture covering HCL parsing, the expression evaluation engine, DAG-based dependency resolution, the gRPC provider plugin protocol, state mechanics, and the plan/apply lifecycle.

How Docker Works Internally: Namespaces, Cgroups, Union Filesystems, and the OCI Runtime Behind Every Container
DevOps ·

How Docker Works Internally: Namespaces, Cgroups, Union Filesystems, and the OCI Runtime Behind Every Container

A deep dive into what happens when you run docker run: Linux namespaces for process isolation, cgroups v2 for resource enforcement, overlay2 union filesystem for layered images, the OCI runtime spec and how runc spawns containers, content-addressable storage, the containerd shim architecture, and networking with veth pairs.

How Kubernetes Works: Pods, Scheduling, and the Reconciliation Loop
DevOps ·

How Kubernetes Works: Pods, Scheduling, and the Reconciliation Loop

A deep dive into Kubernetes internals covering the control plane architecture, etcd watch mechanics, the scheduler's filter and score phases, controller manager reconciliation loops, kubelet pod assignment, the CRI and CNI plugin models, and production considerations for resource requests, pod disruption budgets, and cluster autoscaler behavior.

How Docker Works: Namespaces, Cgroups, Union Filesystems, and the Container Runtime from Build to Execution
DevOps ·

How Docker Works: Namespaces, Cgroups, Union Filesystems, and the Container Runtime from Build to Execution

A deep dive into Docker internals covering Linux namespace isolation, cgroup resource constraints, OverlayFS copy-on-write layer mechanics, Dockerfile build cache invalidation rules, the containerd and runc OCI runtime stack, and production considerations for image hardening and startup latency.