Skip to content

Latest commit

 

History

History
163 lines (128 loc) · 8.32 KB

File metadata and controls

163 lines (128 loc) · 8.32 KB

Mailbox Gateway (overfwd)

Public URL: https://mailbox.overslash.com (dev: https://mailbox.dev.overslash.com)

What it is

The shared overfwd deployment behind services/email.yaml — a stateless REST facade over IMAP/SMTP. It holds no credentials and no mail at rest: the mailbox login arrives on every request as X-Mailbox-Auth, is used, and is forgotten. One deployment serves every org (D39); an org that wants its own points its instances at it with instance.url or an org layer's instance_defaults.url (D36), and is unaffected by everything below.

  • Terraform: infra/modules/cloud-run-overfwd/, wired in infra/main.tf behind enable_overfwd. Cloud Run service overslash-{env}-overfwd.
  • Image: third-party, digest-pinned in infra/variables.tf (overfwd_image), pulled through the Artifact Registry Docker Hub mirror (overslash-{env}-dockerhub). Nothing in this repo builds it and there is no Cloud Build trigger for it.
  • Auth: OVERFWD_REQUIRE_API_KEY=true. GET /openapi.json is the only unauthenticated route (it is also the startup/liveness probe — overfwd ships no /health). Everything under /email/* needs Authorization: Bearer.
  • Networking: deliberately no VPC connector. It needs plain internet egress to IMAP 993 / SMTP 465, and the smaller its reachable surface the better while the explicit-endpoint SSRF check is still upstream work.
  • Sizing: cpu = 1 (below one vCPU Cloud Run pins concurrency to 1), memory = 512Mi (~17MiB at rest; the headroom is for concurrent get calls buffering whole messages), cpu_idle = true (request-based billing), concurrency capped at 20.

The gateway key

One GSM secret, overslash-{env}-overfwd-gateway-key, generated by Terraform and read by both services:

  • overfwd reads it as OVERFWD_API_KEY.
  • The API reads it as OVERSLASH_PLATFORM_GATEWAY_KEY, and injects it for any org that has not stored overfwd_gateway_key itself — the platform rung of D39. It is only ever sent to OVERSLASH_PLATFORM_GATEWAY_HOST.

Precedence, most specific first: instance.credentials["gateway"] → the org vault's overfwd_gateway_key → the platform key. So an org can always override, and nothing changes for a self-hosted or keyless gateway.

The gateway host

services/email.yaml does not hardcode a host. Its servers[0] is https://${MAILBOX_HOST}, resolved from OVERSLASH_TEMPLATE_VAR_MAILBOX_HOST (D44), and Terraform derives that from the same overfwd_domain that feeds OVERSLASH_PLATFORM_GATEWAY_HOST. That shared source is the point: when the two were maintained separately, dev shipped the prod host in the YAML and deployed the dev host in the infra, so dev instances sent mail to the prod gateway and silently lost the platform key (which only ever goes to OVERSLASH_PLATFORM_GATEWAY_HOST).

There is deliberately no default. Unset ⇒ the email template does not load at all, which is the right answer for a deployment with no Mailbox Gateway; the alternative would point it at somebody else's. If GET /v1/templates is missing email, check the API's boot log for template variable expansion failed.

Rotation

ENV=prod   # or dev
PROJECT=overslash   # overslash-dev for dev
openssl rand -hex 24 | tr -d '\n' | \
  gcloud secrets versions add overslash-$ENV-overfwd-gateway-key \
    --project=$PROJECT --data-file=-

# Roll BOTH services so they pick up the new version. Order matters: roll the
# API first and the gateway will 401 its calls until the second roll; roll the
# gateway first and the same gap opens the other way. It is a short window
# either way — do them back to back, off-peak.
gcloud run services update overslash-$ENV-overfwd --project=$PROJECT \
  --region=europe-west1 --update-env-vars=ROTATED_AT=$(date +%s)
gcloud run services update overslash-$ENV-api --project=$PROJECT \
  --region=europe-west1 --update-env-vars=ROTATED_AT=$(date +%s)

Terraform will not clobber the new value: the secret version carries ignore_changes = [secret_data].

Deploying

make tofu-plan ENV=dev && make tofu-apply ENV=dev

New environments need two manual steps Terraform cannot do (the overslash.com zone is at the registrar, enable_dns = false):

  1. CNAME mailbox (prod) / mailbox.dev (dev) → ghs.googlehosted.com.
  2. The apex must be verified in Google Search Console for the account running the apply, or google_cloud_run_domain_mapping fails.

Until the CNAME propagates, the service is reachable at its run.app URL — tofu output overfwd_url.

Upgrading the image

overfwd releases to Docker Hub. Pin the new digest, never a tag:

docker pull docker.io/angelmanuel/overfwd:<tag>
docker images --digests | grep overfwd     # take the sha256: for that tag

Set overfwd_image = "angelmanuel/overfwd@sha256:…" in infra/variables.tf (or per-env tfvars), then plan/apply. The variable rejects a non-digest value: a moving tag would be an unreviewed third-party code change reaching production on the next revision roll.

Smoke tests

BASE=https://mailbox.overslash.com

# 1. Alive, and the only public route answers.
curl -s -o /dev/null -w '%{http_code}\n' $BASE/openapi.json          # 200

# 2. Not an open proxy.
curl -s -o /dev/null -w '%{http_code}\n' -X POST $BASE/email/search \
  -H 'content-type: application/json' -d '{}'                        # 401

# 3. Authenticated but no mailbox credential — proves the bearer is accepted.
KEY=$(gcloud secrets versions access latest \
  --secret=overslash-prod-overfwd-gateway-key --project=overslash)
curl -s -o /dev/null -w '%{http_code}\n' -X POST $BASE/email/search \
  -H "authorization: Bearer $KEY" -H 'content-type: application/json' -d '{}'
                                                                     # 400, not 401

End-to-end, through Overslash: create an email service instance with no url, bind mailbox_user / mailbox_pass, and call search. If the org has stored no overfwd_gateway_key, a 200 proves the platform rung.

Troubleshooting

Symptom Cause
email is missing from GET /v1/templates OVERSLASH_TEMPLATE_VAR_MAILBOX_HOST is unset, so the template's ${MAILBOX_HOST} never resolved and the whole file was skipped at load. The boot log says template variable expansion failed.
Every email call returns upstream 401 The platform rung is off. Check the API service has all three of OVERSLASH_PLATFORM_GATEWAY_SECRET_NAME / _HOST / _KEY — a partial config disables the rung by design. _HOST and OVERSLASH_TEMPLATE_VAR_MAILBOX_HOST must agree; Terraform derives both from overfwd_domain so they only diverge if set by hand.
401 only for one org That org stored its own overfwd_gateway_key, or bound credentials["gateway"], and the value is wrong. The org's own binding always wins.
needs_authentication instead of a call The mailbox slots are unbound. The gateway key is never injected alone (a half-authenticated request would just fail downstream).
host_unreachable from the gateway The mailbox domain publishes no autoconfig and no endpoint is pinned. Set X-Mailbox-Imap / X-Mailbox-Smtp on the instance (D33) or org-wide on a layer (D36) — overfwd wants both or neither.
Revision fails to start Almost always OVERFWD_REQUIRE_API_KEY=true with an unresolvable OVERFWD_API_KEY: overfwd refuses to boot rather than serve unauthenticated. Check the Cloud Run SA can read the secret.

SSRF

The endpoint headers are tenant-influenced, so the deployment runs with OVERFWD_BLOCK_PRIVATE_ENDPOINTS=true (overfwd v0.3.0+): an X-Mailbox-Imap / X-Mailbox-Smtp target that is or resolves to a loopback / RFC1918 / link-local / CGNAT / IPv6-ULA address — or is named localhost, *.local, *.internal — is refused with bad_request before anything is dialled. Behind it: no VPC connector (so "internal" is the container and the metadata endpoint, nothing of ours), and those headers being x-overslash-instance-config, i.e. org-admin-set rather than agent-supplied.

Two consequences to know: the pinned image must never be rolled back below v0.3.0, and an org whose corporate IMAP host is on a private range genuinely cannot use the shared gateway — that org runs its own overfwd (default-off there) and points instance.url at it. See D39.