CNP AWS Initial Setup - Small Containerized HIPAA Boundary
Purpose
This document is the setup runbook for the first CNP AWS environment.
The target system is intentionally small:
- One AWS account
- One EC2 virtual machine
- One Docker Compose stack
- OpenEMR container
- MariaDB container
- Python sidecar container for integrations, glue, and Bedrock calls
- Reverse proxy container
- Encrypted local storage
- Encrypted offsite backups
- Route 53 for DNS
- Optional CloudFront and AWS WAF only if edge controls justify the added cost
The invariant this document protects:
PHI may only exist inside the approved HIPAA perimeter: the CNP AWS account, the encrypted EC2 host, encrypted backups, and approved AWS HIPAA-eligible services covered by the executed AWS BAA.
Cloudflare Enterprise BAA coverage is assumed to be out of budget for the initial deployment. Do not route PHI-bearing traffic through Cloudflare Free, Pro, or Business plans.
Local and test environments must use generated, synthetic, deidentified, or “white” data only. No production PHI should be copied to local machines, GitHub, CI logs, test fixtures, laptops, OrbStack volumes, or developer scratch files.
Deployment Assumptions
People:
- Two developers who can administer AWS and deploy code.
- One business manager who can handle billing, BAA acceptance, and user lifecycle.
- One operations manager who can handle day-to-day operator access and support.
- Three OpenEMR operator classes that only log in to OpenEMR, not AWS.
Business/operations managers should not need deep AWS knowledge. Their AWS responsibility should be limited to:
- Accept the AWS Business Associate Addendum (BAA).
- Maintain billing contacts and payment method.
- Receive cost/security alerts.
- Add and revoke business/application users through documented workflows.
Developers own:
- AWS technical setup.
- Infrastructure as code.
- Docker Compose runtime.
- Deployments.
- Backup verification.
- Bedrock integration controls.
- Operational troubleshooting.
High-Level Architecture
Internet
|
Route 53
|
+---------------+-------------------+
| |
Lowest cost path Optional AWS edge path
direct DNS to EC2 CloudFront + optional AWS WAF
| |
+---------------+-------------------+
|
AWS EC2 public IP
|
Host firewall / SG
|
Reverse proxy
|
+---------------+----------------+
| | |
OpenEMR MariaDB Python sidecar
| | |
+---------------+----------------+
|
Encrypted gp3 EBS volume
|
Restic encrypted backups
|
S3 backup bucket
Services To Use
Use these AWS services for the initial deployment:
- EC2
- EBS
- S3
- IAM Identity Center
- IAM roles and policies
- Systems Manager Session Manager
- CloudTrail
- CloudWatch alarms/logs, if configured carefully
- AWS Budgets
- KMS, where customer-managed keys are useful
- Systems Manager Parameter Store for production secrets
- Bedrock, only after BAA acceptance and explicit data controls
- Route 53
- AWS Certificate Manager, if using CloudFront
- CloudFront, optional
- AWS WAF, optional and only with CloudFront or another supported resource
- AWS Shield Standard, included automatically for supported AWS services
Do not use Cloudflare for PHI-bearing production traffic on the initial deployment.
Cloudflare may be used only for non-PHI experiments or DNS-only experiments outside production if CNP accepts the operational complexity. Route 53 is the production default.
Do not use these by default:
- RDS
- ECS
- EKS
- Fargate
- NAT Gateway
- Application Load Balancer
- API Gateway
- OpenSearch
- AWS Backup as the primary backup system
- Multi-account AWS Organizations
- AWS WAF, unless CloudFront is also added and the monthly cost is approved
Those services may be added later, but only when the operational need is explicit.
Phase 0 - Required Decisions
Before production go-live, collect:
- Legal business name.
- Authorized signer for AWS BAA.
- Billing contact.
- Payment method.
- Primary domain name.
- Domain registrar access.
- Microsoft 365 or Google Workspace identity source.
- Production AWS region.
- List of developer/admin users.
- List of business/operations users.
- OpenEMR operator classes and who approves membership.
Recommended production region:
us-east-1
Use a different region only if there is a clear data residency, latency, or operational reason.
Phase 0.5 - Developer Workstation Preflight
Do this from a developer workstation before running AWS CLI commands.
Required Tools
- AWS CLI v2
- Docker
- Docker Compose
- Git
- OpenTofu or Terraform
- A password manager approved by CNP
CLI Setup
Install or verify AWS CLI:
aws --versionConfigure a named profile:
aws configure sso --profile cnp-prodSet local shell defaults for this work session:
export AWS_PROFILE=cnp-prod
export AWS_REGION=us-east-1Verify identity before every destructive or cost-bearing operation:
aws sts get-caller-identityExpected result:
- Account ID matches the CNP AWS account.
- ARN is an IAM Identity Center role, not the root user.
- Region is the intended production region.
Local Safety Rules
- Do not store PHI on the developer workstation.
- Do not put real secrets in shell history.
- Do not commit
.env, generated credentials, database dumps, CSV exports, XLSX files, or backup snapshots. - Use synthetic data in OrbStack.
- Use a separate AWS profile for any personal or unrelated AWS account.
Phase 1 - Create The AWS Account
Website Steps
- Open
https://aws.amazon.com/. - Create the AWS account using a CNP-owned email address, not a personal consultant address.
- Use a CNP-owned phone number for recovery.
- Add payment information.
- Log in as root.
- Enable MFA on the root user.
- Store root credentials and MFA recovery information in the CNP-approved password vault.
- Configure alternate contacts:
- Billing: business manager
- Operations: operations manager
- Security: lead developer or shared security contact
CLI Verification
After a developer has CLI access:
aws sts get-caller-identity
aws account get-alternate-contact --alternate-contact-type BILLING
aws account get-alternate-contact --alternate-contact-type OPERATIONS
aws account get-alternate-contact --alternate-contact-type SECURITYIf aws account get-alternate-contact is not available
for the credentials being used, verify alternate contacts in the AWS
Console.
Phase 2 - Accept The AWS BAA
No PHI may enter AWS until the AWS BAA is accepted.
Website Steps
The authorized CNP representative should:
- Log in to the AWS Console.
- Open AWS Artifact.
- Open Agreements.
- Find the AWS Business Associate Addendum.
- Review the agreement.
- Accept it on behalf of CNP.
- Save the confirmation PDF or screenshot in CNP’s compliance records.
Console path:
AWS Console -> Artifact -> Agreements -> Business Associate Addendum
Evidence To Retain
- Acceptance date.
- Name of authorized signer.
- AWS account ID.
- Agreement confirmation.
- Location of stored record.
Phase 3 - DNS And Edge Decision
Cloudflare is not the production default because its public HIPAA material says Cloudflare’s BAA is available only for Enterprise-level customers with minimum spending thresholds. That does not fit CNP’s initial cost envelope.
Use AWS-native services covered by the AWS BAA instead.
Decision
Choose one:
Mode A - lowest cost direct host
Route 53 points directly to the EC2 Elastic IP. TLS terminates on the EC2 reverse proxy.
Mode B - low-cost AWS edge
Route 53 points to CloudFront. CloudFront forwards to the EC2 origin. AWS WAF may be attached to CloudFront if the added monthly cost is approved.
Start with Mode A unless there is a concrete need for managed edge filtering.
Mode A - Lowest Cost Direct Host
Use:
- Route 53 hosted zone.
Arecord for OpenEMR.- EC2 Elastic IP.
- Caddy or Nginx on the EC2 host.
- Let’s Encrypt certificate on the host.
- Host firewall.
- Security group allowing inbound 443.
- Application-level rate limiting and fail2ban where practical.
Benefits:
- Lowest monthly cost.
- Fewest moving parts.
- Easy to understand and restore.
- No extra edge service in the PHI path beyond AWS DNS.
Trade-off:
- No managed AWS WAF in front of the host.
- DDoS protection is limited to AWS Shield Standard and host-level controls.
Mode B - Low-Cost AWS Edge
Use:
- Route 53 hosted zone.
- ACM certificate in
us-east-1. - CloudFront distribution.
- EC2 origin.
- Optional AWS WAF web ACL attached to CloudFront.
Benefits:
- CloudFront and AWS WAF are HIPAA-eligible AWS services under the AWS BAA when properly configured.
- AWS WAF can block common web exploits before traffic reaches the host.
- Origin can be restricted to CloudFront-facing traffic if configured carefully.
Trade-off:
- More moving parts.
- More cost.
- Cache, header, TLS, and origin behavior must be configured carefully for an authenticated healthcare app.
Route 53 Website Steps
- Open Route 53.
- Create a public hosted zone for the CNP domain.
- Copy the assigned Route 53 nameservers.
- Log in to the domain registrar.
- Replace registrar nameservers with the Route 53 nameservers.
- Create an
Arecord foropenemr.example.org. - In Mode A, point the record to the EC2 Elastic IP.
- In Mode B, create an alias record pointing to CloudFront.
Route 53 CLI Example
aws route53 create-hosted-zone \
--name example.org \
--caller-reference cnp-$(date +%Y%m%d%H%M%S)After the hosted zone is created, update records with a change batch:
aws route53 change-resource-record-sets \
--hosted-zone-id HOSTED_ZONE_ID \
--change-batch file://route53-openemr-record.jsonExample route53-openemr-record.json for Mode A:
{
"Changes": [
{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "openemr.example.org",
"Type": "A",
"TTL": 300,
"ResourceRecords": [
{
"Value": "203.0.113.10"
}
]
}
}
]
}Optional CloudFront And AWS WAF
Add CloudFront only if CNP wants managed edge controls enough to accept the added cost and complexity.
AWS WAF cannot attach directly to a bare EC2 instance. If AWS WAF is required, place CloudFront or another supported AWS resource in front of the EC2 host.
For OpenEMR behind CloudFront:
- Disable caching for authenticated application paths.
- Forward required headers and cookies.
- Do not log PHI in URLs or query strings.
- Use Full TLS from viewer to CloudFront and CloudFront to origin.
- Attach AWS WAF in count mode first.
- Move rules to block only after reviewing false positives.
Phase 4 - IAM Identity Center And Human Access
AWS access should be role-based and minimal.
OpenEMR user access is separate from AWS access. Operators should not receive AWS accounts.
Permission Sets
Create these AWS IAM Identity Center permission sets:
| Permission Set | Users | Purpose |
|---|---|---|
CNP-AWS-Administrator |
Two developers | Full setup and emergency admin |
CNP-AWS-DeveloperOps |
Two developers | EC2, SSM, logs, deploy support |
CNP-AWS-BillingReadOnly |
Business manager | Cost and billing visibility |
CNP-AWS-SecurityAuditReadOnly |
Operations manager | Read-only security and audit visibility |
Business and operations managers should not normally have administrator access.
Website Steps
- Open IAM Identity Center.
- Enable IAM Identity Center.
- Choose the identity source:
- Prefer CNP Microsoft 365/Entra ID or Google Workspace if already managed.
- Otherwise use the built-in Identity Center directory for the first deployment.
- Create users for the two developers, business manager, and operations manager.
- Require MFA.
- Create the four permission sets listed above.
- Assign users to permission sets.
CLI Checks
aws identitystore list-users --identity-store-id IDENTITY_STORE_ID
aws sso-admin list-permission-sets --instance-arn SSO_INSTANCE_ARN
aws sso-admin list-account-assignments \
--instance-arn SSO_INSTANCE_ARN \
--account-id AWS_ACCOUNT_ID \
--permission-set-arn PERMISSION_SET_ARNThe exact IDENTITY_STORE_ID,
SSO_INSTANCE_ARN, and permission set ARNs are easiest to
get from the console during first setup.
Phase 5 - Business-Owned OpenEMR User Lifecycle
CNP should be able to add and revoke OpenEMR users without developer involvement.
This should happen inside OpenEMR, not AWS.
Required OpenEMR Roles
Define OpenEMR operator classes before go-live:
| OpenEMR Role | Intended Users | AWS Access |
|---|---|---|
CNP Business Manager |
Business manager | Optional billing read-only only |
CNP Operations Manager |
Operations manager | Optional security read-only only |
CNP Operator Class 1 |
Front-line workflow group 1 | None |
CNP Operator Class 2 |
Front-line workflow group 2 | None |
CNP Operator Class 3 |
Front-line workflow group 3 | None |
Website Steps In OpenEMR
After OpenEMR is installed:
- Log in as the initial OpenEMR administrator.
- Create the business manager user.
- Create the operations manager user.
- Give business/operations managers the minimum OpenEMR permissions
needed to:
- Add users.
- Disable users.
- Reset passwords or trigger password resets.
- Assign users to approved operator classes.
- Review login/audit activity.
- Create the three operator classes.
- Test that each operator class can access only the intended workflows.
- Document the add/revoke process for the business manager.
User Revocation Rule
Revocation must disable access in the system where the user authenticates:
- OpenEMR operator leaves: disable in OpenEMR.
- Developer leaves: remove IAM Identity Center access, revoke SSH/SSM/deploy keys, rotate shared secrets if any.
- Business manager leaves: disable OpenEMR user, remove billing read-only AWS access, update AWS alternate contacts if needed.
Phase 6 - Cost Controls
The expected baseline cost is roughly one EC2 instance, one EBS volume, low S3 backup storage, logs, and Bedrock usage if enabled.
Budget Thresholds
Create AWS budgets at:
$25$50$100$250
The $250 alert is an incident for this deployment
model.
Website Steps
- Open AWS Billing and Cost Management.
- Open Budgets.
- Create a monthly cost budget named
cnp-monthly-total. - Add alert thresholds at 25, 50, 100, and 250 dollars.
- Notify:
- Business manager
- Operations manager
- Both developers
- Create a second budget named
cnp-bedrock-monthlyif Bedrock will be used. - Start the Bedrock budget low, such as
$10or$25, until usage is understood.
CLI Example
The Budgets CLI requires a JSON file. Create
budget-cnp-monthly-total.json locally:
{
"BudgetName": "cnp-monthly-total",
"BudgetLimit": {
"Amount": "100",
"Unit": "USD"
},
"TimeUnit": "MONTHLY",
"BudgetType": "COST"
}Then run:
aws budgets create-budget \
--account-id AWS_ACCOUNT_ID \
--budget file://budget-cnp-monthly-total.jsonFor first setup, the website is less error-prone.
Cost Guardrails
Do not create these without explicit approval:
- NAT Gateway
- RDS
- ECS/EKS/Fargate
- Load balancer
- OpenSearch
- Multi-AZ managed databases
- High-volume CloudWatch log ingestion
- Large Bedrock batch jobs
- GPU instances
Tag all resources:
project=cnp
environment=prod
phi=true
owner=cnp
managed_by=opentofu
Phase 7 - Baseline Logging And Audit
The system should be self-confessing: when something fails, the logs should make the failure obvious without forensic reconstruction.
AWS Signals
Enable:
- CloudTrail management events.
- S3 server access or CloudTrail data events for backup bucket if cost is acceptable.
- EC2 status alarms.
- EBS volume usage alarm from the host.
- Budget alerts.
- Bedrock usage budget if Bedrock is enabled.
Avoid logging PHI into CloudWatch. Application logs should use metadata, not patient content.
CloudTrail Website Steps
- Open CloudTrail.
- Create a trail named
cnp-account-trail. - Apply to all regions.
- Create or select an encrypted S3 bucket for CloudTrail logs.
- Enable log file validation.
- Do not enable high-volume data events unless the cost is intentional.
CLI Example
aws s3api create-bucket \
--bucket cnp-cloudtrail-ACCOUNT_ID-us-east-1 \
--region us-east-1
aws cloudtrail create-trail \
--name cnp-account-trail \
--s3-bucket-name cnp-cloudtrail-ACCOUNT_ID-us-east-1 \
--is-multi-region-trail \
--enable-log-file-validation
aws cloudtrail start-logging \
--name cnp-account-trailPhase 8 - KMS And Encryption
Use AWS-managed encryption where that keeps the system simpler. Use customer-managed KMS keys when there is a clear operational reason, such as backup separation or explicit audit requirements.
Required Encryption
- EBS root volume encrypted.
- EBS data volume encrypted.
- S3 backup bucket encrypted.
- Restic repository encrypted before upload.
- TLS 1.2+ for OpenEMR.
- TLS 1.2+ for sidecar outbound calls.
CLI Example - Create A Backup KMS Key
aws kms create-key \
--description "CNP backup encryption key"
aws kms create-alias \
--alias-name alias/cnp-backup-kms \
--target-key-id KEY_IDKeep the key policy simple. Do not lock out the administrator role.
Phase 8.5 - Secrets Management With SSM Parameter Store
Production secrets should live in AWS Systems Manager Parameter Store
as SecureString values.
This keeps the production secret boundary inside the AWS account covered by the AWS BAA and avoids a separate secrets vendor.
Rules
- Store production secrets in SSM Parameter Store.
- Use
SecureStringfor secrets. - Use
Stringonly for non-sensitive configuration. - Do not commit
.envfiles. - Do not put secrets directly in
compose.yaml. - Do not print secret values in deployment logs.
- Do not paste secrets into tickets, Slack, email, or support requests.
- Give the EC2 instance role read access only to the production path it needs.
- Give developers write access through IAM Identity Center, not long-lived IAM users.
Parameter Naming
Use one namespace per environment:
/cnp/prod/...
/cnp/stage/...
/cnp/local/...
Use production parameters like:
/cnp/prod/mariadb/root_password
/cnp/prod/mariadb/database
/cnp/prod/mariadb/user
/cnp/prod/mariadb/password
/cnp/prod/openemr/site_id
/cnp/prod/openemr/admin_user
/cnp/prod/openemr/admin_password
/cnp/prod/openemr/base_url
/cnp/prod/backup/restic_password
/cnp/prod/backup/restic_repository
/cnp/prod/sidecar/caseworthy_base_url
/cnp/prod/sidecar/caseworthy_api_key
/cnp/prod/sidecar/bedrock_region
/cnp/prod/sidecar/bedrock_model_id
Only store /cnp/local/... in AWS if there is a specific
reason. Local OrbStack should normally use .env.local with
synthetic-only credentials.
Generate Initial Secret Values
Generate passwords on a developer workstation. Do not paste these commands into shared notes with output visible.
openssl rand -base64 36Generate separate values for:
- MariaDB root password.
- MariaDB OpenEMR application password.
- OpenEMR bootstrap administrator password.
- Restic repository password.
Use different values in prod, stage, and
local.
For values rendered into Docker env files, prefer generated secrets
without whitespace or newlines. Base64 output from
openssl rand -base64 36 is acceptable.
Create MariaDB Parameters
Set shell defaults:
export AWS_PROFILE=cnp-prod
export AWS_REGION=us-east-1Create non-secret database name and username:
aws ssm put-parameter \
--name /cnp/prod/mariadb/database \
--type String \
--value openemr \
--overwrite
aws ssm put-parameter \
--name /cnp/prod/mariadb/user \
--type String \
--value openemr \
--overwriteCreate secret passwords. Replace the placeholder values before running:
aws ssm put-parameter \
--name /cnp/prod/mariadb/root_password \
--type SecureString \
--value 'REPLACE_WITH_GENERATED_ROOT_PASSWORD' \
--overwrite
aws ssm put-parameter \
--name /cnp/prod/mariadb/password \
--type SecureString \
--value 'REPLACE_WITH_GENERATED_OPENEMR_DB_PASSWORD' \
--overwriteCreate OpenEMR Parameters
aws ssm put-parameter \
--name /cnp/prod/openemr/site_id \
--type String \
--value default \
--overwrite
aws ssm put-parameter \
--name /cnp/prod/openemr/admin_user \
--type String \
--value admin \
--overwrite
aws ssm put-parameter \
--name /cnp/prod/openemr/admin_password \
--type SecureString \
--value 'REPLACE_WITH_GENERATED_OPENEMR_ADMIN_PASSWORD' \
--overwrite
aws ssm put-parameter \
--name /cnp/prod/openemr/base_url \
--type String \
--value 'https://openemr.example.org' \
--overwriteThe OpenEMR bootstrap administrator password must be changed or disabled according to the OpenEMR hardening runbook after business/operations administrator users are created.
Create Backup Parameters
aws ssm put-parameter \
--name /cnp/prod/backup/restic_password \
--type SecureString \
--value 'REPLACE_WITH_GENERATED_RESTIC_PASSWORD' \
--overwrite
aws ssm put-parameter \
--name /cnp/prod/backup/restic_repository \
--type String \
--value 's3:s3.amazonaws.com/cnp-prod-backups-ACCOUNT_ID/restic' \
--overwriteCreate Sidecar Parameters
Create these when the corresponding integrations are ready:
aws ssm put-parameter \
--name /cnp/prod/sidecar/caseworthy_base_url \
--type String \
--value 'https://caseworthy.example.org' \
--overwrite
aws ssm put-parameter \
--name /cnp/prod/sidecar/caseworthy_api_key \
--type SecureString \
--value 'REPLACE_WITH_CASEWORTHY_API_KEY' \
--overwrite
aws ssm put-parameter \
--name /cnp/prod/sidecar/bedrock_region \
--type String \
--value us-east-1 \
--overwrite
aws ssm put-parameter \
--name /cnp/prod/sidecar/bedrock_model_id \
--type String \
--value 'REPLACE_WITH_APPROVED_MODEL_ID' \
--overwriteDo not create placeholder production API keys. If an integration is not ready, leave its secret absent and make the sidecar fail clearly with a missing-configuration error.
If an optional integration is not ready, do not include it in the production render script yet. Required OpenEMR, MariaDB, and backup parameters should be present before go-live.
Verify Parameter Names Without Printing Secrets
List names:
aws ssm get-parameters-by-path \
--path /cnp/prod \
--recursive \
--query 'Parameters[].Name' \
--output textVerify one secret exists without returning decrypted value:
aws ssm get-parameter \
--name /cnp/prod/mariadb/password \
--no-with-decryption \
--query 'Parameter.{Name:Name,Type:Type,Version:Version,LastModifiedDate:LastModifiedDate}'Only retrieve decrypted values on the EC2 host or during controlled break-glass operations:
aws ssm get-parameter \
--name /cnp/prod/mariadb/password \
--with-decryption \
--query 'Parameter.Value' \
--output textEC2 Instance Role Policy
Attach a policy like this to the EC2 instance role. Scope
ACCOUNT_ID, region, and paths exactly.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath"
],
"Resource": "arn:aws:ssm:us-east-1:ACCOUNT_ID:parameter/cnp/prod/*"
}
]
}If using a customer-managed KMS key for SecureString values, also
grant the instance role kms:Decrypt for that key.
Developer Write Policy
Developers need controlled write access to /cnp/prod/*
during setup and rotation.
Prefer an IAM Identity Center permission set that allows:
ssm:PutParameterssm:GetParameterssm:GetParametersssm:GetParametersByPathssm:DeleteParameter, only if the operational process requires itssm:AddTagsToResource
Restrict the resource path to:
arn:aws:ssm:us-east-1:ACCOUNT_ID:parameter/cnp/*
Render Runtime Env File On The Host
The EC2 host should pull SSM values into a root-owned env file before starting Docker Compose.
Create:
/srv/cnp/compose/.env.prod
Permissions:
sudo touch /srv/cnp/compose/.env.prod
sudo chown root:root /srv/cnp/compose/.env.prod
sudo chmod 600 /srv/cnp/compose/.env.prodExample render script:
#!/usr/bin/env bash
set -euo pipefail
out="/srv/cnp/compose/.env.prod"
tmp="$(mktemp)"
get_plain() {
aws ssm get-parameter \
--name "$1" \
--query 'Parameter.Value' \
--output text
}
get_secret() {
aws ssm get-parameter \
--name "$1" \
--with-decryption \
--query 'Parameter.Value' \
--output text
}
{
printf 'MARIADB_DATABASE=%s\n' "$(get_plain /cnp/prod/mariadb/database)"
printf 'MARIADB_USER=%s\n' "$(get_plain /cnp/prod/mariadb/user)"
printf 'MARIADB_PASSWORD=%s\n' "$(get_secret /cnp/prod/mariadb/password)"
printf 'MARIADB_ROOT_PASSWORD=%s\n' "$(get_secret /cnp/prod/mariadb/root_password)"
printf 'OPENEMR_SITE_ID=%s\n' "$(get_plain /cnp/prod/openemr/site_id)"
printf 'OPENEMR_ADMIN_USER=%s\n' "$(get_plain /cnp/prod/openemr/admin_user)"
printf 'OPENEMR_ADMIN_PASSWORD=%s\n' "$(get_secret /cnp/prod/openemr/admin_password)"
printf 'OPENEMR_BASE_URL=%s\n' "$(get_plain /cnp/prod/openemr/base_url)"
printf 'RESTIC_PASSWORD=%s\n' "$(get_secret /cnp/prod/backup/restic_password)"
printf 'RESTIC_REPOSITORY=%s\n' "$(get_plain /cnp/prod/backup/restic_repository)"
} > "$tmp"
sudo install -o root -g root -m 600 "$tmp" "$out"
rm -f "$tmp"Store the script itself in Git, but never store the rendered
.env.prod.
Add sidecar integration values to this script only when those integrations are ready for production.
Compose Usage
Docker Compose should reference the rendered env file:
docker compose --env-file /srv/cnp/compose/.env.prod up -dThe compose file may reference variable names, but must not contain secret values.
Rotation
When rotating a secret:
- Put the new value into SSM Parameter Store.
- Render
/srv/cnp/compose/.env.prodagain. - Restart only the affected containers.
- Verify health checks.
- Record the rotation event.
For MariaDB passwords, coordinate rotation carefully:
- Create or update the database user password inside MariaDB.
- Update the SSM parameter.
- Re-render env file.
- Restart OpenEMR.
- Confirm OpenEMR can connect.
- Remove old credentials if a temporary user was used.
Phase 9 - Network Model
Default model:
- One VPC.
- One public subnet.
- One EC2 instance.
- No NAT Gateway.
- No load balancer.
- No public SSH.
- HTTPS only from allowed sources.
- SSM Session Manager for shell access.
Security Group
Inbound:
| Port | Source | Purpose |
|---|---|---|
| 443 | 0.0.0.0/0 in Mode A, or CloudFront origin-facing
controls in Mode B |
OpenEMR HTTPS |
| 80 | Temporary only for certificate bootstrap, if needed | ACME challenge |
| 22 | None | Do not use SSH |
Outbound:
Allow HTTPS outbound for:
- OS package updates.
- Container image pulls.
- S3 backup upload.
- Bedrock API calls.
- CaseWorthy or other integration APIs.
- OS and container security updates.
CLI Example
aws ec2 create-security-group \
--group-name cnp-prod-openemr-sg \
--description "CNP production OpenEMR host" \
--vpc-id VPC_ID
aws ec2 authorize-security-group-ingress \
--group-id SECURITY_GROUP_ID \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0If CloudFront is used, configure the origin so direct-to-origin access is intentionally controlled. The simplest starting control is a strong origin secret header validated by the reverse proxy. More advanced controls can use AWS-managed prefix lists for CloudFront origin-facing traffic where appropriate.
Phase 10 - S3 Backup Bucket
Backups should be portable and restorable without AWS-specific application services.
Primary backup mechanism:
- Dump MariaDB.
- Capture OpenEMR files/configuration.
- Encrypt with Restic.
- Upload to S3.
- Record success/failure.
- Regularly test restore.
Website Steps
- Open S3.
- Create bucket named like
cnp-prod-backups-ACCOUNT_ID. - Block all public access.
- Enable bucket versioning.
- Enable default encryption.
- Add lifecycle policy:
- Keep daily backups for 30 days.
- Keep weekly backups for 12 weeks.
- Keep monthly backups for 12 months.
- Consider Object Lock before production if the retention policy is stable.
Object Lock must be enabled at bucket creation time. If ransomware resistance is a priority, decide this before creating the final production backup bucket.
CLI Example
aws s3api create-bucket \
--bucket cnp-prod-backups-ACCOUNT_ID \
--region us-east-1
aws s3api put-public-access-block \
--bucket cnp-prod-backups-ACCOUNT_ID \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
aws s3api put-bucket-versioning \
--bucket cnp-prod-backups-ACCOUNT_ID \
--versioning-configuration Status=Enabled
aws s3api put-bucket-encryption \
--bucket cnp-prod-backups-ACCOUNT_ID \
--server-side-encryption-configuration '{
"Rules": [
{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}
]
}'Phase 11 - EC2 Host
Instance Recommendation
Start with:
t4g.large
Rationale:
- Enough memory headroom for OpenEMR, MariaDB, reverse proxy, and Python sidecar.
- Lower risk during upgrades.
- ARM64 price/performance is good.
Fallback:
t3.large or t3a.large
Use x86_64 only if a required image or dependency does not support ARM64.
Storage
Recommended:
- Root volume: 30 GB gp3, encrypted.
- Data volume: 100 GB gp3, encrypted, mounted at
/srv/cnp.
All persistent Docker volumes should live under
/srv/cnp.
IAM Role For EC2
Create an instance role with only:
- SSM Session Manager access.
- Read access to
/cnp/prod/*SSM Parameter Store values. - Read access to required deployment artifacts, if any.
- Write access to the S3 backup bucket path.
- Bedrock invoke permissions only if the sidecar on this host calls Bedrock.
Do not put long-lived AWS keys on the server.
Website Steps
- Open EC2.
- Launch instance.
- Select Ubuntu LTS or Amazon Linux 2023 ARM64.
- Select
t4g.large. - Attach IAM instance profile.
- Disable public SSH access.
- Attach the security group from Phase 9.
- Create encrypted root volume.
- Add encrypted 100 GB gp3 data volume.
- Enable detailed monitoring only if the cost is acceptable.
CLI Sketch
The exact AMI ID changes by region. Resolve it first, then launch:
aws ec2 run-instances \
--image-id AMI_ID \
--instance-type t4g.large \
--iam-instance-profile Name=cnp-prod-ec2-profile \
--security-group-ids SECURITY_GROUP_ID \
--subnet-id SUBNET_ID \
--block-device-mappings file://block-devices.json \
--tag-specifications 'ResourceType=instance,Tags=[{Key=project,Value=cnp},{Key=environment,Value=prod},{Key=phi,Value=true},{Key=Name,Value=cnp-prod-openemr}]'Prefer OpenTofu for the final version of this step once the manual shape is confirmed.
Phase 12 - Host Bootstrap
The host should be reproducible. Manual shell work is acceptable during initial discovery but must become a script before production.
Bootstrap Tasks
- Install OS updates.
- Install Docker.
- Install Docker Compose plugin.
- Install SSM agent if not present.
- Mount encrypted data volume at
/srv/cnp. - Create directories:
/srv/cnp/compose/srv/cnp/openemr/srv/cnp/mariadb/srv/cnp/sidecar/srv/cnp/backups/srv/cnp/logs/srv/cnp/bin
- Configure host firewall.
- Configure unattended security updates.
- Install CloudWatch agent only if log handling is configured to avoid PHI.
CLI Steps On Host
Use SSM Session Manager:
aws ssm start-session --target INSTANCE_IDThen bootstrap:
sudo apt-get update
sudo apt-get upgrade -y
sudo apt-get install -y ca-certificates curl gnupg ufw
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo mkdir -p /srv/cnp/{compose,openemr,mariadb,sidecar,backups,logs,bin}
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 443/tcp
sudo ufw --force enableAdapt package commands if using Amazon Linux 2023.
Phase 13 - Docker Compose Runtime
Production should use the same Compose shape as local test, with different secrets and volumes.
Production secrets come from SSM Parameter Store and are rendered
into /srv/cnp/compose/.env.prod before Compose starts. The
rendered env file is root-owned, mode 600, and never
committed.
Required services:
proxyopenemrmariadbsidecarbackup
Expected Compose Responsibilities
proxy:
- TLS termination.
- Routes OpenEMR.
- Does not log request bodies.
openemr:
- Application runtime.
- OpenEMR audit logging enabled.
mariadb:
- Persistent database under
/srv/cnp/mariadb. - Healthcheck enabled.
sidecar:
- Python integrations.
- Bedrock calls.
- CaseWorthy or other integration calls.
- Structured logs with no PHI.
backup:
- Scheduled database dump.
- Restic backup.
- Backup verification metadata.
Minimum Health Checks
The deployment is not healthy unless these pass:
- Reverse proxy returns HTTPS.
- OpenEMR login page loads.
- MariaDB healthcheck passes.
- Sidecar healthcheck passes.
- Backup job has succeeded within the required window.
- Disk usage is below alert threshold.
Phase 14 - Bedrock Controls
Bedrock may be used only after:
- AWS BAA is accepted.
- Bedrock is confirmed as HIPAA eligible for the chosen region and usage.
- The sidecar has explicit logging controls.
- Prompt/response retention behavior is understood.
- Cost budget is active.
Design Rule
All Bedrock calls should go through the Python sidecar. OpenEMR should not call Bedrock directly.
This gives one place to enforce:
- Prompt construction.
- PHI minimization.
- Deidentification where possible.
- Model selection.
- Timeouts.
- Retries.
- Cost limits.
- Audit metadata.
- Redaction.
Do Not Log
- Full prompts containing PHI.
- Full model responses containing PHI.
- Patient names.
- Addresses.
- Phone numbers.
- Dates of birth.
- Medical details.
- Free-text clinical notes.
Log Instead
- Timestamp.
- Workflow name.
- Calling user or service.
- Model ID.
- Input token count.
- Output token count.
- Success/failure.
- Error class.
- Correlation ID.
CLI Example - Minimal IAM Policy Shape
Scope this to the exact foundation models and region once chosen:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
],
"Resource": "*"
}
]
}Start broad only during discovery. Narrow before production.
Phase 15 - Local Simulacrum On Mac Studio With OrbStack
Local test should prove the deployment process and runtime behavior without using PHI.
Local Invariant
Local data must be:
- Generated.
- Synthetic.
- Deidentified.
- White/test-only.
Local data must not be:
- Production database dumps.
- Real exports.
- Real identities.
- Real patient notes.
- Real phone numbers or addresses.
Local Runtime
Use OrbStack with the same Compose services:
proxyopenemrmariadbsidecarbackup
Differences from production:
- Local-only domain or
localhost. - Local bind mounts.
- Test secrets.
- Fake Bedrock mode by default.
- Simulated integration endpoints by default.
- No PHI.
Local Setup Sketch
docker compose -f compose.yaml -f compose.local.yaml up -d
docker compose ps
docker compose logs --tail=100 sidecarRequired Local Confidence Checks
Before production deployment, local must prove:
- Fresh install works from empty volumes.
- Seeded synthetic data loads.
- OpenEMR login works.
- Sidecar can run a dry-run integration job.
- Bedrock calls can be mocked or run against non-PHI prompts.
- Backup job creates a restic snapshot.
- Restore from local backup works into a clean volume.
- Compose restart preserves state.
- Upgrade rehearsal can be performed against synthetic data.
Phase 16 - Code Deployment Process
The deployment process must be boring, repeatable, and observable.
Recommended Flow
Developer change
|
v
Pull request
|
v
Automated checks
|
v
Build container images
|
v
Run local OrbStack simulacrum
|
v
Tag release
|
v
Deploy to EC2 through SSM or a controlled deploy script
|
v
Run post-deploy health checks
|
v
Record deployment event
Required Checks Before Deploy
- Python tests pass.
- Compose config validates.
- Container images build.
- No secrets in repository.
- No PHI-like test fixtures.
- Local synthetic runtime starts.
- Database migration or OpenEMR upgrade path is documented.
- Backup has completed before production update.
CLI Examples
Local:
docker compose config
docker compose build
docker compose up -d
docker compose psProduction over SSM:
aws ssm start-session --target INSTANCE_IDOn host:
cd /srv/cnp/compose
docker compose pull
docker compose up -d
docker compose psPost-deploy:
curl -fsS https://openemr.example.org/ || exit 1
docker compose ps
docker compose logs --tail=100 sidecarDeployment Record
Each production deployment should record:
- Git commit SHA.
- Image tags or digests.
- Operator/developer who deployed.
- Start and finish time.
- Health check result.
- Backup snapshot ID before deploy.
- Rollback instruction.
Phase 17 - Backup And Restore
Backups are not complete until restore has been tested.
Backup Schedule
- Nightly database dump and file backup.
- Daily retention: 30 days.
- Weekly retention: 12 weeks.
- Monthly retention: 12 months.
Required Backup Signals
- Last successful backup timestamp.
- Last backup size.
- Last restic snapshot ID.
- Last failure reason.
- Last restore test date.
Restore Test Cadence
Minimum:
- Before production go-live.
- After major OpenEMR upgrade.
- Quarterly.
Restore tests should use a clean environment and must not export PHI to local machines.
Phase 18 - Go-Live Checklist
Do not put PHI into the system until all required items are complete.
Required:
- AWS account is CNP-owned.
- Root MFA enabled.
- Alternate contacts configured.
- AWS BAA accepted and recorded.
- DNS/edge mode decided: direct Route 53 to EC2 or Route 53 to CloudFront.
- Cloudflare is not in the PHI-bearing production path unless Enterprise BAA coverage is later purchased and documented.
- IAM Identity Center enabled.
- Developer access configured.
- Business billing read-only access configured.
- EC2 instance deployed.
- No SSH open to the internet.
- SSM Session Manager works.
- EBS encryption enabled.
- S3 backup bucket private, encrypted, and versioned.
- SSM Parameter Store namespace created for
/cnp/prod/*. - Required MariaDB parameters created.
- Required OpenEMR parameters created.
- Required backup parameters created.
- EC2 instance role can read required SSM parameters.
- Developers can update SSM parameters through IAM Identity Center.
- Rendered
/srv/cnp/compose/.env.prodexists on host with mode600. - Docker Compose stack deployed.
- OpenEMR admin account secured.
- Business/operations OpenEMR user management tested.
- Operator classes tested.
- Sidecar logging verified to avoid PHI.
- Bedrock budget configured if Bedrock is enabled.
- Bedrock logging reviewed if Bedrock is enabled.
- Backup completed.
- Restore tested.
- Cost budgets configured.
- Runbook delivered.
Phase 19 - Operator Runbooks To Deliver
Deliver these short runbooks to CNP:
- Add an OpenEMR user.
- Disable an OpenEMR user.
- Reset an OpenEMR password.
- Review OpenEMR login/audit activity.
- Respond to a cost alert.
- Respond to a failed backup alert.
- Contact developer support.
- Confirm AWS BAA record location.
- Confirm Route 53 hosted zone and DNS ownership.
Open Questions To Resolve Before Production
These do not block initial AWS setup, but they should be resolved before go-live:
- Which domain and hostname will be used for OpenEMR?
- Will production start with direct Route 53 to EC2, or Route 53 to CloudFront with optional AWS WAF?
- Which exact OpenEMR operator classes map to CNP workflows?
- Which Bedrock workflows are allowed to process PHI, if any?
- Which AWS region will be final?
- What is the expected restore time objective?
- What is the expected restore point objective?
- Who receives after-hours alerts?
- Who is authorized to approve production deployments?
References
- AWS HIPAA compliance:
https://aws.amazon.com/compliance/hipaa-compliance/ - AWS Artifact:
https://aws.amazon.com/artifact/ - Amazon Bedrock security and compliance:
https://aws.amazon.com/bedrock/security-compliance/ - AWS HIPAA eligible services reference:
https://aws.amazon.com/compliance/hipaa-eligible-services-reference/ - Amazon Route 53 compliance validation:
https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/route-53-compliance.html - Amazon CloudFront compliance validation:
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/compliance.html - AWS Systems Manager Session Manager:
https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html - Restic:
https://restic.net/