1. The problem I’m having:
I have successfully migrated from nginx + certbot to caddy.
However, I had registered my Account ID used by and with certbot here: ECDSA availability in production environment - API Announcements - Let's Encrypt Community Support
This allowed me to get certificates with full ECDSA chain (ISRG Root X2 / E1 as shown here Chain of Trust - Let's Encrypt).
In order to get this same ECDSA chain from caddy, I must reuse the same Account ID as in certbot.
I did find this ID in certbot data files and tried to put it in caddy_data/caddy/acme/acme-v02.api.letsencrypt.org-directory/users/my@email.com/letsencrypt.json
I also copied the pkey to caddy_data/caddy/acme/acme-v02.api.letsencrypt.org-directory/users/my@email.com/letsencrypt.key
(I had to convert it to PEM because certbot stores it in JWK)
I then delete the certificate files and restart caddy.
But it seems that caddy doesn’t pickup these values and registers a new account each time.
In the logs hereunder, 1608793527
is a new Account ID generated on the fly, not the one I’m trying to use.
I also set preferred_chains in Caddyfile.
What am I missing?
2. Error messages and/or full log output:
caddy | 2024-03-08T16:29:54.437117775Z {"level":"info","ts":1709915394.4368443,"logger":"http","msg":"waiting on internal rate limiter","identifiers":["abc.example.com"],"ca":"https://acme-v02.api.letsencrypt.org/directory","account":"my@email.com"}
caddy | 2024-03-08T16:29:54.437216126Z {"level":"info","ts":1709915394.4370458,"logger":"http","msg":"done waiting on internal rate limiter","identifiers":["abc.example.com"],"ca":"https://acme-v02.api.letsencrypt.org/directory","account":"my@email.com"}
caddy | 2024-03-08T16:29:54.817002937Z {"level":"info","ts":1709915394.816721,"logger":"http.acme_client","msg":"trying to solve challenge","identifier":"abc.example.com","challenge_type":"dns-01","ca":"https://acme-v02.api.letsencrypt.org/directory"}
caddy | 2024-03-08T16:29:59.596502148Z {"level":"info","ts":1709915399.5958266,"logger":"http.acme_client","msg":"authorization finalized","identifier":"abc.example.com","authz_status":"valid"}
caddy | 2024-03-08T16:29:59.596688553Z {"level":"info","ts":1709915399.5960453,"logger":"http.acme_client","msg":"validations succeeded; finalizing order","order":"https://acme-v02.api.letsencrypt.org/acme/order/1608793527/250653778277"}
caddy | 2024-03-08T16:30:00.532433354Z {"level":"info","ts":1709915400.5321703,"logger":"http.acme_client","msg":"successfully downloaded available certificate chains","count":2,"first_url":"https://acme-v02.api.letsencrypt.org/acme/cert/0383e04defea93bbb346fbbe28b5cdf69c50"}
caddy | 2024-03-08T16:30:00.533385715Z {"level":"warn","ts":1709915400.533001,"logger":"http","msg":"did not find chain matching preferences; using first"}
caddy | 2024-03-08T16:30:00.534770312Z {"level":"info","ts":1709915400.5345626,"logger":"tls.obtain","msg":"certificate obtained successfully","identifier":"abc.example.com"}
caddy | 2024-03-08T16:30:00.535024495Z {"level":"info","ts":1709915400.5349102,"logger":"tls.obtain","msg":"releasing lock","identifier":"abc.example.com"}
3. Caddy version:
v2.7.6 h1:w0NymbG2m9PcvKWsrXO6EEkY9Ru4FJK8uQbYcev1p3A=
4. How I installed and ran Caddy:
a. System environment:
Docker version 25.0.3, build 4debf41
b. Command:
c. Service/unit/compose file:
d. My complete Caddy config:
{
# Global options block.
# Customizes the admin API endpoint. Accepts placeholders. Takes network addresses.
# Default: localhost:2019, unless the CADDY_ADMIN environment variable is set.
# If set to off, then the admin endpoint will be disabled. When disabled, config changes will be impossible without stopping and starting the server, since the caddy reload command uses the admin API to push the new config to the running server.
admin off
log {
output stderr
format json
}
# Disable HTTP-to-HTTPS redirects.
# https://caddyserver.com/docs/caddyfile/options#auto-https
auto_https disable_redirects
# TLS Options
email my@email.com
# key_type ed25519|p256|p384|rsa2048|rsa4096
key_type p384
cert_issuer acme {
email my@email.com
preferred_chains {
root_common_name "ISRG Root X2"
}
}
acme_dns ovh {
# https://github.com/caddy-dns/ovh
# https://github.com/ovh/go-ovh
endpoint ovh-eu
application_key redacted
application_secret redacted
consumer_key redacted
}
# GeoIP2
# https://github.com/zhangjiayin/caddy-geoip2
order geoip2_vars first
# Only configure databaseDirectory and editionID when autoupdate is not desired.
geoip2 {
# accountId ooo
databaseDirectory "/tmp/"
# licenseKey "ooo"
lockFile "/tmp/geoip2.lock"
editionID "GeoLite2-Country"
# updateUrl "https://updates.maxmind.com"
# updateFrequency 86400 # in seconds
}
}
(headers) {
map {host} {src-policies} {
abc.example.com redacted
def.example.com redacted
ghi.example.com redacted
default "default-src 'self'; object-src 'none'; style-src 'self'; script-src 'self'; font-src 'self';"
}
map {host} {referrer-policy} {
jkl.example.com redacted
default "no-referrer"
}
# https://owasp.org/www-project-secure-headers/
# https://infosec.mozilla.org/guidelines/web_security
header {
# https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
Content-Security-Policy "{src-policies} form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests;"
# https://owasp.org/www-project-secure-headers/#referrer-policy
Referrer-Policy {referrer-policy}
# Enable HTTP Strict Transport Security (HSTS)
Strict-Transport-Security "max-age=15768000; includeSubDomains;"
# Disallow sniffing of X-Content-Type-Options
X-Content-Type-Options "nosniff"
X-Download-Options "noopen"
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
# Disallow the site to be rendered within a frame (clickjacking protection)
# If you want to use FIDO2 WebAuthn, set X-Frame-Options to "SAMEORIGIN" or the Browser will block those requests
X-Frame-Options "SAMEORIGIN"
X-Permitted-Cross-Domain-Policies "none"
# Prevent search engines from indexing (optional)
X-Robots-Tag "noindex, nofollow"
# Disable cross-site filter (XSS)
# Deprecated in favor of the Content-Security-Policy, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection
# It is thus recommended to set the header as X-XSS-Protection: 0
X-XSS-Protection "0"
# Remove headers
# Remove Server name
-Server
# Remove X-Powered-By though this shouldn t be an issue, better opsec to remove
-X-Powered-By
# Remove Last-Modified because etag is the same and is as effective
-Last-Modified
}
}
(geoip2) {
# strict: Alway ignore 'X-Forwarded-For' header
# wild: Trust 'X-Forwarded-For' header if existed
# trusted_proxies: Trust 'X-Forwarded-For' header if trusted_proxies is also valid (see https://caddyserver.com/docs/caddyfile/options#trusted-proxies)
# default: trusted_proxies
geoip2_vars strict
# Add country code to the header
header geoip-country "{geoip2.country_code}"
# Block any country not France, allowing private ranges
# private_ranges: https://caddyserver.com/docs/caddyfile/matchers#client-ip
@denied {
not client_ip private_ranges
not expression ({geoip2.country_code} == "FR")
}
respond @denied 444 {
close
}
}
(only_private) {
# Block any non private ranges
# private_ranges: https://caddyserver.com/docs/caddyfile/matchers#client-ip
@denied {
not client_ip private_ranges
}
respond @denied 444 {
close
}
}
(logs) {
log {args[0]}_access {
format json
output file /logs/{args[0]}_access.log {
roll_size 10MB
roll_keep 10
}
}
}
mno.example.com {
reverse_proxy http://10.2.6.2:80
rewrite * /alexa{uri}
uri strip_suffix /
import headers
import logs mno.example.com
@denied {
not header_regexp useragent_asksonic User-Agent "ghi"
}
respond @denied 444 {
close
}
}
def.example.com {
reverse_proxy http://10.2.2.3:80
import headers
import logs def.example.com
import geoip2
}
ghi.example.com {
reverse_proxy http://10.2.7.2:80
import headers
import logs ghi.example.com
import only_private
}
abc.example.com {
reverse_proxy http://10.2.1.3:80
import headers
import logs abc.example.com
geoip2_vars strict
header geoip-country "{geoip2.country_code}"
@denied {
not client_ip private_ranges
not expression ({geoip2.country_code} == "US")
not header User-Agent "bcd"
not header_regexp useragent_hasslambda User-Agent "abc"
}
respond @denied 444 {
close
}
}
jkl.example.com {
reverse_proxy http://10.2.3.2:80
import headers
import logs jkl.example.com
import only_private
}