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>
6.5 KiB
6.5 KiB
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
- 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). TheSFTP_PATHsecret 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_keyson 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 -p 222 -i ~/.ssh/cameleer-website-deploy user@wwwNNN.your-server.de "ls -la" - 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.txtVerify the fingerprint against what your manual SSH session displayed before saving the secret —ssh-keyscandoesn'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.
.htaccesscaveat (important): Hetzner Webhosting L runs Apache withAllowOverride Noneon the user docroot, so any.htaccessfile yoursyncis 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.htaccesspanel 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)
DNS
Arecordwww.cameleer.io→ Hetzner IP. Proxied (orange).Arecord@(apex) → Hetzner IP. Proxied (orange).A/CNAMEforauth.cameleer.io→ SaaS ingress. Proxied.A/CNAMEforplatform.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 tohttps://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 |
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 |
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) |
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
- Fill in
src/pages/imprint.astrooperatorobject with real legal details. - Fill in
operatorContactinsrc/pages/privacy.astro. - Review the "Why us" / nJAMS wording in
src/components/sections/WhyUs.astrofor trademark safety. - Confirm MID-tier retention: spec says 7 days;
cameleer-saas/HOWTO.mdsays 30 days. Reconcile one side or the other.
5. First deploy
The deploy workflow is manual-only — it does NOT auto-fire on push to main. After merging, trigger it explicitly.
- Merge a PR to
main. - In Gitea: Actions → deploy → Run workflow on
main. - Watch the single
deployjob (build + tests + rsync + smoke test in one step). - 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.
- 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.