This is a continuation of the previous note DNS and SSL at Home.

Here is how to actually configure the central Caddy and all the others to use a single self-signed certificate authority.

Central Caddy

Caddyfile:

https://ca.internal.example.com {
    bind 192.168.1.1
     acme_server {
        lifetime 768h
        allow {
            domains internal.example.com *.internal.example.com
            ip_ranges 192.168.1.0/24 127.0.0.0/8 ::1/128
        }
    }
    tls internal

    # EST (RFC 7030)
    @est_cacerts path /.well-known/est/cacerts /cacerts
    handle @est_cacerts {
        root * /opt/etc/caddy/tpl
        rewrite * /cacerts.pem
        header Content-Type "application/x-pem-file"
        templates {
            mime application/x-pem-file
        }
        file_server
    }

    root * /opt/share/caddy/pki/authorities/local
    @certs path /root.crt /intermediate.crt
    handle @certs {
        header Content-Disposition "attachment; filename={http.request.uri.path.file}"
        file_server
    }

    @fallback not path /acme/*
    handle @fallback {
        respond "use /root.crt , /intermediate.crt or /.well-known/est/cacerts" 200
    }
}

192.168.1.1 {
    tls internal
    root * /opt/share/caddy/start
    file_server
}

:80 {
    root * /opt/share/caddy/start
    file_server
}

/opt/etc/caddy/tpl/cacerts.pem:

{{- httpInclude "/root.crt"}}{{httpInclude "/intermediate.crt" -}}

Secondary Caddy Instances

Caddyfile:

{
    # Key line — point to the internal CA instead of Let's Encrypt
    acme_ca https://ca.internal.example.com:8443/acme/local/directory
}

app1.internal.example.com {
    reverse_proxy 127.0.0.1:8080
}

app2.internal.example.com {
    reverse_proxy 127.0.0.1:8081
}

Computers

# Download
curl -kfL -o ./ca-internal-example-com.crt https://ca.internal.example.com/root.crt
curl -kfL -o ./ca-internal-example-com.pem https://ca.lan.stepin.ru/.well-known/est/cacerts

# Debian/Ubuntu:
sudo cp ca-internal-example-com.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

# RHEL/Rocky/Alma:
sudo cp ca-internal-example-com.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust

# Macos
sudo security add-trusted-cert \
  -d \
  -r trustRoot \
  -k /Library/Keychains/System.keychain \
  ./ca-internal-example-com.pem
sudo security find-certificate -c "Caddy Internal CA" /Library/Keychains/System.keychain

# Firefox
brew install nss
PROFILE=$(ls -d ~/Library/Application\ Support/Firefox/Profiles/* 2>/dev/null | head -1)
echo "profile: $PROFILE"
# Add the CA with trust C,T,C (trusted for SSL/TLS)
certutil -A \
  -n "Caddy Internal CA" \
  -t "C,T,C" \
  -i ./ca-internal-example-com.pem \
  -d sql:"$PROFILE"
certutil -L -d sql:"$PROFILE" | grep -i caddy

# Windows
# Win+R → certlm.msc → Trusted Root Certification Authorities → Certificates → right-click → All Tasks → Import.

A few words about security: Caddy is configured to issue certificates only for internal IPs and the home network domain. So it should be quite safe to add it to the system.

Testing

curl -k https://ca.lan.stepin.ru/.well-known/est/cacerts
curl -sk https://ca.internal.example.com:8443/acme/local/directory | jq .
curl -v https://app1.internal.example.com/ 2>&1 | grep -E "subject|issuer|expire"