Skip to main content

Cloud Run — HIPAA Configuration

Overview

Cloud Run is a HIPAA-eligible service under Google's BAA. This document covers the configuration required to deploy HIPAA-compliant containerized applications on Cloud Run, including ingress controls, VPC networking, secrets management, and PHI-safe logging.


1. Approved Regions

Deploy Cloud Run services only in approved US regions:

Region IDLocation
us-central1Iowa
us-east1South Carolina
us-east4Northern Virginia
us-west1Oregon
us-west2Los Angeles

2. Deploying a HIPAA-Compliant Cloud Run Service

gcloud run deploy phi-app \
--image=us-central1-docker.pkg.dev/YOUR_PROJECT_ID/phi-repo/phi-app:latest \
--region=us-central1 \
--platform=managed \
--no-allow-unauthenticated \ # Require IAM auth (or LB auth)
--ingress=internal-and-cloud-load-balancing \ # Block direct public access
--service-account=phi-app-sa@YOUR_PROJECT_ID.iam.gserviceaccount.com \
--vpc-connector=phi-connector \
--vpc-egress=private-ranges-only \ # Route only private traffic through VPC
--add-cloudsql-instances=YOUR_PROJECT_ID:us-central1:phi-db-instance \
--set-secrets=DB_PASSWORD=db-password:latest,REDIS_AUTH=redis-auth-string:latest \
--min-instances=1 \ # Keep warm to avoid cold starts exposing timing
--max-instances=10 \
--memory=512Mi \
--cpu=1 \
--timeout=60 \
--concurrency=80 \
--project=YOUR_PHI_PROJECT_ID

Key Flags for HIPAA

FlagValuePurpose
--no-allow-unauthenticated(flag)Require IAM token or Cloud LB to call the service
--ingress=internal-and-cloud-load-balancing(value)Block direct internet access; only LB and VPC
--service-accountDedicated SALeast-privilege identity (not default compute SA)
--vpc-connectorphi-connectorRoute private traffic to Cloud SQL and Redis
--vpc-egress=private-ranges-only(value)Only private IP traffic goes through VPC
--set-secretsSecret Manager refsNever put credentials in env vars or image

3. Dedicated Service Account

Create a dedicated service account for each Cloud Run service — do not use the default compute SA.

# Create a dedicated SA for the Cloud Run service
gcloud iam service-accounts create phi-app-sa \
--display-name="PHI App Cloud Run SA" \
--project=YOUR_PHI_PROJECT_ID

# Grant only what the service needs
gcloud projects add-iam-policy-binding YOUR_PHI_PROJECT_ID \
--member="serviceAccount:phi-app-sa@YOUR_PHI_PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/cloudsql.client"

gcloud projects add-iam-policy-binding YOUR_PHI_PROJECT_ID \
--member="serviceAccount:phi-app-sa@YOUR_PHI_PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"

gcloud projects add-iam-policy-binding YOUR_PHI_PROJECT_ID \
--member="serviceAccount:phi-app-sa@YOUR_PHI_PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/cloudtrace.agent"

4. HTTPS and Custom Domain

Cloud Run provides automatic TLS for its *.run.app domain. For a custom domain:

# Map a custom domain to the Cloud Run service
gcloud run domain-mappings create \
--service=phi-app \
--domain=app.yourcompany.com \
--region=us-central1 \
--project=YOUR_PHI_PROJECT_ID

Security Headers in Application Code

# Flask
@app.after_request
def set_security_headers(response):
response.headers['Strict-Transport-Security'] = \
'max-age=31536000; includeSubDomains; preload'
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate'
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
return response

5. Ingress and Cloud Armor WAF

5.1 Cloud Load Balancer + Cloud Armor

# Create a Serverless NEG pointing to the Cloud Run service
gcloud compute network-endpoint-groups create phi-run-neg \
--region=us-central1 \
--network-endpoint-type=serverless \
--cloud-run-service=phi-app \
--project=YOUR_PHI_PROJECT_ID

# Create Cloud Armor WAF policy
gcloud compute security-policies create phi-waf-policy \
--description="HIPAA WAF for Cloud Run" \
--project=YOUR_PHI_PROJECT_ID

# OWASP rules
gcloud compute security-policies rules create 1000 \
--security-policy=phi-waf-policy \
--expression="evaluatePreconfiguredExpr('sqli-v33-stable')" \
--action=deny-403

gcloud compute security-policies rules create 1001 \
--security-policy=phi-waf-policy \
--expression="evaluatePreconfiguredExpr('xss-v33-stable')" \
--action=deny-403

# Rate limiting (prevent brute force)
gcloud compute security-policies rules create 2000 \
--security-policy=phi-waf-policy \
--expression="true" \
--action=rate-based-ban \
--rate-limit-threshold-count=100 \
--rate-limit-threshold-interval-sec=60 \
--ban-duration-sec=600

6. VPC Connector

gcloud compute networks vpc-access connectors create phi-connector \
--region=us-central1 \
--subnet=phi-connector-subnet \
--subnet-project=YOUR_PHI_PROJECT_ID \
--min-instances=2 \
--max-instances=10 \
--machine-type=e2-micro \
--project=YOUR_PHI_PROJECT_ID

7. Secret Management

# Inject secrets as environment variables
gcloud run deploy phi-app \
--set-secrets=DB_PASSWORD=db-password:latest,REDIS_AUTH=redis-auth-string:latest

# Or inject as files (more secure — not visible in process env)
gcloud run deploy phi-app \
--set-secrets=/secrets/db-password=db-password:latest
# File mount approach (preferred)
with open("/secrets/db-password") as f:
db_password = f.read().strip()

Never build secrets into the container image. Always inject at deploy time from Secret Manager.


8. Container Security

Dockerfile Best Practices

FROM python:3.12-slim-bookworm

RUN groupadd -r appuser && useradd -r -g appuser appuser

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=appuser:appuser . .

USER appuser
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", "main:app"]

.dockerignore:

.env
*.key
*.pem
secrets/
tests/fixtures/phi_*
__pycache__
.git

9. PHI-Safe Logging

PHI_FIELDS = {"ssn", "dob", "name", "address", "phone", "email", "diagnosis", "mrn"}

def safe_log(level: str, event: str, **context):
if PHI_FIELDS.intersection(context.keys()):
raise ValueError(f"Blocked attempt to log PHI fields")
getattr(logging, level)(json.dumps({"event": event, **context}))

# Correct usage
safe_log("info", "patient_record_accessed", record_internal_id=record_id)
# NEVER: safe_log("info", "patient accessed", name=patient.name)

10. Session Management

app.config.update(
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE='Strict',
PERMANENT_SESSION_LIFETIME=1800, # 30-minute HIPAA auto-logoff
SESSION_COOKIE_NAME='__Host-session'
)

Next: Database →