Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions custom-domain/dstack-ingress/DNS_PROVIDERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ This guide explains how to configure dstack-ingress to work with different DNS p
- `GATEWAY_DOMAIN` - dstack gateway domain (e.g., `_.dstack-prod5.phala.network`)
- `CERTBOT_EMAIL` - Email for Let's Encrypt registration
- `TARGET_ENDPOINT` - Backend application endpoint to proxy to
- `DNS_PROVIDER` - DNS provider to use (`cloudflare`, `linode`, `namecheap`)
- `DNS_PROVIDER` - DNS provider to use (`cloudflare`, `linode`, `namecheap`, `route53`)

### Optional Variables

Expand Down Expand Up @@ -104,8 +104,8 @@ PolicyDocument:
```

**Important Notes for Route53:**
- The certbot plugin uses the format `certbot-dns-route53` package
- CAA will merge AWS & Let's Encrypt CA domains to existing records if they exist
- The certbot plugin uses the `certbot-dns-route53` package
- CAA will merge AWS & Let's Encrypt CA domains into existing records if they exist
- It is essential that the AWS service account used can only assume the limited role. See cloudformation example.

## Docker Compose Examples
Expand Down Expand Up @@ -187,7 +187,8 @@ services:
CERTBOT_EMAIL: ${CERTBOT_EMAIL}
TARGET_ENDPOINT: http://backend:8080
SET_CAA: 'true'

volumes:
cert-data:
```

## Migration from Cloudflare-only Setup
Expand Down
33 changes: 0 additions & 33 deletions custom-domain/dstack-ingress/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,39 +53,6 @@ The dstack-ingress now supports multiple domains in a single container:
- Each domain gets its own SSL certificate
- Flexible nginx configuration per domain

### Wildcard Domain Support

You can use a wildcard domain (e.g. `*.myapp.com`) to route all subdomains to a single dstack application:

- The TXT record is automatically set as `_dstack-app-address-wildcard.myapp.com` (instead of `_dstack-app-address.*.myapp.com`)
- CAA records use the `issuewild` tag on the base domain
- Requires dstack-gateway with wildcard TXT resolution support ([dstack#545](https://github.com/Dstack-TEE/dstack/pull/545))

```yaml
services:
dstack-ingress:
image: dstacktee/dstack-ingress:latest
ports:
- "443:443"
environment:
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
- DOMAIN=*.myapp.com
- GATEWAY_DOMAIN=_.dstack-prod5.phala.network
- CERTBOT_EMAIL=${CERTBOT_EMAIL}
- SET_CAA=true
- TARGET_ENDPOINT=http://app:80
volumes:
- /var/run/dstack.sock:/var/run/dstack.sock
- /var/run/tappd.sock:/var/run/tappd.sock
- cert-data:/etc/letsencrypt
restart: unless-stopped
app:
image: nginx
restart: unless-stopped
volumes:
cert-data:
```

## Usage

### Prerequisites
Expand Down
23 changes: 2 additions & 21 deletions custom-domain/dstack-ingress/scripts/dns_providers/route53.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from .base import DNSProvider, DNSRecord, CAARecord, RecordType



class Route53DNSProvider(DNSProvider):
"""DNS provider implementation for AWS Route53."""

Expand Down Expand Up @@ -42,17 +41,7 @@ def __init__(self):


def setup_certbot_credentials(self) -> bool:
"""Setup AWS credentials file for certbot.

This container will be provided with aws credentials purely for the purpose
of assuming a role. Doing so will enable the boto platform to provision
temporary access key and secret keys on demand!

Using this strategy we can impose least permissive and fast expiring access
to our domain.

"""

"""Setup AWS credentials file for certbot."""
try:
# Pre-fetch hosted zone ID if we have a domain
domain = os.getenv("DOMAIN")
Expand All @@ -77,11 +66,7 @@ def validate_credentials(self) -> bool:
return False

def _get_hosted_zone_info(self, domain: str) -> Optional[tuple[str, str]]:
"""Get the hosted zone ID and name for a domain.

Returns:
Tuple of (hosted_zone_id, hosted_zone_name) or None
"""
"""Get the hosted zone ID and name for a domain."""
try:
# List all hosted zones
paginator = self.client.get_paginator("list_hosted_zones")
Expand Down Expand Up @@ -348,10 +333,6 @@ def delete_dns_record(self, record_id: str, domain: str) -> bool:
def create_caa_record(self, caa_record: CAARecord) -> bool:
"""
Create or merge a CAA record set on the apex of the Route53 hosted zone.

- Ignores the specific subdomain in caa_record.name for placement
- Uses it only to locate the correct hosted zone
- Merges hard-coded issuers with any existing CAA values on the apex
"""
# Ensure we know which hosted zone this belongs to
hosted_zone_id = self._ensure_hosted_zone_id(caa_record.name)
Expand Down
34 changes: 7 additions & 27 deletions custom-domain/dstack-ingress/scripts/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,6 @@ EOF
setup_py_env

setup_nginx_conf() {
local cert_name
cert_name=$(cert_dir_name "$DOMAIN")

local client_max_body_size_conf=""
if [ -n "$CLIENT_MAX_BODY_SIZE" ]; then
client_max_body_size_conf=" client_max_body_size ${CLIENT_MAX_BODY_SIZE};"
Expand Down Expand Up @@ -151,8 +148,8 @@ server {
server_name ${DOMAIN};

# SSL certificate configuration
ssl_certificate /etc/letsencrypt/live/${cert_name}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${cert_name}/privkey.pem;
ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;

# Modern SSL configuration - TLS 1.2 and 1.3 only
ssl_protocols TLSv1.2 TLSv1.3;
Expand All @@ -169,7 +166,7 @@ server {
# Enable OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/${cert_name}/fullchain.pem;
ssl_trusted_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;

Expand Down Expand Up @@ -234,16 +231,8 @@ set_txt_record() {
fi
APP_ID=${APP_ID:-"$DSTACK_APP_ID"}

local txt_domain
if [[ "$domain" == \*.* ]]; then
# Wildcard domain: *.myapp.com → _dstack-app-address-wildcard.myapp.com
txt_domain="${TXT_PREFIX}-wildcard.${domain#\*.}"
else
txt_domain="${TXT_PREFIX}.${domain}"
fi

dnsman.py set_txt \
--domain "$txt_domain" \
--domain "${TXT_PREFIX}.${domain}" \
--content "$APP_ID:$PORT"

if [ $? -ne 0 ]; then
Expand All @@ -268,20 +257,11 @@ set_caa_record() {
return
fi

local caa_domain caa_tag
if [[ "$domain" == \*.* ]]; then
caa_domain="${domain#\*.}"
caa_tag="issuewild"
else
caa_domain="$domain"
caa_tag="issue"
fi

ACCOUNT_URI=$(jq -j '.uri' "$account_file")
echo "Adding CAA record ($caa_tag) for $caa_domain, accounturi=$ACCOUNT_URI"
echo "Adding CAA record for $domain, accounturi=$ACCOUNT_URI"
dnsman.py set_caa \
--domain "$caa_domain" \
--caa-tag "$caa_tag" \
--domain "$domain" \
--caa-tag "issue" \
--caa-value "letsencrypt.org;validationmethods=dns-01;accounturi=$ACCOUNT_URI"

if [ $? -ne 0 ]; then
Expand Down
8 changes: 0 additions & 8 deletions custom-domain/dstack-ingress/scripts/functions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,6 @@ sanitize_proxy_buffers() {
fi
}

# Get the certbot certificate directory name for a domain.
# Certbot stores wildcard certs without the "*." prefix:
# *.example.com → /etc/letsencrypt/live/example.com/
cert_dir_name() {
local domain="$1"
echo "${domain#\*.}"
}

get_letsencrypt_account_path() {
local base_path="/etc/letsencrypt/accounts"
local api_endpoint="acme-v02.api.letsencrypt.org"
Expand Down
2 changes: 1 addition & 1 deletion custom-domain/dstack-ingress/scripts/generate-evidences.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ fi
# Copy all certificate files
while IFS= read -r domain; do
[[ -n "$domain" ]] || continue
cert_file="/etc/letsencrypt/live/$(cert_dir_name "$domain")/fullchain.pem"
cert_file="/etc/letsencrypt/live/${domain}/fullchain.pem"
if [ -f "$cert_file" ]; then
cp "$cert_file" "cert-${domain}.pem"
else
Expand Down