# HTTPS and certificates
URL: https://synquic.tech/docs/guides/https-certificates.md

How Synq gets free Let's Encrypt certificates over HTTP-01 without Cloudflare, issues wildcard certificates through a DNS provider, installs your own certificate, redirects HTTP to HTTPS, and renews everything.

import { Step, Steps } from 'fumadocs-ui/components/steps';

Every Synq server runs an **edge**: OpenResty listening on ports 80 and 443 in front of all your apps. The edge
ends HTTPS for each domain with its own certificate. This page explains where those certificates come from and
what you can change.

In short:

- A normal domain gets a free **Let's Encrypt** certificate over **HTTP-01**. You only need an `A` record and
  port 80 open. No Cloudflare account, no DNS provider, no API token.
- A **wildcard** (`*.example.com`) needs **DNS-01**, which needs a connected DNS provider (Cloudflare today).
- You can **upload your own certificate** instead, for example a Cloudflare Origin CA certificate.
- Once a real certificate is in place, plain `http://` is **redirected to HTTPS** automatically.
- Certificates **renew by themselves** 14 days before they expire.

## Let's Encrypt over HTTP-01 (no Cloudflare needed)

HTTP-01 is how Let's Encrypt checks that you control a name: it asks for a file at
`http://<hostname>/.well-known/acme-challenge/<token>` on port 80. The edge answers that request itself, so
your apps keep serving while it happens, and a successful answer proves the name points at this server.

### What the domain needs

1. **An `A` record** for the hostname pointing at the server's public IPv4 address. The Domains page and the
   project's DNS records panel show the exact value.
2. **Port 80 reachable from the internet.** Open it in the server's firewall and in any cloud security group
   or NAT in front of it. Port 443 must be open for visitors too.
3. **No `AAAA` record pointing somewhere else.** Let's Encrypt tries IPv6 first. Either point the `AAAA` at
   this server or delete it.
4. **No other `A` records** for the same name pointing at other machines; the challenge may land on one of them.
5. **The Synq edge owns port 80.** If nginx, Apache or Caddy still holds it, take it over first (see
   [The edge](/docs/guides/the-edge#taking-over-a-server-thats-already-serving)).
6. **With a CDN in front** (for example Cloudflare's proxy, the orange cloud), turn the proxy off (**DNS only**)
   until the certificate is issued, or make sure the CDN forwards plain HTTP on port 80 to the server.

<Callout title="Verification is the certificate" type="info">
On a Synq server, "verifying" a domain means obtaining its certificate. Synq doesn't compare your DNS with the
server's IP and doesn't ask for an ownership `TXT` record: a certificate can only be issued if the name really
reaches this server, which is the proof. The live DNS check on the Domains page is advice to help you get
there.
</Callout>

### Issue a certificate

You can start issuance from three places. All of them stream certbot's log live, so you see the real reason if
it fails.

- **Domains page**: open the row's **⋯** menu and choose **Verify and issue certificate** (or **Issue
  certificate** for a domain that's already verified).
- **Project → Domains & Routes**: press **Verify \{hostname\}** under the DNS records, or the **Verify** button on the
  domain's card.
- **CLI**: `synq domain verify <domainId>` (get the ID from `synq domain list --project <projectId>`).

```bash
synq domain list --project <projectId>
synq domain verify <domainId>
```

A new domain whose project has never deployed doesn't request a certificate yet: the first deploy creates the
route, and the certificate follows once the domain verifies. You don't need to redeploy after a successful
verify; the edge re-registers the existing route with TLS.

If the server already holds a valid certificate for the name with more than 14 days left, Synq reuses it and
doesn't ask Let's Encrypt again ("a valid certificate is already present; no new certificate was requested").

### Automatic retries

You don't have to keep pressing Verify. The **Domain DNS verification** job (`domains:verify-pending`) runs
about every 13 minutes. It first checks a pending domain once it is 10 minutes old, then backs off after each
failure: 15 minutes, 30 minutes, 1 hour, 2 hours, 4 hours, then every 6 hours. It also finishes certificates
for verified domains whose first issuance failed. Only domains of projects with a live deployment are
retried. The domain's details show the **Next scheduled check**; press **Verify** for an immediate retry.
You can see and change the job under **Jobs** in the sidebar.

<Callout title="Mind Let's Encrypt's rate limits" type="warn">
Let's Encrypt limits how often a name may fail validation and how many certificates you can get for the same
names each week. When issuance fails, read the reason in the log and fix it before retrying. To experiment
freely, switch to the staging directory (see [Certificate authority settings](#certificate-authority-settings)).
</Callout>

## Wildcard certificates with a DNS provider

A wildcard certificate such as `*.example.com` covers every single-level subdomain (`a.example.com`,
`b.example.com`) with one certificate. Let's Encrypt only issues wildcards through **DNS-01**: Synq creates a
temporary `_acme-challenge` `TXT` record through your DNS provider's API, waits for Let's Encrypt to read it,
then deletes it.

<Steps>

<Step>

### Connect your DNS provider

Cloudflare is the supported provider. In Cloudflare, create an API token at
[dash.cloudflare.com/profile/api-tokens](https://dash.cloudflare.com/profile/api-tokens) with **Zone:Read** and
**DNS:Edit** on the zones Synq should manage.

In Synq, open **Settings → Credentials** (organization admins), choose **Cloudflare**, give it a **Label**,
paste the **API token** and save. Synq checks the token with Cloudflare before storing it, encrypted. The
Certificates tab of the Domains page then says "A DNS provider is connected, so wildcard certificates are
available."

</Step>

<Step>

### Point the wildcard at the server

At Cloudflare, add an `A` record for `*` (that is, `*.example.com`) pointing at the server's IP, so traffic
for the subdomains reaches the edge.

</Step>

<Step>

### Add the wildcard domain

Open the project's **Domains & Routes** tab, press **Add domain**, choose **Custom domain** and type
`*.example.com`. **Use DNS-01 challenge** switches on by itself and can't be turned off for a wildcard
("Wildcard certificates require DNS-01 and a connected DNS provider."), and **Include www** is disabled.
Press **Add domain**. The Domains page wizard accepts a wildcard hostname too.

</Step>

<Step>

### Verify

Press **Verify**. The log shows "Requesting a certificate for \*.example.com (DNS-01 via connected DNS
provider)" and, when it succeeds, the certificate is installed for every subdomain.

</Step>

</Steps>

You can also use DNS-01 for a normal hostname: turn on **Use DNS-01 challenge** when adding it ("Issue and
renew the certificate through your connected DNS provider. Use this when port 80 cannot be reached."). Renewals
then use DNS-01 too.

Limits:

- A wildcard covers one level: `*.example.com` covers `app.example.com` but not `example.com` itself or
  `a.b.example.com`. Add the apex as its own domain if you need it.
- Only Cloudflare is supported as a DNS provider. Without a connected provider, verification stops with
  "DNS-01 challenge for \*.example.com requires a connected DNS provider (e.g. Cloudflare) in Settings → DNS."
  (the provider lives under **Settings → Credentials** in the current panel).
- If Cloudflare rejects the stored token, the message starts with "DNS provider credential rejected". Replace
  the token under **Settings → Credentials**.

<Callout title="Free subdomains don't need a wildcard certificate" type="info">
The free `<app>.<base>` subdomains rely on a wildcard **DNS record** (`*.<base>`), not a wildcard certificate.
Each app gets its own HTTP-01 certificate, so no DNS provider is needed for them.
</Callout>

## Upload your own certificate

Use this when the certificate must come from somewhere else:

- **Cloudflare Full (strict)**: Cloudflare connects to your server over HTTPS and requires a certificate it
  trusts. A free [Cloudflare Origin CA](https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/)
  certificate (valid up to 15 years) is a good fit.
- A certificate from your company's CA, or one you already bought.

<Steps>

<Step>

### Verify the domain first

The domain must be verified before a certificate can be installed. A domain behind a proxy that owns TLS is
usually added with **External ingress & TLS**, which verifies without issuing anything.

</Step>

<Step>

### Open the upload dialog

On the **Domains** page, open the row's **⋯** menu and choose **Upload certificate**, or use **Upload
certificate** in the domain's menu on the project's **Domains & Routes** tab.

</Step>

<Step>

### Paste the PEM files

Paste the **Certificate (PEM, full chain)**, starting with `-----BEGIN CERTIFICATE-----` and including the
intermediates, and the matching **Private key (PEM)**. Press **Install certificate**.

</Step>

</Steps>

Before touching the edge, Synq checks that the certificate parses, that the key matches it, that it covers the
hostname and that it hasn't expired. A certificate that fails is refused with "Invalid certificate: ..." and
nothing changes, because a broken pair would stop OpenResty from reloading for every other domain too.

After the upload the certificate shows as **Uploaded**, with **Auto-renew: Manual**. Synq stops running certbot
for that domain and never renews it. There is no expiry reminder for uploaded certificates: watch the countdown
on the Certificates tab and upload a new certificate before it lapses.

## HTTPS redirect

You don't switch the redirect on; it follows the certificate. The **HTTP to HTTPS** column on the Domains page
shows the current state:

| State | Behaviour |
| --- | --- |
| **On** | The domain has a real certificate (issued or uploaded). Every plain `http://` request gets a `301` to `https://<hostname><path>?<query>`, on every path the domain serves. |
| **After certificate** | No certificate yet. HTTP requests are served normally so the site works while the certificate is pending; the redirect starts as soon as it is active. |
| **Upstream** | TLS is handled by your own proxy or CDN (external ingress). Visitors reach HTTPS through that proxy, so configure the HTTP to HTTPS redirect there. |

Two details matter behind a CDN:

- A request that arrives with the header `X-Forwarded-Proto: https` is **not** redirected. That header means a
  CDN (for example Cloudflare in **Flexible** mode) already served HTTPS to the visitor and reached the server
  on port 80; redirecting it would loop forever. The app is told the request was HTTPS.
- Let's Encrypt's challenge path `/.well-known/acme-challenge/` is never redirected, so renewals keep working.

Synq doesn't send an HSTS header. If you want one, add it for a domain with
[custom proxy config](/docs/guides/custom-proxy-config#examples).

## Renewal

The **SSL certificate renewal** job (`ssl:renew`) runs once a day at 03:17 (server time). It renews every
certificate that Synq issued and that has fewer than 14 days left, up to 50 per run. Uploaded certificates and
domains whose TLS ends at your own proxy are skipped.

- The **Automatic renewal** card on the Domains page's Certificates tab shows the next and last run.
- **Renew \{n\} due** on the Domains tab renews every due certificate right away.
- **Renew certificate** in a row's menu forces one renewal; `synq domain renew <domainId>` does the same, and
  `synq domain renew-all` renews every near-expiry certificate in your organization.
- **Recheck certificate** (`synq domain verify-ssl <domainId>`) only reads what's on the server, without asking
  Let's Encrypt, and updates the state.

When the job fails to renew a certificate, the domain's certificate turns **Failed** and an **SSL cert
expiring** notification is sent (see [Notifications](/docs/guides/notifications)).

```bash
synq domain renew <domainId>
synq domain renew-all
synq domain verify-ssl <domainId>
```

## Certificate authority settings

By default certbot uses Let's Encrypt's production directory. You can point it at another ACME certificate
authority, add a contact email, or change the key type with environment variables on the server. The
**Certificate authority** card on the Certificates tab shows the values in effect.

| Variable | What it sets |
| --- | --- |
| `SYNQ_ACME_EMAIL` | Contact email for the ACME account; the CA sends expiry notices here. With no email at all, certificates are requested without a contact address. An instance administrator can override it on the Certificates tab. |
| `SYNQ_ACME_DIRECTORY_URL` | ACME directory. Unset means Let's Encrypt production. Staging is `https://acme-staging-v02.api.letsencrypt.org/directory`; ZeroSSL is `https://acme.zerossl.com/v2/DV90`. |
| `SYNQ_ACME_EAB_KID` and `SYNQ_ACME_EAB_HMAC_KEY` | External account binding, required by CAs such as ZeroSSL. Set both or neither. |
| `SYNQ_ACME_KEY_TYPE` | `ec256`, `ec384`, `rsa2048` or `rsa4096`. Unset uses certbot's default. |
| `SYNQ_ACME_CA_BUNDLE` | Absolute path to a CA root bundle, for a private ACME server whose certificate isn't publicly trusted. |
| `SYNQ_ACME_TOS_AGREED` | Accept the CA's terms of service non-interactively. Defaults to `true`. |

To change them on a server installed with the installer, add the lines to `/root/.synq/compose/.env` on the
server (your additions are kept across updates), then apply them with `sudo synq up`. For example, to test with
the staging directory:

```bash
# on the server
echo 'SYNQ_ACME_DIRECTORY_URL=https://acme-staging-v02.api.letsencrypt.org/directory' | sudo tee -a /root/.synq/compose/.env
sudo synq up
```

The **Provider** row on the card then reads **Let's Encrypt (staging)**. Browsers don't trust staging
certificates, so remove the line and apply again to go back to production.

## What visitors see before an app is live

The edge answers every hostname that reaches it, even when nothing is deployed there. So you know what you're
looking at:

| Situation | What the visitor gets |
| --- | --- |
| **Not deployed yet**: a verified custom domain whose project has never had a live deployment | A `503` page titled "\{hostname\} is not deployed yet": "This domain is connected and ready. The site will appear here as soon as its first deployment goes live." |
| **Last deploy failed** before anything went live | A `503` page titled "\{hostname\} is not live": "The last deploy failed, so there is nothing to serve yet. See the dashboard for the deploy log." |
| The app's container isn't answering (restarting, crashed, booting) | A `502` page titled "Application unavailable". |
| A hostname no route knows (a typo, a stray wildcard record, a visit by IP) | A `404` page titled "Service not found". |

The "not deployed yet" page is served with the domain's **real certificate**, so the browser shows a padlock
rather than a certificate warning, and it tells browsers and crawlers to retry later (`Retry-After: 120`,
`Cache-Control: no-store`, `noindex`). It appears after the domain is verified, and the first successful deploy
replaces it with your app.

## Related

<Cards>
  <Card title="Custom domains" href="/docs/guides/custom-domains" description="Add a domain to a project, with www, redirects and external ingress." />
  <Card title="Domains page" href="/docs/dashboard/domains" description="Every domain and certificate at a glance, with renew and upload." />
  <Card title="Cloudflare Tunnel and external proxies" href="/docs/guides/cloudflare-tunnel" description="Let another proxy own HTTPS for a domain." />
  <Card title="Domains, DNS & SSL troubleshooting" href="/docs/troubleshooting/domains-ssl" description="Every failure message explained." />
</Cards>
