Skip to content

Custom Hostnames

By default, a Runway workload on EKS or GKE is served from a Runway-owned hostname in its own Cloudflare zone:

  • Production: <runway-service-id>.svc.gitlab.net
  • Staging: staging.<runway-service-id>.svc.gitlab.net

Custom hostnames let you serve the same workload from a hostname in a zone that Runway does not own, such as secrets.gitlab.com or auth.aws.gitlab.com.

Serving your workload on a hostname you own takes two things: a CNAME from that hostname to <runway-service-id>.svc.gitlab.net, and a registration on the Runway side that authorises the CNAME. Without the registration, Cloudflare will not serve the record.

The reason is that the record is proxied and its target is a Cloudflare zone in a different Cloudflare account. Runway workload zones live in a dedicated Cloudflare account, separate from the account that holds the zone your hostname lives in. Cloudflare blocks that combination unless the target zone has explicitly authorised the hostname, returning “CNAME Cross-User Banned”.

Cloudflare for SaaS custom hostnames are that authorisation. Registering your hostname on the Runway workload zone tells Cloudflare the cross-account CNAME is expected, and Cloudflare issues a Domain Validated (DV) certificate for it. Once the registration is in place, the proxied CNAME works normally.

Using secrets-manager-gke and secrets.gitlab.com as the example:

flowchart TD
    client["client"]
    edge["Cloudflare edge"]
    origin["<b>secrets-manager-gke.svc.gitlab.net</b><br/>proxied record in the workload zone"]
    regional["<b>secrets-manager-gke.us-east1.gcp.runway.gitlab.net</b><br/>Cloud DNS / Route53"]
    lb["GKE Gateway / EKS ALB"]
    svc["Service"]
    pods["Pods"]

    client -->|"https://secrets.gitlab.com"| edge
    edge -->|"custom_origin_server"| origin
    origin -->|"CNAME"| regional
    regional --> lb
    lb --> svc --> pods

Cloudflare presents its own DV certificate for secrets.gitlab.com to the client.

custom_origin_server is a property of the registered custom hostname rather than a hop of its own: it names the origin the edge dials, and that name is the workload zone’s own proxied record. The regional record it resolves to is written by external-dns from inside the cluster.

Staging follows the same chain through staging.secrets-manager-gke.svc.gitlab.net and secrets-manager-gke.us-east1.gcp.staging.runway.gitlab.net.

Custom hostnames are configured in the provisioner repository, not in your service repository. Add a cloudflare.custom_hostnames block to your entry in the workload inventory:

config/runtimes/gke/workloads.yml
- runway_service_id: secrets-manager-gke
project_id: 81750412
regions:
- us-east1
groups:
- gitlab-org/maintainers/openbao
cloudflare:
custom_hostnames:
staging: secrets.staging.gitlab.com
production: secrets.gitlab.com

The same block on EKS:

config/runtimes/eks/workloads.yml
- runway_service_id: ai-gateway-eks
project_id: 39903947
regions:
- us-east-1
groups:
- gitlab-org/maintainers/ai-gateway
cloudflare:
custom_hostnames:
staging: ai-gateway-eks.cloud.staging.gitlab.com
production: ai-gateway-eks.cloud.gitlab.com

Open a merge request against the provisioner and assign it to a Runway team member (see CODEOWNERS). Atlantis plans automatically on the merge request. The apply is gated on the MR being approved and mergeable, so it cannot run before a Runway maintainer approves. Once approved, comment atlantis apply to apply the change; Atlantis merges the MR itself after a successful apply.

Field Type Default Notes
custom_hostnames.production string none Hostname for the production environment
custom_hostnames.staging string none Hostname for the staging environment
block_direct_zone_access.production boolean true See Direct zone access blocking
block_direct_zone_access.staging boolean true See Direct zone access blocking

Both environments are optional and independent. Setting only production is valid; the other environment is simply skipped.

Hostnames are validated in CI against the regex ^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$. That means lowercase only, at least two labels, no underscores, no trailing dot, no wildcard, and no leading or trailing hyphen within a label.

No change is needed in your service repository

Section titled “No change is needed in your service repository”

This is worth stating explicitly, because it looks like magic: you do not need to touch .runway/${RUNWAY_SERVICE_ID}/default-values.yaml or your runtime-specific deployment file.

Two things make this work:

  1. The origin certificate that cert-manager issues for your workload already carries <runway-service-id>.svc.gitlab.net and *.<runway-service-id>.svc.gitlab.net as SANs, which are exactly the two origins a custom hostname points at. The workload zone runs in ssl = strict mode, so a missing SAN would surface as a Cloudflare 526.
  2. Neither the GKE HTTPRoute nor the EKS Ingress generated by Runway pins a hostnames or host field, so the origin accepts traffic regardless of the Host header Cloudflare forwards.

Applying the provisioner MR is only half the job. Cloudflare will not serve the custom hostname until you add validation records to the zone that actually owns the domain (for example gitlab.com), which lives in config-mgmt on ops.gitlab.net. Those records depend on tokens that only exist after the provisioner apply, so the two changes have to be sequenced rather than landed together.

Get the provisioner MR approved and applied with an atlantis apply comment. The apply creates:

  • A cloudflare_custom_hostname_fallback_origin for the workload, pointing at <runway-service-id>.svc.gitlab.net
  • One cloudflare_custom_hostname per configured environment, with ssl.method = txt and ssl.type = dv
  • An ownership verification TXT challenge and a DV certificate validation TXT challenge

Ask a Runway maintainer for the two TXT challenges.

Then open a config-mgmt MR against the owning zone adding:

Record Value
TXT Ownership verification challenge from phase 1
TXT DV certificate validation challenge from phase 1
CNAME Your custom hostname → <runway-service-id>.svc.gitlab.net

For a worked example, cloud-connect-stg/zone.tf adds the proxied CNAME for ai-gateway-eks.cloud.staging.gitlab.com together with the TXT record that validates the custom hostname.

Until this lands, the custom hostname stays in pending_validation and serves nothing.

Once the custom hostname is registered, the proxied CNAME works fine even though it crosses Cloudflare zones and accounts. That is exactly what the registration authorises.

Phase 3: enable direct zone access blocking

Section titled “Phase 3: enable direct zone access blocking”

New workloads get blocking on by default, so plan for this before phase 1 rather than after. See Direct zone access blocking.

Once a custom hostname exists, the underlying zone hostname is still reachable. Traffic that arrives directly at <runway-service-id>.svc.gitlab.net bypasses any Cloudflare configuration attached to the custom hostname, so Runway blocks it by default.

The rule only matches requests whose Host is the zone hostname, and traffic arriving via the custom hostname is untouched. Blocking is therefore safe as long as every caller reaches your service through the custom hostname. The only thing it breaks is a caller still pointed at <runway-service-id>.svc.gitlab.net.

The block is a Cloudflare firewall rule in the http_request_firewall_custom phase matching http.host eq "<zone hostname>".

If you already know every caller uses the custom hostname, take the default and skip this. Otherwise, for a workload adopting custom hostnames on an already-live service, opt out of both environments first, then re-enable one at a time so you can verify each environment before it starts blocking:

  1. Add custom_hostnames together with block_direct_zone_access set to false for both environments. Observable behaviour is unchanged: nothing is blocked.

    cloudflare:
    custom_hostnames:
    staging: staging-api.example.com
    production: api.example.com
    block_direct_zone_access:
    staging: false
    production: false
  2. Complete phase 2 and confirm traffic flows through the custom hostname.

  3. Delete the staging: false line to enable blocking in staging, and verify.

  4. Once production is confirmed safe, delete the whole block_direct_zone_access block so the default applies to both environments.

Interaction with other Cloudflare features

Section titled “Interaction with other Cloudflare features”

A request to a custom hostname passes through two Cloudflare zones in sequence, which Cloudflare calls Orange-to-Orange: the zone that owns your domain first, then the Runway workload zone. Runway’s rules run in that second pass.

So rate limit rules apply to your custom hostname as well as the zone hostname. Runway attaches them to the workload zone and includes each configured custom hostname in the rules’ host match, so there is nothing to configure in the zone that owns your domain.

This applies to the auth-failure rate limit, the user rate limits, and the allow_only_rate_limited_paths 404 rule.

Adding a custom hostname does not change which paths are allowed. The 404 rule uses the same host match and the same path list for both hostnames, so the custom hostname behaves exactly like the zone hostname.

Custom hostnames work with the global load balancer and need no extra configuration. When the load balancer is enabled, Runway does not create the workload zone’s proxied DNS records; it creates a cloudflare_load_balancer instead, named to match the custom_origin_server value the custom hostname points at, so the origin still resolves.

Cloudflare AOP and IP filtering are unaffected. They operate between Cloudflare and the cloud load balancer, downstream of custom hostname resolution.

If you route to Runway with proxied DNS records alone, which is the common case, there is nothing to do here. That path is straightforward and needs no extra configuration.

Runway itself creates no Cloudflare origin rules; its rulesets only use the http_request_firewall_custom and http_ratelimit phases. Some teams do add their own origin rules in the owning zone, for example to override the origin host, SNI, or port. cloud.gitlab.com is one such zone. If that applies to you, there is a constraint worth knowing about.

cloud.staging.gitlab.com is a worked example of that pattern. cloud-connect-stg/rules.tf defines an origin rule whose target is ai-gateway-eks.cloud.staging.gitlab.com, and that name is a proxied CNAME created in the same zone in zone.tf. The rule and the record it points at live together, which is what makes it work.

Symptom Cause Fix
CI json:schema fails on an unknown key The cloudflare object sets additionalProperties: false, so custom_hostname (singular) or any typo is rejected Use custom_hostnames exactly
CI json:schema fails on pattern Uppercase, underscore, trailing dot, wildcard, or a single-label hostname Fix the hostname to match ^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$
Apply succeeds but nothing is created cloudflare.enabled: false on the workload Enable Cloudflare, or accept that custom hostnames are unavailable
Apply succeeds but nothing is created The environment key is not literally staging or production (for example prod or dev). This passes schema validation and is then ignored Use staging or production
Hostname serves nothing, stuck in pending_validation Phase 2 TXT records are missing from the owning zone Add the ownership and DV TXT records in config-mgmt
<service>.svc.gitlab.net suddenly returns 403 block_direct_zone_access defaults to true Set the environment to false, or migrate callers to the custom hostname
Cloudflare 526 (invalid SSL certificate) The origin certificate is missing the <service>.svc.gitlab.net SAN and the zone is ssl = strict Contact the Runway team; the SAN is part of the standard cert-manager Certificate
Cloudflare 525 (SSL handshake failed) Origin TLS problem. Runway alerts on this via a Cloudflare notification policy with a 99.9% SLO Contact the Runway team in #f_runway
CNAME Cross-User Banned A CNAME was added in the owning zone before the custom hostname was registered Complete phase 1 first, then add the CNAME
An origin rule with a DNS record override fails The override targets a proxied record in a different zone from the rule, which does not work even within one account Target a proxied record in the same zone as the rule

For questions about custom hostnames, retrieving validation tokens, or planning a rollout, contact the Runway team in #f_runway.