Ingress and automatic TLS

How to expose applications on port 443 with certificates issued and renewed automatically, without operating an external router.

Networking·7 min read·last reviewed 2026-04-26

HeroCtl ships with an integrated router embedded in the control plane. You do not install, configure, or update it. When the cluster comes up, the router is already active on ports 80 and 443 of every server node.

That means exposing an application on the internet is a single line in the job spec.

The shortest path

job: minha-api
ingress:
  host: api.exemplo.com
  port: http
  tls: true

With that the cluster does three things in sequence:

  1. Resolves the host api.exemplo.com to any healthy allocation of the job.
  2. Requests a valid certificate for that domain on the first request.
  3. Renews that certificate before expiry, with no intervention.

You only need to point the DNS for api.exemplo.com to the server node IPs. The rest runs by itself.

How the certificate is issued

HeroCtl uses Let's Encrypt as the default certificate authority. Behind that there are two supported validation mechanisms.

HTTP-01 (default)

Works for any public domain that points to the cluster. When an authority asks for proof of ownership, the integrated router responds at the /.well-known/acme-challenge/ path automatically. Nothing needs to be configured.

Warning: HTTP-01 requires port 80 to be externally accessible. If the firewall blocks 80, issuance fails.

DNS-01 (for wildcards)

If you need a wildcard certificate (*.exemplo.com), use DNS-01. Configure the DNS provider in the cluster spec:

acme:
  challenge: dns-01
  provider: cloudflare
  credentials:
    api_token: ${secret.cloudflare_token}

Natively supported providers: Cloudflare, Route53, DigitalOcean, Hostinger, Hetzner. Others go through an external plugin.

With DNS-01 active, just declare:

ingress:
  host: "*.exemplo.com"
  tls: true

HTTP → HTTPS redirect

When tls: true, all traffic arriving on port 80 is redirected with 301 to 443. You do not need to duplicate configuration.

To force HSTS:

ingress:
  host: api.exemplo.com
  tls: true
  hsts:
    enabled: true
    max_age: 31536000
    include_subdomains: true
    preload: false

Do not enable preload without understanding the commitment. It is hard to reverse.

Multiple domains for the same app

Common pattern: www.exemplo.com and exemplo.com pointing to the same application, with one being canonical.

ingress:
  host: exemplo.com
  redirect_from:
    - www.exemplo.com
  tls: true

Each domain in redirect_from gets its own certificate and answers 301 to the canonical host.

Path-based routing

Same host, different applications on different paths:

# job: site-marketing
ingress:
  host: exemplo.com
  path: /
  tls: true

# job: api-publica
ingress:
  host: exemplo.com
  path: /api
  tls: true

The router resolves precedence by specificity. /api/users lands in api-publica. /sobre lands in site-marketing.

Custom headers

To add or remove headers in the response:

ingress:
  host: api.exemplo.com
  tls: true
  headers:
    add:
      X-Frame-Options: DENY
      X-Content-Type-Options: nosniff
      Referrer-Policy: strict-origin
    remove:
      - Server
      - X-Powered-By

Basic rate limiting

First-line defense against abuse:

ingress:
  host: api.exemplo.com
  tls: true
  rate_limit:
    requests_per_second: 100
    burst: 200
    by: ip

More elaborate limits (per token, per endpoint, with bypass for partners) belong in the application.

Troubleshooting

Certificate was not issued

Check three things, in order:

SymptomLikely causeWhat to do
dial tcp: timeout in the challenge logPort 80 closedOpen 80/tcp inbound on server nodes
unauthorized: Invalid responseDNS points elsewheredig +short api.exemplo.com should return a server node IP
too many failed authorizationsYou hit the Let's Encrypt rate limitWait 1h and review the config before trying again

Inspect the issuance state:

heroctl ingress cert-status api.exemplo.com

Certificate expired without renewing

Renewal runs 30 days before expiry. If you are past that, errors have piled up:

heroctl ingress cert-renew api.exemplo.com --force

Also check whether redirect_from still points to the cluster. A domain that was moved to another provider without being removed from the spec breaks renewal silently.

Intermittent ACME challenge

Symptom: sometimes it issues, sometimes it fails. Usually it is DNS round-robin with one IP that does not respond on 80, or a Cloudflare in front in proxy mode. For HTTP-01, the domain needs to resolve directly to the cluster during the challenge. Use DNS-01 if you keep Cloudflare proxy on.

Next steps

  • Configure firewall to allow only what is needed.
  • Understand the secrets model before injecting DNS tokens into the spec.
#ingress#tls#https#acme#lets-encrypt#network