docs+ci: own security headers at Cloudflare, drop dead .htaccess path
All checks were successful
ci / build-test (push) Successful in 3m33s
All checks were successful
ci / build-test (push) Successful in 3m33s
Hetzner Webhosting L runs Apache with AllowOverride None on the user docroot, so file-based .htaccess is silently ignored — directives in public/.htaccess never applied. Confirmed via direct-origin tests: neither Header, Rewrite, nor FilesMatch fired regardless of the file being present and readable. The only origin-side override path on this tier is konsoleH's per- directory Serverkonfiguration UI, which writes to a separate Apache config file outside the user's filesystem (and thus outside any deploy pipeline). Make the architecture honest: - Delete public/.htaccess (dead code Apache never reads). - Remove the "Copy .htaccess into dist" CI step (now a no-op). - Update deploy.yml header comment to point at Cloudflare for headers. - Update OPERATOR-CHECKLIST.md §1 with the three Webhosting-L gotchas: port 222 for SSH, SFTP_PATH must match the actual vhost docroot (default is bare public_html/), and AllowOverride None. - Update §5 to reflect manual workflow_dispatch (no auto-deploy on push) and 5-header expectation. - Update README.md deploy section likewise. Headers (HSTS, CSP, XFO, X-Content-Type-Options, Referrer-Policy, Permissions-Policy) are now owned by Cloudflare Transform Rules, documented in OPERATOR-CHECKLIST.md §2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,20 +4,25 @@ One-time setup that lives outside code. Do these before the first `main` merge t
|
||||
|
||||
## 1. Hetzner Webhosting L
|
||||
|
||||
- [ ] Provision Webhosting L plan. Note the SSH hostname and SFTP path.
|
||||
- [ ] In the Hetzner control panel, **enable SSH access** for the main user.
|
||||
- [ ] Provision Webhosting L plan. Note the SSH hostname (e.g. `wwwNNN.your-server.de`) and the user login.
|
||||
- [ ] In konsoleH, **enable SSH access** for the user.
|
||||
- [ ] In konsoleH → Domainverwaltung, register the production domain (`www.cameleer.io`) on this hosting and confirm what document root Apache uses for it. On Webhosting L, the Apache vhost docroot for the addon domain is typically the bare `~/public_html/` (NOT a subdirectory). The `SFTP_PATH` secret must match this exactly — wrong path = 404 from origin.
|
||||
- [ ] Generate an ed25519 SSH key pair locally (once):
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/cameleer-website-deploy -C "cameleer-website CI"
|
||||
```
|
||||
- [ ] Add the **public** key to `~/.ssh/authorized_keys` on the Hetzner account.
|
||||
- [ ] Test SSH: `ssh -i ~/.ssh/cameleer-website-deploy user@hetzner-host "ls -la"`.
|
||||
- [ ] Create a subdirectory for the site (typical path: `public_html/www.cameleer.io/`).
|
||||
- [ ] Grab the SSH host key for pinning:
|
||||
- [ ] Add the **public** key to `~/.ssh/authorized_keys` on the Hetzner account (or via konsoleH SSH-Schlüssel UI).
|
||||
- [ ] Test SSH on **port 222** (Hetzner Webhosting splits SFTP=22 / SSH-shell=222; rsync needs 222):
|
||||
```bash
|
||||
ssh-keyscan -t ed25519 hetzner-host > hetzner-known-hosts.txt
|
||||
ssh -p 222 -i ~/.ssh/cameleer-website-deploy user@wwwNNN.your-server.de "ls -la"
|
||||
```
|
||||
- [ ] Install Let's Encrypt (or use Hetzner's built-in) for the origin hostname. Cloudflare Full (strict) requires a valid origin cert.
|
||||
- [ ] Grab the SSH host key for pinning, also on **port 222**:
|
||||
```bash
|
||||
ssh-keyscan -p 222 -t rsa,ed25519,ecdsa wwwNNN.your-server.de > hetzner-known-hosts.txt
|
||||
```
|
||||
Verify the fingerprint against what your manual SSH session displayed before saving the secret — `ssh-keyscan` doesn't authenticate.
|
||||
- [ ] **Origin TLS:** Cloudflare Full (strict) requires a valid origin cert. Hetzner Webhosting auto-issues Let's Encrypt — confirm the cert is active in konsoleH → SSL → SSL-Zertifikate.
|
||||
- [ ] **`.htaccess` caveat (important):** Hetzner Webhosting L runs Apache with `AllowOverride None` on the user docroot, so any `.htaccess` file you `rsync` is **silently ignored** by Apache. The only way to set Apache directives on this tier is via konsoleH → Einstellungen → Serverkonfiguration (per-directory wrench panel). This repo therefore owns no `.htaccess`; all response headers live in Cloudflare (see §2). The konsoleH `.htaccess` panel is left empty by default; defense-in-depth header copies there are optional and survive rsync deploys (different storage location).
|
||||
|
||||
## 2. Cloudflare (zone: cameleer.io)
|
||||
|
||||
@@ -67,7 +72,7 @@ Add these under Repository settings → Actions → Secrets (or variables):
|
||||
|------|------|-------|
|
||||
| `SFTP_HOST` | secret | Hetzner SSH hostname |
|
||||
| `SFTP_USER` | secret | Hetzner SSH user |
|
||||
| `SFTP_PATH` | secret | Absolute path to document root (e.g., `/usr/home/cameleer/public_html/www.cameleer.io`) |
|
||||
| `SFTP_PATH` | secret | Absolute path to the Apache vhost docroot configured in konsoleH (typically `/usr/www/users/<login>/public_html`). Mismatch → 404 on origin. |
|
||||
| `SFTP_KEY` | secret | Contents of `~/.ssh/cameleer-website-deploy` (private key, PEM) |
|
||||
| `SFTP_KNOWN_HOSTS` | secret | Contents of `hetzner-known-hosts.txt` (captured via `ssh-keyscan`) |
|
||||
| `PUBLIC_AUTH_SIGNIN_URL` | secret | `https://auth.cameleer.io/sign-in` |
|
||||
@@ -87,10 +92,13 @@ workflows read them via the `${{ secrets.* }}` context.
|
||||
|
||||
## 5. First deploy
|
||||
|
||||
The `deploy` workflow is **manual-only** — it does NOT auto-fire on push to `main`. After merging, trigger it explicitly.
|
||||
|
||||
1. Merge a PR to `main`.
|
||||
2. Watch the Gitea Actions run: `build`, then `deploy`.
|
||||
3. The workflow includes a post-deploy smoke check — if HSTS / CSP / XFO are missing from the live response, the deploy fails and must be debugged at the Cloudflare Transform Rule layer.
|
||||
4. Manually verify:
|
||||
- `curl -sI https://www.cameleer.io/` returns all six security headers.
|
||||
2. In Gitea: **Actions → deploy → Run workflow** on `main`.
|
||||
3. Watch the single `deploy` job (build + tests + rsync + smoke test in one step).
|
||||
4. The workflow's post-deploy smoke check verifies HSTS / CSP / X-Frame-Options on the live response. If any fail, the deploy step exits non-zero — debug at the **Cloudflare Transform Rule** layer (§2 above), since headers no longer come from the origin.
|
||||
5. Manually verify:
|
||||
- `curl -sI https://www.cameleer.io/` returns all 5 security headers (HSTS, CSP, XFO, X-Content-Type-Options, Referrer-Policy, Permissions-Policy).
|
||||
- `https://cameleer.io/` → `https://www.cameleer.io/` 301 redirect.
|
||||
- Open the site in an incognito window on desktop + mobile.
|
||||
|
||||
Reference in New Issue
Block a user