Apoxy

Custom domains

Attach a customer-owned domain to Apoxy's default Gateway with zero-downtime TLS provisioning.

A DomainRecord binds a hostname you own (e.g., api.example.com) to a Gateway, Proxy, Tunnel, or EdgeFunction on Apoxy. Apoxy handles DNS delegation, ACME certificate issuance, and renewal.

How it works

For customer-owned hostnames, Apoxy uses a CNAME model. You keep authority over your zone; Apoxy only needs you to point the hostname at a zone we control (cname.apoxy.net) so we can serve traffic and issue certificates on your behalf.

Minimal setup

apiVersion: core.apoxy.dev/v1alpha3
kind: DomainRecord
metadata:
  name: api.example.com
spec:
  name: api.example.com
  tls: {}
  target:
    ref:
      group: gateway.apoxy.dev
      kind: Gateway
      name: default

Apply the DomainRecord, then create the CNAME at your registrar:

api.example.com.   CNAME   api.example.com.cname.apoxy.net.

Apoxy finds your project-scoped target via apoxy domainrecord get <name> -o yaml — the controller puts the exact value into status.

Once the CNAME propagates, Apoxy issues a TLS certificate using the ACME HTTP-01 challenge (Let's Encrypt / Google Trust Services). This takes roughly 30–60 seconds.

Zero-downtime migration (pre-issuance)

The default HTTP-01 flow has a side effect: the certificate is only issued after you flip your main CNAME. Until status.conditions.TLSReady=True, browsers hitting the new endpoint see a certificate error — typically a 30–60 second window.

To eliminate that window, delegate the ACME validation to Apoxy before you cut over:

Step 1 — apply the DomainRecord

Same as above.

Step 2 — add the ACME challenge delegation CNAME

At your registrar, create an additional CNAME:

_acme-challenge.api.example.com.   CNAME   _acme-challenge.api.example.com.cname.apoxy.net.

The target must be _acme-challenge.<your-full-domain>.cname.apoxy.net. — include every label of your hostname verbatim, including the TLD. Dropping the TLD or a mid-label is the most common mistake and the delegation check will never pass.

HostnameCorrect targetCommon wrong target
api.example.com_acme-challenge.api.example.com.cname.apoxy.net._acme-challenge.api.example.cname.apoxy.net. (missing .com)
app.example.co.uk_acme-challenge.app.example.co.uk.cname.apoxy.net._acme-challenge.app.example.co.cname.apoxy.net. (missing .uk)

If you want the exact string without typing it yourself, apoxy domainrecord get <name> -o yaml prints the expected target in the ACMEChallengeDelegationReady condition message.

This tells the ACME CA to look up the validation TXT record inside our zone, so we can prove domain ownership via DNS-01 without any traffic flowing through Apoxy yet.

Step 3 — wait for the certificate

Apoxy detects the delegation and pre-issues the certificate. Watch the conditions:

apoxy domainrecord get api.example.com -o yaml

You should see:

status:
  conditions:
  - type: ACMEChallengeDelegationReady
    status: "True"
    reason: ACMEChallengeDelegationConfigured
  - type: TLSReady
    status: "True"
    reason: CertificateIssued
  - type: CNAMEConfigured
    status: "False"      # main record not flipped yet — expected
    reason: CNAMENotConfigured

TLSReady=True will land before CNAMEConfigured=True. At that point the certificate is sitting in our gateway, ready for traffic.

Step 4 — flip the main CNAME

Now change your main record at your registrar:

api.example.com.   CNAME   api.example.com.cname.apoxy.net.

The next DNS resolution hits Apoxy. Our gateway serves the already-issued certificate from the first request — no cert error, no refresh loop.

Step 5 — keep the delegation CNAME

Leave _acme-challenge.api.example.com pointing at our zone. Renewal uses the same DNS-01 path, so keeping the delegation means renewals stay zero-intervention.

Zero-downtime migration — HTTP-only (no TLS)

If your DomainRecord leaves spec.tls unset (HTTP-only), there is no ACME certificate whose issuance could double as proof of ownership. Apoxy instead accepts an explicit ownership challenge at a dedicated label:

_apoxy-challenge.api.example.com.   CNAME   _apoxy-verify.cname.apoxy.net.

The target is a single fixed string — no FQDN embedding, no per-domain token. Copy it verbatim.

Once the challenge resolves, the controller marks the DomainRecord with OwnershipVerified=True and admits the hostname to the data plane. From that point, the moment you flip your main CNAME the gateway starts serving traffic for it — the same zero-downtime effect as the TLS flow, without needing a certificate.

TLS customers get OwnershipVerified=True for free once their cert lands (a live cert from ACME is ownership proof). Setting _apoxy-challenge in addition is optional for TLS — the _acme-challenge delegation already covers both the pre-issued cert and ownership.

You can leave the _apoxy-challenge delegation in place permanently; it's idempotent and doesn't need to be touched across renewals or cutovers.

Condition reference

ConditionMeaning
OwnershipVerifiedGates serving. True when Apoxy has proof you control this hostname — either via TLSReady=True (ACME verified), a valid _apoxy-challenge CNAME, or (for non-TLS records) a successful main-CNAME cutover. This is what the edge gateway actually checks before admitting the hostname.
TLSReadyA certificate has been issued and is ready to serve.
ACMEChallengeDelegationReadyYour _acme-challenge.<fqdn> CNAME points into our managed zone, so we can pre-issue certs via DNS-01.
CNAMEConfiguredYour main CNAME currently resolves to our zone. Operator-facing; does not gate serving.
TargetReadyThe backend (Gateway / Proxy / Tunnel / EdgeFunction) this record points at is healthy.
ReadyAggregate: all of the customer-visible setup above is green. Dashboards and CLI show this; the data plane does not key on it.

Fallback

If you skip step 2 (no _acme-challenge CNAME), Apoxy falls back to HTTP-01 after you flip the main CNAME. That still works — we will try to create certificates on the fly and you just get the standard 30–60s window between cutover and TLSReady=True.

Troubleshooting

  • ACMEChallengeDelegationReady=False after adding the CNAME — DNS caching. Wait up to the record's TTL at the customer resolver, then re-check with dig CNAME _acme-challenge.api.example.com.

  • TLSReady=False with reason CertificateError — check the status message. Common causes: delegation CNAME points to the wrong target (verify it ends in .<cname-zone-suffix>. exactly — see the label-match table in Step 2), or the ACME account is rate-limited.

  • Want to see what target to useapoxy domainrecord get <name> -o yaml shows the full delegation target in the condition message.

  • Fixed a typo in the delegation CNAME but it still shows False — two DNS caches may hold stale answers until they expire:

    • The old CNAME value at recursive resolvers — pinned for the TTL you set at your registrar (commonly 300s–3600s).
    • The NXDOMAIN for the broken target inside cname.apoxy.net — cached up to the zone's SOA MINIMUM TTL, 300s (5 minutes).

    To minimize this window, drop your registrar's CNAME TTL (e.g. to 60s) before you first publish it, so any typo is easy to correct. If you've already been bitten, there's no shortcut — wait out the longer of the two TTLs, then apoxy domainrecord get <name> -o yaml should flip ACMEChallengeDelegationReady=True on the next reconcile (runs every ~30s).

  • ACME rate-limit errors in cosmos logs — issuance workflows now honor the CA's Retry-After header and sleep in-place instead of thrashing, so a single rate-limited domain will not burn the shared quota. A certificate that was in flight during a rate-limit event will resume automatically once the window clears; check apoxy domainrecord get <name> — TLSReady will flip to True without further action.

On this page