Skip to content

Locked out of the console by a certificate

When to use: a browser refuses postmaster.<your domain>, webmail.<your domain> or the mail hostname with a certificate error (NET::ERR_CERT_DATE_INVALID, SEC_ERROR_EXPIRED_CERTIFICATE, a name mismatch) and offers no “proceed anyway” link, or mailctl doctor reports a certificate … row as ✖. Preconditions: SSH to the host. Nothing here needs the console, which is the point.

The admin panel and the mail client send Strict-Transport-Security (six months). A browser that has seen it refuses to let anybody click past a certificate error on those names until the header expires, so a certificate that lapses, or one that does not name the host, locks that browser out instead of showing a warning. This is deliberate: the warning is what an attacker in the middle would show too. Mail clients (IMAP, SMTP) do not read the header, but they refuse a bad certificate on their own, so they are probably failing as well.

The fix is a valid certificate. As soon as one is back, every browser works again. Waiting does not fix it: a browser keeps the header for six months after it last saw it.

  1. See what is wrong, on the host:

    Terminal window
    mailctl doctor

    The certificate <name> rows say, per name, whether a browser would accept the certificate and when it expires: CERT_HAS_EXPIRED, ERR_TLS_CERT_ALTNAME_INVALID (the certificate does not name that host) or DEPTH_ZERO_SELF_SIGNED_CERT. To look by hand:

    Terminal window
    openssl s_client -connect 127.0.0.1:443 -servername postmaster.<your domain> </dev/null 2>/dev/null |
    openssl x509 -noout -subject -ext subjectAltName -dates
  2. Let’s Encrypt certificates (tls.mode: acme). Caddy renews 30 days before expiry, so an expired one means renewal has been failing for weeks. Read why:

    Terminal window
    docker logs mailserver-caddy-1 2>&1 | grep -iE 'acme|obtain|renew|challenge' | tail -20

    The usual causes: port 80 closed by a firewall (the HTTP challenge needs it), the name’s DNS record no longer points at this host, or a Let’s Encrypt rate limit after many failures. Fix the cause. Caddy keeps retrying with back-off; docker restart mailserver-caddy-1 makes it try now, and it keeps the configuration it was running.

  3. Your own certificate (tls.mode: byo). Copy the new chain and key into the certs volume, exactly as for a renewal (Bring your own TLS certificate → Steps 3), then restart the edge so it loads them:

    Terminal window
    docker run --rm -v mailserver_certs:/certs -v "$PWD":/in alpine \
    sh -c 'cp /in/fullchain.pem /certs/byo/fullchain.pem && cp /in/privkey.pem /certs/byo/privkey.pem && chown 2525:2525 /certs/byo/*.pem && chmod 640 /certs/byo/privkey.pem'
    docker restart mailserver-caddy-1

    cert-sync hands the same certificate to Postfix and Dovecot within 30 s. The certificate must name the mail hostname, postmaster. and webmail.: one that misses a name locks that name out the same way.

  4. Do not switch TLS off to get in. tls.mode: none is refused on a public address, and a browser that has seen the header would refuse the plain-HTTP console anyway.

mailctl doctor shows every certificate … row ✔ with a future expiry, and https://postmaster.<your domain>/ opens in a browser that was locked out, with nothing cleared on its side.

If one browser still refuses after that, it has cached the bad certificate’s session: close it completely and open it again. As a last resort, and only for that one browser, Chrome forgets the header at chrome://net-internals/#hsts (Delete domain security policies); Firefox forgets it when you Forget About This Site in its history.

Nothing here changes configuration. A certificate restored over SSH is the one the server was supposed to have.