Public URL: https://mailbox.overslash.com (dev: https://mailbox.dev.overslash.com)
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 ininfra/main.tfbehindenable_overfwd. Cloud Run serviceoverslash-{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.jsonis the only unauthenticated route (it is also the startup/liveness probe — overfwd ships no/health). Everything under/email/*needsAuthorization: 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 concurrentgetcalls buffering whole messages),cpu_idle = true(request-based billing), concurrency capped at 20.
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 storedoverfwd_gateway_keyitself — the platform rung of D39. It is only ever sent toOVERSLASH_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.
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.
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].
make tofu-plan ENV=dev && make tofu-apply ENV=devNew environments need two manual steps Terraform cannot do (the overslash.com
zone is at the registrar, enable_dns = false):
- CNAME
mailbox(prod) /mailbox.dev(dev) →ghs.googlehosted.com. - The apex must be verified in Google Search Console for the account
running the apply, or
google_cloud_run_domain_mappingfails.
Until the CNAME propagates, the service is reachable at its run.app URL —
tofu output overfwd_url.
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 tagSet 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.
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 401End-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.
| 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. |
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.