> For the complete documentation index, see [llms.txt](https://docs.kangal.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kangal.dev/gateway/custom-domains.md).

# Custom domains

Custom domains connect a customer-owned hostname to a Kangal gateway or developer portal. The workflow verifies DNS ownership, issues a certificate, creates the certificate and SNI resources, and records the target binding.

DNS records and the serving edge remain under operator control. Plan both the Kangal workflow and the DNS/ingress change before announcing the hostname.

## Workflow overview

1. Create a custom-domain resource for a concrete hostname.
2. Publish the generated DNS TXT ownership record.
3. Ask Kangal to verify the record.
4. A certificate worker issues certificate material and creates an SNI mapping.
5. Publish the hostname's traffic record and configure the serving edge.
6. Verify the public certificate and application response.

The DNS verifier is read-only. It checks ownership records but never creates, changes, or deletes records in the customer's DNS zone.

## Requirements

* An existing gateway in the same tenant.
* Administrator access to the Kangal Admin API or Console.
* Permission to create TXT and traffic records in the authoritative DNS zone.
* A running custom-domain certificate worker.
* For public certificates, a Kubernetes cluster with cert-manager, a configured issuer, DNS-01 solver access, and the production issuance guards enabled.
* An ingress or edge listener that can serve the issued certificate and forward the hostname to the selected gateway or portal.

Hostnames must be concrete fully qualified domain names. Kangal normalizes case, removes a trailing dot, and converts internationalized names to IDNA. Wildcards, IP addresses, `localhost`, single-label names, labels longer than 63 characters, and labels beginning or ending with `-` are rejected. A normalized hostname can be owned by only one custom-domain or manual SNI resource across the deployment.

## Choose a target and providers

### Target

| Value     | Use                                                                                                             |
| --------- | --------------------------------------------------------------------------------------------------------------- |
| `gateway` | Send the hostname to gateway proxy traffic.                                                                     |
| `portal`  | Send the hostname to the tenant developer portal. The binding records `/portal/{tenant_id}` as the portal path. |

### DNS verification provider

| Value        | Configuration                                                                            | Behavior                                               |
| ------------ | ---------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| `manual`     | Optional `nameservers` list and `timeout_seconds` from 1 to 15 in `dns_provider_config`. | Resolves the TXT record through DNS.                   |
| `cloudflare` | `zone_id` in `dns_provider_config` and `api_token` in `dns_credentials`.                 | Reads matching TXT records through the Cloudflare API. |

Cloudflare credentials require read access to the selected zone's DNS records. Kangal protects stored credential values and redacts them from responses. Use a least-privilege token and rotate it according to your credential policy.

### Certificate provider

| Value          | Use                                               | Important behavior                                                                                                                                       |
| -------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `test-ca`      | Local, development, and workflow testing.         | Issues an ECDSA certificate from Kangal's private test CA for 1 to 90 days; browsers and public clients do not trust it. `validity_days` defaults to 30. |
| `cert-manager` | Staging or production certificates in Kubernetes. | Creates a `cert-manager.io/v1` Certificate and reads the resulting TLS Secret.                                                                           |

For `cert-manager`, `acme_config` accepts `namespace`, `issuer_name`, `issuer_kind`, `issuer_group`, `duration`, and `renew_before`. Defaults are the pod namespace or `default`, `letsencrypt-staging`, `ClusterIssuer`, `cert-manager.io`, `2160h`, and `360h`.

`dry_run` defaults to `true`. For `cert-manager`, dry-run validates Kubernetes admission without creating a TLS Secret. Use this for a staging check, then retry the verified domain with `dry_run: false` after production guards are enabled. The `test-ca` provider always issues its private test certificate; use it only in non-production environments.

## Create a domain

### Console workflow

1. Open a gateway and select **Custom domains**.
2. Select **Add domain**.
3. Enter the complete hostname.
4. Choose **Gateway** or **Portal**.
5. Choose the DNS verification provider and enter its configuration.
6. Choose **Test CA** for local testing or **cert-manager** for Kubernetes.
7. Leave dry-run enabled for the initial cert-manager admission check.
8. Create the domain and copy the generated TXT name and value.

### Admin API: manual DNS and test CA

```bash
export KANGAL_ADMIN_URL="https://control.example.com"
export KANGAL_TOKEN="replace-with-admin-token"
export KANGAL_TENANT_ID="tenant_123"
export KANGAL_GATEWAY_ID="gateway_123"

curl -sS -X POST \
  "$KANGAL_ADMIN_URL/api/v1/admin/gateways/$KANGAL_GATEWAY_ID/custom-domains" \
  -H "Authorization: Bearer $KANGAL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "'"$KANGAL_TENANT_ID"'",
    "hostname": "api.example.com",
    "target": "gateway",
    "dns_provider": "manual",
    "dns_provider_config": {
      "timeout_seconds": 5
    },
    "acme_provider": "test-ca",
    "acme_config": {
      "validity_days": 30
    },
    "dry_run": true
  }'
```

The new resource starts in `pending_dns` and returns:

* `challenge_record_name`, such as `_kangal-verification.api.example.com`
* `challenge_value`, beginning with `kangal-domain-verification=`
* `challenge_expires_at`, 24 hours after creation
* `challenge_version`

Treat the challenge value as ownership evidence and publish only the value for the intended hostname.

### Admin API: Cloudflare and cert-manager

```bash
curl -sS -X POST \
  "$KANGAL_ADMIN_URL/api/v1/admin/gateways/$KANGAL_GATEWAY_ID/custom-domains" \
  -H "Authorization: Bearer $KANGAL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "'"$KANGAL_TENANT_ID"'",
    "hostname": "portal.example.com",
    "target": "portal",
    "dns_provider": "cloudflare",
    "dns_provider_config": {
      "zone_id": "replace-with-zone-id"
    },
    "dns_credentials": {
      "api_token": "replace-with-read-token"
    },
    "acme_provider": "cert-manager",
    "acme_config": {
      "namespace": "kangal",
      "issuer_name": "letsencrypt-staging",
      "issuer_kind": "ClusterIssuer"
    },
    "dry_run": true
  }'
```

## Publish and verify the ownership record

Create a TXT record with the exact returned name and value. DNS providers differ in whether the zone name is appended automatically, so inspect the final FQDN in the provider before continuing.

Check authoritative visibility:

```bash
dig +short TXT _kangal-verification.api.example.com
```

Then verify through Kangal:

```bash
export KANGAL_DOMAIN_ID="domain_123"

curl -sS -X POST \
  "$KANGAL_ADMIN_URL/api/v1/admin/gateways/$KANGAL_GATEWAY_ID/custom-domains/$KANGAL_DOMAIN_ID/verify?tenant_id=$KANGAL_TENANT_ID" \
  -H "Authorization: Bearer $KANGAL_TOKEN"
```

A successful response includes matched TXT values and a certificate job ID. The challenge is consumed once; subsequent certificate work uses the verified domain state rather than reusing the token.

If the 24-hour challenge expires while the domain is still `pending_dns` or `verification_failed`, rotate it:

```bash
curl -sS -X POST \
  "$KANGAL_ADMIN_URL/api/v1/admin/gateways/$KANGAL_GATEWAY_ID/custom-domains/$KANGAL_DOMAIN_ID/challenge?tenant_id=$KANGAL_TENANT_ID" \
  -H "Authorization: Bearer $KANGAL_TOKEN"
```

Replace the old TXT value with the new challenge before verifying again.

## Follow certificate issuance

The domain progresses through status values that describe ownership and certificate work:

| Status                         | Meaning                                                        | Operator action                              |
| ------------------------------ | -------------------------------------------------------------- | -------------------------------------------- |
| `pending_dns`                  | Waiting for the ownership TXT record.                          | Publish the record and verify.               |
| `verifying`                    | DNS verification is in progress.                               | Wait for the verification lease to finish.   |
| `verification_failed`          | The TXT record was missing, different, expired, or unreadable. | Correct DNS or rotate an expired challenge.  |
| `issuance_pending` / `issuing` | Certificate work is queued or running.                         | Monitor the certificate worker and issuer.   |
| `issuance_failed`              | Certificate work exhausted its attempts or failed.             | Correct provider configuration and retry.    |
| `active`                       | Certificate, SNI, and target binding are recorded.             | Configure and verify the serving edge.       |
| `renewal_due` / `renewing`     | Renewal is queued or running.                                  | Monitor the worker and issuer.               |
| `renewal_failed`               | Renewal failed; the previous binding remains selected.         | Resolve the error and request renewal again. |

Read the domain to inspect `last_error`, certificate IDs, expiry, renewal time, and alerts:

```bash
curl -sS \
  "$KANGAL_ADMIN_URL/api/v1/admin/gateways/$KANGAL_GATEWAY_ID/custom-domains/$KANGAL_DOMAIN_ID?tenant_id=$KANGAL_TENANT_ID" \
  -H "Authorization: Bearer $KANGAL_TOKEN"
```

The Console refreshes active work periodically. For automation, poll the get operation with backoff and stop when the domain reaches `active`, `issuance_failed`, or `renewal_failed`.

## Promote a cert-manager dry run

Before public issuance:

1. Install cert-manager and configure the intended `Issuer` or `ClusterIssuer` with a working DNS-01 solver.
2. Verify the staging issuer, certificate chain, ingress route, and rollback process.
3. Set Helm `customDomainAutomation.allowPublicAcme: true` and `customDomainAutomation.dryRun: false`.
4. Set the worker/API environment guard `CUSTOM_DOMAIN_ALLOW_PUBLIC_ACME=true` through the deployment configuration.
5. Retry a verified domain whose dry-run issuance reached `issuance_failed`.

```bash
curl -sS -X POST \
  "$KANGAL_ADMIN_URL/api/v1/admin/gateways/$KANGAL_GATEWAY_ID/custom-domains/$KANGAL_DOMAIN_ID/retry?tenant_id=$KANGAL_TENANT_ID" \
  -H "Authorization: Bearer $KANGAL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "promote_after_staging_validation",
    "dry_run": false
  }'
```

The explicit deployment and request guards prevent accidental public ACME issuance. Account registration, DNS-01 solver changes, CAA policy, propagation, and ACME rate-limit management belong to cert-manager and the configured issuer.

## Publish traffic and configure the edge

An `active` domain has Kangal certificate, SNI, and binding records. Complete the serving path separately:

1. Create the required `A`, `AAAA`, or `CNAME` record for the hostname. Point it to the real ingress or load-balancer address, not the ownership TXT record.
2. Configure the ingress or edge with the hostname and issued TLS Secret or certificate material.
3. Forward gateway targets to the data-plane proxy listener. Forward portal targets to the recorded tenant portal path.
4. Apply or release the edge configuration and wait for DNS propagation.
5. Test SNI, certificate trust, hostname routing, and application behavior.

For the Kangal Helm chart, declare release-time ingress bindings with each hostname and `tlsSecretName`. The database binding is operational state; it is not a substitute for applying the corresponding ingress release configuration.

Verify DNS and TLS:

```bash
dig +short api.example.com

openssl s_client -connect api.example.com:443 \
  -servername api.example.com -showcerts </dev/null

curl -i https://api.example.com/health
```

The test CA is private. For a test-CA hostname, add its CA to the test client's trust store or use explicit test-only trust options; never distribute the test CA as a public production trust anchor.

## Renewal

Kangal schedules renewal at certificate expiry minus the smaller of 30 days or one third of the certificate lifetime. Expiry alerts are:

* `warning` within 30 days
* `critical` within 7 days
* `expired` at or after expiry

Request an early renewal when required:

```bash
curl -sS -X POST \
  "$KANGAL_ADMIN_URL/api/v1/admin/gateways/$KANGAL_GATEWAY_ID/custom-domains/$KANGAL_DOMAIN_ID/renew?tenant_id=$KANGAL_TENANT_ID" \
  -H "Authorization: Bearer $KANGAL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason": "scheduled_operator_rotation"}'
```

Renewal is accepted only for an issued domain. A successful renewal creates a new managed certificate and switches the SNI/binding after activation. If renewal fails, the previous certificate binding remains in place; compare its expiry with the next planned retry.

## Delete a custom domain

Delete the Kangal resource:

```bash
curl -sS -X DELETE \
  "$KANGAL_ADMIN_URL/api/v1/admin/gateways/$KANGAL_GATEWAY_ID/custom-domains/$KANGAL_DOMAIN_ID?tenant_id=$KANGAL_TENANT_ID" \
  -H "Authorization: Bearer $KANGAL_TOKEN"
```

Deletion removes managed SNI and binding records, cancels active domain jobs, and marks the managed certificate retired. The operator must also remove the traffic record, ownership TXT record, Helm/ingress binding, and any provider resources retained by the external DNS or certificate systems.

## Security requirements

* Use least-privilege DNS credentials and store them only in the custom-domain credential field or deployment secret store.
* Restrict the Admin API by tenant and protect bearer tokens, audit data, database backups, and encryption keys.
* Keep the production ACME guards disabled in test environments.
* Review CAA records and issuer account ownership before public issuance.
* Treat an ownership challenge as sensitive until it is consumed or expires.
* Monitor the certificate worker, job retries, expiry alerts, and public certificate independently.

## Troubleshooting

### Verification finds no TXT value

Confirm the final record name, remove accidental quoting, query the authoritative nameserver directly, and wait for propagation. If using custom `nameservers`, ensure each value is an address accepted by the resolver.

### Cloudflare verification fails

Check `zone_id`, token validity, zone read permission, and the exact TXT record name. Kangal reads Cloudflare records; create the TXT record through your DNS workflow before verification.

### Verification returns `409`

Another verification is running, the challenge changed, the challenge expired, or it was already consumed. Read the latest domain state. Rotate only a pending or failed ownership challenge.

### Dry-run issuance ends in `issuance_failed`

cert-manager dry-run validates admission without producing the TLS Secret that activation requires. After staging validation and both production guards are enabled, call `retry` with `dry_run: false`.

### cert-manager issuance stays pending

Inspect the generated Certificate, cert-manager events, issuer readiness, DNS-01 solver records, CAA policy, and TLS Secret. The worker retries pending work with backoff and records the final reason in `last_error`.

### The domain is active but the hostname is unreachable

Check the traffic DNS record, load-balancer address, ingress host and TLS Secret, data-plane reachability, and release status. `active` confirms Kangal's managed certificate/SNI/binding state; the external serving path must also be applied.

### The public endpoint presents an old certificate

Compare `certificate_id`, `previous_certificate_id`, issuer Secret, ingress binding, and every load-balancer listener. Confirm the edge has reloaded after renewal and DNS is not directing clients to an older environment.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kangal.dev/gateway/custom-domains.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
