diff --git a/README.md b/README.md index 524184c..19f268f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Docker Compose configuration for self-hosting Shroud.email. Please read our [deployment documentation](https://shroud.email/docs/deployment/self-host) on our website. -If you just want to get up and running with Shroud.email quickly, you can sign up for our hosted version [here](https://app.shroud.email/users/register). +If you want to get up and running with Shroud.email quickly, and don't want to maintain your own mailserver, you can sign up for our hosted version [here](https://app.shroud.email/users/register). Copy `haraka/haraka_config/config/me.example` to `haraka/haraka_config/config/me` and set your mail hostname. @@ -24,7 +24,7 @@ leave the defaults get HTTP-01 and never need a Bunny key. ## Living on the edge -The committed `docker-compose.yaml` tracks the stable `:1` image. If you'd rather +The committed `docker-compose.yaml` tracks the stable image. If you'd rather run the latest `:edge` build (rebuilt on every push to `main`) and have it auto-update, copy the example override and bring the stack up: @@ -35,4 +35,36 @@ docker compose up -d This points the `web` service at `:edge` and adds [Watchtower](https://containrrr.dev/watchtower/), which polls every 5 minutes and auto-recreates `web` (and only `web`) when a new -image is published. \ No newline at end of file +image is published. + +## Cap CAPTCHA + +The compose file includes a [Cap](https://trycap.dev) self-hosted CAPTCHA +instance. It is **opt-in at the application level**: the +services run by default, but the widget is not rendered and verification +is not performed until you set all three `CAP_*` variables on the `web` +service. + +> **Public ingress required.** `CAP_INSTANCE_URL` must be a URL a user's +> browser can reach over HTTPS. + +### Setup + +1. Generate an admin key and set `CAP_ADMIN_KEY` in `.env`: + ```bash + openssl rand -hex 32 + ``` + +2. Start the services: + ```bash + docker compose up -d cap valkey + ``` + +3. Create a site key. Cap authenticates with a + session token issued by logging in with the `ADMIN_KEY. Create a `siteKey` and `secretKey` in the Cap UI. + +4. Set `CAP_INSTANCE_URL`, `CAP_SITE_KEY`, and `CAP_SECRET_KEY` in `.env`, then + restart `web`: + ```bash + docker compose restart web + ``` diff --git a/caddy/Caddyfile b/caddy/Caddyfile index 695a7bf..d19505f 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -14,6 +14,13 @@ } } +{$CAP_DOMAIN:disabled.localhost} { + reverse_proxy cap:3000 + tls { + issuer acme + } +} + {$EMAIL_DOMAIN} { tls { issuer acme diff --git a/caddy/Caddyfile.bunny b/caddy/Caddyfile.bunny index ab5b53d..c317987 100644 --- a/caddy/Caddyfile.bunny +++ b/caddy/Caddyfile.bunny @@ -14,6 +14,13 @@ } } +{$CAP_DOMAIN:disabled.localhost} { + reverse_proxy cap:3000 + tls { + dns bunny {env.BUNNY_API_KEY} + } +} + {$EMAIL_DOMAIN} { tls { issuer acme diff --git a/docker-compose.yaml b/docker-compose.yaml index 3810cb1..e6966a5 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -43,6 +43,23 @@ services: - ./haraka/haraka_config:/app/haraka_config - pem_certs:/app/haraka_config/config/certs + cap: + image: tiago2/cap:3 + restart: unless-stopped + depends_on: + - valkey + environment: + - ADMIN_KEY=${CAP_ADMIN_KEY} + - REDIS_URL=redis://valkey:6379 + - CORS_ORIGIN=https://${APP_DOMAIN} + + valkey: + image: valkey/valkey:9-alpine + restart: unless-stopped + command: valkey-server --save 60 1 --loglevel warning --maxmemory 256mb --maxmemory-policy noeviction + volumes: + - valkey_data:/data + web: image: ghcr.io/shroud-email/shroud.email:1 restart: unless-stopped @@ -76,6 +93,9 @@ services: - LOOPS_ACTIVE_USERS_LIST_ID=${LOOPS_ACTIVE_USERS_LIST_ID} - CHATWOOT_MAC_TOKEN=${CHATWOOT_MAC_TOKEN} - CHATWOOT_BASE_URL=${CHATWOOT_BASE_URL} + - CAP_INSTANCE_URL=${CAP_INSTANCE_URL:-https://${CAP_DOMAIN}} + - CAP_SITE_KEY=${CAP_SITE_KEY} + - CAP_SECRET_KEY=${CAP_SECRET_KEY} caddy: build: ./caddy @@ -85,6 +105,7 @@ services: - APP_DOMAIN=${APP_DOMAIN} - EMAIL_DOMAIN=${EMAIL_DOMAIN} - BUNNY_API_KEY=${BUNNY_API_KEY} + - CAP_DOMAIN=${CAP_DOMAIN} ports: - "80:80" - "443:443" @@ -105,3 +126,4 @@ volumes: db_data: caddy_data: pem_certs: + valkey_data: diff --git a/example.env b/example.env index 5ed78d9..cb15137 100644 --- a/example.env +++ b/example.env @@ -50,3 +50,34 @@ CADDYFILE_PATH=./caddy/Caddyfile DB_USER=postgres DB_DATABASE=shroud + +## Cap CAPTCHA (optional but included in the default compose). +## Set all three to enable Cap on the signup/login/reset forms. +## CAP_ADMIN_KEY: dashboard password. Generate with: openssl rand -hex 32 +CAP_ADMIN_KEY= +## CAP_DOMAIN: the public hostname Caddy will serve the Cap instance on +## (e.g. cap.shroud.email). Required to enable Cap — create a DNS A-record +## for this host pointing at the server, the same as APP_DOMAIN. +## Caddy auto-provisions TLS for it (HTTP-01 by default, or Bunny DNS-01 if +## you set CADDYFILE_PATH=./caddy/Caddyfile.bunny). When unset, Caddy +## leaves the Cap route inert (no cert, no traffic) and Cap stays disabled. +CAP_DOMAIN= +## CAP_INSTANCE_URL: the PUBLIC, browser-reachable HTTPS URL of your Cap +## instance. Defaults to https://${CAP_DOMAIN}. The widget renders this into +## data-cap-api-endpoint, so a user's browser must be able to reach it over +## HTTPS (http:// will be blocked as mixed content on your https://APP_DOMAIN +## pages). Override only if you front Cap with a path-based or off-domain URL. +CAP_INSTANCE_URL= +## Create a site key (rsw + instrumentation) by logging in with ADMIN_KEY +## first (Cap's Bot scheme is for API keys, not the admin key): +## RESP=$(curl -s -X POST http://localhost:3000/auth/login \ +## -H "Content-Type: application/json" \ +## -d "{\"admin_key\":\"$CAP_ADMIN_KEY\"}") +## BEARER=$(printf '{"token":"%s","hash":"%s"}' \ +## $(echo "$RESP" | jq -r .session_token) \ +## $(echo "$RESP" | jq -r .hashed_token) | base64 -w0) +## curl -X POST http://localhost:3000/server/keys \ +## -H "Authorization: Bearer $BEARER" \ +## -d '{"name":"shroud-email","instrumentation":true,"rsw":true}' +CAP_SITE_KEY= +CAP_SECRET_KEY=