# EC2 cutover — TLS detection + host-level HTTPS contingency

Companion to the EC2 migration plan. Covers Phase 4: (1) detect where TLS is
terminated today, and (2) if it turns out to be host-level (NOT Cloudflare),
a ready-to-apply certbot + nginx-443 setup for the EC2 box.

---

## 0. CRITICAL prerequisite — Django's SSL toggles

`config/settings/production.py` defaults:

```
SECURE_SSL_REDIRECT     = True   (env: SECURE_SSL_REDIRECT)
SESSION_COOKIE_SECURE   = True   (env: SESSION_COOKIE_SECURE)
CSRF_COOKIE_SECURE      = True   (env: CSRF_COOKIE_SECURE)
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
```

Consequence: **you cannot validate the EC2 box over plain `http://<elastic-ip>/`
while these are on.** Django 301-redirects every HTTP request to HTTPS (the raw
IP has no cert → dead), and secure cookies aren't sent over HTTP → login fails.

**Phase 2 (raw-IP test, no TLS yet):** set these in `.env.production` to reach
the box over HTTP:

```
SECURE_SSL_REDIRECT=False
SESSION_COOKIE_SECURE=False
CSRF_COOKIE_SECURE=False
```

Re-`make redeploy` after editing. **Turn them back to `True` once TLS is live**
(Cloudflare or the 443 block below), and redeploy again.

> If the current `.env.production` already sets `SECURE_SSL_REDIRECT=False`
> (the old box answers on `http://173.249.17.166`, which hints it might), the
> raw-IP test works as-is — but you still want it `True` behind real TLS.

---

## 1. Detect where TLS terminates today

Run from your Mac against the LIVE domain (still pointing at the old box):

```sh
# Cloudflare leaves fingerprints: cf-ray / server: cloudflare, and the resolved
# IP is a Cloudflare range (104.x / 172.67.x / 188.114.x), NOT 173.249.17.166.
dig +short iilmp.ruforum.org
curl -sSI https://iilmp.ruforum.org | grep -iE 'server:|cf-ray|cf-cache'
```

- **`cf-ray` present / `server: cloudflare` / CF-range IP** → **Cloudflare**
  fronts it. TLS is Cloudflare's; the origin only needs HTTP. Go to §2.
- **No CF headers and `dig` returns `173.249.17.166`** → **host-level TLS** on
  the old box (certbot/nginx or similar). That config is not in this repo. Go to §3.

---

## 2. If Cloudflare (easiest path)

1. In Cloudflare DNS, change the `iilmp.ruforum.org` (+ `www`) A record's origin
   to the EC2 **Elastic IP**. Leave the orange cloud (proxied) on.
2. Set Cloudflare SSL mode:
   - **Flexible** → CF↔origin is HTTP; EC2 only needs port 80. No nginx changes.
   - **Full / Full (strict)** → CF↔origin is HTTPS; you still need a cert on the
     origin (a CF Origin Certificate is easiest — issue in CF dashboard, drop the
     cert/key on the box, use the §3 nginx 443 block but skip certbot).
3. Open the right ports in the EC2 security group (80 for Flexible; 80+443 for
   Full). Best practice: restrict origin ingress to Cloudflare IP ranges.
4. Restore the secure toggles (§0) to `True` and `make redeploy`.

Done — no repo changes required for Flexible mode.

---

## 3. If host-level TLS — certbot + nginx 443 on EC2

Run this AFTER the DNS A record points at the EC2 Elastic IP (Let's Encrypt's
HTTP-01 challenge must reach the box). Three edits + a one-off cert issue.

### 3a. `docker/nginx.conf` — add ACME location + 443 server block

In the existing **port-80 server**, add the ACME challenge path and redirect the
rest to HTTPS (keep static/uploads served directly if you prefer, but the
redirect is simplest):

```nginx
  server {
    listen 80;
    server_name iilmp.ruforum.org www.iilmp.ruforum.org;

    # Let's Encrypt HTTP-01 challenge — must stay on plain HTTP.
    location /.well-known/acme-challenge/ {
      root /var/www/certbot;
    }

    location / {
      return 301 https://$host$request_uri;
    }
  }
```

Then add a **new 443 server** mirroring the current proxy block (same headers,
resolver, static/uploads aliases):

```nginx
  server {
    listen 443 ssl;
    http2 on;
    server_name iilmp.ruforum.org www.iilmp.ruforum.org;

    ssl_certificate     /etc/letsencrypt/live/iilmp.ruforum.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/iilmp.ruforum.org/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_session_cache   shared:SSL:10m;

    location /static/ {
      alias /app/staticfiles/;
      access_log off;
      expires 30d;
      add_header Cache-Control "public, immutable";
    }
    location /uploads/ {
      alias /app/uploads/;
      access_log off;
    }
    location / {
      set $upstream http://web:8000;
      proxy_pass $upstream;
      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;   # = "https" here → Django sees TLS
      proxy_redirect off;
    }
  }
```

`X-Forwarded-Proto $scheme` on the 443 block sends `https`, which Django's
`SECURE_PROXY_SSL_HEADER` already trusts — so secure cookies + HSTS work and you
can return the §0 toggles to `True`.

### 3b. `docker-compose.prod.yml` — publish 443 + mount cert volumes

In the `nginx:` service:

```yaml
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./docker/nginx.conf:/etc/nginx/nginx.conf:ro
      - static_data:/app/staticfiles:ro
      - uploads_data:/app/uploads:ro
      - letsencrypt:/etc/letsencrypt
      - certbot_www:/var/www/certbot
```

And add to the top-level `volumes:` block:

```yaml
volumes:
  postgres_data:
  uploads_data:
  static_data:
  letsencrypt:
  certbot_www:
```

### 3c. Issue the certificate (one-off, on the EC2 box)

The port-80 server must already be serving `/.well-known/acme-challenge/` (deploy
3a+3b first: `make redeploy`). Then:

```sh
cd /var/www/ruforum-iilmp
docker run --rm \
  -v letsencrypt:/etc/letsencrypt \
  -v certbot_www:/var/www/certbot \
  certbot/certbot certonly --webroot -w /var/www/certbot \
  -d iilmp.ruforum.org -d www.iilmp.ruforum.org \
  --email <ops-email> --agree-tos --no-eff-email
docker compose -f docker-compose.prod.yml restart nginx
```

> Note the volume names are bare (`letsencrypt`), not `<project>_letsencrypt`, in
> the `docker run` above — match whatever `docker volume ls` shows after the
> compose change (compose prefixes them with the project name, e.g.
> `ruforum-iilmp_letsencrypt`). Use the prefixed name in the certbot command.

### 3d. Auto-renewal

Let's Encrypt certs last 90 days. Add a host cron (simplest):

```sh
# crontab -e  (on the EC2 box)
0 3 * * 1 cd /var/www/ruforum-iilmp && \
  docker run --rm -v ruforum-iilmp_letsencrypt:/etc/letsencrypt \
    -v ruforum-iilmp_certbot_www:/var/www/certbot certbot/certbot renew --quiet && \
  docker compose -f docker-compose.prod.yml exec -T nginx nginx -s reload
```

---

## 4. Post-TLS checklist

- `.env.production`: `SECURE_SSL_REDIRECT/SESSION_COOKIE_SECURE/CSRF_COOKIE_SECURE`
  back to `True`; `PUBLIC_APP_BASE_URL=https://iilmp.ruforum.org`;
  `ALLOWED_HOSTS`/`CSRF_TRUSTED_ORIGINS` use the domain (drop the bare EC2 IP).
- `make redeploy`.
- `curl -sSI https://iilmp.ruforum.org` → HTTP 200, valid cert, HSTS header.
- `curl -sI http://iilmp.ruforum.org` → 301 to https.
- Log in (confirms secure cookies round-trip over real TLS).
