gabriel / musehub public
ssl.md markdown
201 lines 6.7 KB
Raw
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b Merge branch 'fix/wire-push-external-parent-manifest' into dev Human 8 days ago

TLS / SSL Configuration

MuseHub uses HTTPS everywhere. The mechanism differs by environment:

Environment Who terminates TLS Certificate type
Local dev uvicorn (direct) mkcert — locally-trusted CA
Staging / Prod nginx on EC2 Cloudflare Origin Certificate

Local Development

How it works

In local dev, uvicorn serves HTTPS directly — there is no nginx in the docker-compose stack. The entrypoint (entrypoint.sh) detects a cert at /tls/localhost.crt and starts uvicorn with --ssl-certfile / --ssl-keyfile. The cert is bind-mounted from ~/.config/musehub/local-tls/ on the host (see docker-compose.yml, volumes: - ${HOME}/.config/musehub/local-tls:/tls:ro).

The certs live outside the repo deliberately — repo operations (checkouts, cleans, directory recreations) cannot delete them.

First-time setup

Use mkcert. It creates a local certificate authority (CA), installs it into macOS Keychain (and Firefox's trust store), and generates certs signed by that CA. Browsers trust the CA permanently, so you never see certificate warnings for any cert it signs.

# Install mkcert
brew install mkcert

# Install the local CA into macOS Keychain (and Firefox if installed)
mkcert -install

# Create the cert directory and generate cert + key for localhost
mkdir -p ~/.config/musehub/local-tls
cd ~/.config/musehub/local-tls
mkcert localhost 127.0.0.1 ::1

# Rename to match the names entrypoint.sh expects
mv localhost+2.pem localhost.crt
mv localhost+2-key.pem localhost.key

Then restart the container:

docker compose restart musehub

Verify:

curl https://localhost:1337/healthz
# → {"status":"ok"}

Files

~/.config/musehub/local-tls/   ← outside the repo — safe from repo operations
  localhost.crt                ← certificate  (never committed)
  localhost.key                ← private key  (never committed)

Each developer generates their own cert + key pair locally with mkcert. Neither is committed — the cert is signed by your local mkcert CA, which is specific to your machine and useless to anyone else. Keeping them in ~/.config/musehub/local-tls/ means they survive muse checkout, cleans, and any other operation that touches the repo directory.

Certificate renewal

mkcert certs are valid for 10 years by default. You will never need to renew them in practice. If you do need to regenerate (e.g. after running mkcert -uninstall and back), just repeat the steps above.


Staging and Production

Architecture

Browser / muse CLI
        │  HTTPS (TLS terminated here)
        ▼
   Cloudflare edge
        │  HTTPS (Cloudflare Origin Certificate)
        ▼
   nginx on EC2  (port 443)
        │  HTTP (internal, loopback only)
        ▼
   Docker container  (port 1337 or 1338, blue-green)

Cloudflare sits in front of EC2. All traffic from the internet hits Cloudflare first. Cloudflare re-encrypts the connection to the origin using a Cloudflare Origin Certificate — a certificate issued by Cloudflare's own CA, valid for 15 years, trusted only by Cloudflare (not by browsers directly). The Cloudflare dashboard SSL/TLS mode must be set to Full (Strict).

The Docker containers themselves run plain HTTP on the loopback interface (127.0.0.1:1337 / 127.0.0.1:1338). nginx proxies to them. Traffic never leaves the EC2 instance unencrypted — it stays on the loopback adapter between nginx and the containers.

Why not Let's Encrypt?

Let's Encrypt requires the cert to be publicly browser-trusted, which means exposing port 80 for ACME challenges (or using DNS-01 challenges). With Cloudflare in front, Cloudflare already handles the browser-trusted cert. The origin cert only needs to satisfy Cloudflare's validator, not browsers. Cloudflare Origin Certs are valid for 15 years and never need ACME renewal — simpler, more reliable.

Generating a Cloudflare Origin Certificate

Do this once per environment (staging and prod are separate).

  1. Cloudflare Dashboard → select the domain → SSL/TLSOrigin Server
  2. Click Create Certificate
  3. Choose: "Generate private key and CSR with Cloudflare", RSA (2048), 15 years
  4. Copy the Certificateorigin.pem
  5. Copy the Private Keyorigin.key

Then install on the EC2 instance:

# SCP the two files to the instance (replace <host> with the instance IP or alias)
scp origin.pem ec2-user@<host>:~/
scp origin.key ec2-user@<host>:~/

# On the instance
sudo mkdir -p /etc/ssl/cloudflare
sudo mv ~/origin.pem /etc/ssl/cloudflare/origin.pem
sudo mv ~/origin.key /etc/ssl/cloudflare/origin.key
sudo chmod 644 /etc/ssl/cloudflare/origin.pem
sudo chmod 640 /etc/ssl/cloudflare/origin.key
sudo chown root:root /etc/ssl/cloudflare/origin.pem /etc/ssl/cloudflare/origin.key

Delete the local copies of origin.pem and origin.key immediately after installing — never commit them or leave them in your home directory.

nginx configuration

deploy/nginx-cf.conf is the nginx config template. It is copied to the EC2 instance during deploy/setup-ec2.sh (or setup-ec2-staging.sh) and updated on every deploy if it has changed.

Key TLS settings in that file:

ssl_certificate     /etc/ssl/cloudflare/origin.pem;
ssl_certificate_key /etc/ssl/cloudflare/origin.key;

ssl_protocols       TLSv1.2 TLSv1.3;
ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:...;
ssl_prefer_server_ciphers off;

TLS 1.0 and 1.1 are disabled. TLS 1.3 is preferred; TLS 1.2 is retained for older clients.

EC2 security group

Port 443 inbound is restricted to Cloudflare's published IP ranges at the EC2 security group level. Port 80 is open only to redirect to HTTPS. Direct-to-origin HTTPS access (bypassing Cloudflare) is blocked by the security group — the Origin Certificate would be untrusted by a browser anyway.

Do not duplicate IP allow/deny rules inside nginx — the real_ip module in the nginx config replaces $remote_addr with the true client IP (from CF-Connecting-IP), which would cause allow/deny blocks to incorrectly reject legitimate Cloudflare traffic.

Certificate renewal

Cloudflare Origin Certificates are valid for 15 years. There is no automatic renewal process. Calendar a reminder to rotate the cert before it expires. The rotation procedure is the same as the initial generation above.


TLS version policy

All environments enforce TLS 1.2 minimum:

  • Local: Python's ssl module default (TLS 1.2+)
  • Staging / Prod: nginx ssl_protocols TLSv1.2 TLSv1.3

TLS 1.0 and 1.1 are disabled everywhere. This is enforced at the protocol layer, not the application layer.

File History 14 commits
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b Merge branch 'fix/wire-push-external-parent-manifest' into dev Human 8 days ago
sha256:fc04e4cae9e1774d6a21b65c45daeed0e6787eb581d13aa1b03bfe9384a34226 Merge branch 'fix/two-column-scroll-layout' into dev Human 8 days ago
sha256:408916fc5973ba59c6e4eebaa80ebdcc801c0a63205651e25009d11548f79454 chore: bump version to 0.2.0.dev2 — nightly.2, matching muse Sonnet 4.6 patch 11 days ago
sha256:d035733f21ccff27735fddebfbbe0ed24565a32a22db8de5885402262671ecd2 chore: bump version to 0.2.0rc15 for musehub#113 fix release Sonnet 4.6 patch 14 days ago
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352 Merge branch 'task/version-tags-phase3-server' into dev Human 17 days ago
sha256:4669620efda9ff41c55bdefd1f7bfe1c239d468428744c84ead9957e5a003a53 merge: rescue snapshot-recovery hardening (c00aa21d) into d… Opus 4.8 minor 29 days ago
sha256:a59da49c4611b970fc4b6ae48678ce4943261c213a07ddbd73ce9201df869b4a fix: remove false-positive proposal_comments index drop fro… Sonnet 4.6 patch 33 days ago
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3 feat: render markdown mists as HTML with heading anchor links Sonnet 4.6 patch 34 days ago
sha256:24a7d47486ebc4ebd1832830580e177ec6f877b48dced8c000e198cdec4ce9d6 Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 35 days ago
sha256:b9ff931d147e0114a1f17060f415b89ed551c170a91ff226c70437aa5c85f9ee Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 35 days ago
sha256:d1122d21e73471879b460037b22c0b50fded7c423444a176f248428f75dac39c Merge 'task/fix-issue-pagination-cursor' into 'dev' — propo… Human 35 days ago
sha256:01e18975e73d2b3cd5b6db7929c895bef9aa6e0d4391dc5b2adfc548b41318dd Merge 'feat/adding-debug-logs-to-staging' into 'dev' — prop… Human 35 days ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f fix: use wire_bytes not mpack_bytes_raw in compute_object_b… Sonnet 4.6 patch 48 days ago
sha256:b99f2455dc346966d040133f5203297e6e3ef5803a93728a2c30568d0a0f7583 rename: delta_add → delta_upsert across wire format, models… Sonnet 4.6 patch 50 days ago