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.
Why a CNAME alone is not enough
Section titled “Why a CNAME alone is not enough”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.
Request flow
Section titled “Request flow”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.
Configuration
Section titled “Configuration”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:
- AWS EKS:
config/runtimes/eks/workloads.yml - GCP GKE:
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.comThe same block on EKS:
- 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.comOpen 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 reference
Section titled “Field reference”| 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:
- The origin certificate that cert-manager issues for your workload already carries
<runway-service-id>.svc.gitlab.netand*.<runway-service-id>.svc.gitlab.netas SANs, which are exactly the two origins a custom hostname points at. The workload zone runs inssl = strictmode, so a missing SAN would surface as a Cloudflare 526. - Neither the GKE
HTTPRoutenor the EKSIngressgenerated by Runway pins ahostnamesorhostfield, so the origin accepts traffic regardless of theHostheader Cloudflare forwards.
Rollout
Section titled “Rollout”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.
Phase 1: register the hostname
Section titled “Phase 1: register the hostname”Get the provisioner MR approved and applied with an atlantis apply comment. The apply
creates:
- A
cloudflare_custom_hostname_fallback_originfor the workload, pointing at<runway-service-id>.svc.gitlab.net - One
cloudflare_custom_hostnameper configured environment, withssl.method = txtandssl.type = dv - An ownership verification TXT challenge and a DV certificate validation TXT challenge
Phase 2: add records to the owning zone
Section titled “Phase 2: add records to the owning zone”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.
Direct zone access blocking
Section titled “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>".
Recommended staged rollout
Section titled “Recommended staged rollout”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:
-
Add
custom_hostnamestogether withblock_direct_zone_accessset tofalsefor both environments. Observable behaviour is unchanged: nothing is blocked.cloudflare:custom_hostnames:staging: staging-api.example.comproduction: api.example.comblock_direct_zone_access:staging: falseproduction: false -
Complete phase 2 and confirm traffic flows through the custom hostname.
-
Delete the
staging: falseline to enable blocking in staging, and verify. -
Once production is confirmed safe, delete the whole
block_direct_zone_accessblock so the default applies to both environments.
Interaction with other Cloudflare features
Section titled “Interaction with other Cloudflare features”Rate limiting and unmatched-path blocking
Section titled “Rate limiting and unmatched-path blocking”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.
Global load balancing
Section titled “Global load balancing”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.
Origin protection
Section titled “Origin protection”Cloudflare AOP and IP filtering are unaffected. They operate between Cloudflare and the cloud load balancer, downstream of custom hostname resolution.
Origin rules in the owning zone
Section titled “Origin rules in the owning zone”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.
Troubleshooting
Section titled “Troubleshooting”| 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 |
Support
Section titled “Support”For questions about custom hostnames, retrieving validation tokens, or planning a
rollout, contact the Runway team in #f_runway.