Custom Domains and Network Isolation for Staging Environments
Two Separate Concerns That Get Conflated
"Making staging secure" often gets treated as one undifferentiated problem, but it actually splits into two genuinely separate concerns that deserve separate configuration:
- How does the outside world address this environment? (Custom domains, SSL)
- What can this environment reach, and what can reach it, on the shared infrastructure? (Network isolation)
The first is about presentation and stakeholder experience. The second is about blast-radius containment when something goes wrong. Both matter for a staging/testing pillar, but conflating them leads to half-solutions — a nicely-branded domain sitting on an environment with no isolation from its neighbors, or a tightly isolated environment addressable only by an ugly auto-generated hostname that undermines a client demo.
Custom Domains: Branded, Stable Hostnames
By default, every Bunnyshell environment gets an auto-generated hostname under bunnyenv.com — this is the pattern used throughout the earlier CLI examples in this pillar (https://web-jfq6ir.bunnyenv.com/). It's a real, stable, HTTPS-secured URL, sufficient for CI jobs and internal review.
For anything stakeholder-facing — a client demo, a staged feature a customer is reviewing before rollout, a QA environment your support team references in tickets — a custom domain reads as intentional rather than auto-generated. Configuring one:
1hosts:
2 - hostname: 'staging.example.com'
3 path: /
4 servicePort: 8080
5 selfManagedDns: true
6 k8s:
7 ingress:
8 tlsSecretName: ''
9 annotations:
10 cert-manager.io/cluster-issuer: letsencrypt-prodselfManagedDns: true tells Bunnyshell you're pointing DNS at this hostname yourself (a CNAME or A record to the cluster's ingress), rather than relying on Bunnyshell's own DNS automation. With cert-manager.io/cluster-issuer: letsencrypt-prod set, cert-manager requests and auto-renews a real Let's Encrypt certificate for the domain once DNS resolves correctly — no manual certificate handling required after the initial DNS record is in place.
For an existing certificate you already manage (an internal CA, a wildcard cert issued elsewhere), reference it as a Kubernetes secret instead:
1hosts:
2 - hostname: 'staging.example.com'
3 path: /
4 servicePort: 8080
5 selfManagedDns: true
6 k8s:
7 ingress:
8 tlsSecretName: my-existing-tls-secretWildcard Domains for Per-PR Environments
For the common case of many ephemeral environments needing unique-but-branded hostnames — pr-142.preview.example.com, pr-891.preview.example.com — a single wildcard DNS record (*.preview.example.com) pointed at the cluster ingress, paired with a wildcard certificate, covers every environment without per-PR DNS or certificate work:
1hosts:
2 - hostname: 'pr-{{ env.unique }}.preview.example.com'
3 path: /
4 servicePort: 8080
5 selfManagedDns: trueThis is the pattern that scales — one DNS record and one certificate configured once, and every subsequent ephemeral environment automatically gets a correctly-routed, correctly-secured branded hostname with zero incremental setup.
Network Isolation: Containing Blast Radius Between Environments
Custom domains solve "how do people find this environment." Network isolation solves a different question: if something goes wrong inside one environment — a compromised dependency, a misconfigured service account, a bug that lets an attacker execute code — what else on the shared cluster can it reach?
Each Bunnyshell environment runs in its own Kubernetes namespace. Combined with network policies scoping traffic to within that namespace (plus explicitly-allowed egress), one environment's pods cannot directly initiate connections to another environment's pods, even though both run on the same underlying cluster nodes. This containment is what makes running many ephemeral PR environments on shared infrastructure a reasonable security posture rather than a shared-blast-radius liability — a bug in one contributor's PR branch cannot pivot laterally into the environment reviewing a completely unrelated PR.
This matters more than it might initially seem in an open-source or external-contributor context specifically: if your CI automatically spins up a preview environment for PRs from forks (a common and useful pattern for reviewing external contributions), you are, by definition, running code from people you have not fully vetted. Namespace-level network isolation is the control that keeps a malicious or simply buggy external PR from being able to reach other environments on the same cluster, even if it can do whatever it wants within its own.
What Isolation Does Not Cover: Egress to the Internet
It's worth being precise that namespace isolation restricts environment-to-environment traffic on your cluster — it does not by default restrict an environment's ability to make outbound calls to the public internet. An environment can still call third-party APIs, download packages, or (in a worst case) exfiltrate data to an external endpoint unless you layer on explicit egress restrictions.
For environments handling genuinely sensitive test data, or for the external-contributor-PR scenario above, egress restriction is a meaningful additional control worth configuring explicitly — a network policy allowlisting only the specific external endpoints (package registries, required third-party APIs) an environment legitimately needs, denying everything else by default.
Putting It Together: A Staging Environment Configured for Both
1kind: Environment
2name: staging
3type: primary
4
5components:
6 - kind: Application
7 name: api
8 dockerCompose:
9 build:
10 context: .
11 hosts:
12 - hostname: 'staging.example.com'
13 path: /
14 servicePort: 8080
15 selfManagedDns: true
16 k8s:
17 ingress:
18 tlsSecretName: ''
19 annotations:
20 cert-manager.io/cluster-issuer: letsencrypt-prod
21 nginx.ingress.kubernetes.io/whitelist-source-range: '203.0.113.0/24'This one host definition combines a branded, auto-renewing-SSL custom domain with IP-level access restriction (covered in depth in IP-Restricting Staging Access) — presentation and access control configured together, in the same place, rather than as an afterthought. Namespace-level network isolation between this and every other environment on the cluster applies automatically, with no additional configuration required.
DNS Provider Specifics: Route53, Cloudflare, and Others
The selfManagedDns: true pattern shown above assumes you're managing DNS records yourself, which in practice means creating a CNAME (or, for a wildcard, an appropriately-scoped record) at whichever DNS provider hosts your domain, pointed at the cluster's ingress hostname or IP.
Route53: create an alias or CNAME record for your custom hostname (or *.preview.yourdomain.com for a wildcard) targeting the ingress controller's load balancer DNS name — visible via kubectl get svc -n ingress-nginx on the cluster, or from your cloud provider's load balancer listing.
Cloudflare: the same CNAME pattern applies, with one extra consideration — if the record is proxied (orange-cloud, not just DNS-only grey-cloud), Cloudflare terminates its own SSL in front of your cluster's ingress, which changes the SSL setup: you'd either rely on Cloudflare's edge certificate and use Flexible or Full SSL mode, or keep the record DNS-only (grey-cloud) and let cert-manager's Let's Encrypt certificate terminate SSL directly at your cluster's ingress as described above. Mixing the two (proxied record plus cert-manager.io/cluster-issuer expecting to serve its own certificate) is a common source of confusing SSL handshake errors, since two different systems both believe they're responsible for termination.
Regardless of provider, propagation delay (typically minutes, occasionally longer depending on TTL) means a freshly-created DNS record won't resolve, and consequently cert-manager's Let's Encrypt validation won't succeed, until the record has actually propagated — a "certificate stuck in pending" state immediately after configuring a new custom domain is very often just this, resolving itself once DNS catches up, not a configuration error to debug further.
Verifying Network Isolation Actually Works
It's worth confirming isolation empirically rather than trusting it by assumption, especially the first time you configure it for a security-sensitive setup (like fork-PR previews). From inside one environment, attempt to reach a component in a different environment by its internal cluster address:
bns exec <component-in-env-A> -- sh -c "curl -m 3 -s http://<component-in-env-B>.env-<namespace-B>.svc.cluster.local:8080/ || echo BLOCKED"A properly isolated setup returns BLOCKED (a timeout or connection refused) — if it instead succeeds in reaching the other environment's service, that's a real gap worth escalating to whoever manages your cluster's network policy configuration, since it means the isolation described throughout this guide isn't actually enforced at the cluster level for your setup.
Common Misconfigurations
Wildcard certificate scope mismatch. A certificate issued for *.preview.example.com does not cover preview.example.com itself (the bare domain, no subdomain) or a second-level wildcard like *.staging.preview.example.com — each needs its own certificate or Subject Alternative Name entry. A "certificate name mismatch" browser warning on what looks like a correctly wildcarded domain is often exactly this kind of scope mismatch.
IP restriction applied to the wrong host in a multi-host environment. With the environment-wide restriction pattern shown in IP-Restricting Staging Access, forgetting to mark a host public: true when it should be publicly reachable (a docs site, a status page) results in a confusing "why can't anyone see this" report, easy to mistake for a DNS or ingress bug rather than the access-control setting it actually is.
Assuming DNS-level isolation implies network-level isolation. A custom domain that only your team knows about is not itself a security control — it's obscurity, not isolation. The network policies discussed in this guide are the actual isolation mechanism; a memorable-but-undisclosed hostname is a nice-to-have on top, not a substitute.
Certificate Renewal: What to Actually Monitor
cert-manager's automatic renewal (typically triggered a configurable window before expiry, commonly 30 days for a 90-day Let's Encrypt certificate) is reliable in the overwhelming majority of cases, but "automatic" is not the same as "unmonitorable," and the one failure mode worth explicitly watching for is a renewal that silently fails because the DNS validation challenge can no longer be satisfied — most commonly because a DNS record was changed or removed after initial issuance (someone cleaning up "unused-looking" DNS entries without realizing one backs a long-lived custom domain). A simple periodic check — either cert-manager's own Certificate resource status via kubectl get certificate -A, or an external uptime monitor that specifically alerts on certificate expiry rather than just endpoint availability — catches this days or weeks before it becomes a "the staging demo link shows a security warning right before the client call" incident.
Custom Domains and Ephemeral Environment Volume
For a team spawning many ephemeral environments per day via automated PR creation, one thing worth confirming ahead of scaling up is that your DNS provider and certificate issuance path handle the resulting volume gracefully. A wildcard certificate covers unlimited subdomains without additional issuance calls per environment, which is exactly why the wildcard pattern shown above is recommended over per-environment individual certificates — an approach that issues a fresh Let's Encrypt certificate per ephemeral environment can run into Let's Encrypt's rate limits (a cap on certificates issued per registered domain per week) far sooner than most teams expect once PR volume is genuinely high, whereas one wildcard certificate, issued once and renewed automatically, has no such per-environment issuance cost at all.
Subdomain Takeover: A Risk Worth Knowing About
A specific, well-documented class of vulnerability worth being aware of when configuring custom domains for ephemeral environments: if a DNS record (particularly a wildcard) points at an environment's hostname, and that environment is later destroyed without the DNS record being cleaned up, the now-dangling DNS entry can, in some configurations, be claimed by an unrelated party who provisions their own resource at the same underlying address — a class of vulnerability generally known as subdomain takeover. This is primarily a risk for DNS records pointed at cloud-provider-assigned hostnames that get reused across tenants (a load balancer DNS name that could be reassigned to a different customer after being released) rather than the wildcard-to-cluster-ingress pattern recommended in this guide, where the wildcard consistently points at your own cluster's stable ingress regardless of which specific ephemeral environment currently owns a given subdomain. Still, if your setup uses selfManagedDns with individual (non-wildcard) DNS records created and torn down per environment rather than one durable wildcard, it's worth confirming your automation actually removes the DNS record when an environment is destroyed, not just the compute — a dangling record pointed at infrastructure that's been torn down and could be reprovisioned by someone else is the precise precondition for this class of issue.
Testing Before You Need It
The single most valuable habit from this guide, worth repeating explicitly: verify custom domain SSL and network isolation actually work the way you intend before the first time it matters — before the client demo, before the first fork-PR from an external contributor. Both are the kind of control that looks identical whether correctly configured or silently broken, right up until the moment something goes wrong and it's the worst possible time to discover the gap.
How This Fits With the Rest of the Staging Pillar
Custom domains and network isolation are the presentation and containment layer of a broader staging security posture. Pair this guide with IP-Restricting Staging Access for who can reach a given host, and with Securing E2E Test Environments for the secrets-handling half of the same overall picture — together these three cover the practical security surface a staging or PR-preview environment actually presents.
Checklist
- Stakeholder-facing environments have a branded custom domain, not a raw auto-generated hostname
- Wildcard DNS + certificate configured once for environments that spawn many per-PR instances
-
cluster-issuer(or an explicittlsSecretName) configured — no environment serving plaintext HTTP where HTTPS is expected - Understood explicitly: namespace isolation contains environment-to-environment traffic; egress to the internet needs separate, explicit restriction if required
- Fork-PR preview environments (if enabled) reviewed specifically for whether egress restriction is warranted
Give staging a real domain and real isolation, not just a URL.
14-day full-feature trial. No credit card required. Pay-as-you-go from $0.007/min per environment.
Frequently Asked Questions
Do staging environments need custom domains, or is the auto-generated URL enough?
Auto-generated URLs (like web-jfq6ir.bunnyenv.com) are sufficient for internal review and CI, since they are stable and real HTTPS URLs. Custom domains matter when stakeholders outside engineering — sales, customers in a demo, a client reviewing a staged feature — need a URL that looks intentional and matches your brand rather than an auto-generated subdomain.
How is SSL handled for a custom domain on a staging environment?
Through cert-manager with a configured cluster issuer (commonly Let's Encrypt), which automatically requests and renews a real certificate for the custom hostname once DNS is pointed at the environment's ingress. No manual certificate management is required after the initial cluster-issuer setup.
What is network isolation between environments, concretely?
Each environment runs in its own Kubernetes namespace with network policies that prevent one environment's pods from directly reaching another environment's pods, even though they share the same underlying cluster. A bug or compromise in one PR's environment cannot pivot laterally into another PR's environment.
Does network isolation mean environments can't reach the internet at all?
No — isolation is about environment-to-environment traffic on the shared cluster, not internet egress. An environment can still make outbound calls to third-party APIs unless you specifically configure egress restrictions, which is a separate, additional control for environments that should not be able to reach the internet at all.
Can I use a wildcard domain for all my ephemeral environments instead of one custom domain per environment?
Yes, and this is the common pattern — a wildcard DNS record and wildcard certificate (e.g. *.preview.example.com) pointed at the cluster ingress, with each ephemeral environment automatically getting a unique subdomain, avoiding manual DNS/cert work per PR.