Chuyển đến nội dung chính

Lesson 17: HIPAA Technical Safeguards — Implementation Checklist

Fully deploy HIPAA Technical Safeguards: Access Control (unique user ID, emergency access, automatic logoff, encryption), Audit Controls (hardware, software, procedural mechanisms), Integrity Controls (authentication of ePHI), Person Authentication, and Transmission Security. Mapping each requirement to a specific implementation with Quarkus, PostgreSQL, Keycloak.

🏗️ Architecture — Lesson 17 Lesson 17: HIPAA Technical Safeguards — Deployment Checklist

Building a Microservices Healthcare System — Quarkus, PostgreSQL, Keycloak with HIPAA standards

Part 5: Compliance, Audit & Data Protection

xdev.asia

1. Overview of HIPAA Security Rule §164.312

HIPAA Security Rule — Administrative, Physical, Technical Safeguards

HIPAA Security Rule requires organizations that handle ePHI (electronic Protected Health Information) to implement Technical Safeguards — technical measures to protect electronic health data. This is the most important part for developers and engineers.

1.1. HIPAA Security Rule structure

HIPAA Security Rule §164.302-318:

  • §164.308 Administrative Safeguards
    • Risk Analysis
    • Workforce Security
    • Information Access Management
    • Security Awareness Training
    • Security Incident Procedures
    • Contingency Plan
    • Evaluation
  • §164.310 Physical Safeguards
    • Facility Access Controls
    • Workstation Use & Security
    • Device and Media Controls
  • §164.312 Technical Safeguards ← THIS POST
    • Access Control (§164.312(a))
    • Audit Controls (§164.312(b))
    • Integrity Controls (§164.312(c))
    • Person/Entity Authentication (§164.312(d))
    • Transmission Security (§164.312(e))
  • §164.314 Organizational Requirements
    • Business Associate Agreements (BAA)
    • Group Health Plan Requirements

1.2. Required vs Addressable

HIPAA classifies implementation specifications into two categories:

TypeMeaningAction
Required (R)REQUIRED implementation, no exceptionsMust implement specification
Addressable (A)Must be evaluated and implemented if reasonable If not implemented, must document reasons and alternative measures

Important: "Addressable" does NOT mean "optional". The organization must assess, implement or document alternative.

2. Access Control — §164.312(a)(1)

Standard: Implement technical policies and procedures for electronic information systems that maintain ePHI to allow access only to those persons or software programs that have been granted access rights.

2.1. Unique User Identification — §164.312(a)(2)(i) [Required]

"Assign a unique name and/or number for identifying and tracking user identity."

Implementation with Keycloak:

package vn.hospital.compliance.access;

import io.quarkus.security.identity.SecurityIdentity;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.jboss.logging.Logger;

/**
 * HIPAA §164.312(a)(2)(i) - Unique User Identification
 * Mỗi user phải có unique identifier để tracking.
 */
@ApplicationScoped
public class UniqueUserIdentificationService {

    private static final Logger LOG = Logger.getLogger(UniqueUserIdentificationService.class);

    @Inject
    JsonWebToken jwt;

    @Inject
    SecurityIdentity identity;

    /**
     * Lấy unique user ID từ JWT token (Keycloak sub claim).
     * Format: UUID assigned bởi Keycloak, không trùng lặp.
     */
    public String getUniqueUserId() {
        String userId = jwt.getSubject(); // Keycloak UUID
        if (userId == null || userId.isBlank()) {
            throw new SecurityException("HIPAA Violation: No unique user ID in token");
        }
        return userId;
    }

    /**
     * Lấy username (preferred_username) cho audit display.
     */
    public String getUsername() {
        return jwt.getClaim("preferred_username");
    }

    /**
     * Lấy toàn bộ user context cho audit trail.
     */
    public UserAuditContext getAuditContext() {
        return new UserAuditContext(
            jwt.getSubject(),
            jwt.getClaim("preferred_username"),
            jwt.getClaim("email"),
            jwt.getGroups(),
            jwt.getClaim("department"),
            jwt.getClaim("facility_id"),
            jwt.getIssuedAtTime(),
            jwt.getExpirationTime()
        );
    }
}

Keycloak Realm Configuration:

{
  "realm": "healthcare",
  "registrationAllowed": false,
  "editUsernameAllowed": false,
  "duplicateEmailsAllowed": false,
  "loginWithEmailAllowed": false,
  "attributes": {
    "userProfileEnabled": "true"
  },
  "users": [
    {
      "username": "dr.nguyen",
      "enabled": true,
      "email": "[email protected]",
      "firstName": "Nguyễn",
      "lastName": "Văn A",
      "attributes": {
        "employee_id": ["EMP-2024-0001"],
        "department": ["cardiology"],
        "facility_id": ["HOSP-HCM-01"],
        "npi_number": ["1234567890"],
        "license_number": ["VN-MD-2020-12345"]
      },
      "credentials": [
        {
          "type": "password",
          "value": "CHANGE_ME",
          "temporary": true
        }
      ],
      "requiredActions": ["UPDATE_PASSWORD", "CONFIGURE_TOTP"],
      "realmRoles": ["physician"],
      "clientRoles": {
        "patient-service": ["patient_read", "patient_write"],
        "lab-service": ["lab_order", "lab_read"]
      }
    }
  ]
}

2.2. Emergency Access Procedure — §164.312(a)(2)(ii) [Required]

"Establish (and implement as needed) procedures for obtaining necessary ePHI during an emergency."

package vn.hospital.compliance.access;

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.*;
import jakarta.transaction.Transactional;
import org.jboss.logging.Logger;

import java.time.Duration;
import java.time.Instant;
import java.util.UUID;

/**
 * HIPAA §164.312(a)(2)(ii) - Emergency Access Procedure
 * "Break the Glass" mechanism cho trường hợp khẩn cấp.
 */
@ApplicationScoped
public class EmergencyAccessService {

    private static final Logger LOG = Logger.getLogger(EmergencyAccessService.class);
    private static final Duration EMERGENCY_ACCESS_DURATION = Duration.ofHours(4);

    @Inject
    EntityManager entityManager;

    @Inject
    AuditService auditService;

    @Inject
    NotificationService notificationService;

    /**
     * "Break the Glass" - cấp quyền truy cập khẩn cấp.
     * Automatic audit + notification cho Privacy Officer.
     */
    @Transactional
    public EmergencyAccessGrant grantEmergencyAccess(EmergencyAccessRequest request) {
        // 1. Validate request
        validateEmergencyRequest(request);

        // 2. Create emergency access grant
        EmergencyAccessGrant grant = new EmergencyAccessGrant();
        grant.setId(UUID.randomUUID());
        grant.setUserId(request.getUserId());
        grant.setPatientId(request.getPatientId());
        grant.setReason(request.getReason());
        grant.setEmergencyType(request.getEmergencyType());
        grant.setGrantedAt(Instant.now());
        grant.setExpiresAt(Instant.now().plus(EMERGENCY_ACCESS_DURATION));
        grant.setActive(true);

        entityManager.persist(grant);

        // 3. Audit log - CRITICAL priority
        auditService.logEmergencyAccess(
            request.getUserId(),
            request.getPatientId(),
            request.getReason(),
            request.getEmergencyType(),
            grant.getId()
        );

        // 4. Notify Privacy Officer và Security Team
        notificationService.notifyEmergencyAccess(grant);

        LOG.warnf("EMERGENCY ACCESS GRANTED: User=%s, Patient=%s, Reason=%s, Grant=%s",
            request.getUserId(), request.getPatientId(),
            request.getReason(), grant.getId());

        return grant;
    }

    /**
     * Kiểm tra user có emergency access đang active không.
     */
    public boolean hasActiveEmergencyAccess(String userId, UUID patientId) {
        Long count = entityManager.createQuery(
            "SELECT COUNT(g) FROM EmergencyAccessGrant g " +
            "WHERE g.userId = :userId AND g.patientId = :patientId " +
            "AND g.active = true AND g.expiresAt > :now",
            Long.class)
            .setParameter("userId", userId)
            .setParameter("patientId", patientId)
            .setParameter("now", Instant.now())
            .getSingleResult();
        return count > 0;
    }

    /**
     * Revoke emergency access (sau khi tình huống khẩn cấp kết thúc).
     */
    @Transactional
    public void revokeEmergencyAccess(UUID grantId, String revokedBy, String reason) {
        EmergencyAccessGrant grant = entityManager.find(EmergencyAccessGrant.class, grantId);
        if (grant != null && grant.isActive()) {
            grant.setActive(false);
            grant.setRevokedAt(Instant.now());
            grant.setRevokedBy(revokedBy);
            grant.setRevocationReason(reason);

            auditService.logEmergencyAccessRevoked(grantId, revokedBy, reason);
        }
    }

    private void validateEmergencyRequest(EmergencyAccessRequest request) {
        if (request.getReason() == null || request.getReason().length() < 20) {
            throw new IllegalArgumentException(
                "Emergency access reason must be detailed (min 20 characters)");
        }
        if (request.getEmergencyType() == null) {
            throw new IllegalArgumentException("Emergency type is required");
        }
    }
}

Entity for Emergency Access Grant:

@Entity
@Table(name = "emergency_access_grants", schema = "compliance")
public class EmergencyAccessGrant {

    @Id
    private UUID id;

    @Column(name = "user_id", nullable = false)
    private String userId;

    @Column(name = "patient_id", nullable = false)
    private UUID patientId;

    @Column(name = "reason", nullable = false, columnDefinition = "TEXT")
    private String reason;

    @Column(name = "emergency_type", nullable = false)
    @Enumerated(EnumType.STRING)
    private EmergencyType emergencyType;

    @Column(name = "granted_at", nullable = false)
    private Instant grantedAt;

    @Column(name = "expires_at", nullable = false)
    private Instant expiresAt;

    @Column(name = "active", nullable = false)
    private boolean active;

    @Column(name = "revoked_at")
    private Instant revokedAt;

    @Column(name = "revoked_by")
    private String revokedBy;

    @Column(name = "revocation_reason")
    private String revocationReason;

    // Getters, setters omitted
    public UUID getId() { return id; }
    public void setId(UUID id) { this.id = id; }
    public String getUserId() { return userId; }
    public void setUserId(String userId) { this.userId = userId; }
    public UUID getPatientId() { return patientId; }
    public void setPatientId(UUID patientId) { this.patientId = patientId; }
    public String getReason() { return reason; }
    public void setReason(String reason) { this.reason = reason; }
    public EmergencyType getEmergencyType() { return emergencyType; }
    public void setEmergencyType(EmergencyType emergencyType) { this.emergencyType = emergencyType; }
    public Instant getGrantedAt() { return grantedAt; }
    public void setGrantedAt(Instant grantedAt) { this.grantedAt = grantedAt; }
    public Instant getExpiresAt() { return expiresAt; }
    public void setExpiresAt(Instant expiresAt) { this.expiresAt = expiresAt; }
    public boolean isActive() { return active; }
    public void setActive(boolean active) { this.active = active; }
    public void setRevokedAt(Instant revokedAt) { this.revokedAt = revokedAt; }
    public void setRevokedBy(String revokedBy) { this.revokedBy = revokedBy; }
    public void setRevocationReason(String reason) { this.revocationReason = reason; }
}

enum EmergencyType {
    LIFE_THREATENING,        // Tình huống đe dọa tính mạng
    NATURAL_DISASTER,        // Thiên tai
    SYSTEM_FAILURE,          // Sự cố hệ thống
    PUBLIC_HEALTH_EMERGENCY  // Dịch bệnh
}

2.3. Automatic Logoff — §164.312(a)(2)(iii) [Addressable]

"Implement electronic procedures that terminate an electronic session after a predetermined time of inactivity."

Keycloak Session Configuration:

{
  "realm": "healthcare",
  "ssoSessionIdleTimeout": 900,
  "ssoSessionMaxLifespan": 28800,
  "accessTokenLifespan": 300,
  "accessTokenLifespanForImplicitFlow": 300,
  "offlineSessionIdleTimeout": 2592000,
  "offlineSessionMaxLifespan": 5184000,
  "clientSessionIdleTimeout": 900,
  "clientSessionMaxLifespan": 28800,
  "actionTokenGeneratedByUserLifespan": 300
}
SettingValueMeaning
ssoSessionIdleTimeout900s (15 minutes)Session automatically expires after 15 minutes of inactivity
ssoSessionMaxLifespan28800s (8 hours)Maximum session 8 hours (1 shift)
accessTokenLifespan300s (5 minutes)Short access token to reduce window of exposure
actionTokenGeneratedByUserLifespan300sToken for reset password, verify email

Quarkus OIDC Token Refresh:

# application.properties - Session management
quarkus.oidc.token.refresh-expired=true
quarkus.oidc.token.refresh-token-time-skew=10S
quarkus.oidc.token.lifespan-grace=5
quarkus.oidc.logout.path=/api/v1/logout
quarkus.oidc.logout.post-logout-path=/

2.4. Encryption and Decryption — §164.312(a)(2)(iv) [Addressable]

"Implement a mechanism to encrypt and decrypt ePHI."

Deployed in detail in Lesson 15 (End-to-End Encryption). Implementation summary:

ComponentsEncryption MethodKey Management
Database columns (PHI)AES-256-GCM via Vault TransitHashiCorp Vault KEK
Inter-service communicationJWE (RSA-OAEP-256 + A256GCM)Vault KV Engine
Kafka messagesEnvelope encryption (DEK + KEK)Vault Transit
Database at-restPostgreSQL TDE or disk encryptionOS/Cloud KMS
BackupsAES-256-CBC via pgBackRestSeparate backup key
TransportationTLS 1.3Certificate Authority

3. Audit Controls — §164.312(b)

3.1. Standard [Required]

"Implement hardware, software, and/or procedural mechanisms that record and examine activity in information systems that contain or use ePHI."

package vn.hospital.compliance.audit;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import org.jboss.logging.Logger;

import java.time.Instant;
import java.util.Map;
import java.util.UUID;

/**
 * HIPAA §164.312(b) - Audit Controls
 * Record and examine all ePHI access activity.
 */
@ApplicationScoped
public class HipaaAuditService {

    private static final Logger LOG = Logger.getLogger(HipaaAuditService.class);

    @Inject
    EntityManager entityManager;

    /**
     * Log mọi access event vào audit trail.
     * HIPAA yêu cầu: WHO, WHAT, WHEN, WHERE, WHY.
     */
    @Transactional
    public void logAccess(AuditEvent event) {
        AuditLogEntry entry = new AuditLogEntry();
        entry.setId(UUID.randomUUID());
        entry.setTimestamp(Instant.now());

        // WHO - Unique User Identification
        entry.setUserId(event.getUserId());
        entry.setUsername(event.getUsername());
        entry.setUserRole(event.getUserRole());
        entry.setDepartment(event.getDepartment());

        // WHAT - Action performed
        entry.setAction(event.getAction());
        entry.setResourceType(event.getResourceType());
        entry.setResourceId(event.getResourceId());

        // WHEN - Timestamp (UTC)
        entry.setTimestamp(Instant.now());

        // WHERE - Source information
        entry.setSourceIp(event.getSourceIp());
        entry.setUserAgent(event.getUserAgent());
        entry.setServiceName(event.getServiceName());

        // WHY - Reason/context
        entry.setReason(event.getReason());
        entry.setEmergencyAccess(event.isEmergencyAccess());

        // Result
        entry.setOutcome(event.getOutcome());
        entry.setErrorMessage(event.getErrorMessage());

        entityManager.persist(entry);

        // Structured log cho ELK
        LOG.infof("AUDIT: action=%s user=%s resource=%s/%s outcome=%s",
            event.getAction(), event.getUsername(),
            event.getResourceType(), event.getResourceId(),
            event.getOutcome());
    }
}

Audit Log Entity:

@Entity
@Table(name = "audit_logs", schema = "compliance",
    indexes = {
        @Index(name = "idx_audit_timestamp", columnList = "timestamp"),
        @Index(name = "idx_audit_user", columnList = "user_id"),
        @Index(name = "idx_audit_resource", columnList = "resource_type, resource_id"),
        @Index(name = "idx_audit_action", columnList = "action")
    })
public class AuditLogEntry {

    @Id
    private UUID id;

    @Column(nullable = false)
    private Instant timestamp;

    // WHO
    @Column(name = "user_id", nullable = false)
    private String userId;
    @Column(nullable = false)
    private String username;
    @Column(name = "user_role")
    private String userRole;
    @Column
    private String department;

    // WHAT
    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private AuditAction action;
    @Column(name = "resource_type", nullable = false)
    private String resourceType;
    @Column(name = "resource_id")
    private String resourceId;

    // WHERE
    @Column(name = "source_ip")
    private String sourceIp;
    @Column(name = "user_agent")
    private String userAgent;
    @Column(name = "service_name")
    private String serviceName;

    // WHY
    @Column
    private String reason;
    @Column(name = "emergency_access")
    private boolean emergencyAccess;

    // RESULT
    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private AuditOutcome outcome;
    @Column(name = "error_message")
    private String errorMessage;

    // Getters, setters omitted
    public UUID getId() { return id; }
    public void setId(UUID id) { this.id = id; }
    public Instant getTimestamp() { return timestamp; }
    public void setTimestamp(Instant timestamp) { this.timestamp = timestamp; }
    public String getUserId() { return userId; }
    public void setUserId(String userId) { this.userId = userId; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getUserRole() { return userRole; }
    public void setUserRole(String userRole) { this.userRole = userRole; }
    public String getDepartment() { return department; }
    public void setDepartment(String department) { this.department = department; }
    public AuditAction getAction() { return action; }
    public void setAction(AuditAction action) { this.action = action; }
    public String getResourceType() { return resourceType; }
    public void setResourceType(String resourceType) { this.resourceType = resourceType; }
    public String getResourceId() { return resourceId; }
    public void setResourceId(String resourceId) { this.resourceId = resourceId; }
    public String getSourceIp() { return sourceIp; }
    public void setSourceIp(String sourceIp) { this.sourceIp = sourceIp; }
    public String getUserAgent() { return userAgent; }
    public void setUserAgent(String userAgent) { this.userAgent = userAgent; }
    public String getServiceName() { return serviceName; }
    public void setServiceName(String serviceName) { this.serviceName = serviceName; }
    public String getReason() { return reason; }
    public void setReason(String reason) { this.reason = reason; }
    public boolean isEmergencyAccess() { return emergencyAccess; }
    public void setEmergencyAccess(boolean emergencyAccess) { this.emergencyAccess = emergencyAccess; }
    public AuditOutcome getOutcome() { return outcome; }
    public void setOutcome(AuditOutcome outcome) { this.outcome = outcome; }
    public String getErrorMessage() { return errorMessage; }
    public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
}

enum AuditAction {
    CREATE, READ, UPDATE, DELETE,
    LOGIN, LOGOUT, LOGIN_FAILED,
    EXPORT, PRINT, DOWNLOAD,
    EMERGENCY_ACCESS, PERMISSION_CHANGE,
    ENCRYPTION, DECRYPTION,
    BACKUP, RESTORE
}

enum AuditOutcome {
    SUCCESS, FAILURE, DENIED, ERROR
}

JAX-RS Filter for Automatic Audit:

package vn.hospital.compliance.audit;

import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.ext.Provider;
import org.eclipse.microprofile.jwt.JsonWebToken;

import java.io.IOException;

/**
 * Automatic audit logging cho mọi API request liên quan đến PHI.
 */
@Provider
@Priority(100)
public class AuditRequestFilter implements ContainerRequestFilter, ContainerResponseFilter {

    @Inject
    HipaaAuditService auditService;

    @Inject
    JsonWebToken jwt;

    @Override
    public void filter(ContainerRequestContext request) throws IOException {
        // Store start time cho response timing
        request.setProperty("audit.startTime", System.currentTimeMillis());
    }

    @Override
    public void filter(ContainerRequestContext request,
                       ContainerResponseContext response) throws IOException {
        String path = request.getUriInfo().getPath();

        // Chỉ audit PHI-related endpoints
        if (!isPhiEndpoint(path)) return;

        AuditAction action = mapHttpMethodToAction(request.getMethod());
        AuditOutcome outcome = response.getStatus() < 400
            ? AuditOutcome.SUCCESS
            : (response.getStatus() == 403 ? AuditOutcome.DENIED : AuditOutcome.FAILURE);

        AuditEvent event = new AuditEvent();
        event.setUserId(jwt.getSubject());
        event.setUsername(jwt.getClaim("preferred_username"));
        event.setUserRole(String.join(",", jwt.getGroups()));
        event.setDepartment(jwt.getClaim("department"));
        event.setAction(action);
        event.setResourceType(extractResourceType(path));
        event.setResourceId(extractResourceId(path));
        event.setSourceIp(request.getHeaderString("X-Forwarded-For"));
        event.setUserAgent(request.getHeaderString("User-Agent"));
        event.setServiceName("patient-service");
        event.setOutcome(outcome);

        auditService.logAccess(event);
    }

    private boolean isPhiEndpoint(String path) {
        return path.startsWith("api/v1/patients")
            || path.startsWith("api/v1/medical-records")
            || path.startsWith("api/v1/lab-results")
            || path.startsWith("api/v1/prescriptions");
    }

    private AuditAction mapHttpMethodToAction(String method) {
        return switch (method) {
            case "GET" -> AuditAction.READ;
            case "POST" -> AuditAction.CREATE;
            case "PUT", "PATCH" -> AuditAction.UPDATE;
            case "DELETE" -> AuditAction.DELETE;
            default -> AuditAction.READ;
        };
    }

    private String extractResourceType(String path) {
        String[] parts = path.split("/");
        return parts.length >= 3 ? parts[2] : "unknown";
    }

    private String extractResourceId(String path) {
        String[] parts = path.split("/");
        return parts.length >= 4 ? parts[3] : null;
    }
}

3.2. SQL Schema for Audit Logs

-- Schema cho HIPAA Audit Trail
CREATE SCHEMA IF NOT EXISTS compliance;

CREATE TABLE compliance.audit_logs (
    id              UUID PRIMARY KEY,
    timestamp       TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    -- WHO
    user_id         VARCHAR(255) NOT NULL,
    username        VARCHAR(255) NOT NULL,
    user_role       VARCHAR(500),
    department      VARCHAR(100),

    -- WHAT
    action          VARCHAR(50) NOT NULL,
    resource_type   VARCHAR(100) NOT NULL,
    resource_id     VARCHAR(255),

    -- WHERE
    source_ip       VARCHAR(45),
    user_agent      TEXT,
    service_name    VARCHAR(100),

    -- WHY
    reason          TEXT,
    emergency_access BOOLEAN DEFAULT FALSE,

    -- RESULT
    outcome         VARCHAR(20) NOT NULL,
    error_message   TEXT
) PARTITION BY RANGE (timestamp);

-- Partition theo tháng cho performance
CREATE TABLE compliance.audit_logs_2024_01
    PARTITION OF compliance.audit_logs
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE compliance.audit_logs_2024_02
    PARTITION OF compliance.audit_logs
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

-- Index cho queries thường dùng
CREATE INDEX idx_audit_timestamp ON compliance.audit_logs (timestamp);
CREATE INDEX idx_audit_user_id ON compliance.audit_logs (user_id);
CREATE INDEX idx_audit_resource ON compliance.audit_logs (resource_type, resource_id);
CREATE INDEX idx_audit_action ON compliance.audit_logs (action);
CREATE INDEX idx_audit_outcome ON compliance.audit_logs (outcome);

-- HIPAA yêu cầu giữ audit logs tối thiểu 6 năm
-- KHÔNG DELETE audit logs trong 6 năm
-- Sử dụng tablespace riêng cho audit data

-- Prevent deletion of audit records
REVOKE DELETE ON compliance.audit_logs FROM PUBLIC;
-- Chỉ superuser mới được DELETE (và phải theo retention policy)

4. Integrity Controls — §164.312(c)(1)

4.1. Standard [Required]

"Implement policies and procedures to protect ePHI from improper alteration or destruction."

4.2. Mechanism to Authenticate ePHI — §164.312(c)(2) [Addressable]

"Implement electronic mechanisms to corroborate that ePHI has not been altered or destroyed in an unauthorized manner."

package vn.hospital.compliance.integrity;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.jboss.logging.Logger;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;

/**
 * HIPAA §164.312(c)(2) - ePHI Integrity Verification
 * Đảm bảo dữ liệu không bị sửa đổi trái phép.
 */
@ApplicationScoped
public class EphiIntegrityService {

    private static final Logger LOG = Logger.getLogger(EphiIntegrityService.class);
    private static final String HMAC_ALGORITHM = "HmacSHA256";

    @Inject
    @io.quarkus.vault.runtime.config.VaultConfigSource
    String integrityKey; // Lấy từ Vault

    /**
     * Tính HMAC-SHA256 cho record integrity verification.
     * HMAC bao gồm tất cả PHI fields + metadata.
     */
    public String computeRecordHmac(PatientRecord record) {
        try {
            String dataToSign = String.join("|",
                record.getId().toString(),
                record.getMrn(),
                record.getFullName(),
                record.getSsn() != null ? record.getSsn() : "",
                record.getDateOfBirth() != null ? record.getDateOfBirth() : "",
                record.getDiagnosisCodes() != null ? record.getDiagnosisCodes() : "",
                record.getLastModifiedBy(),
                record.getLastModifiedAt().toString()
            );

            Mac mac = Mac.getInstance(HMAC_ALGORITHM);
            SecretKeySpec keySpec = new SecretKeySpec(
                integrityKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
            mac.init(keySpec);
            byte[] hmacBytes = mac.doFinal(dataToSign.getBytes(StandardCharsets.UTF_8));

            return Base64.getEncoder().encodeToString(hmacBytes);
        } catch (Exception e) {
            throw new RuntimeException("HMAC computation failed", e);
        }
    }

    /**
     * Verify record integrity - so sánh stored HMAC với computed HMAC.
     */
    public boolean verifyRecordIntegrity(PatientRecord record) {
        String storedHmac = record.getIntegrityHash();
        if (storedHmac == null) {
            LOG.warnf("No integrity hash for record: %s", record.getId());
            return false;
        }

        String computedHmac = computeRecordHmac(record);
        return MessageDigest.isEqual(
            storedHmac.getBytes(StandardCharsets.UTF_8),
            computedHmac.getBytes(StandardCharsets.UTF_8)
        );
    }
}

PostgreSQL Trigger for Integrity Tracking:

-- Trigger function để track mọi thay đổi trên PHI records
CREATE OR REPLACE FUNCTION compliance.track_phi_changes()
RETURNS TRIGGER AS $$
DECLARE
    changes JSONB;
BEGIN
    -- Build changes JSON
    IF TG_OP = 'UPDATE' THEN
        changes = jsonb_build_object(
            'operation', 'UPDATE',
            'table_name', TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME,
            'record_id', NEW.id,
            'old_values', to_jsonb(OLD),
            'new_values', to_jsonb(NEW),
            'changed_by', current_setting('app.current_user_id', true),
            'changed_at', NOW()
        );
    ELSIF TG_OP = 'DELETE' THEN
        changes = jsonb_build_object(
            'operation', 'DELETE',
            'table_name', TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME,
            'record_id', OLD.id,
            'deleted_values', to_jsonb(OLD),
            'changed_by', current_setting('app.current_user_id', true),
            'changed_at', NOW()
        );
    ELSIF TG_OP = 'INSERT' THEN
        changes = jsonb_build_object(
            'operation', 'INSERT',
            'table_name', TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME,
            'record_id', NEW.id,
            'new_values', to_jsonb(NEW),
            'changed_by', current_setting('app.current_user_id', true),
            'changed_at', NOW()
        );
    END IF;

    -- Insert vào change log (immutable)
    INSERT INTO compliance.data_change_log (id, change_data, created_at)
    VALUES (gen_random_uuid(), changes, NOW());

    RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;

-- Apply trigger cho patients table
CREATE TRIGGER trg_patients_phi_changes
    AFTER INSERT OR UPDATE OR DELETE ON healthcare.patients
    FOR EACH ROW EXECUTE FUNCTION compliance.track_phi_changes();

-- Change log table (append-only, no UPDATE/DELETE)
CREATE TABLE compliance.data_change_log (
    id          UUID PRIMARY KEY,
    change_data JSONB NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
) PARTITION BY RANGE (created_at);

REVOKE UPDATE, DELETE ON compliance.data_change_log FROM PUBLIC;

5. Person or Entity Authentication — §164.312(d)

5.1. Standard [Required]

"Implement procedures to verify that a person or entity seeking access to ePHI is the one stated."

Multi-Factor Authentication with Keycloak:

{
  "realm": "healthcare",
  "browserFlow": "healthcare-browser-flow",
  "authenticationFlows": [
    {
      "alias": "healthcare-browser-flow",
      "description": "Healthcare MFA browser flow",
      "providerId": "basic-flow",
      "topLevel": true,
      "builtIn": false,
      "authenticationExecutions": [
        {
          "authenticatorFlow": false,
          "authenticator": "auth-cookie",
          "requirement": "ALTERNATIVE",
          "priority": 10
        },
        {
          "authenticatorFlow": true,
          "flowAlias": "healthcare-forms",
          "requirement": "ALTERNATIVE",
          "priority": 20
        }
      ]
    },
    {
      "alias": "healthcare-forms",
      "description": "Username/password + TOTP",
      "providerId": "basic-flow",
      "topLevel": false,
      "authenticationExecutions": [
        {
          "authenticator": "auth-username-password-form",
          "requirement": "REQUIRED",
          "priority": 10
        },
        {
          "authenticator": "auth-otp-form",
          "requirement": "REQUIRED",
          "priority": 20
        }
      ]
    }
  ],
  "requiredActions": [
    {
      "alias": "CONFIGURE_TOTP",
      "name": "Configure OTP",
      "providerId": "CONFIGURE_TOTP",
      "enabled": true,
      "defaultAction": true,
      "priority": 10
    }
  ],
  "otpPolicyType": "totp",
  "otpPolicyAlgorithm": "HmacSHA256",
  "otpPolicyDigits": 6,
  "otpPolicyPeriod": 30,
  "otpPolicyInitialCounter": 0,
  "bruteForceProtected": true,
  "maxFailureWaitSeconds": 900,
  "failureFactor": 5,
  "waitIncrementSeconds": 60,
  "maxDeltaTimeSeconds": 43200
}
ConfigurationValueHIPAA Purpose
MFA RequiredTOTP (6 digits, 30s)Strong Identity Authentication
Brute Force Protection5 failed attempts → 15 minute lockAnti-brute force
Password PolicyMin 12 chars, uppercase, number, specialStrong passwords
Session Timeout15 minutes idle, 8 hours maxAutomatic logoff
Account Lockout5 failed attemptsPrevent unauthorized access

6. Transmission Security — §164.312(e)(1)

6.1. Standard [Required]

"Implement technical security measures to guard against unauthorized access to ePHI that is being transmitted over an electronic communications network."

6.2. Integrity Controls — §164.312(e)(2)(i) [Addressable]

"Implement security measures to ensure that electronically transmitted ePHI is not improperly modified without detection until disposed of."

6.3. Encryption — §164.312(e)(2)(ii) [Addressable]

"Implement a mechanism to encrypt ePHI whenever considered appropriate."

# application.properties - Transmission Security

# === TLS 1.3 Only ===
quarkus.http.ssl.protocols=TLSv1.3
quarkus.http.insecure-requests=disabled
quarkus.http.ssl-port=8443

# === Strong Cipher Suites ===
quarkus.http.ssl.cipher-suites=\
  TLS_AES_256_GCM_SHA384,\
  TLS_AES_128_GCM_SHA256,\
  TLS_CHACHA20_POLY1305_SHA256

# === HSTS Header ===
quarkus.http.header."Strict-Transport-Security".value=max-age=31536000; includeSubDomains; preload
quarkus.http.header."Strict-Transport-Security".path=/

# === Certificate Configuration ===
quarkus.http.ssl.certificate.key-store-file=${TLS_KEYSTORE_PATH}
quarkus.http.ssl.certificate.key-store-password=${TLS_KEYSTORE_PASSWORD}
quarkus.http.ssl.certificate.key-store-file-type=PKCS12

# === REST Client TLS ===
quarkus.rest-client."vn.hospital.client.LabServiceClient".trust-store=${TLS_TRUSTSTORE_PATH}
quarkus.rest-client."vn.hospital.client.LabServiceClient".trust-store-password=${TLS_TRUSTSTORE_PASSWORD}

7. Comprehensive Compliance Matrix

7.1. HIPAA Technical Safeguards Compliance Matrix

§SafeguardSpecR/AImplementationStatus
312(a)(1)Access ControlStandardRKeycloak OIDC + Quarkus RBAC
312(a)(2)(i)Unique User IDSpecRKeycloak UUID (sub claim)
312(a)(2)(ii)Emergency AccessSpecRBreak-the-Glass service
312(a)(2)(iii)Automatic LogoffSpecAKeycloak session timeout 15min
312(a)(2)(iv)Encryption/DecryptionSpecAVault Transit AES-256-GCM
312(b)Audit ControlsStandardRAuditLogEntry + JAX-RS filter
312(c)(1)IntegrityStandardRHMAC-SHA256 + change tracking
312(c)(2)Auth ePHISpecAHMAC verification + triggers
312(d)Person AuthenticationStandardRKeycloak MFA (TOTP)
312(e)(1)Transmission SecurityStandardRTLS 1.3 + mTLS + HSTS
312(e)(2)(i)Integrity ControlsSpecATLS integrity + JWE
312(e)(2)(ii)EncryptionSpecATLS 1.3, AES-256-GCM ciphers

7.2. Automated Compliance Check Script

#!/bin/bash
# hipaa-compliance-check.sh
# Script tự động kiểm tra HIPAA Technical Safeguards compliance

set -euo pipefail

REPORT_FILE="hipaa-compliance-report-$(date +%Y%m%d).txt"
PASS=0
FAIL=0
WARN=0

log_result() {
    local status=$1
    local section=$2
    local check=$3
    local detail=$4

    echo "[$status] §164.$section - $check: $detail" | tee -a "$REPORT_FILE"
    case $status in
        PASS) ((PASS++)) ;;
        FAIL) ((FAIL++)) ;;
        WARN) ((WARN++)) ;;
    esac
}

echo "=== HIPAA Technical Safeguards Compliance Check ===" | tee "$REPORT_FILE"
echo "Date: $(date -u +"%Y-%m-%dT%H:%M:%SZ")" | tee -a "$REPORT_FILE"
echo "=================================================" | tee -a "$REPORT_FILE"

# --- §164.312(a)(2)(i) Unique User Identification ---
echo "" | tee -a "$REPORT_FILE"
echo "--- Access Control ---" | tee -a "$REPORT_FILE"

# Check Keycloak user configuration
KC_USERS=$(curl -s -H "Authorization: Bearer $KC_ADMIN_TOKEN" \
    "$KEYCLOAK_URL/admin/realms/healthcare/users?max=1" | jq length 2>/dev/null || echo "ERROR")
if [ "$KC_USERS" != "ERROR" ]; then
    log_result "PASS" "312(a)(2)(i)" "Unique User ID" "Keycloak users configured"
else
    log_result "FAIL" "312(a)(2)(i)" "Unique User ID" "Cannot verify Keycloak users"
fi

# --- §164.312(a)(2)(iii) Automatic Logoff ---
KC_SESSION_TIMEOUT=$(curl -s -H "Authorization: Bearer $KC_ADMIN_TOKEN" \
    "$KEYCLOAK_URL/admin/realms/healthcare" | jq '.ssoSessionIdleTimeout' 2>/dev/null || echo "0")
if [ "$KC_SESSION_TIMEOUT" -le 900 ] && [ "$KC_SESSION_TIMEOUT" -gt 0 ]; then
    log_result "PASS" "312(a)(2)(iii)" "Automatic Logoff" \
        "Session idle timeout: ${KC_SESSION_TIMEOUT}s (≤15min)"
else
    log_result "FAIL" "312(a)(2)(iii)" "Automatic Logoff" \
        "Session idle timeout: ${KC_SESSION_TIMEOUT}s (should be ≤900s)"
fi

# --- §164.312(a)(2)(iv) Encryption ---
# Check TLS version
TLS_VERSION=$(echo | openssl s_client -connect "$APP_HOST:8443" -tls1_3 2>/dev/null \
    | grep "Protocol" | awk '{print $NF}' || echo "UNKNOWN")
if [ "$TLS_VERSION" = "TLSv1.3" ]; then
    log_result "PASS" "312(a)(2)(iv)" "Encryption - TLS" "TLS 1.3 enabled"
else
    log_result "FAIL" "312(a)(2)(iv)" "Encryption - TLS" \
        "TLS version: $TLS_VERSION (should be TLSv1.3)"
fi

# Check Vault Transit
VAULT_KEY=$(vault read -format=json transit/keys/phi-data 2>/dev/null | \
    jq -r '.data.type' || echo "NONE")
if [ "$VAULT_KEY" = "aes256-gcm96" ]; then
    log_result "PASS" "312(a)(2)(iv)" "Encryption - Vault" "Transit key: aes256-gcm96"
else
    log_result "FAIL" "312(a)(2)(iv)" "Encryption - Vault" \
        "Vault Transit key not found or wrong type"
fi

# --- §164.312(b) Audit Controls ---
AUDIT_TABLE=$(psql "$DB_URL" -tAc \
    "SELECT COUNT(*) FROM information_schema.tables \
     WHERE table_schema='compliance' AND table_name='audit_logs'" 2>/dev/null || echo "0")
if [ "$AUDIT_TABLE" = "1" ]; then
    log_result "PASS" "312(b)" "Audit Controls" "Audit log table exists"
else
    log_result "FAIL" "312(b)" "Audit Controls" "Audit log table NOT found"
fi

# Check audit log has recent entries
RECENT_AUDITS=$(psql "$DB_URL" -tAc \
    "SELECT COUNT(*) FROM compliance.audit_logs \
     WHERE timestamp > NOW() - INTERVAL '24 hours'" 2>/dev/null || echo "0")
if [ "$RECENT_AUDITS" -gt 0 ]; then
    log_result "PASS" "312(b)" "Audit Controls - Active" \
        "$RECENT_AUDITS audit entries in last 24h"
else
    log_result "WARN" "312(b)" "Audit Controls - Active" \
        "No audit entries in last 24h"
fi

# --- §164.312(c) Integrity Controls ---
CHANGE_LOG=$(psql "$DB_URL" -tAc \
    "SELECT COUNT(*) FROM information_schema.tables \
     WHERE table_schema='compliance' AND table_name='data_change_log'" 2>/dev/null || echo "0")
if [ "$CHANGE_LOG" = "1" ]; then
    log_result "PASS" "312(c)" "Integrity Controls" "Change log table exists"
else
    log_result "FAIL" "312(c)" "Integrity Controls" "Change log table NOT found"
fi

# --- §164.312(d) Person Authentication ---
KC_OTP=$(curl -s -H "Authorization: Bearer $KC_ADMIN_TOKEN" \
    "$KEYCLOAK_URL/admin/realms/healthcare" | jq -r '.otpPolicyType' 2>/dev/null || echo "NONE")
if [ "$KC_OTP" = "totp" ]; then
    log_result "PASS" "312(d)" "Person Authentication" "MFA enabled (TOTP)"
else
    log_result "FAIL" "312(d)" "Person Authentication" "MFA NOT configured"
fi

# --- §164.312(e) Transmission Security ---
# Check if insecure HTTP is disabled
HTTP_REDIRECT=$(curl -s -o /dev/null -w "%{http_code}" \
    "http://$APP_HOST:8080/health" 2>/dev/null || echo "000")
if [ "$HTTP_REDIRECT" = "000" ] || [ "$HTTP_REDIRECT" = "301" ]; then
    log_result "PASS" "312(e)" "Transmission Security" \
        "HTTP disabled/redirected (code: $HTTP_REDIRECT)"
else
    log_result "FAIL" "312(e)" "Transmission Security" \
        "HTTP still accessible (code: $HTTP_REDIRECT)"
fi

# --- Summary ---
echo "" | tee -a "$REPORT_FILE"
echo "=== COMPLIANCE SUMMARY ===" | tee -a "$REPORT_FILE"
echo "PASS: $PASS" | tee -a "$REPORT_FILE"
echo "FAIL: $FAIL" | tee -a "$REPORT_FILE"
echo "WARN: $WARN" | tee -a "$REPORT_FILE"
TOTAL=$((PASS + FAIL + WARN))
if [ $TOTAL -gt 0 ]; then
    SCORE=$((PASS * 100 / TOTAL))
    echo "Score: ${SCORE}%" | tee -a "$REPORT_FILE"
fi

if [ $FAIL -gt 0 ]; then
    echo "STATUS: NON-COMPLIANT" | tee -a "$REPORT_FILE"
    exit 1
else
    echo "STATUS: COMPLIANT" | tee -a "$REPORT_FILE"
fi

8. BAA (Business Associate Agreement) — Technical Requirements

8.1. BAA Technical Obligations

When using third-party services (cloud providers, SaaS), BAA requires the following technical guarantees:

Cloud Provider (AWS/GCP/Azure):

  • Encryption at rest: AES-256
  • Encryption in transit: TLS 1.2+
  • Access logging: CloudTrail/Cloud Audit Logs
  • Data residency: Specify region
  • Incident notification: ≤ 60 days

Database Service (RDS/Cloud SQL):

  • Encrypted storage volumes + backups
  • Audit logging enabled
  • Network isolation (VPC)
  • IAM authentication

Monitoring Service (Datadog/New Relic):

  • PHI masking before sending
  • Data processing agreement
  • EU/US data residency
  • Log retention controls

Email Service (SendGrid/SES):

  • TLS enforce
  • No PHI in email content
  • Breach notification capability

8.2. BAA Compliance Verification

package vn.hospital.compliance.baa;

import jakarta.enterprise.context.ApplicationScoped;
import java.time.LocalDate;
import java.util.List;

/**
 * Track Business Associate Agreements và technical compliance.
 */
@ApplicationScoped
public class BaaComplianceService {

    /**
     * Danh sách Business Associates cần BAA.
     */
    public List<BusinessAssociate> getBusinessAssociates() {
        return List.of(
            new BusinessAssociate(
                "AWS", "Cloud Infrastructure",
                "BAA-AWS-2024-001", LocalDate.of(2024, 1, 15),
                List.of("AES-256 at rest", "TLS 1.2+ in transit",
                         "CloudTrail logging", "VPC isolation")
            ),
            new BusinessAssociate(
                "Elastic Cloud", "Log Management (ELK)",
                "BAA-ELASTIC-2024-002", LocalDate.of(2024, 2, 1),
                List.of("Encrypted clusters", "RBAC", "Audit logging",
                         "Data residency controls")
            ),
            new BusinessAssociate(
                "HashiCorp Cloud", "Vault (Key Management)",
                "BAA-HASHI-2024-003", LocalDate.of(2024, 3, 1),
                List.of("FIPS 140-2 HSMs", "SOC 2 Type II",
                         "Encryption in transit", "Access logging")
            )
        );
    }
}

record BusinessAssociate(
    String name,
    String serviceDescription,
    String baaId,
    LocalDate effectiveDate,
    List<String> technicalSafeguards
) {}

9. Decree 13/2023/ND-CP — Vietnam Personal Data Protection

9.1. Overview of Decree 13

Decree 13/2023/ND-CP on personal data protection (effective from July 1, 2023) is Vietnam's first regulation on data protection, similar to the EU's GDPR. For health systems, health data falls under sensitive personal data.

9.2. Mapping Decree 13 with HIPAA Controls

Decree 13ArticleHIPAA EquivalentImplementation
Consent to data processingArticle 11Authorization §164.508Consent management service
Data Access RightsArticle 9Right of Access §164.524Patient portal API
Right to data deletionArticle 16N/A (HIPAA holds 6 years)Soft delete + anonymization
Notice of violationArticle 23Breach Notification §164.408Incident response workflow
Impact assessmentArticle 24Risk Analysis §164.308(a)(1)DPIA template
Data securityArticle 26Technical Safeguards §164.312Encryption + access control
Cross-border data transferArticle 25N/AData residency controls
DPO (Protection Officer)Article 28Privacy OfficerRole assignment

9.3. Consent Management Service

package vn.hospital.compliance.consent;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;

import java.time.Instant;
import java.util.List;
import java.util.UUID;

/**
 * Nghị định 13 Điều 11 - Đồng ý xử lý dữ liệu cá nhân.
 * Bệnh nhân phải đồng ý trước khi xử lý dữ liệu sức khỏe.
 */
@ApplicationScoped
public class ConsentManagementService {

    @Inject
    EntityManager entityManager;

    /**
     * Ghi nhận consent của bệnh nhân.
     */
    @Transactional
    public ConsentRecord recordConsent(ConsentRequest request) {
        ConsentRecord record = new ConsentRecord();
        record.setId(UUID.randomUUID());
        record.setPatientId(request.getPatientId());
        record.setPurpose(request.getPurpose());
        record.setScope(request.getScope());
        record.setConsentGiven(request.isConsentGiven());
        record.setConsentMethod(request.getConsentMethod());
        record.setConsentedAt(Instant.now());
        record.setExpiresAt(request.getExpiresAt());
        record.setVersion(request.getConsentFormVersion());

        entityManager.persist(record);
        return record;
    }

    /**
     * Kiểm tra bệnh nhân đã consent cho mục đích cụ thể chưa.
     */
    public boolean hasValidConsent(UUID patientId, String purpose) {
        Long count = entityManager.createQuery(
            "SELECT COUNT(c) FROM ConsentRecord c " +
            "WHERE c.patientId = :patientId AND c.purpose = :purpose " +
            "AND c.consentGiven = true AND c.revokedAt IS NULL " +
            "AND (c.expiresAt IS NULL OR c.expiresAt > :now)",
            Long.class)
            .setParameter("patientId", patientId)
            .setParameter("purpose", purpose)
            .setParameter("now", Instant.now())
            .getSingleResult();
        return count > 0;
    }

    /**
     * Thu hồi consent (Điều 12 - Quyền rút lại đồng ý).
     */
    @Transactional
    public void revokeConsent(UUID consentId, String revokedBy, String reason) {
        ConsentRecord record = entityManager.find(ConsentRecord.class, consentId);
        if (record != null) {
            record.setRevokedAt(Instant.now());
            record.setRevokedBy(revokedBy);
            record.setRevocationReason(reason);
        }
    }

    /**
     * Lấy tất cả consent records cho một bệnh nhân.
     * Nghị định 13 Điều 9 - Quyền truy cập dữ liệu.
     */
    public List<ConsentRecord> getPatientConsents(UUID patientId) {
        return entityManager.createQuery(
            "SELECT c FROM ConsentRecord c WHERE c.patientId = :patientId " +
            "ORDER BY c.consentedAt DESC", ConsentRecord.class)
            .setParameter("patientId", patientId)
            .getResultList();
    }
}

9.4. Data Residency Check

package vn.hospital.compliance.residency;

import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.Logger;

import java.util.Set;

/**
 * Nghị định 13 Điều 25 - Chuyển dữ liệu cá nhân ra nước ngoài.
 * Yêu cầu đánh giá tác động trước khi transfer.
 */
@ApplicationScoped
public class DataResidencyService {

    private static final Logger LOG = Logger.getLogger(DataResidencyService.class);

    // Danh sách regions được phép lưu trữ dữ liệu y tế VN
    private static final Set<String> ALLOWED_REGIONS = Set.of(
        "ap-southeast-1",   // Singapore (gần VN, có BAA)
        "ap-east-1"         // Hong Kong
        // VN region khi available
    );

    /**
     * Kiểm tra region có được phép lưu trữ dữ liệu không.
     */
    public boolean isAllowedRegion(String region) {
        return ALLOWED_REGIONS.contains(region);
    }

    /**
     * Validate trước khi cross-border transfer.
     */
    public TransferAssessment assessCrossBorderTransfer(
            String sourceRegion, String targetRegion, String dataType) {

        boolean allowed = ALLOWED_REGIONS.contains(targetRegion);

        return new TransferAssessment(
            sourceRegion, targetRegion, dataType,
            allowed,
            allowed ? "Transfer allowed" :
                "Transfer requires DPIA and regulatory approval per Nghị định 13 Điều 25"
        );
    }
}

record TransferAssessment(
    String sourceRegion,
    String targetRegion,
    String dataType,
    boolean allowed,
    String assessment
) {}

Summary

In this lesson, we have fully mapped HIPAA Technical Safeguards §164.312 to a specific implementation:

  1. Access Control §164.312(a): Keycloak unique user IDs, Break-the-Glass emergency access, automatic logoff (15-min idle timeout), field-level encryption with Vault Transit
  2. Audit Controls §164.312(b): Comprehensive audit trail (WHO/WHAT/WHEN/WHERE/WHY), JAX-RS filter automatically logs PHI access, partitioned audit tables
  3. Integrity Controls §164.312(c): HMAC-SHA256 record integrity, PostgreSQL triggers for change tracking, immutable change log
  4. Person Authentication §164.312(d): Keycloak MFA (TOTP), password policies, brute force protection, account lockout
  5. Transmission Security §164.312(e): TLS 1.3 only, strong cipher suites, HSTS, mTLS for inter-service
  6. Compliance Automation: Automatic testing script, compliance matrix, scoring
  7. BAA Technical Requirements: Cloud provider obligations, PHI masking for third-party services
  8. Decree 13/2023/ND-CP: Consent management, data residency controls, cross-border transfer assessment, mapping with HIPAA

Exercises

  1. Compliance Matrix: Create a spreadsheet or database table containing the full compliance matrix for your project. Mapping all 12 HIPAA Technical Safeguard specifications to a specific implementation. Evaluate the status (Compliant/Non-compliant/In Progress) for each item. Create action plans for Non-compliant items.

  2. Emergency Access: Implement complete Break-the-Glass service. Create REST API: POST /api/v1/emergency-access (request access), DELETE /api/v1/emergency-access/{id} (revoke). Integrate with Keycloak to grant temporary roles. Verify audit log records complete information. Test: accessing PHI without emergency access → 403.

  3. Automated Compliance Check: Customize script hipaa-compliance-check.sh for your environment. Add checks for: database encryption at rest, backup encryption, password policy strength, MFA enrollment percentage. Integrate into CI/CD pipeline (run per deployment). Output report in JSON format for dashboard monitoring.

  4. Consent Management (Decree 13): Implement REST API for consent management. Create consent form for 3 purposes: treatment, research, data sharing. Implement: record consent, check consent, revoke consent, list consents. Create a patient portal endpoint that displays consent history. Verify: cannot access PHI when consent has not been granted.



◀ Previous articleNext article ▶
Lesson 16: mTLS, Service Mesh & Secure Inter-Service CommunicationLesson 18: Centralized Audit Trail with OpenTelemetry & ELK Stack