mseller docs

Dominios personalizados y TLS

Cómo están configurados los hostnames de prod (*.mseller.app) y dev (*.mseller.dev), y cómo agregar/cambiar uno nuevo.

Las APIs se sirven sobre dos dominios distintos según el entorno — prod sobre *.mseller.app y dev sobre *.mseller.dev. La infra del hostname está repartida así:

  • Dominio mseller.app (prod) — registrado en GoDaddy, DNS gestionado en cPanel (acceso vía https://mobile-seller.com/cpanel). Ahí viven los CNAME y TXT de los hostnames de producción.
  • Dominio mseller.dev (dev) — DNS gestionado en Cloudflare. Ahí viven los CNAME y TXT de los hostnames de dev. Inicialmente todos los registros van DNS-only (gray cloud); más adelante el de portal.api.mseller.dev se pone en modo proxied para meter Cloudflare WAF / Access encima del UI y /swagger (ver sección al final).
  • Certificado TLS (prod) — emitido y renovado automáticamente por cPanel AutoSSL (emisor actual: DigiCert / GeoTrust, validez ~6 meses). El cert se sube a Azure Container Apps como custom cert; el terminador TLS sigue siendo Azure.
  • Certificado TLS (dev) — cert administrado por Azure Container Apps (auto-emisión vía DigiCert/Let's Encrypt al validar el hostname). No hay cPanel ni AutoSSL en el medio para dev.

Azure no gestiona el cert de prod por su cuenta — esto es importante recordarlo cuando se hace troubleshooting: si el cert de prod vence y nadie sincronizó la renovación de cPanel a Azure, HTTPS deja de funcionar aunque el binding del hostname siga bien. En dev el cert es administrado por Azure, así que no requiere sincronía manual.

Hostnames vigentes

EnvAPIHostname públicoContainer App Environment
devPortalhttps://portal.api.mseller.devmseller-dev-cae (Central US)
devIngestionhttps://ingestion.api.mseller.devmseller-dev-cae (Central US)
devConsumohttps://consumo.api.mseller.devmseller-dev-cae (Central US)
prodPortalhttps://portal.api.mseller.appmseller-prod-cae (East US 2)
prodIngestionhttps://ingestion.api.mseller.appmseller-prod-cae (East US 2)
prodConsumohttps://consumo.api.mseller.appmseller-prod-cae (East US 2)

El patrón es {api}.api.mseller.app para prod y {api}.api.mseller.dev para dev — el TLD distinto es lo que indica el entorno. Esto hace obvio en qué env estás cuando mirás un URL en logs o documentación.

Swagger en dev

Cada API de dev expone Swagger en /swagger sobre su hostname público — útil para probar endpoints sin levantar el stack local:

En prod los /swagger están deshabilitados a propósito; usá los de dev para explorar el contrato.

¿Y los FQDNs `*.azurecontainerapps.io`?

Los nombres mseller-{env}-{api}-api.{env-id}.{region}.azurecontainerapps.io siguen funcionando como FQDN de fallback (Azure no los desactiva al agregar custom domains). Útiles para debug o para apuntar tráfico cross-env sin tocar DNS de mseller.app / mseller.dev. Pero todo cliente real (mobile, portal admin, integraciones) tiene que usar el hostname con dominio propio — protege contra lock-in al proveedor.

Cómo está configurado (resumen rápido)

Prod — mseller.app (cPanel + AutoSSL):

GoDaddy (registrar de mseller.app)
  └─ Name servers → cPanel (mobile-seller.com/cpanel)

cPanel: Zone Editor de mseller.app
  ├─ portal.api               CNAME → mseller-prod-portal-api.mangoocean-fef986ad.eastus2.azurecontainerapps.io.
  ├─ ingestion.api            CNAME → mseller-prod-ingestion-api.mangoocean-fef986ad.eastus2.azurecontainerapps.io.
  ├─ consumo.api              CNAME → mseller-prod-consumo-api.mangoocean-fef986ad.eastus2.azurecontainerapps.io.
  └─ asuid.{cada-subdominio}  TXT   → <verification-id>  (3 TXT records, mismo valor)

cPanel: SSL/TLS Status (AutoSSL)
  ├─ Emite cert (DigiCert / GeoTrust, ~6 meses) cubriendo los hosts de arriba
  └─ Renovación automática vía AutoSSL (~30 días antes del vencimiento)

Azure Container Apps (mseller-prod-cae, East US 2)
  ├─ Verifica ownership leyendo asuid.{host} TXT == customDomainVerificationId
  ├─ Recibe el cert subido desde cPanel como custom certificate
  └─ Bind del cert al Container App → SniEnabled (Azure termina TLS)

Dev — mseller.dev (Cloudflare + cert administrado por Azure):

Cloudflare: DNS de mseller.dev
  ├─ portal.api               CNAME → mseller-dev-portal-api.nicebeach-03e666f2.centralus.azurecontainerapps.io.    (DNS-only / gray cloud)
  ├─ ingestion.api            CNAME → mseller-dev-ingestion-api.nicebeach-03e666f2.centralus.azurecontainerapps.io. (DNS-only / gray cloud)
  ├─ consumo.api              CNAME → mseller-dev-consumo-api.nicebeach-03e666f2.centralus.azurecontainerapps.io.   (DNS-only / gray cloud)
  └─ asuid.{cada-subdominio}  TXT   → <verification-id>  (3 TXT records, mismo valor)

Azure Container Apps (mseller-dev-cae, Central US)
  ├─ Verifica ownership leyendo asuid.{host} TXT == customDomainVerificationId
  ├─ Emite cert administrado (managed certificate) automáticamente
  └─ Bind del cert al Container App → SniEnabled (Azure termina TLS)

El customDomainVerificationId es uno solo por tenant de Azure AD — por eso los 6 TXT records (3 en cPanel + 3 en Cloudflare) tienen el mismo valor. Si rotás el tenant o creás otro environment en otro tenant, vas a tener un valor distinto.

En dev los registros inician en DNS-only (gray cloud). Más adelante el de portal.api.mseller.dev se cambia a proxied (orange cloud) para meter Cloudflare WAF / Access encima del UI y /swagger (ver la sección final). consumo.api e ingestion.api se quedan DNS-only para no agregar latencia ni romper integraciones externas.

Agregar un nuevo hostname (por ejemplo, una API nueva)

Decidir el hostname siguiendo la convención

{api-nueva}.api.mseller.app para prod, {api-nueva}.api.mseller.dev para dev. No agregues más niveles de subdominio salvo que tengas una razón concreta.

Obtener el FQDN del Container App + verification ID

# CAE-id + región del Container App ya existente. En tu nuevo Container
# App van a venir de propiedades equivalentes.
APP_NAME=mseller-prod-<api>-api
RG=mseller-prod-rg

az containerapp show -n $APP_NAME -g $RG \
  --query "{fqdn:properties.configuration.ingress.fqdn, verificationId:properties.customDomainVerificationId}" \
  -o json

Agregar los 2 registros DNS

Para prod — en cPanel → Zone Editor → mseller.app, agregar:

TipoNombreValorTTL
CNAME{api}.api<fqdn-azure>. (con punto final)14400
TXTasuid.{api}.api<verification-id> (64 hex chars)14400

El valor TXT NO debe llevar comillas (cPanel)

cPanel agrega las comillas alrededor del valor cuando sirve el record. Si vos las ponés en el campo, el TXT termina con doble-comilla (""5F6A..."") y Azure rechaza la verificación. Pegá el valor pelado.

Para dev — en Cloudflare → DNS → mseller.dev, agregar:

TipoNombreContenidoProxy statusTTL
CNAME{api}.api<fqdn-azure> (sin punto final en Cloudflare)DNS only (gray cloud)Auto
TXTasuid.{api}.api<verification-id> (64 hex chars)n/aAuto

Proxy debe estar OFF (gray cloud) al inicio

Si dejás Cloudflare proxy activo (orange cloud) antes de validar el hostname y bindear el cert administrado en Azure, la verificación falla porque Azure no ve directamente el FQDN del Container App. Recién después de tener SniEnabled y el cert verde se puede cambiar portal.api a proxied (ver sección final). El TXT asuid.* siempre va DNS-only.

Verificar DNS antes de seguir

Esperá ~5-30 min (la propagación puede tardar más si el TTL anterior fue alto) y verificá:

EXPECTED_TXT="<verification-id>"
HOST="{api}.api.mseller.app"          # o {api}.api.mseller.dev para dev

# CNAME debe apuntar a *.azurecontainerapps.io
dig +short CNAME $HOST @1.1.1.1

# TXT debe match exacto
[ "$(dig +short TXT asuid.$HOST @1.1.1.1 | tr -d '"')" = "$EXPECTED_TXT" ] \
  && echo "✓ ready" || echo "✗ TXT mismatch"

Agregar el custom_hostname en Terraform

En envs/prod/main.tf (o envs/dev/main.tf), el bloque local.custom_hostnames ya genera los nombres a partir de local.apis + var.apex_domain. Si la API nueva ya está en la lista local.apis, no hace falta tocar nada acá — el module.api la levanta automáticamente.

Si querés un override custom (por ejemplo un hostname con un nivel de subdominio distinto), editá el local en main.tf:

custom_hostnames = local.apex_domain == null ? {} : {
  portal    = "portal.api.${local.apex_domain}"
  ingestion = "ingestion.api.${local.apex_domain}"
  consumo   = "consumo.api.${local.apex_domain}"
  nueva-api = "nuevaapi.${local.apex_domain}"   # custom path, no usa el patrón api.*
}

Apply de terraform

Si CI está activo, mergeá el PR → terraform-apply.yml corre solo. Si lo hacés local:

cd envs/prod   # o envs/dev
terraform plan -out=tfplan
terraform apply tfplan

El plan debe mostrar 1 add: module.api["<api>"].azurerm_container_app_custom_domain.this[0].

Asociar el cert y hacer el bind

Terraform crea el binding del hostname pero deja bindingType = Disabled intencionalmente (ver el lifecycle.ignore_changes en modules/api-container-app/main.tf). El cert se gestiona out-of-band y la forma depende del entorno:

Prod (cert de cPanel AutoSSL):

  1. Exportar el cert desde cPanel. En cPanel → SSL/TLS → Manage SSL Sites, ubicá el dominio *.mseller.app (o el SAN específico del nuevo host) y copiá el bloque PEM del certificado y la private key. Guardalos como mseller-app.crt y mseller-app.key localmente. Si el cert es nuevo y AutoSSL todavía no lo emitió, forzá una corrida desde AutoSSL antes de continuar.

  2. Subir a Azure y bindear. Cargá el cert al CAE y ataalo al hostname:

    # 1) subir el cert al Container App Environment (una vez por cert)
    az containerapp env certificate upload \
      --name <cae-name> -g <rg> \
      --certificate-file mseller-app.crt \
      --certificate-key mseller-app.key \
      --certificate-name mseller-app-<yyyy-mm>
    
    # 2) bindear el hostname al cert subido
    az containerapp hostname bind \
      --name <app-name> -g <rg> \
      --hostname <hostname> \
      --environment <cae-name> \
      --certificate mseller-app-<yyyy-mm>

Tarda ~1-3 min en práctica (el WARNING dice "up to 20 minutes"; en nuestra experiencia siempre fue mucho menos). Después de esto el cert queda SniEnabled y HTTPS funciona.

Dev (cert administrado por Azure):

En dev no se sube cert manual — Azure emite y renueva un cert administrado por sí solo una vez que el hostname está validado y el CNAME (DNS-only en Cloudflare) resuelve al FQDN del Container App.

# bindear el hostname pidiendo un cert administrado
az containerapp hostname bind \
  --name <app-name> -g <rg> \
  --hostname <hostname> \
  --environment <cae-name> \
  --validation-method CNAME

La emisión tarda ~3-15 min. Después de esto el cert queda SniEnabled y HTTPS funciona, y Azure se encarga de renovar automáticamente.

Prod: cuando AutoSSL renueva, hay que repetir el paso 2

cPanel AutoSSL emite el cert de prod cada ~6 meses pero no lo empuja a Azure. La rotación necesita un upload + bind manual (o un script). Si HTTPS se rompe ~6 meses después de un setup que andaba en prod, esa es la causa #1. Ver la sección de troubleshooting. En dev el cert es administrado por Azure y no requiere esta sincronía.

Verificar HTTPS funcionando

# prod
curl -sI https://{api}.api.mseller.app/health
# dev
curl -sI https://{api}.api.mseller.dev/health

Tiene que devolver HTTP/2 200. Si tira Could not resolve host o se queda colgado, esperá unos minutos más (puede ser cache DNS) y volvé a verificar el cert binding:

az containerapp show -n <app-name> -g <rg> \
  --query "properties.configuration.ingress.customDomains[].{name:name, bindingType:bindingType}" \
  -o table

bindingType debe ser SniEnabled. Si está Disabled todavía, el cert no se emitió — revisar los logs del comando hostname bind.

Cambiar un hostname existente

No es muy común, pero si necesitás cambiar (por ejemplo, mover de portal.api.mseller.app a api.mseller.app):

  1. Agregar primero el hostname nuevo siguiendo los pasos arriba. Va a convivir con el viejo durante la transición.
  2. Notificar a clientes (mobile, portal admin, otros consumers) que actualicen su base URL — dales una ventana de 7-30 días.
  3. Monitorear tráfico sobre el hostname viejo. Cuando llegue a ~0 reqs/min:
  4. Remover el hostname viejo — eliminar el bloque del Terraform (o quitar la entry de custom_hostnames), apply, y borrar los DNS records en cPanel.

Los certs viejos se borran solos cuando el hostname se desvincula.

Rollback / desactivar custom domains

Si por alguna razón querés volver a los FQDNs *.azurecontainerapps.io (por ejemplo, debug profundo o el provider tiene un problema con custom domains):

# Setear apex_domain = null en terraform.tfvars y aplicar
# Esto remueve los 3 azurerm_container_app_custom_domain.this resources
# pero deja los Container Apps + DNS intactos.
cd envs/prod
terraform apply

Después el hostname custom (portal.api.mseller.app en prod, portal.api.mseller.dev en dev, etc.) deja de responder sobre HTTPS porque Azure ya no tiene el binding. Los Container Apps siguen sirviendo en <app>.<env-id>.<region>.azurecontainerapps.io como siempre. Los registros DNS en cPanel (prod) o Cloudflare (dev) quedan apuntando al FQDN viejo (pueden quedarse así o limpiarse).

Troubleshooting

Could not resolve host: portal.api.mseller.app (o .mseller.dev)

DNS no propagó todavía o el record falta. Verificá:

# prod
dig +short CNAME portal.api.mseller.app @1.1.1.1
dig +short CNAME portal.api.mseller.app @8.8.8.8   # otro resolver para comparar

# dev
dig +short CNAME portal.api.mseller.dev @1.1.1.1
dig +short CNAME portal.api.mseller.dev @8.8.8.8

Si está vacío en uno y poblado en otro, esperá la propagación. Si está vacío en ambos, falta el record — en cPanel para prod (mseller.app) o en Cloudflare para dev (mseller.dev).

TLS handshake falla / curl: (35)

El binding del cert está Disabled. Probablemente el az containerapp hostname bind falló o nunca se corrió. Revisá:

az containerapp show -n <app> -g <rg> \
  --query "properties.configuration.ingress.customDomains[]" -o json

Si bindingType es Disabled, corré el comando hostname bind nuevamente.

Cannot verify ownership of hostname

Azure no pudo leer el asuid TXT en el momento del binding. Causas comunes:

  1. TXT con valor mal copiado — debe ser exactamente el customDomainVerificationId (64 hex chars), sin comillas, sin dots abreviados.
  2. DNS no propagó — esperar y reintentar.
  3. TXT en el subdominio equivocado — debe ser asuid.<host>, no asuid solo ni <host> solo.
  4. Cloudflare proxy activo en dev — si el CNAME de {api}.api.mseller.dev está proxied (orange cloud) antes de validar, Azure no resuelve directo al Container App y la verificación falla. Dejá el record DNS-only hasta tener cert SniEnabled.

El cert de prod andaba y dejó de funcionar ~6 meses después

cPanel AutoSSL renueva su cert automáticamente, pero la copia que vive en Azure es una snapshot del momento que se subió — no se sincroniza sola. Aplica sólo a prod (en dev el cert es administrado por Azure y se renueva solo). Verificá fechas:

# 1) Qué cert tiene Azure asociado al hostname
az containerapp env certificate list -g <rg> -n <cae-name> -o table

# 2) Qué cert está sirviendo el host ahora mismo
echo | openssl s_client -connect <hostname>:443 -servername <hostname> 2>/dev/null \
  | openssl x509 -noout -dates -issuer

Si el cert que sirve <hostname> está vencido o el notAfter de Azure es viejo, repetí los dos pasos del bloque "Prod (cert de cPanel AutoSSL)" con el cert nuevo de cPanel. Antes de eso, confirmá en cPanel que AutoSSL ya emitió el nuevo cert (a veces hay que forzar la corrida si fue justo cerca del vencimiento).

Cert status Failed en Azure después de un upload

Causas comunes:

  • Cert subido no cubre el hostname — el SAN del cert tiene que incluir el host exacto. AutoSSL puede emitir certs separados por subdominio; revisá en cPanel que el SAN list cubra el host antes de exportar.
  • CNAME del hostname cambió y rompió la verificación — revisá que el CNAME (cPanel para prod, Cloudflare para dev) siga apuntando al FQDN correcto del Container App.

Restringiendo el UI y Swagger de dev con Cloudflare

Por defecto los hostnames de dev resuelven directo a Azure (DNS-only, gray cloud) — útil mientras se hace el setup inicial y para que mobile e integraciones externas peguen sin Cloudflare en el medio. Pero el Portal de dev expone /swagger y rutas de admin UI que no deberían ser públicas; para eso se mete Cloudflare como WAF / Access encima de portal.api.mseller.dev solamente.

Cambiar portal.api.mseller.dev a proxied

Una vez que el cert administrado de Azure está SniEnabled y HTTPS funciona DNS-only, en Cloudflare → DNS de mseller.dev, editar el registro portal.api y cambiar el proxy status a Proxied (orange cloud).

consumo.api.mseller.dev se queda DNS-only (gray cloud) — la app móvil pega directo a Azure, sin Cloudflare en el medio (latencia + una capa menos que puede romper sesiones largas o subidas grandes).

ingestion.api.mseller.dev también se queda DNS-only, a menos que se conozcan las IPs de origen de SAP / integraciones externas y se puedan allow-listear en una regla. Si no, el riesgo de bloquear un callback legítimo es mayor que el beneficio.

Configurar SSL/TLS mode "Full (strict)"

En Cloudflare → SSL/TLS → Overview de la zona mseller.dev, poner el modo en Full (strict). Esto hace que Cloudflare verifique el cert del origen (el cert administrado de Azure) en cada request.

Cualquier modo más laxo (Flexible, Full sin strict) deja huecos: el tráfico Cloudflare→Azure puede ir en HTTP o aceptar certs inválidos, lo que rompe el propósito de tener TLS end-to-end.

Bloquear /swagger y rutas de admin con Cloudflare Access

En Cloudflare → Zero Trust → Access → Applications, crear una Self-hosted application sobre mseller.dev con:

  • Application domain: cloud.mseller.dev (portal admin de dev) y portal.api.mseller.dev (paths /swagger, /swagger/*, /swagger-ui, /swagger-ui/*, más rutas de admin UI específicas).
  • Identity provider: el que la cuenta de Cloudflare tenga conectado (Microsoft Entra ID, Google Workspace, o el One-Time PIN por email si no hay IdP).
  • Access policy (Allow):
    • Emails ending in @mseller.app — el equipo interno.
    • Emails ending in @itsoluclick.com — el equipo de ITSoluClick.

Cualquier otro email es rechazado y Cloudflare Access muestra la pantalla "You do not have permission". El usuario autorizado pasa el SSO una vez y la sesión vale ~24 h (configurable en la application).

Como respaldo, se puede crear una WAF custom rule en Cloudflare → Security → WAF → Custom rules de mseller.dev que bloquee las mismas rutas si el request no trae la cookie de sesión de Cloudflare Access (CF_Authorization). Esto cubre el caso en que un cambio en la app de Access se desconfigure o quede en modo bypass.

Los emails que aparecen en la sesión de Access son los que el IdP devuelve verificados — no es necesario mantener allow-lists individuales: la regla por dominio se aplica al instante a cualquier nuevo empleado que se añada al directorio.

Documentar las reglas en algún lado

Las reglas Cloudflare hoy se configuran a mano en el dashboard. Si en el futuro queremos infra-as-code para esto (replicar el setup en otra zona, code review de cambios, rollback), hay que añadir el provider terraform-provider-cloudflare en mseller-infrastructure. Mientras tanto, screenshots o un export de la regla en un issue son suficientes.

Referencias