The S3 Custom Domains plugin lets clients serve their public bucket over their own subdomain (for example files.customer.com), sold as a configurable-option add-on. Requests are DNS-verified automatically, then require staff approval before they go live. Its page lives in the staff navigation as S3 Custom Domains (under Tools).
Reads only. SigV4 binds host and path, so the S3 API,
mc, and presigned share links keep using the platform domain. Custom domains serve anonymous public-bucket reads. This is stated in the client UI and cannot be configured away.
Each S3 server (module row) picks the backend under Custom Domain Backend:
| Caddy (on-demand TLS) | Cloudflare for SaaS | |
|---|---|---|
| TLS certificates | Let's Encrypt, issued on demand, gated by an "ask" endpoint | Cloudflare-issued |
| Per-region origins | Native — the Host header is rewritten to each region | All custom hostnames land on the zone's one fallback origin; the origin (HAProxy snippet) routes each hostname to its region |
| Multi-region | Native | Works; remote-region bytes pass through the origin host |
Apex domains (example.com) |
No (subdomains only) | Yes, when the client's DNS provider supports ALIAS records or CNAME flattening |
| Video and large files | Served over the custom domain | Redirected to the platform address (CDN terms); keep video-heavy tenants on Caddy |
| External dependency | None beyond a Caddy reverse proxy | A Cloudflare zone with Cloudflare for SaaS enabled + API token |
active). Scope the token to the zone with SSL and Certificates: Edit and Custom Hostnames: Edit, then click Test Connection. The test fails when the fallback origin is missing or inactive, the top cause of Cloudflare error 1016.fr.sf-objectstorage.com). The public domain is the CNAME target in Caddy mode, the Host the proxy normalizes to, and the origin advertised on the map endpoint. It must be a valid hostname (or left blank to disable) — the field is validated when you save the row.proxy/SETUP-custom-domains.md (Caddy or Cloudflare mode). When another proxy already owns port 443 on the region host, run Caddy on a dedicated host: set the Public Domain to that host's name and point the Caddyfile upstream at the region endpoint (guide section A6).A client cannot register a custom domain that equals or is a subdomain of any of these reserved platform hostnames: any server's Public Domain (every module row in the company, not just the one their service lives on), the Cloudflare CNAME target, or your Blesta company hostname.
The client tab only offers the feature when the service carries a configurable option named custom_domain with a truthy value:
custom_domain (a checkbox/enabled option or a select whose selected value is not empty/0/false).The plugin page has Pending Review, All, and Setup tabs.
pending_dns --(CNAME + TXT verified)--> verified --(staff emailed once)
pending_dns --(no verification within 14 days)--> removing
verified --(approve)--> approved --(cron provisions)--> active
verified --(reject)--> rejected (name freed; client may re-submit it)
active <-> suspended (service suspend/unsuspend)
any live --> removing --(cron tears down)--> removed
Verification now proves ownership, not just a CNAME. Since module/plugin
2.4.1/1.0.1, a request must satisfy both a CNAME pointed at the expected
target AND a _sf-challenge.<domain> TXT record matching a token generated at
request time, before it moves from pending_dns to verified. Requests
created on earlier versions have no token and continue to verify on the CNAME
alone (grandfathered — nothing to do on upgrade). A CNAME match without the
TXT record is not treated as an error; the row simply stays pending_dns
until the client adds it.
Abandoned requests expire after 14 days. A pending_dns row that never
completes verification is automatically flagged for removal (freeing the
domain name) 14 days after it was requested, so unclaimed names don't pile up.
The gate is enforcing: the ask endpoint and the map endpoint only ever include approved/active rows, and in Cloudflare mode the custom hostname is not created until approval.
Suspension holds in-flight domains still. While a service is suspended, its domain does not progress — the cron skips verifying/provisioning it and Approve is blocked until the service is active again. Only active domains flip to suspended; a pending_dns/verified/approved domain simply pauses.
Dropping the add-on removes the domain. If the client removes the custom_domain option from an active service (or the service is deleted), the cron detects this on its next run (it reconciles each live domain against the service's current options) and tears the domain down — there is no need to click Remove on the client tab.
Rejected/removed names are reusable. When a domain reaches rejected or removed its reserved name is freed (the audit row is kept, with the original domain shown in the queue and preserved in its note), so the same hostname can be requested again later.
/plugin/minio_custom_domains/check/?domain=) — Caddy's on-demand-TLS ask URL. Returns 200 only for approved/active domains, else 404./plugin/minio_custom_domains/map/) — text/plain domain bucket origin_host lines for approved+active rows of the company that owns the token. The token is sent as an X-Map-Token request header (preferred; proxy/caddy/map-sync.sh uses this) or, for backward compatibility, the ?token= query parameter. 403 on a bad/missing token._sf-challenge.<domain>) matches the token shown — both are required on requests created after the ownership-verification update; older pending requests verify on the CNAME alone. The client's "Check now" button forces a re-check (rate-limited to once a minute).approved until Cloudflare reports both the hostname and its certificate active. Check the token scope and that the proxied CNAME-target record exists. A terminal Cloudflare state (moved, blocked, validation_timed_out, expired) is recorded on the row with Cloudflare's reason; fix the cause, then Retry. A hostname that already exists in the zone is adopted, not duplicated.