- ACME Server implementation (http-01 challenge)
- Built-in CA to sign/revoke certificates (can be replaced with an external CA), CA rollover is supported
- Notification Mails (account created, certificate will expire soon, certificate is expired) with customizable templates
- Web UI (certificate log) with customizable templates
Tested with Certbot, Traefik, Caddy, uacme, acme.sh.
$ openssl genrsa -out ca.key 4096
$ openssl req -new -x509 -nodes -days 3650 -subj "/C=DE/O=Demo" -key ca.key -out ca.pem
Docker Compose snippet:
version: '2.4'
services:
acme-ca-server:
image: knrdl/acme-ca-server
restart: always
environment:
EXTERNAL_URL: http://localhost:8080
DB_DSN: postgresql://postgres:secret@db/postgres
# ports:
# - "8080:8080"
networks:
- net
volumes:
- ./ca.key:/import/ca.key:ro # needed once to import new ca
- ./ca.pem:/import/ca.pem:ro # needed once to import new ca
mem_limit: 250m
db:
image: postgres:16-alpine
restart: always
environment:
POSTGRES_PASSWORD: secret
networks:
- net
volumes:
- ./db:/var/lib/postgresql/data
mem_limit: 250m
networks:
net:Serve the app behind a TLS terminating reverse proxy, e.g. as https://acme.mydomain.org
The app listens on port 8080 for http traffic.
docker run -it --rm certbot/certbot certonly --server https://acme.mydomain.org/acme/directory --standalone --no-eff-email --email user1@mydomain.org -v --domains test1.mydomain.org| Env Var | Default | Description |
|---|---|---|
| EXTERNAL_URL | The HTTPS address the server will be reachable from, e.g. https://acme.mydomain.org | |
| DB_DSN | Postgres connection string, e.g. postgresql://username:password@host/dbname (database will be initialized on startup) |
|
| ACME_TERMS_OF_SERVICE_URL | None |
Optional URL which the ACME client can show when the user has to accept the terms of service, e.g. https://acme.mydomain.org/terms |
| ACME_MAIL_TARGET_REGEX | any mail address | restrict the email address which must be provided to the ACME client by the user. E.g. [^@]+@mydomain\.org only allows mail addresses from mydomain.org |
| ACME_TARGET_DOMAIN_REGEX | any non-wildcard domain name | restrict the domain names for which certificates can be requested via ACME. E.g. [^\*]+\.mydomain\.org only allows domain names from mydomain.org |
| CA_ENABLED | True |
whether the internal CA is enabled, set this to false when providing a custom CA implementation |
| CA_CERT_LIFETIME | 60 days (60d) |
how often certs must be replaced by the ACME client |
| CA_CRL_LIFETIME | 7 days (7d) |
how often the certificate revocation list will be rebuilt (despite rebuild on every certificate revocation) |
| CA_ENCRYPTION_KEY | will be generated if not provided | the key to protect the CA private keys on rest (encrypted in the database) |
| CA_IMPORT_DIR | /import |
where the ca.pem and ca.key are initially imported from, see 2. CA rollover is as simple as placing a new cert in this directory |
| MAIL_ENABLED | False |
if sending emails is enabled |
| MAIL_HOST | None |
smtp host |
| MAIL_PORT | None |
smtp port (default depends on encryption method) |
| MAIL_USERNAME | None |
smtp auth username |
| MAIL_PASSWORD | None |
smtp auth password |
| MAIL_ENCRYPTION | tls |
transport encryption method: tls (recommended), starttls or plain (unencrypted) |
| MAIL_SENDER | None |
the email address shown when sending mails, e.g. acme@mydomain.org |
| MAIL_NOTIFY_ON_ACCOUNT_CREATION | True |
whether to send a mail when the user runs ACME for the first time |
| MAIL_WARN_BEFORE_CERT_EXPIRES | 20 days (20d) |
when to warn the user via mail that a certificate has not been renewed in time (can be disabled by providing false as value) |
| MAIL_NOTIFY_WHEN_CERT_EXPIRED | True |
whether to inform the user that a certificate finally expired which has not been renewed in time |
| WEB_ENABLED | True |
whether to also provide UI endpoints or just the ACME functionality |
| WEB_ENABLE_PUBLIC_LOG | False |
whether to show a transparency log of all certificates generated via ACME |
| WEB_APP_TITLE | ACME CA Server |
title shown in web and mails |
| WEB_APP_DESCRIPTION | Self hosted ACME CA Server |
description shown in web and mails |
Templates consist of subject.txt and body.html (see here). Overwrite the following files:
- /app/mail/templates/cert-expired-info/{subject.txt,body.html}
- /app/mail/templates/cert-expires-warning/{subject.txt,body.html}
- /app/mail/templates/new-account-info/{subject.txt,body.html}
Template parameters:
app_title:strapplication title fromWEB_APP_TITLEapp_desc:strapplication description fromWEB_APP_DESCRIPTIONweb_url:strweb index url fromEXTERNAL_URLacme_url:stracme directory urldomains:list[str]list of expiring domainsexpires_at:datetimedomain expiration dateexpires_in_days:intdays until cert will expireserial_number:strexpiring certs serial number (hex)
Custom files to be served by the http server can be placed in /app/web/www.
Overwrite templates (see here):
- /app/web/templates/cert-log.html (Certificate Listing)
- /app/web/templates/domain-log.html (Domain Listing)
- /app/web/templates/index.html (Startpage)
Template parameters:
app_title:strapplication title fromWEB_APP_TITLEapp_desc:strapplication description fromWEB_APP_DESCRIPTIONweb_url:strweb index url fromEXTERNAL_URLacme_url:stracme directory urlcerts:listlist of certs forcert-log.htmldomains:listlist of domains fordomain-log.html
First set env var CA_ENABLED=False. Then overwrite the file /app/ca/service.py (see here) in the docker image. It must provide two functions:
async def sign_csr(csr: x509.CertificateSigningRequest, subject_domain: str, san_domains: list[str]) -> SignedCertInfo:
...csr: a x509.CertificateSigningRequest objectsubject_domain: the main domain name for the certificatesan_domains: subject alternative names, all domain names (includingsubject_domain) for the certificate- returns: instance of
SignedCertInfo()
class SignedCertInfo:
cert: x509.Certificate
cert_chain_pem: strcert: a x509.Certificate objectcert_chain_pem: a PEM-encoded text file containing the created cert as well as the root or also intermediate cert. This file will be used by the ACME client
async def revoke_cert(serial_number: str, revocations: set[tuple[str, datetime]]) -> None:
...serial_number: certificate serial number to revoke as hex valuerevocations: all revoked certificates including the one specified byserial_number. It's a set of tuples containing(serial_number, revocation_date)- returns: no error on success, throw exception otherwise
A custom CA backend must also handle the CRL (certificate revocation list) distribution.
flowchart LR
accounts -->|1:0..n| orders
orders -->|1:1..n| authorizations
authorizations -->|1:1| challenges
orders -->|1:0..1| certificates

