Users' guide

Domains, routing and default certificates

This page explains how ScalWS picks the application for a request from its host name, how it picks the TLS certificate for a connection from the SNI name, and how the two relate.

Two separate lookups are involved:

  • Routing uses the request’s host (the Host header or the request target’s authority) and the domains of all applications. It decides which application answers.
  • Certificate selection uses the SNI name of the TLS handshake and the server_names of the listener’s certificates (plus ACME certificates). It decides which certificate the client sees.

A certificate only has to be valid for the name; it does not choose the application.

Configuring domains

tenants:
  - name: shop
    applications:
      - name: storefront
        domains: [shop.example.com, www.shop.example.com]
        root: /srv/www/shop
        runtime: {type: static}
      - name: customers
        domains: ["*.customers.example.com"]
        runtime: {type: proxy, upstream: "http://127.0.0.1:8080"}

Every application needs at least one domain (at least one domain is required). Domains are global: routing does not depend on the listener, so an application answers on every listener (plain HTTP and TLS) for its domains. Use https_redirect to send plain-HTTP requests to HTTPS.

Normalisation

Configured names are normalised when the configuration is loaded:

  • lower-cased, and a trailing dot removed (Example.COM. is example.com);
  • each label is 1 to 63 characters of a-z, 0-9 and -, and does not start or end with -; the whole name is at most 253 characters;
  • internationalised names must be written in punycode (xn--...);
  • an IPv4 address such as 127.0.0.1 is a valid domain (the packaged configuration serves localhost and 127.0.0.1); IPv6 addresses are not.

An invalid name fails the check with invalid domain "...": labels must be 1-63 chars of a-z, 0-9 and '-' (use punycode for IDNs).

Request hosts are normalised the same way before the lookup:

  • the port is removed (example.com:8443 is example.com), so routing does not depend on the port;
  • lower-cased, a trailing dot removed;
  • an IPv6 literal is written in its canonical form ([0::1] and [::0:1] are both [::1]);
  • a host with an empty label, a character other than letters, digits, - and _, a label longer than 63 characters, an invalid port, or more than 255 characters in all is refused with 400.

Exact names and wildcards

  • An exact name matches only itself.

  • *.example.com covers exactly one extra label: it matches a.example.com, but not example.com and not a.b.example.com. List example.com separately if it should be served too.

  • The suffix of a wildcard must have at least two labels: *.com fails with wildcard "*.com" must cover a multi-label suffix. The * may only be the whole first label (foo.*.com is invalid).

  • **.example.com covers one or more extra labels: it matches a.example.com and a.b.example.com, never example.com itself. Its suffix needs two labels as well (**.com is invalid) and it cannot be combined with acme: true (it would need DNS-01).

  • The lookup order is: the exact name, *. (one label), **. (the longest matching suffix first, so **.b.example.com wins over **.example.com), then the catch-all "*".

  • An exact name wins over a wildcard, whatever the order in the configuration:

    domains: ["*.example.com"]        # application A
    # ...
    domains: [api.example.com]        # application B: api.example.com goes here

Duplicate domains

A normalised name may be used by one application only, across all tenants and all site files. A second use fails the check, for example:

tenants[1].applications[0].domains[0]: domain "example.com" is already used by blog/blog

Example.com, example.com and example.com. are the same name. *.example.com and a.example.com are different names and may belong to different applications (the exact name wins).

Unknown hosts

A request whose host matches no domain gets 404 Not Found. It is counted with tenant and application _unmatched in the request metrics and the access log. There is no catch-all application; to answer such requests differently, add the names you expect (for example the server’s IP address) to an application’s domains.

A request without a host, with more than one Host header, or with a Host that differs from the authority of the request target gets 400. The full order of checks is in How a request is processed.

Site files and www

A site file in sites.d/ describes one site with a main domain, optional aliases and a www setting. It is expanded into applications with the same rules as above:

wwwResult
none (the default in the file)www.<domain> is not served (unless another application lists it).
aliaswww.<domain> is added to the site’s domains.
redirectA second application <name>-www serves www.<domain> with a 301 redirect to <domain>, path and query kept. The target is https:// when the site uses ACME or the server has a TLS listener, http:// otherwise.

scalwsctl site add uses --www redirect by default for a domain with one dot such as example.com, otherwise none. A www. name that is already another site’s domain or alias is a duplicate and fails the check.

Certificates and SNI

Each TLS listener has its own certificate table, built from its certificates and, with acme: true, the ACME certificates of the applications.

server:
  listeners:
    - name: https
      address: "0.0.0.0:443"
      tls:
        certificates:
          - cert: /etc/scalws/tls/example.com/fullchain.pem
            key: /etc/scalws/tls/example.com/privkey.pem
            server_names: [example.com, "*.example.com"]
            default: true
          - cert: /etc/scalws/tls/example.net/fullchain.pem
            key: /etc/scalws/tls/example.net/privkey.pem
            server_names: [example.net]

For each new TLS connection the certificate is chosen from the SNI name:

  1. an exact entry in server_names (or an ACME certificate for that name);
  2. otherwise a wildcard entry, with the same one-label rule as for domains;
  3. otherwise the listener’s default certificate;
  4. otherwise the handshake fails. It is counted in scalws_rejections_total with reason="tls_handshake".

A client that sends no SNI (for example a request to an IP address) gets the default certificate, or a failed handshake when there is none.

Rules checked when the configuration is loaded:

  • every name in server_names is normalised like a domain and must be covered by the certificate (certificate is not valid for server name "...");
  • a name may be assigned to one certificate per listener (server name "..." is assigned to more than one certificate);
  • at most one certificate per listener may be default (at most one certificate may be marked default);
  • a TLS listener needs at least one certificate or acme: true (a TLS listener needs at least one certificate (or acme: true));
  • when a configured certificate and an ACME certificate cover the same name, the configured one wins. ACME certificates are exact names only: with acme: true on an application, wildcard domains, IP addresses and localhost are refused.

Until an application has its first ACME certificate, handshakes for its names get the listener’s default certificate, or fail without one. Marking a certificate default therefore keeps HTTPS answering (with a certificate warning in browsers) while orders are pending. More in tls.md.

Strict SNI and 421

Browsers may reuse one HTTP/2 connection for several host names when the certificate covers them all (connection coalescing). The request’s host then differs from the SNI name of the connection, and ScalWS routes it by the host as usual.

With strict_sni: true on a TLS listener, such a request gets 421 Misdirected Request, and the client retries on a new connection whose SNI matches. The comparison uses the normalised host: for an exact domain the configured name, otherwise the request host. Requests on connections without SNI are not affected, and a host that matches no application still gets 404 (the host lookup comes first). Refusals are counted in scalws_security_blocked_total with reason="misdirected".

strict_sni on a listener without TLS fails the check with only applies to TLS listeners.

See also