2026-04-24 17:25:53 +02:00
# Operator Checklist — `cameleer-website`
One-time setup that lives outside code. Do these before the first `main` merge that ships live.
## 1. Hetzner Webhosting L
docs+ci: own security headers at Cloudflare, drop dead .htaccess path
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>
2026-04-24 23:04:09 +02:00
- [ ] 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.
2026-04-24 17:25:53 +02:00
- [ ] Generate an ed25519 SSH key pair locally (once):
```bash
ssh-keygen -t ed25519 -f ~/.ssh/cameleer-website-deploy -C "cameleer-website CI"
```
docs+ci: own security headers at Cloudflare, drop dead .htaccess path
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>
2026-04-24 23:04:09 +02:00
- [ ] 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):
2026-04-24 17:25:53 +02:00
```bash
docs+ci: own security headers at Cloudflare, drop dead .htaccess path
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>
2026-04-24 23:04:09 +02:00
ssh -p 222 -i ~/.ssh/cameleer-website-deploy user@wwwNNN .your-server.de "ls -la"
2026-04-24 17:25:53 +02:00
```
docs+ci: own security headers at Cloudflare, drop dead .htaccess path
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>
2026-04-24 23:04:09 +02:00
- [ ] 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).
2026-04-24 17:25:53 +02:00
## 2. Cloudflare (zone: cameleer.io)
### DNS
- [ ] `A` record `www.cameleer.io` → Hetzner IP. **Proxied (orange). **
- [ ] `A` record `@` (apex) → Hetzner IP. **Proxied (orange). **
- [ ] `A` /`CNAME` for `auth.cameleer.io` → SaaS ingress. **Proxied. **
- [ ] `A` /`CNAME` for `platform.cameleer.io` → SaaS ingress. **Proxied. **
- [ ] NO bare MX. If email is needed at `@cameleer.io` , use **Cloudflare Email Routing ** or a distinct hostname on a different provider.
### SSL/TLS
- [ ] Mode: **Full (strict) ** .
- [ ] Minimum TLS: **1.2 ** .
- [ ] TLS 1.3: **on ** .
- [ ] Always Use HTTPS: **on ** .
- [ ] Automatic HTTPS Rewrites: **on ** .
- [ ] HSTS: `max-age=31536000; includeSubDomains; preload` . (Add the domain to `https://hstspreload.org/` only after the site is stable and serving HSTS cleanly for a couple of weeks.)
### Security
- [ ] WAF → **Cloudflare Managed Ruleset ** : enabled (Free plan includes this since 2024).
- [ ] Bot Fight Mode: **on ** .
- [ ] Browser Integrity Check: **on ** .
- [ ] Security Level: **medium ** .
- [ ] Email Obfuscation: **on ** .
- [ ] Rate Limiting rule: 20 req/min per IP on `/*` (marketing pages).
### Transform Rules (edge-level security headers)
Create a Transform Rule — "Modify Response Header" — matching `http.host eq "www.cameleer.io"` :
| Operation | Header | Value |
|-----------|--------|-------|
| Set | `Content-Security-Policy` | `default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'none'; object-src 'none'` |
| Set | `X-Content-Type-Options` | `nosniff` |
| Set | `X-Frame-Options` | `DENY` |
| Set | `Referrer-Policy` | `strict-origin-when-cross-origin` |
| Set | `Permissions-Policy` | `geolocation=(), microphone=(), camera=(), payment=(), usb=()` |
### Page Rules / Redirect
- [ ] `cameleer.io/*` → `https://www.cameleer.io/$1` (301 permanent).
## 3. Gitea Actions secrets (in the repo settings)
Add these under Repository settings → Actions → Secrets (or variables):
| Name | Type | Value |
|------|------|-------|
| `SFTP_HOST` | secret | Hetzner SSH hostname |
| `SFTP_USER` | secret | Hetzner SSH user |
docs+ci: own security headers at Cloudflare, drop dead .htaccess path
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>
2026-04-24 23:04:09 +02:00
| `SFTP_PATH` | secret | Absolute path to the Apache vhost docroot configured in konsoleH (typically `/usr/www/users/<login>/public_html` ). Mismatch → 404 on origin. |
2026-04-24 17:25:53 +02:00
| `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` ) |
2026-04-24 18:04:16 +02:00
| `PUBLIC_AUTH_SIGNIN_URL` | secret | `https://auth.cameleer.io/sign-in` |
| `PUBLIC_AUTH_SIGNUP_URL` | secret | `https://auth.cameleer.io/sign-in?first_screen=register` |
| `PUBLIC_SALES_EMAIL` | secret | `sales@cameleer.io` (or whatever sales alias you set up) |
2026-04-24 17:25:53 +02:00
2026-04-24 18:04:16 +02:00
These three are not actually secret (they end up in the built HTML), but Gitea's
Actions UI puts them in the **Secrets ** tab alongside the SFTP credentials. The
workflows read them via the `${{ secrets.* }}` context.
## 4. Content TODO — before go-live
2026-04-24 17:25:53 +02:00
- [ ] Fill in `src/pages/imprint.astro` `operator` object with real legal details.
- [ ] Fill in `operatorContact` in `src/pages/privacy.astro` .
- [ ] Review the "Why us" / nJAMS wording in `src/components/sections/WhyUs.astro` for trademark safety.
- [ ] Confirm MID-tier retention: spec says **7 days ** ; `cameleer-saas/HOWTO.md` says **30 days ** . Reconcile one side or the other.
## 5. First deploy
docs+ci: own security headers at Cloudflare, drop dead .htaccess path
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>
2026-04-24 23:04:09 +02:00
The `deploy` workflow is **manual-only ** — it does NOT auto-fire on push to `main` . After merging, trigger it explicitly.
2026-04-24 17:25:53 +02:00
1. Merge a PR to `main` .
docs+ci: own security headers at Cloudflare, drop dead .htaccess path
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>
2026-04-24 23:04:09 +02:00
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).
2026-04-24 17:25:53 +02:00
- `https://cameleer.io/` → `https://www.cameleer.io/` 301 redirect.
- Open the site in an incognito window on desktop + mobile.