docs: Updates to TLS page (Caddy, testing, etc) (#3981)
This commit is contained in:
parent
942920615c
commit
ac22caf74e
|
@ -481,113 +481,108 @@ DSM-generated letsencrypt certificates get auto-renewed every three months.
|
||||||
|
|
||||||
### Caddy
|
### Caddy
|
||||||
|
|
||||||
For Caddy v2 you can specify the `key_type` in your server's global settings, which would end up looking something like this if you're using a `Caddyfile`:
|
[Caddy][web::caddy] is an open-source web server with built-in TLS certificate generation. You can use the [official Docker image][dockerhub::caddy] and write your own `Caddyfile`.
|
||||||
|
|
||||||
```caddyfile
|
!!! example
|
||||||
{
|
|
||||||
debug
|
|
||||||
admin localhost:2019
|
|
||||||
http_port 80
|
|
||||||
https_port 443
|
|
||||||
default_sni example.com
|
|
||||||
key_type rsa2048
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If you are instead using a json config for Caddy v2, you can set it in your site's TLS automation policies:
|
```yaml title="compose.yaml"
|
||||||
|
services:
|
||||||
|
# Basic Caddy service to provision certs:
|
||||||
|
reverse-proxy:
|
||||||
|
image: caddy:2.7
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 443:443
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- ${CADDY_DATA_DIR}:/data
|
||||||
|
|
||||||
??? example "Caddy v2 JSON example snippet"
|
# Share the Caddy data volume for certs and configure SSL_TYPE to `letsencrypt`
|
||||||
|
mailserver:
|
||||||
|
image: ghcr.io/docker-mailserver/docker-mailserver:latest
|
||||||
|
hostname: mail.example.com
|
||||||
|
environment:
|
||||||
|
SSL_TYPE: letsencrypt
|
||||||
|
# While you could use a named data volume instead of a bind mount volume, it would require the long-syntax to rename cert files:
|
||||||
|
# https://docs.docker.com/compose/compose-file/05-services/#volumes
|
||||||
|
volumes:
|
||||||
|
- ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.crt:/etc/letsencrypt/live/mail.example.com/fullchain.pem
|
||||||
|
- ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.key:/etc/letsencrypt/live/mail.example.com/privkey.pem
|
||||||
|
```
|
||||||
|
|
||||||
```json
|
```caddyfile title="Caddyfile"
|
||||||
{
|
mail.example.com {
|
||||||
"apps": {
|
tls internal {
|
||||||
"http": {
|
key_type rsa2048
|
||||||
"servers": {
|
|
||||||
"srv0": {
|
|
||||||
"listen": [
|
|
||||||
":443"
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"match": [
|
|
||||||
{
|
|
||||||
"host": [
|
|
||||||
"mail.example.com",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "subroute",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"body": "",
|
|
||||||
"handler": "static_response"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"terminal": true
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tls": {
|
|
||||||
"automation": {
|
|
||||||
"policies": [
|
|
||||||
{
|
|
||||||
"subjects": [
|
|
||||||
"mail.example.com",
|
|
||||||
],
|
|
||||||
"key_type": "rsa2048",
|
|
||||||
"issuer": {
|
|
||||||
"email": "admin@example.com",
|
|
||||||
"module": "acme"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"issuer": {
|
|
||||||
"email": "admin@example.com",
|
|
||||||
"module": "acme"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Optional, can be useful for troubleshooting
|
||||||
|
# connection to Caddy with correct certificate:
|
||||||
|
respond "Hello DMS"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The generated certificates can then be mounted:
|
While DMS does not need a webserver to work, this workaround will provision a TLS certificate for DMS to use.
|
||||||
|
|
||||||
```yaml
|
- [`tls internal`][caddy-docs::tls-internal] will create a local self-signed cert for testing. This targets only the site-address, unlike the global `local_certs` option.
|
||||||
volumes:
|
- [`key_type`][caddy-docs::key-type] can be used in the `tls` block if you need to enforce RSA as the key type for certificates provisioned. The default is currently ECDSA (P-256).
|
||||||
- ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.crt:/etc/letsencrypt/live/mail.example.com/fullchain.pem
|
|
||||||
- ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.key:/etc/letsencrypt/live/mail.example.com/privkey.pem
|
|
||||||
```
|
|
||||||
|
|
||||||
### Traefik v2
|
??? example "With `caddy-docker-proxy`"
|
||||||
|
|
||||||
[Traefik][traefik::github] is an open-source application proxy using the [ACME protocol][ietf::rfc::acme]. [Traefik][traefik::github] can request certificates for domains and subdomains, and it will take care of renewals, challenge negotiations, etc. We strongly recommend to use [Traefik][traefik::github]'s major version 2.
|
Using [`lucaslorentz/caddy-docker-proxy`][github::caddy-docker-proxy] allows you to generate a `Caddyfile` by adding labels to your services in `compose.yaml`:
|
||||||
|
|
||||||
[Traefik][traefik::github]'s storage format is natively supported if the `acme.json` store is mounted into the container at `/etc/letsencrypt/acme.json`. The file is also monitored for changes and will trigger a reload of the mail services (Postfix and Dovecot).
|
```yaml title="compose.yaml"
|
||||||
|
services:
|
||||||
|
reverse-proxy:
|
||||||
|
image: lucaslorentz/caddy-docker-proxy:2.8
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 443:443
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ${CADDY_DATA_DIR}:/data
|
||||||
|
labels:
|
||||||
|
# Set global config here, this option has an empty value to enable self-signed certs for local testing:
|
||||||
|
# NOTE: Remove this label when going to production.
|
||||||
|
caddy.local_certs: ""
|
||||||
|
|
||||||
Wildcard certificates are supported. If your FQDN is `mail.example.com` and your wildcard certificate is `*.example.com`, add the ENV: `#!bash SSL_DOMAIN=example.com`.
|
# Use labels to configure Caddy to provision DMS certs
|
||||||
|
mailserver:
|
||||||
|
image: ghcr.io/docker-mailserver/docker-mailserver:latest
|
||||||
|
hostname: mail.example.com
|
||||||
|
environment:
|
||||||
|
SSL_TYPE: letsencrypt
|
||||||
|
volumes:
|
||||||
|
- ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.crt:/etc/letsencrypt/live/mail.example.com/fullchain.pem
|
||||||
|
- ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.key:/etc/letsencrypt/live/mail.example.com/privkey.pem
|
||||||
|
labels:
|
||||||
|
# Set your DMS FQDN here to add the site-address into the generated Caddyfile:
|
||||||
|
caddy_0: mail.example.com
|
||||||
|
# Add a dummy directive is required:
|
||||||
|
caddy_0.respond: "Hello DMS"
|
||||||
|
# Uncomment to make a proxy for Rspamd
|
||||||
|
# caddy_1: rspamd.example.com
|
||||||
|
# caddy_1.reverse_proxy: "{{upstreams 11334}}"
|
||||||
|
```
|
||||||
|
|
||||||
DMS will select it's certificate from `acme.json` checking these ENV for a matching FQDN (_in order of priority_):
|
!!! warning "Caddy certificate location varies"
|
||||||
|
|
||||||
1. `#!bash ${SSL_DOMAIN}`
|
The path contains the certificate provisioner used. This path may be different from the example above for you and may change over time when multiple provisioner services are used][dms-pr-feedback::caddy-provisioning-gotcha].
|
||||||
2. `#!bash ${HOSTNAME}`
|
|
||||||
3. `#!bash ${DOMAINNAME}`
|
|
||||||
|
|
||||||
This setup only comes with one caveat: The domain has to be configured on another service for [Traefik][traefik::github] to actually request it from _Let's Encrypt_, i.e. [Traefik][traefik::github] will not issue a certificate without a service / router demanding it.
|
This can make the volume mounting for DMS to find the certificates non-deterministic, but you can [restrict provisioning to single service via the `acme_ca` setting][caddy::restrict-acme-provisioner].
|
||||||
|
|
||||||
|
### Traefik
|
||||||
|
|
||||||
|
[Traefik][traefik::github] is an open-source application proxy using the [ACME protocol][ietf::rfc::acme]. Traefik can request certificates for domains and subdomains, and it will take care of renewals, challenge negotiations, etc.
|
||||||
|
|
||||||
|
Traefik's storage format is natively supported if the `acme.json` store is mounted into the container at `/etc/letsencrypt/acme.json`. The file is also monitored for changes and will trigger a reload of the mail services (Postfix and Dovecot).
|
||||||
|
|
||||||
|
DMS will select it's certificate from `acme.json` prioritizing a match for the DMS FQDN (hostname), while also checking one DNS level up (_eg: `mail.example.com` => `example.com`_). Wildcard certificates are supported.
|
||||||
|
|
||||||
|
This setup only comes with one caveat - The domain has to be configured on another service for Traefik to actually request it from _Let's Encrypt_ (_i.e. Traefik will not issue a certificate without a service / router demanding it_).
|
||||||
|
|
||||||
???+ example "Example Code"
|
???+ example "Example Code"
|
||||||
|
|
||||||
Here is an example setup for [`docker-compose`](https://docs.docker.com/compose/):
|
Here is an example setup for [`docker-compose`](https://docs.docker.com/compose/):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -716,7 +711,7 @@ The local and internal paths may be whatever you prefer, so long as both `SSL_CE
|
||||||
|
|
||||||
## Testing a Certificate is Valid
|
## Testing a Certificate is Valid
|
||||||
|
|
||||||
- From your host:
|
!!! example "Connect to DMS on port 25"
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker exec mailserver openssl s_client \
|
docker exec mailserver openssl s_client \
|
||||||
|
@ -725,26 +720,42 @@ The local and internal paths may be whatever you prefer, so long as both `SSL_CE
|
||||||
-CApath /etc/ssl/certs/
|
-CApath /etc/ssl/certs/
|
||||||
```
|
```
|
||||||
|
|
||||||
- Or:
|
The response should show the certificate chain with a line further down: `Verify return code: 0 (ok)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This example runs within the DMS container itself to verify the cert is working locally.
|
||||||
|
|
||||||
|
- Adjust the `-connect` IP if testing externally from another system. Additionally testing for port 143 (Dovecot IMAP) is encouraged (_change the protocol for `-starttls` from `smtp` to `imap`_).
|
||||||
|
- `-CApath` will help verify the certificate chain, provided the location contains the root CA that signed your TLS cert for DMS.
|
||||||
|
|
||||||
|
??? example "Verify certificate dates"
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker exec mailserver openssl s_client \
|
docker exec mailserver openssl s_client \
|
||||||
-connect 0.0.0.0:143 \
|
-connect 0.0.0.0:25 \
|
||||||
-starttls imap \
|
-starttls smtp \
|
||||||
-CApath /etc/ssl/certs/
|
-CApath /etc/ssl/certs/ \
|
||||||
|
2>/dev/null | openssl x509 -noout -dates
|
||||||
```
|
```
|
||||||
|
|
||||||
And you should see the certificate chain, the server certificate and: `Verify return code: 0 (ok)`
|
!!! tip "Testing and troubleshooting"
|
||||||
|
|
||||||
In addition, to verify certificate dates:
|
If you need to test a connection without resolving DNS, `curl` can connect with `--resolve` option to map an FQDN + Port to an IP address, instead of the request address provided.
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
docker exec mailserver openssl s_client \
|
# NOTE: You may want to use `--insecure` if the cert was provisioned with a private CA not present on the curl client:
|
||||||
-connect 0.0.0.0:25 \
|
# Use `--verbose` for additional insights on the connection.
|
||||||
-starttls smtp \
|
curl --resolve mail.example.com:443:127.0.0.1 https://mail.example.com
|
||||||
-CApath /etc/ssl/certs/ \
|
```
|
||||||
2>/dev/null | openssl x509 -noout -dates
|
|
||||||
```
|
Similarly with `openssl` you can connect to an IP as shown previously, but provide an explicit SNI if necessary with `-servername mail.example.com`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Both `curl` and `openssl` also support `-4` and `-6` for enforcing IPv4 or IPv6 lookup.
|
||||||
|
|
||||||
|
This can be useful, such as when [DNS resolves the IP to different servers leading to different certificates returned][dms-discussion::gotcha-fqdn-bad-dns]. As shown in that link, `step certificate inspect` is also handy for viewing details of the cert returned or on disk.
|
||||||
|
|
||||||
## Plain-Text Access
|
## Plain-Text Access
|
||||||
|
|
||||||
|
@ -919,3 +930,13 @@ Despite this, if you must use non-standard DH parameters or you would like to sw
|
||||||
[acme-companion::standalone]: https://github.com/nginx-proxy/acme-companion/blob/main/docs/Standalone-certificates.md
|
[acme-companion::standalone]: https://github.com/nginx-proxy/acme-companion/blob/main/docs/Standalone-certificates.md
|
||||||
[acme-companion::standalone-changes]: https://github.com/nginx-proxy/acme-companion/blob/main/docs/Standalone-certificates.md#picking-up-changes-to-letsencrypt_user_data
|
[acme-companion::standalone-changes]: https://github.com/nginx-proxy/acme-companion/blob/main/docs/Standalone-certificates.md#picking-up-changes-to-letsencrypt_user_data
|
||||||
[acme-companion::service-loop]: https://github.com/nginx-proxy/acme-companion/blob/main/docs/Container-utilities.md
|
[acme-companion::service-loop]: https://github.com/nginx-proxy/acme-companion/blob/main/docs/Container-utilities.md
|
||||||
|
|
||||||
|
[web::caddy]: https://caddyserver.com
|
||||||
|
[dockerhub::caddy]: https://hub.docker.com/_/caddy
|
||||||
|
[github::caddy-docker-proxy]: https://github.com/lucaslorentz/caddy-docker-proxy
|
||||||
|
[dms-pr-feedback::caddy-provisioning-gotcha]: https://github.com/docker-mailserver/docker-mailserver/pull/3485/files#r1297512818
|
||||||
|
[caddy-docs::tls-internal]: https://caddyserver.com/docs/caddyfile/directives/tls#syntax
|
||||||
|
[caddy-docs::key-type]: https://caddyserver.com/docs/caddyfile/options#key-type
|
||||||
|
[caddy::restrict-acme-provisioner]: https://caddy.community/t/is-there-a-way-on-a-caddyfile-to-force-a-specific-acme-ca/14506
|
||||||
|
|
||||||
|
[dms-discussion::gotcha-fqdn-bad-dns]: https://github.com/docker-mailserver/docker-mailserver/issues/3955#issuecomment-2027882633
|
||||||
|
|
Loading…
Reference in New Issue