Catalog PyPI
PACKAGE EXPLORERView in registry

acmeeh

PyPI

Enterprise ACME (RFC 8555) server for internal PKI

acmecacertificatespkirfc8555tls
Versions collected1
Direct dependencies15From release 2.1.0
Last observed20 Oct, 18:18 UTC

README

Version 2.1.0 documentation

ACMEEH

Enterprise ACME (RFC 8555) server for internal PKI

Tests PyPI Python License Codecov Code style: ruff Docker Docker Image Size Docs

Documentation | PyPI | Docker Hub | GitHub | Admin Client


Overview

ACMEEH is a production-ready ACME server built for organizations that need automated certificate management on their internal network. It fully implements RFC 8555 and is compatible with every standards-compliant ACME client — certbot, acme.sh, Caddy, Traefik, Lego, and others.

Plug in your own CA — a local root key, an HSM, HashiCorp Vault, or an upstream ACME provider — and ACMEEH handles the rest: challenge validation, certificate issuance, revocation, CRL distribution, renewal information, and lifecycle hooks.

Built with Python 3.12+, Flask, PostgreSQL, and gunicorn.

Features

  • Pluggable CA Backends — Internal file-based CA, external HTTP API (Vault, EJBCA), PKCS#11 HSM, ACME proxy to upstream CA, or bring your own with ext: plugins. All backends include a circuit breaker for resilience.
  • Challenge Validation — HTTP-01, DNS-01, and TLS-ALPN-01 with configurable timeouts, retries, and background validation workers.
  • Revocation Infrastructure — Built-in CRL generation and ACME Renewal Information (ARI) — each independently toggleable.
  • Admin REST API — Token-authenticated API for user management, audit logs, EAB credentials, identifier allowlists, CSR profiles, certificate search, bulk revocation, and maintenance mode. See acmeeh-admin for the official Python client and CLI.
  • Hook System — 10 lifecycle events (account registration, order creation, challenge validation, certificate issuance/revocation/delivery, CT submission) with pluggable handlers.
  • Security Controls — Per-endpoint rate limiting, key size and algorithm policies, identifier allowlists, External Account Binding (EAB), CAA enforcement (RFC 8659), CSR validation profiles, and per-account quotas.
  • Prometheus Metrics — /metrics endpoint for certificate counts, issuance rates, challenge stats, and CA backend health.
  • Email Notifications — SMTP alerts for certificate expiration with configurable warning days, retry with backoff, and Jinja2 templates.
  • Certificate Transparency — RFC 6962 pre-certificate submission to multiple CT logs with SCT collection.
  • Structured Logging & Audit — JSON and text log formats, audit trail with file/syslog output, and optional webhook export.
  • Background Workers — Daemon threads for challenge validation, certificate expiration checks, nonce/order/challenge cleanup, and data retention — all HA-safe with PostgreSQL advisory locks.

Installation

From PyPI

Linux / macOS:

python3.12 -m venv .venv
source .venv/bin/activate

pip install acmeeh

# Optional: HSM (PKCS#11) support
pip install acmeeh[hsm]

Windows (PowerShell):

python -m venv .venv
.venv\Scripts\Activate.ps1

pip install acmeeh

# Optional: HSM (PKCS#11) support
pip install acmeeh[hsm]

After installation the acmeeh command is available:

acmeeh -c config.yaml --validate-only
acmeeh -c config.yaml --dev

From Source

Linux / macOS:

git clone https://github.com/miichoow/ACMEEH.git
cd ACMEEH

python3.12 -m venv .venv
source .venv/bin/activate

# Install in editable mode (includes all dependencies)
pip install -e ".[dev]"

# Or install dependencies manually
pip install -r requirements.txt

Windows (PowerShell):

git clone https://github.com/miichoow/ACMEEH.git
cd ACMEEH

python -m venv .venv
.venv\Scripts\Activate.ps1

# Install in editable mode (includes all dependencies)
pip install -e ".[dev]"

# Or install dependencies manually
pip install -r requirements.txt

Docker

# 1. Copy the example env file and set your database password
cp docker/.env.example .env
vi .env   # set POSTGRES_PASSWORD

# 2. Place your CA root certificate and key
mkdir -p certs
cp /path/to/root.pem     certs/root.pem
cp /path/to/root-key.pem certs/root-key.pem

# 3. Build and start (ACMEEH + PostgreSQL)
docker compose up -d

# 4. Verify
curl http://localhost:8443/livez
curl http://localhost:8443/directory

Build with optional features:

# HSM (PKCS#11) support
docker compose build --build-arg INSTALL_HSM=1

# Gevent async workers
docker compose build --build-arg INSTALL_GEVENT=1

See the Docker documentation for the full reference — environment variables, configuration, common operations, reverse proxy setup, scaling, and troubleshooting.

Dependencies

Core dependencies (installed automatically via pip install acmeeh):

PackagePurpose
FlaskWeb framework
cryptographyX.509, JWS, key operations
dnspythonDNS-01 challenge validation
Jinja2Notification email templates
psycopg[binary]PostgreSQL driver
pyConfigKitConfiguration management
PyPGKitDatabase repository layer

Optional:

PackageInstall withPurpose
python-pkcs11pip install acmeeh[hsm]HSM backend via PKCS#11
acmeowpip install acmeeh[acme-proxy]ACME proxy CA backend
gunicornpip install acmeeh[server]Production server (Linux/macOS only)
pytest, pytest-covpip install acmeeh[dev]Testing and coverage

Quick Start

# Set up PostgreSQL
psql -U postgres -c "CREATE USER acmeeh WITH PASSWORD 'secret';"
psql -U postgres -c "CREATE DATABASE acmeeh OWNER acmeeh;"

# Create config.yaml (see Configuration section below)

# Validate config
acmeeh -c config.yaml --validate-only

# Start development server
DB_PASSWORD=secret acmeeh -c config.yaml --dev

Note: On Windows, gunicorn is not available. Use --dev for the Flask development server, or deploy behind a WSGI server like waitress.

Configuration

ACMEEH uses a single YAML configuration file with 27 settings sections. Only three fields are required:

server:
  external_url: https://acme.example.com

database:
  database: acmeeh
  user: acmeeh
  password: ${DB_PASSWORD}          # env var substitution
  auto_setup: true                  # create tables on first run

ca:
  backend: internal
  internal:
    root_cert_path: /path/to/root-ca.pem
    root_key_path: /path/to/root-ca-key.pem

challenges:
  enabled:
    - http-01

Environment variables are supported via ${VAR} or ${VAR:-default} syntax anywhere in the YAML.

See the full configuration reference for all settings.

CLI Reference

CommandDescription
acmeeh -c config.yamlStart the server (gunicorn)
acmeeh -c config.yaml --devStart Flask development server
acmeeh -c config.yaml --validate-onlyValidate config and exit
acmeeh -c config.yaml db statusCheck database connectivity
acmeeh -c config.yaml db migrateRun database migrations
acmeeh -c config.yaml ca test-signTest CA signing with ephemeral CSR
acmeeh -c config.yaml crl rebuildForce CRL rebuild
acmeeh -c config.yaml admin create-user --username admin --email admin@example.comCreate admin user
acmeeh -c config.yaml inspect order <id>Inspect an order
acmeeh -c config.yaml inspect certificate <id>Inspect a certificate
acmeeh -c config.yaml inspect account <id>Inspect an account

Global flags: -c/--config (required), --debug, --dev, --validate-only, -v/--version

CA Backends

BackendDescription
internalSign with a root CA key stored as PEM files on disk
externalDelegate signing to a remote HTTP API (e.g., HashiCorp Vault, EJBCA)
hsmSign using a Hardware Security Module via PKCS#11
acme_proxyProxy to an upstream ACME CA (e.g., Let's Encrypt)
ext:<path>Load a custom backend class (e.g., ext:mycompany.pki.VaultBackend)

All backends support a circuit breaker that prevents cascading failures on repeated signing errors.

See the CA backends documentation for detailed setup instructions.

Challenge Types

TypeValidation
http-01HTTP request to http://{domain}/.well-known/acme-challenge/{token} on port 80
dns-01DNS TXT record query at _acme-challenge.{domain}
tls-alpn-01TLS connection to port 443 with ALPN protocol acme-tls/1
dns-persist-01Long-lived DNS TXT record at _validation-persist.{domain} (draft-ietf-acme-dns-persist)

DNS-PERSIST-01

Unlike the other types, dns-persist-01 has no per-challenge token. The subscriber publishes one long-lived TXT record that binds an Issuer Domain Name to their ACME account, and that record authorizes future issuance — so subsequent renewals need no DNS changes at all:

_validation-persist.example.com. IN TXT "ca.example; accounturi=https://acme.example/acct/1"

The record value uses the RFC 8659 issue-value syntax (the CAA record format). The challenge object omits token and instead carries accounturi and issuer-domain-names, telling the client exactly what to publish.

Add policy=wildcard to authorize wildcards and subdomains:

_validation-persist.example.com. IN TXT "ca.example; accounturi=https://acme.example/acct/1; policy=wildcard"

This record authorizes example.com and *.example.com. Letting a record also authorize subdomains beneath it (e.g. one record on example.com covering foo.example.com) is off by default — opt in with challenges.dnspersist01.allow_subdomain_policy: true. An optional persistUntil=<unix-timestamp> parameter stops the record being used for new validations after a chosen time.

Enable it by listing the type and configuring at least one Issuer Domain Name — the type stays inert until you do:

challenges:
  enabled: [http-01, dns-persist-01]
  dnspersist01:
    issuer_domain_names: [ca.example]

See docs/configuration.rst for the full option list.

For the acme_proxy backend, set challenges.auto_accept: true to auto-accept all downstream challenges (real validation happens upstream).

Custom challenge validators can be added as plugins.

Architecture

ACME Clients (certbot, acme.sh, Caddy, ...)
                    │
             HTTPS / RFC 8555
                    │
    ┌───────────────────────────────┐
    │         Flask API Layer       │
    │  directory, nonce, account,   │
    │  order, authz, challenge,     │
    │  certificate, key-change      │
    ├───────────────────────────────┤
    │        Service Layer          │
    │  AccountService, OrderService │
    │  ChallengeService, CertSvc    │
    ├──────────┬────────────────────┤
    │Repository│    CA Backend      │
    │  Layer   │ internal/external/ │
    │(PyPGKit) │ hsm/acme_proxy/    │
    │          │ ext:custom         │
    ├──────────┴────────────────────┤
    │     DI Container (context.py) │
    └───────────────┬───────────────┘
                    │
             PostgreSQL 14+

Testing

If installed with pip install -e ".[dev]", pytest is already available and PYTHONPATH is handled automatically.

Linux / macOS:

# Run all tests
pytest

# Run a specific test
pytest tests/test_config.py::test_name -v

# Run with coverage
pytest --cov=acmeeh --cov-report=html

Windows (PowerShell):

# Run all tests
pytest

# Run a specific test
pytest tests\test_config.py::test_name -v

# Run with coverage
pytest --cov=acmeeh --cov-report=html

If you installed dependencies manually (without pip install -e .), prefix commands with PYTHONPATH=src on Linux or set it on Windows:

# Linux / macOS
PYTHONPATH=src python -m pytest tests/
# Windows (PowerShell)
$env:PYTHONPATH = "src"; python -m pytest tests/

Documentation

Full documentation is available in the docs/ folder (Sphinx/reStructuredText, compatible with Read the Docs):

To build the docs locally:

pip install -r docs/requirements.txt
sphinx-build -b html docs docs/_build/html

License

Apache License 2.0

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Versions 1

Collected releases
VersionPublishedDirect dependenciesOpen version
2.1.020 Jul 202615

Package advisories

0
No matching advisories collected

This describes our collected data; it is not a security assessment.

Repository

PENDING
miichoow/acmeeh

Shared repository data · GitHub

License detected in repository
Not collected

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 18:18 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package:bd2222c86f860fd37bb79ff30378482e9fd655d3bf2319efcde013ee4bc5c59d