# HTTPS with Let's Encrypt

URL: https://scalws.com/docs/howto/lets-encrypt/
Source: `docs/howto/lets-encrypt.md` in the ScalWS repository

Get and renew certificates from Let's Encrypt automatically, test against the staging CA
first, then redirect HTTP to HTTPS and `www.` to the bare domain.

ScalWS has its own ACME client ([ADR-0018](https://scalws.com/docs/adr/0018-acme/)); no certbot is needed. It
uses the HTTP-01 challenge only.

## Prerequisites

* DNS `A`/`AAAA` records of every name pointing at the server, including `www.<domain>`
  when the site serves or redirects it.
* Ports 80 and 443 reachable from the internet and ending at ScalWS (not at another web
  server or a load balancer that redirects). The package's first install opens them in an
  active ufw or a running firewalld; otherwise open them yourself
  ([firewall](https://scalws.com/docs/guide/installation/#firewall)).
* No wildcard names, IP addresses or `localhost`: they cannot get HTTP-01 certificates.
  Use a certificate file for them (see [TLS](https://scalws.com/docs/guide/tls/#tls-listeners)).

## 1. Listeners and `server.acme`, against staging first

Edit `/etc/scalws/scalws.yaml`. Three things are required: a plain-HTTP listener for the
challenges, a TLS listener with `acme: true`, and `server.acme`:

```yaml
include: sites.d/*.yaml
server:
  listeners:
    - {name: http, address: "0.0.0.0:80"}
    - {name: https, address: "0.0.0.0:443", tls: {acme: true}}
  acme:
    enabled: true
    directory: https://acme-staging-v02.api.letsencrypt.org/directory
    state_dir: /var/lib/scalws/acme-staging
    contact: ["mailto:ops@example.com"]
    terms_of_service_agreed: true
```

* [`server.acme.directory`](https://scalws.com/docs/reference/server-acme/#server.acme.directory): the
  staging CA, to stay clear of the production rate limits while you test.
* [`server.acme.state_dir`](https://scalws.com/docs/reference/server-acme/#server.acme.state_dir): a
  separate directory for staging. Certificates are kept per site in the state directory
  and are not ordered again just because `directory` changed.
* [`server.acme.contact`](https://scalws.com/docs/reference/server-acme/#server.acme.contact): `mailto:`
  addresses only.
* [`server.acme.terms_of_service_agreed`](https://scalws.com/docs/reference/server-acme/#server.acme.terms_of_service_agreed)
  must be `true`.
* [`tls: {acme: true}`](https://scalws.com/docs/reference/server-listeners/#server.listeners-.tls.acme) on
  the listener serves the ACME certificates.

Apply. A new listener needs a restart under `scalwsd.service`; the controller binds it on
reload:

```sh
sudo scalwsctl check
sudo scalws-controller reload        # under scalws-controller
sudo systemctl restart scalwsd       # under scalwsd.service
```

## 2. The site

With `server.acme` enabled, every site file without a [`tls`](https://scalws.com/docs/reference/site-file/#tls)
key gets `tls: auto`: a certificate for all its names. `scalwsctl site add` needs no
extra option:

```sh
sudo scalwsctl site add example.com --root /srv/www/example.com
```

```yaml
# /etc/scalws/sites.d/example.com.yaml
domain: example.com
root: /srv/www/example.com
www: redirect
```

With [`www: redirect`](https://scalws.com/docs/reference/site-file/#www) (the default of `site add` for a
name with one dot), `www.example.com` answers with a `301` to `https://example.com`,
path and query kept. That redirect is an application of its own with its own certificate,
so `www.example.com` needs DNS too. `www: alias` serves `www.example.com` as part of the
site (one certificate for both names); `www: none` does not serve it.

In `scalws.yaml` itself the equivalent is
[`acme: true`](https://scalws.com/docs/reference/applications/#tenants-.applications-.acme) on the
application.

## 3. Watch the order

```sh
scalwsctl certs                                           # under scalwsd.service
scalwsctl --socket /run/scalws/web-0/admin.sock certs     # under scalws-controller
```

`certs` lists each ACME application with its state (`ok`, `pending`, `error`, `missing`),
days left, names, and the last error with the time of the next retry. Log lines use the
target `scalws::acme`:

```sh
journalctl -u scalwsd | grep scalws::acme
sudo grep '"scalws::acme"' /var/lib/scalws-controller/logs/web-0.log
```

A new certificate is activated without a restart: new handshakes get it, existing
connections keep theirs. Staging certificates are not trusted by browsers;
`curl -k https://example.com/` shows that the chain is in place.

## 4. Switch to production

Remove the `directory` and `state_dir` lines (the defaults are the production CA and
`/var/lib/scalws/acme`), check and reload:

```yaml
include: sites.d/*.yaml
server:
  listeners:
    - {name: http, address: "0.0.0.0:80"}
    - {name: https, address: "0.0.0.0:443", tls: {acme: true}}
  acme:
    enabled: true
    contact: ["mailto:ops@example.com"]
    terms_of_service_agreed: true
```

```sh
sudo scalwsctl check
sudo scalws-controller reload        # or: sudo scalwsctl reload
curl -sI https://example.com/
```

## 5. Redirect HTTP to HTTPS

Once `certs` shows `ok`, add [`https_redirect`](https://scalws.com/docs/reference/site-file/#https_redirect)
to the site:

```yaml
domain: example.com
root: /srv/www/example.com
www: redirect
https_redirect: true
```

(`site add ... --https-redirect` sets it for new sites.) Plain-HTTP requests then get a
`301` to the same host, path and query over `https://` (always port 443). ACME challenges
are still answered over HTTP, so renewals keep working. ScalWS does not add
`Strict-Transport-Security`; send it from the application or with
`Header set Strict-Transport-Security "max-age=31536000"` in an
[`.htaccess`](https://scalws.com/docs/howto/htaccess/) file.

## Renewals

Nothing to schedule. The certificate manager checks every ACME application at start,
every [`check_interval`](https://scalws.com/docs/reference/server-acme/#server.acme.check_interval)
(default `12h`) and after every configuration change, and orders a new certificate when:

* the application has none yet,
* its names changed (an added alias, `www` changed), or
* it expires within [`renew_before`](https://scalws.com/docs/reference/server-acme/#server.acme.renew_before)
  (default `30d`).

Under scalws-controller the instances share the state directory: one places the order,
all of them answer the challenge and load the certificate. The metric
`scalws_acme_certificate_expiry_seconds{tenant,app}` holds each expiry time, for alerts.

Back up the state directory yourself: `scalwsctl backup` does not include it.

## Common problems

* **`state` stays `error`**: DNS does not point here, port 80 does not reach ScalWS, or a
  rate limit was hit. A failed order is retried after 5 minutes, then with doubling delays
  up to 24 hours. The delay is kept in the server's memory; after fixing DNS or the
  firewall, `sudo systemctl restart scalwsd` (`sudo systemctl restart scalws-controller` under the controller) retries at once.
* **`ACME HTTP-01 needs a plain HTTP listener`**: add the port-80 listener.
* **`ACME is enabled but no TLS listener has acme: true`**: add `tls: {acme: true}` to the
  HTTPS listener.
* **`must be true (the CA's terms of service are accepted on account creation)`**: set
  `terms_of_service_agreed: true`.
* **`wildcard domains need DNS-01, which is not supported`**: use a certificate file and
  `tls: manual` for that site.
* **Browsers warn after switching to production**: the staging certificate is still in
  use because `state_dir` was not changed back; remove the `state_dir` line.
* **Handshake fails before the first certificate**: until an application has its first
  certificate, its names get the listener's `default` certificate if there is one, and a
  failed handshake otherwise.

See also: [TLS](https://scalws.com/docs/guide/tls/#lets-encrypt-acme),
[quick start](https://scalws.com/docs/guide/quick-start/#5-https-with-lets-encrypt),
[troubleshooting](https://scalws.com/docs/guide/troubleshooting/#certificates-and-acme).
