CA Backends
Certificate signing backends: internal, external, HSM, ACME proxy, and custom
ACMEEH supports multiple CA backends for signing certificates. The backend is selected
via ca.backend in the configuration. All backends implement the CABackend
abstract base class with sign() and revoke() methods.
Backend |
Use Case |
Key Storage |
|---|---|---|
|
File-based root CA signing |
PEM files on disk |
|
Delegate to HTTP API (e.g., Vault, EJBCA) |
Remote system |
|
Hardware Security Module via PKCS#11 |
HSM device |
|
Proxy to upstream ACME CA (e.g., Let’s Encrypt) |
Upstream CA |
|
Custom plugin class |
User-defined |
CABackend Interface
All CA backends extend the CABackend abstract base class defined in
acmeeh.ca.base. A backend must implement the two abstract methods
(sign and revoke) and may optionally override startup_check.
Abstract Methods (must implement)
sign(csr, *, profile, validity_days, serial_number=None, ct_submitter=None) -> IssuedCertificate
Sign a parsed x509.CertificateSigningRequest object and return an IssuedCertificate.
The profile parameter selects the certificate profile (key usages, extended key usages).
validity_days controls the certificate lifetime. serial_number may be
pre-assigned by the caller; if None, the backend generates one. ct_submitter
is an optional Certificate Transparency submitter used by backends that support CT pre-certificate flow.
revoke(*, serial_number, certificate_pem, reason=None) -> None
Revoke a previously issued certificate. serial_number is the hex-encoded serial.
certificate_pem is the full PEM of the certificate to revoke. reason
is an optional RevocationReason enum value (per RFC 5280, e.g., unspecified, keyCompromise).
Optional Methods
startup_check() -> None
Called once on application startup to verify backend connectivity and configuration.
The default implementation is a no-op. Backends should raise CAError if
the check fails (e.g., HSM token not reachable, external API unreachable).
deferred (property) -> bool
Indicates whether sign() may block for an unbounded amount of time (e.g., waiting
for upstream DNS propagation or external approval). When a backend returns True,
the order finalization handler runs sign() in a background thread and returns
the order with status processing (RFC 8555 §7.4) so the HTTP request is not blocked.
The default implementation returns False. The built-in acme_proxy backend
overrides this to return True.
IssuedCertificate Dataclass
Returned by sign() on success:
Field |
Type |
Description |
|---|---|---|
|
|
Full PEM certificate chain (leaf + intermediates) |
|
|
Certificate validity start |
|
|
Certificate validity end |
|
|
Hex-encoded serial number |
|
|
SHA-256 hex digest of leaf certificate DER |
CAError Exception
Raised by backends on failure. The retryable flag indicates whether the operation may succeed if retried:
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
— |
Human-readable failure description |
|
|
|
Whether the failure is transient |
Note
Retryable vs. Non-Retryable
A retryable CAError signals a transient failure (network timeout, temporary unavailability).
The circuit breaker counts only retryable errors toward its failure threshold. Non-retryable errors
(invalid CSR, permission denied) are passed through immediately.
Internal Backend
The internal backend signs certificates directly using a root CA certificate and private key stored as PEM files on disk. This is the simplest backend and works well for development and small-scale internal PKI deployments.
Configuration
ca:
backend: internal
default_validity_days: 90
max_validity_days: 397
internal:
root_cert_path: /etc/acmeeh/ca/root-ca.pem
root_key_path: /etc/acmeeh/ca/root-ca-key.pem
chain_path: /etc/acmeeh/ca/chain.pem # optional intermediate chain
serial_source: database # database or random
hash_algorithm: sha256 # sha256, sha384, sha512
Field |
Default |
Description |
|---|---|---|
|
|
Path to the CA certificate PEM file |
|
|
Path to the CA private key PEM file |
|
|
Optional intermediate certificate chain |
|
|
Serial number generation: |
|
|
Signature hash algorithm |
Warning
Security Note
The private key file must be readable only by the ACMEEH process. Set file permissions to 0600 or 0400.
Serial Numbers
Serial numbers are generated per RFC 5280: a random 20-byte value with the high bit cleared (maximum 159 bits), ensuring positive ASN.1 INTEGER encoding. Two sources are available:
database— Sequential serial numbers allocated from the database. Guarantees uniqueness across multiple ACMEEH instances sharing the same database.random— Cryptographically random 20-byte values. Suitable for single-instance deployments.
Certificate Transparency Support
The internal backend supports Certificate Transparency (CT) pre-certificate flow when CT logging
is enabled (ct_logging.enabled: true in the configuration). The signing process works
as follows:
A pre-certificate is built with the CT poison extension (OID
1.3.6.1.4.1.11129.2.4.3) and signed by the issuer.The pre-certificate is submitted to the configured CT logs.
Received SCTs (Signed Certificate Timestamps) are embedded in the final certificate as an SCT list extension (OID
1.3.6.1.4.1.11129.2.4.2).The final certificate (with embedded SCTs) is signed by the issuer and returned.
If no SCTs are received from any configured CT log, the backend falls back to standard signing without embedded SCTs.
External Backend
The external backend delegates certificate signing and revocation to a remote HTTP API. This is useful for integrating with existing PKI infrastructure like HashiCorp Vault, EJBCA, or any custom signing service.
Configuration
ca:
backend: external
external:
sign_url: https://vault.internal:8200/v1/pki/sign/acmeeh
revoke_url: https://vault.internal:8200/v1/pki/revoke
auth_header: X-Vault-Token
auth_value: ${VAULT_TOKEN}
ca_cert_path: /etc/acmeeh/ca/vault-ca.pem
client_cert_path: /etc/acmeeh/tls/client.pem # optional mTLS
client_key_path: /etc/acmeeh/tls/client-key.pem
timeout_seconds: 30
max_retries: 3
retry_delay_seconds: 1.0
Field |
Default |
Description |
|---|---|---|
|
|
URL of the signing endpoint |
|
|
URL of the revocation endpoint |
|
|
Authentication header name |
|
|
Authentication header value |
|
|
CA certificate for TLS verification |
|
|
Client certificate for mTLS |
|
|
Client key for mTLS |
|
|
HTTP request timeout |
|
|
Retry count on failure |
|
|
Delay between retries |
API Contract
The external backend communicates with the remote signing service using a simple JSON-over-HTTP protocol. Below is the exact request and response format for each operation.
Sign Request (POST to sign_url)
{
"csr": "-----BEGIN CERTIFICATE REQUEST-----\n...",
"profile": "default",
"validity_days": 90
}
Sign Response (HTTP 200)
{
"certificate_chain": "-----BEGIN CERTIFICATE-----\n..."
}
The certificate_chain field contains the full PEM chain (leaf certificate followed by any intermediates), concatenated.
Revoke Request (POST to revoke_url)
{
"serial_number": "0a1b2c...",
"reason": 0
}
Revoke Response
HTTP 200 on success. No response body is required.
Error Handling and Retries
Retryable errors: HTTP 5xx responses and network errors (connection refused, timeout). These raise a
CAErrorwithretryable=True.Non-retryable errors: HTTP 4xx responses. These raise a
CAErrorwithretryable=False.Retry strategy: Exponential backoff calculated as
retry_delay_seconds * 2^attempt(e.g., 1s, 2s, 4s for a 1-second base delay).
Tip
Optional Revocation
If revoke_url is not configured, revocation requests are logged as a warning but do not
fail. This is useful when the remote signing service does not support revocation or when revocation
is handled out-of-band.
HSM Backend
The HSM backend uses a PKCS#11 interface to sign certificates with keys stored in a Hardware Security Module. This provides the highest level of private key protection.
Note
Additional Dependency
Install the python-pkcs11 package: pip install python-pkcs11
Configuration
ca:
backend: hsm
hsm:
pkcs11_library: /usr/lib/softhsm/libsofthsm2.so
token_label: acmeeh-signing
pin: ${HSM_PIN}
key_label: signing-key
key_type: ec # ec or rsa
hash_algorithm: sha256
issuer_cert_path: /etc/acmeeh/ca/issuer.pem
chain_path: /etc/acmeeh/ca/chain.pem
serial_source: database
login_required: true
session_pool_size: 4
session_pool_timeout_seconds: 30
Field |
Default |
Description |
|---|---|---|
|
|
Path to the PKCS#11 shared library |
|
|
Token label (use this or |
|
|
Slot ID (alternative to |
|
|
Token PIN |
|
|
Key label (use this or |
|
|
Key ID (alternative to |
|
|
Key type: |
|
|
Hash algorithm: sha256, sha384, sha512 |
|
|
Path to the issuer certificate PEM |
|
|
Optional intermediate chain |
|
|
Serial number source |
|
|
Require PIN login to token |
|
|
PKCS#11 session pool size |
|
|
Session acquisition timeout |
How HSM Signing Works
The HSM backend uses a “dummy-key-then-re-sign” pattern:
Build the certificate with an ephemeral key and sign it locally
Extract the TBS (To-Be-Signed) certificate data
Send the TBS data to the HSM for signing via PKCS#11
Reassemble the final DER-encoded certificate with the HSM signature
This approach allows using the standard cryptography library for certificate building while keeping the private key exclusively in the HSM.
ACME Proxy Backend
The ACME proxy backend acts as a client to an upstream ACME CA (like Let’s Encrypt) to obtain certificates. This allows ACMEEH to serve as a unified ACME front-end while delegating actual issuance to a public CA.
Configuration
ca:
backend: acme_proxy
acme_proxy:
directory_url: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
storage_path: /var/lib/acmeeh/acme-proxy
challenge_type: dns-01
challenge_handler: my_dns.CloudflareDNS
challenge_handler_config:
api_token: ${CF_API_TOKEN}
eab_kid: ${ACME_EAB_KID:-}
eab_hmac_key: ${ACME_EAB_HMAC:-}
proxy_url: null # optional HTTP proxy
verify_ssl: true
timeout_seconds: 300
Field |
Default |
Description |
|---|---|---|
|
|
Upstream ACME directory URL |
|
|
Contact email for upstream account |
|
|
Local storage for account keys and state |
|
|
Challenge type for upstream validation |
|
|
Python class path for challenge handling |
|
|
Config dict passed to challenge handler |
|
|
EAB Key ID (if upstream requires External Account Binding) |
|
|
EAB HMAC key (base64url-encoded) |
|
|
HTTP proxy for upstream requests |
|
|
Verify upstream TLS certificates |
|
|
Request timeout |
Behavioral Notes
The
profileandvalidity_daysparameters are accepted by thesign()method but are ignored — the upstream CA determines the certificate profile and validity period.Certificate Transparency logging is handled entirely by the upstream CA, not by ACMEEH.
All operations (sign and revoke) are serialized with a thread lock for safety, since the underlying ACME client library is not thread-safe.
Revocation is best-effort: failures are logged but do not raise errors back to the caller.
EAB credentials (
eab_kidandeab_hmac_key) are configured on the ACMEOW client viaset_external_account_binding()before account registration. If the upstream CA requires EAB, both fields must be set.The
acme_proxybackend setsdeferred=True, so order finalization runssign()in a background thread. The thread timeout is controlled byca.deferred_signing_timeout(default: 600 seconds). This prevents slow upstream CA operations (DNS propagation, approval workflows) from triggering gunicorn worker timeouts.
Note
Dependency
The ACME proxy backend requires the ACMEOW library (pip install acmeow).
The directory_url and email config fields map to the server_url and email
parameters of the ACMEOW AcmeClient constructor.
Custom Backend
Load a custom CA backend from any Python package using the ext: prefix.
Your class must extend CABackend from acmeeh.ca.base.
Configuration
ca:
backend: ext:mycompany.pki.VaultBackend
Implementation
from acmeeh.ca.base import CABackend, IssuedCertificate
class VaultBackend(CABackend):
def __init__(self, ca_settings):
# ca_settings is the CASettings object (ca section of config)
super().__init__(ca_settings)
...
def sign(self, csr, *, profile, validity_days,
serial_number=None, ct_submitter=None):
# csr is a parsed x509.CertificateSigningRequest object
# Sign the CSR and return an IssuedCertificate
return IssuedCertificate(
pem_chain=cert_pem,
not_before=not_before,
not_after=not_after,
serial_number=serial_hex,
fingerprint=fingerprint_hex,
)
def revoke(self, *, serial_number, certificate_pem, reason=None):
# Revoke a certificate
...
def startup_check(self):
# Optional: verify connectivity on startup
...
Circuit Breaker
All CA backends are wrapped with a circuit breaker that prevents cascading failures.
After circuit_breaker_failure_threshold consecutive failures, the circuit
opens and requests fail immediately for circuit_breaker_recovery_timeout
seconds before attempting recovery.
ca:
circuit_breaker_failure_threshold: 5
circuit_breaker_recovery_timeout: 30
State Machine
The circuit breaker operates as a three-state machine:
State |
Behavior |
Transition |
|---|---|---|
CLOSED |
Normal operation. All requests pass through to the backend. Consecutive failures are counted. |
After |
OPEN |
Requests fail immediately with a retryable |
After |
HALF_OPEN |
A single probe request is allowed through to test if the backend has recovered. |
On success, transitions to CLOSED and resets the failure counter. On failure, transitions back to OPEN. |
Warning
Only Retryable Errors Count
Only retryable CAError exceptions count toward the failure threshold. Non-retryable errors
(such as invalid CSR or permission denied) are passed through to the caller without affecting the
circuit breaker state.
Failover Backend
The failover backend wraps multiple CA backends and tries them in order. If a backend returns a retryable error, the next backend in the list is attempted. This provides automatic failover for high-availability deployments.
Behavior
Operation |
Behavior |
|---|---|
|
Tries backends in order. Stops on the first success or non-retryable error. Falls through to the next backend only on retryable errors. Raises the last error if all backends fail. |
|
Best-effort across all backends. Attempts revocation on every backend regardless of individual failures. Raises the last error only if all backends fail. |
|
Checks all backends. Requires at least one backend to be healthy. Logs warnings for unhealthy backends but does not fail unless all are unreachable. |
The failover backend tracks health status per backend after each operation, allowing it to prioritize healthy backends in subsequent requests.
Note
Internal Use
The failover backend is used internally when multiple backends are configured. It is not
configured directly via ca.backend but is automatically engaged by the backend
registry when appropriate.
Certificate Profiles
Profiles control the key usages, extended key usages, and validity of issued certificates.
A default profile is always available. Custom profiles can be defined and assigned
per-account via the admin API.
ca:
profiles:
default:
key_usages: [digital_signature, key_encipherment]
extended_key_usages: [server_auth]
client_auth:
key_usages: [digital_signature]
extended_key_usages: [client_auth]
validity_days: 365
max_validity_days: 730
code_signing:
key_usages: [digital_signature]
extended_key_usages: [code_signing]
validity_days: 180
Field |
Required |
Description |
|---|---|---|
|
Yes |
List of key usage flags (e.g., |
|
Yes |
List of extended key usage OIDs (e.g., |
|
No |
Profile-specific certificate validity in days. Overrides |
|
No |
Profile-specific maximum validity in days. Overrides |
Tip
Validity Precedence
When a profile defines validity_days, it takes precedence over the global
ca.default_validity_days. Similarly, max_validity_days on a profile
overrides the global ca.max_validity_days. If neither is set on the profile,
the global values are used.
Certificate Extensions
The internal and HSM backends add the following X.509v3 extensions to issued certificates. External and ACME proxy backends delegate extension handling to the remote signing service or upstream CA respectively.
Extension |
Critical |
Source / Value |
|---|---|---|
Subject Alternative Name (SAN) |
No |
Populated from the CSR. Contains DNS names and/or IP addresses as requested by the client. |
Basic Constraints |
Yes |
|
Key Usage |
Yes |
Determined by the certificate profile (e.g., |
Extended Key Usage |
No |
Determined by the certificate profile (e.g., |
Authority Key Identifier |
No |
Derived from the issuer (CA) certificate. Allows clients to chain certificates to the correct issuer. |
Subject Key Identifier |
No |
Derived from the public key in the CSR, per RFC 5280. |
Subject Common Name
The certificate Subject CN (Common Name) is automatically set to the first DNS name from the Subject Alternative Name extension. If no DNS names are present (e.g., an IP-only certificate), the first IP address is used instead. This ensures the Subject CN is always populated for compatibility with legacy clients that do not support SAN-based validation.
Testing the Backend
Use the CLI to verify your CA backend works correctly:
# Test signing with an ephemeral CSR
PYTHONPATH=src python -m acmeeh -c config.yaml ca test-sign
This creates a temporary CSR, submits it to the configured backend, and reports success or failure.