TL;DR: Give every game studio tenant one asset hostname, point it at a shared delivery hostname with a CNAME, and sign the final URL on the server with a tenant-specific key. Treat the accompanying company-mail MX change as a separate record set with its own propagation clock. Lower the old records' TTL before the cutover, keep old paths valid during the overlap, and measure authoritative answers rather than assuming a dashboard save means the Internet has converged.
That split is the design. The CNAME selects the delivery edge; the signature authorizes one path until one expiry time. MX records select mail exchangers. Combining those jobs in application config adds knobs without making DNS move faster.
How should a vanity asset hostname use CNAME plus signed URLs?
A recursive resolver can cache a DNS answer for the record's TTL. Changing the authoritative zone does not recall copies already cached elsewhere. A short TTL set after the old value has been cached for a long time therefore does little for the current cutover. Reduce the relevant TTL ahead of the change, wait out the previous TTL, publish the new target, and retain the old service through the overlap. For a game company changing both asset delivery and company mail, write two checkpoints into the run sheet: the vanity hostname's CNAME must reach the prepared asset service, while the organizational domain's MX answers must name prepared mail exchangers. Those checks share a maintenance window, not a success condition.
CNAME and MX need separate checks.
There is a hard DNS constraint here: a CNAME owner name cannot also carry other data. An asset subdomain is a cleaner boundary than a name that also needs MX or TXT records. Keep mail on the organizational domain and give assets a dedicated child name. Less glue.
Choose the shortest cutover window the existing TTL permits, not the window the release calendar wants. If fast rollback matters, keep both origins able to serve already-issued links until their validity window and DNS overlap have ended. This is an explicit trade-off: a longer overlap consumes more operating capacity, but a shorter one can strand cached DNS answers or unexpired URLs.
DNS wins this argument.
The smallest working signer
The signing contract needs few fields: tenant, normalized path, and expiry. The verifier must reconstruct the exact same bytes. This TypeScript uses HMAC-SHA-256 from Node.js, rejects an unknown tenant, keeps the key off the client, and emits a URL under an already-validated asset hostname.
import { createHmac } from "node:crypto";
type Tenant = { assetHost: string; signingKey: string };
const tenants: Record<string, Tenant> = {
red: {
assetHost: "assets.guild.example",
signingKey: process.env.RED_ASSET_SIGNING_KEY ?? "",
},
};
function signAssetUrl(tenantId: string, rawPath: string, expiresAt: number): URL {
const tenant = tenants[tenantId];
if (!tenant || !tenant.signingKey) throw new Error("Tenant is not configured");
if (!Number.isInteger(expiresAt)) throw new Error("Expiry must be an integer");
const path = new URL(rawPath, "https://asset.invalid").pathname;
const payload = `${tenantId}\n${path}\n${expiresAt}`;
const signature = createHmac("sha256", tenant.signingKey)
.update(payload, "utf8")
.digest("base64url");
const url = new URL(`https://${tenant.assetHost}${path}`);
url.searchParams.set("tenant", tenantId);
url.searchParams.set("expires", String(expiresAt));
url.searchParams.set("signature", signature);
return url;
}
Production verification also needs constant-time signature comparison, an expiry check, strict host-to-tenant binding, key rotation, and a decision about query parameters. If the edge includes a parameter in its canonical payload, the signer must do the same. One mismatch means every request fails.
I would benchmark signature throughput for the actual path distribution and time-to-first-valid-request from a cold process. A wrapper that saves three lines but hides canonicalization is config bloat with better branding. The benchmark should pin the Node.js version, key length, path corpus, concurrency, and machine shape; otherwise the result cannot support a decision. Compare the direct node:crypto call with the proposed wrapper under the same inputs, report median and tail latency, then retain the simpler implementation unless the abstraction removes a measured bottleneck.
Three lines are cheap.
How do you cut over without losing assets or mail?
Start with ownership and service readiness, not DNS. Validate that the delivery layer knows the tenant hostname and certificate, that both asset paths return the same bytes, and that the new mail service accepts expected recipients. Only then shorten TTLs.
- Inventory current authoritative CNAME and MX answers, TTLs, and the signed-link maximum lifetime.
- Reduce only the TTLs involved, then wait at least the former TTL.
- Provision the asset hostname and mail exchangers before publishing either DNS change.
- Change one record set at a time. Query authoritative servers and a fixed set of recursive resolvers; record answer, TTL, and timestamp.
- Keep the previous asset route and mail service available during the measured overlap. Restore previous record values if validation fails.
Do not repeatedly edit records because two resolvers disagree. During propagation, disagreement is expected. The useful signal is whether each resolver changes after its cached TTL ages out while the authoritative answer remains correct.
Mail adds an authentication boundary. DMARC policy is published in DNS and builds on identifier alignment involving SPF and DKIM. Moving MX does not update those policies or the systems allowed to send mail for the domain. Audit sending sources and authentication records separately. Receiving and sending are related operationally, but they are not the same DNS action.
Failure modes worth testing
The nastiest asset failure is host confusion: a valid signature minted for one tenant is replayed through another tenant's hostname. Bind tenant ID and path into the signed payload, map each hostname to exactly one tenant during verification, and reject disagreement. Test encoded path separators, duplicate query keys, an expiry one second in the past, and a signature with one changed byte.
Clock skew deserves a stated policy. Huge grace periods weaken expiry; zero grace makes small clock differences visible. Pick a bounded tolerance, monitor clock health, and test both boundary instants. The trade-off belongs in the interface contract, because changing it later changes which requests the verifier accepts.
Test the edge.
For DNS, capture answers from authoritative servers, then from the same recursive resolver at intervals. Record p50 and p95 observed convergence across that fixed resolver set, but label the result correctly: it is a sample, not a promise for every resolver. Numbers without the resolver list are theater.
Mail validation should cover MX preference order, delivery to expected recipients, SPF evaluation, DKIM signatures from each legitimate sender, and DMARC alignment. A successful inbound message does not prove outbound authentication is right.
What I would change at scale
At dozens of tenants, a map in process stops being useful. Put hostname ownership, key version, active routing state, and audit timestamps in a control-plane record. Push only the minimum verification material to the request path. Key identifiers let the verifier accept old and new keys during a bounded rotation rather than invalidating every live URL.
Separate deployment state from observed state. desiredCname is configuration; resolver samples are evidence. A dashboard should show both, plus the old TTL and the time it changed. This costs storage and a small probe fleet, but removes false certainty in DNS work.
Keep the state machine small: pending ownership, ready, cutting over, active, or rolling back. More states feel precise and usually turn into manual repair work. The trade-off is that coarse states need good event logs. I will take that deal.
No vendor changes the caching model. Product selection can change certificate automation, key storage, log quality, and how much integration code the team owns. Evaluate those with a timed setup exercise and failure injection, then keep the DNS and signing contracts portable.
References
- https://datatracker.ietf.org/doc/html/rfc1034
- https://datatracker.ietf.org/doc/html/rfc1035
- https://datatracker.ietf.org/doc/html/rfc1912
- https://datatracker.ietf.org/doc/html/rfc7489
- https://nodejs.org/api/crypto.html
Further reading
For CNAME coexistence rules, read RFC 1034 and RFC 1912. For the mail-authentication boundary around an MX migration, use RFC 7489. The Node.js Crypto documentation defines the HMAC and encoding APIs used in the TypeScript example.
Top comments (0)