gabriel / musehub public
ssl.md markdown
201 lines 6.7 KB
Raw
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor ⚠ breaking 1 day 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 1 commit
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor 1 day ago