Security Best Practices
This guide covers security configuration for production deployments of OctoFHIR.
Authentication Overview
Section titled “Authentication Overview”OctoFHIR implements OAuth 2.0 and SMART on FHIR for authentication and authorization:
- OAuth 2.0 - Industry standard authorization framework
- SMART on FHIR - Healthcare-specific OAuth 2.0 profile
- Policy Engine - Fine-grained access control with JavaScript policies
TLS Configuration
Section titled “TLS Configuration”Nginx Example
Section titled “Nginx Example”server { listen 443 ssl http2; server_name fhir.example.com;
ssl_certificate /etc/letsencrypt/live/fhir.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/fhir.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
location / { proxy_pass http://127.0.0.1:8888; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }}Caddy Example
Section titled “Caddy Example”fhir.example.com { reverse_proxy localhost:8888}OAuth 2.0 Configuration
Section titled “OAuth 2.0 Configuration”Secure Token Settings
Section titled “Secure Token Settings”[auth]# Must match your public URLissuer = "https://fhir.example.com"
[auth.oauth]# Short-lived access tokensaccess_token_lifetime = "15m"# Longer refresh tokens for UXrefresh_token_lifetime = "7d"# Rotate refresh tokens on each userefresh_token_rotation = true# Only allow needed grant typesgrant_types = ["authorization_code", "refresh_token"]JWT Key Management
Section titled “JWT Key Management”For production, configure persistent signing keys:
-
Generate RSA keys
Terminal window # Generate private key (PKCS#8 format required)openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:3072# Extract public keyopenssl rsa -in private.pem -pubout -out public.pem -
Configure in octofhir.toml
[auth.signing]algorithm = "RS384"kid = "prod-key-2024"private_key_pem = """-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----"""public_key_pem = """-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----""" -
Or use environment variables
Terminal window OCTOFHIR__AUTH__SIGNING__PRIVATE_KEY_PEM="$(cat private.pem)"OCTOFHIR__AUTH__SIGNING__PUBLIC_KEY_PEM="$(cat public.pem)"
SMART on FHIR Setup
Section titled “SMART on FHIR Setup”Client Registration
Section titled “Client Registration”Register OAuth clients for your applications:
curl -X POST https://fhir.example.com/admin/clients \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "client_id": "my-ehr-app", "client_name": "My EHR Application", "redirect_uris": ["https://app.example.com/callback"], "grant_types": ["authorization_code", "refresh_token"], "token_endpoint_auth_method": "client_secret_basic", "scope": "openid fhirUser launch/patient patient/*.read" }'Confidential vs Public Clients
Section titled “Confidential vs Public Clients”| Client Type | Use Case | Auth Method |
|---|---|---|
| Confidential | Server-side apps | client_secret_basic or private_key_jwt |
| Public | SPAs, mobile apps | PKCE required |
[auth.smart]# Production: disable public clients or require PKCEpublic_clients_allowed = falseconfidential_symmetric_allowed = trueconfidential_asymmetric_allowed = trueAccess Policies
Section titled “Access Policies”OctoFHIR uses a policy engine for fine-grained access control.
Default Deny
Section titled “Default Deny”[auth.policy]default_deny = true # Deny if no policy matchesExample Policies
Section titled “Example Policies”Admin Full Access:
// Policy: admin-full-accessif (user.roles.includes('admin')) { return { allow: true };}Patient Compartment Access:
// Policy: patient-compartmentif (request.resourceType === 'Patient' && request.resourceId === user.patientId) { return { allow: true };}if (request.resource?.subject?.reference === `Patient/${user.patientId}`) { return { allow: true };}return { allow: false, reason: 'Access denied to this patient data' };Read-Only for Researchers:
// Policy: researcher-readonlyif (user.roles.includes('researcher') && request.method === 'GET') { return { allow: true };}Rate Limiting
Section titled “Rate Limiting”Protect against abuse and DoS:
[auth.rate_limiting]# Token endpointtoken_requests_per_minute = 30token_requests_per_hour = 500
# Authorization endpointauth_requests_per_minute = 20
# Brute force protectionmax_failed_attempts = 5lockout_duration = "15m"Session Security
Section titled “Session Security”[auth.session]# Idle timeout (sliding window)idle_timeout = "15m"# Absolute timeout (max session length)absolute_timeout = "8h"# Limit concurrent sessionsmax_concurrent_sessions = 5
[auth.cookie]secure = true # HTTPS onlyhttp_only = true # No JavaScript accesssame_site = "strict" # CSRF protectionAudit Logging
Section titled “Audit Logging”Enable comprehensive audit trails:
[audit]enabled = truelog_fhir_operations = truelog_auth_events = truelog_read_operations = truelog_search_operations = trueAudit events are stored as AuditEvent resources and can be queried:
curl "https://fhir.example.com/fhir/AuditEvent?date=gt2024-01-01&_count=100" \ -H "Authorization: Bearer $ADMIN_TOKEN"Validation Security
Section titled “Validation Security”[validation]# Disable skip validation in productionallow_skip_validation = falseDatabase Security
Section titled “Database Security”Connection Security
Section titled “Connection Security”[storage.postgres]# Use SSL for database connectionsurl = "postgres://user:pass@db.example.com:5432/octofhir?sslmode=require"Principle of Least Privilege
Section titled “Principle of Least Privilege”Create a dedicated database user with minimal permissions:
CREATE USER octofhir_app WITH PASSWORD 'secure-password';GRANT CONNECT ON DATABASE octofhir TO octofhir_app;GRANT USAGE ON SCHEMA public TO octofhir_app;GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO octofhir_app;GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO octofhir_app;Production Checklist
Section titled “Production Checklist”- TLS/HTTPS - Always use HTTPS with TLS 1.2+
- Credentials - Change all default passwords
- JWT Keys - Configure persistent signing keys
- Cookies - Set
secure = trueandsame_site = "strict" - Rate Limiting - Enable and tune rate limits
- Audit Logging - Enable audit trail
- Validation - Disable skip validation header
- GraphQL - Disable introspection
- Database - Use SSL connections
- Secrets - Use environment variables for sensitive config
Security Headers
Section titled “Security Headers”Configure your reverse proxy to add security headers:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;add_header X-Content-Type-Options "nosniff" always;add_header X-Frame-Options "DENY" always;add_header X-XSS-Protection "1; mode=block" always;add_header Content-Security-Policy "default-src 'self'" always;Reporting Security Issues
Section titled “Reporting Security Issues”If you discover a security vulnerability, please report it privately:
- Email: security@octofhir.io
- Do not create public GitHub issues for security vulnerabilities