Overview
ACME Manager is a distributed system designed to automate the creation, management, and deployment of ACME (Automated Certificate Management Environment) certificates. It provides a robust solution for managing TLS/SSL certificates across your infrastructure with automatic renewal and secure storage.
Table of Contents
- Key Features
- Getting Started
- Client Mode
- API Reference
- Authentication
- Certificate Management
- Monitoring
Key Features
- Automatic Certificate Renewal: Certificates are automatically renewed 20-30 days before expiration
- High Availability: Operates in a distributed cluster with automatic leader election
- Secure Storage: All certificates are securely stored in HashiCorp Vault
- Multiple Challenge Types: Supports both DNS and HTTP ACME challenges
- Web UI: User-friendly interface for certificate and token management
- Client Mode: Deploy certificates locally on remote servers
- Command Execution: Run custom commands after certificate deployment
- Real-time Monitoring: Prometheus metrics and web dashboard
- Plugin Support: Extensible DNS provider plugins
Getting Started
Prerequisites
- Access to an ACME Manager server URL
- An authentication token (provided by your administrator)
- For client mode: ACME Manager client binary
Authentication
ACME Manager uses two types of authentication:
1. API Key Authentication (Admin Only)
Used for token management operations:
curl -X GET 'http://localhost:8989/api/v1/token/{id}' \
-H 'X-API-Key: your-api-key'
2. Bearer Token Authentication (Users)
Used for certificate operations:
curl -X GET 'http://localhost:8989/api/v1/certificate/{issuer}/{domain}' \
-H 'Authorization: Bearer your-token-here'
Client Mode
The ACME Manager client runs on your servers to automatically fetch and deploy certificates from the ACME Manager cluster.
Installation
- Download the
acme-manager-clientbinary - Create a configuration file
- Run the client
Client Configuration
Create a client-config.yml file:
common:
# Default certificate settings
cert_days: 90 # Default validity period
cert_days_renewal: "20-30" # Default renewal window
# Certificate deployment
certificate_deploy: true
certificate_backup: false # Backup certs/keys to Vault
certificate_dir: /etc/ssl/certificates
certificate_dir_perm: 0700
certificate_file_perm: 0600
certificate_keyfile_perm: 0600
certificate_file_ext: ".crt"
certificate_keyfile_ext: ".key"
certificate_ca_file_ext: ".ca.crt" # Used when bundle=false
certificate_keyfile_no_generate: false # Use existing local key
certificate_timeout: 180 # Timeout for cert operations
# Command execution
cmd_enabled: true
pre_cmd_run: "" # Command before deployment
pre_cmd_timeout: 60
post_cmd_run: "systemctl reload nginx"
post_cmd_timeout: 60
# Certificate lifecycle
revoke_on_update: false
revoke_on_delete: false
delay_before_delete: "" # e.g., "24h"
# HTTP client retry configuration
http_client_retry_max: 4 # Max retry attempts (default: 4)
http_client_retry_wait_min: 1 # Min wait between retries in seconds (default: 1)
http_client_retry_wait_max: 30 # Max wait between retries in seconds (default: 30)
http_client_retry_status_code: [] # Extra HTTP status codes to retry on (e.g. [429, 503])
http_client_debug: false # Log full request/response for debugging
certificate:
- domain: "example.com"
issuer: "letsencrypt"
dns_challenge: "cloudflare"
san: "www.example.com,api.example.com"
days: 90 # Override default validity
renewal_days: "30" # Override default renewal window
bundle: true
key_type: "ec256"
labels: "env=prod,team=infra"
profile: "" # ACME profile (if CA supports it)
storage:
vault:
url: "https://vault.example.com"
role_id: "your-role-id"
secret_id: "your-secret-id"
cert_prefix: "secret/acme-manager/certificates" # For certificate_backup
Running the Client
acme-manager-client \
-client.config-path=client-config.yml \
-client.manager-url=https://acme-manager.example.com \
-client.manager-token=your-bearer-token \
-client.check-config-interval=5m
Client CLI Options
| Flag | Default | Description |
|---|---|---|
-client.config-path |
client-config.yml |
Path to the client configuration file |
-client.manager-url |
http://localhost:8989/api/v1 |
ACME Manager server URL (or env var ACME_MANAGER_URL) |
-client.manager-token |
”” | Bearer token for authentication (or env var ACME_MANAGER_TOKEN) |
-client.check-config-interval |
5m |
Interval to check for config changes and sync certificates |
-client.pull-only |
false |
Enable pull-only (slave) mode |
-client.cleanup-enabled |
false |
Enable cleanup of orphaned local certificate files |
-client.cleanup-interval |
30m |
Interval to check for orphaned files to cleanup |
-client.tls-ca-file |
”” | TLS CA certificate file for server connection |
-client.tls-cert-file |
”” | TLS client certificate file |
-client.tls-key-file |
”” | TLS client key file |
-client.tls-skip-verify |
false |
Skip TLS certificate verification |
-config-check |
false |
Validate client config file and exit (see below) |
-server.listen-address |
:8989 |
Address for the metrics HTTP server |
-log.level |
info |
Log level (debug, info, warn, error) |
-log.format |
logfmt |
Log format (logfmt, json) |
Master/Slave Mode
ACME Manager client supports two operational modes that enable a master/slave architecture for certificate management:
Master Mode (Default)
In master mode, the client is responsible for:
- Managing the full certificate lifecycle (create, update, delete)
- Generating CSRs and private keys
- Syncing certificates from the local config file to the ACME Manager server
- Deploying certificates locally
acme-manager-client \
-client.config-path=client-config.yml \
-client.manager-url=https://acme-manager.example.com \
-client.manager-token=your-bearer-token
Slave Mode (Pull-Only)
In slave mode (-client.pull-only=true), the client only:
- Lists certificates from the KV ring (server storage)
- Pulls and deploys certificates locally
- Restores private keys from Vault backup (requires
certificate_backupenabled on master) - Does NOT create, update, or delete certificates on the server
This mode is useful for:
- Deploying certificates to multiple servers without managing them
- Separating certificate management from deployment
- High-availability setups where one master manages certificates and multiple slaves deploy them
acme-manager-client \
-client.pull-only=true \
-client.config-path=client-config.yml \
-client.manager-url=https://acme-manager.example.com \
-client.manager-token=your-bearer-token
Note: In pull-only mode, the certificate section in the config file is not required. The client will fetch all certificates available to the token from the server.
Master/Slave Architecture Example
┌─────────────────────────────────────────────────────────────┐
│ ACME Manager Server │
│ (Certificate Store) │
└─────────────────────────────────────────────────────────────┘
▲
┌────────────────┼────────────────┐
│ │ │
┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐
│ Master │ │ Slave 1 │ │ Slave 2 │
│ Client │ │ Client │ │ Client │
│ │ │ (pull-only) │ │ (pull-only) │
│ - Manages │ │ │ │ │
│ certs │ │ - Deploys │ │ - Deploys │
│ - Creates │ │ only │ │ only │
│ - Updates │ │ │ │ │
│ - Deletes │ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ App Server │ │ App Server │ │ App Server │
│ #1 │ │ #2 │ │ #3 │
└─────────────┘ └─────────────┘ └─────────────┘
Configuration for Slave Mode:
common:
certificate_deploy: true
certificate_dir: /etc/ssl/certificates
certificate_file_ext: ".crt"
certificate_keyfile_ext: ".key"
cmd_enabled: true
post_cmd_run: "systemctl reload nginx"
# certificate section is optional in pull-only mode
# The client will pull all certificates available to the token
storage:
vault:
url: "https://vault.example.com"
role_id: "your-role-id"
secret_id: "your-secret-id"
cert_prefix: "secret/acme-manager/certificates"
Config Validation (-config-check)
The -config-check flag validates the client configuration file and exits immediately — it does not connect to the ACME Manager server or Vault.
acme-manager-client -client.config-path client-config.yml -config-check
- Exits 0 and prints
config file is validon success - Exits 1 and prints the error on failure
Validation covers:
- YAML structure and field types (
UnmarshalYAML) certificate_dirmust not be the same directory as the config file itself
This flag is suitable for use in configuration management pipelines (Ansible, Puppet, etc.) to verify a rendered config before deploying it.
On-Demand Certificate Check (SIGUSR1)
The client handles SIGUSR1 to trigger an immediate certificate check or pull without waiting for the next periodic interval.
kill -USR1 <pid>
- In normal (master) mode: calls
CheckCertificate, which compares the local config against the server and applies any necessary changes. - In
--client.pull-onlymode: callsPullAndCheckCertificateFromRing, which fetches all certificates available to the token from the ring.
Both functions use an internal TryLock. If a run is already in progress when the signal arrives, the signal is ignored and a message is logged at debug level. This prevents overlapping runs.
A common pattern for systemd-managed deployments is:
# Force an immediate certificate refresh without restarting the service
systemctl kill --signal=USR1 acme-manager-client
Client Workflow
┌─────────────────────────────────────────────────────────────┐
│ ACME Manager Client │
└─────────────────────────────────────────────────────────────┘
│
│ 1. Read config file
▼
┌───────────────┐
│ Configuration │
└───────────────┘
│
│ 2. Fetch certificates
▼
┌───────────────────────┐
│ ACME Manager API │
└───────────────────────┘
│
│ 3. Deploy locally
▼
┌───────────────┐
│ /etc/ssl/... │
└───────────────┘
│
│ 4. Run post command
▼
┌───────────────┐
│ systemctl... │
└───────────────┘
│
│ 5. Check for updates (every 5m)
▼
[Repeat]
API Reference
Certificate Operations
Get Certificate
Retrieve a specific certificate by issuer and domain:
GET /api/v1/certificate/{issuer}/{domain}
Authorization: Bearer {token}
Or by name (for named certificates):
GET /api/v1/certificate/{name}
Authorization: Bearer {token}
Response:
{
"cert": "-----BEGIN CERTIFICATE-----\n...",
"ca_issuer": "-----BEGIN CERTIFICATE-----\n...",
"issuer": "letsencrypt",
"domain": "example.com",
"owner": "username",
"expires": "2025-10-14 12:00:00",
"renewal_date": "2025-09-14 12:00:00"
}
Create Certificate
Create a new certificate:
POST /api/v1/certificate
Authorization: Bearer {token}
Content-Type: application/json
{
"domain": "example.com",
"issuer": "letsencrypt",
"dns_challenge": "cloudflare",
"san": "www.example.com,api.example.com",
"renewal_days": "30",
"bundle": true,
"key_type": "RSA2048"
}
Update Certificate
Update an existing certificate (revokes the old one):
PUT /api/v1/certificate
Authorization: Bearer {token}
Content-Type: application/json
{
"domain": "example.com",
"issuer": "letsencrypt",
"dns_challenge": "cloudflare",
"san": "www.example.com,api.example.com,new.example.com",
"renewal_days": "30",
"revoke": true
}
Delete Certificate
Delete and optionally revoke a certificate by issuer and domain:
DELETE /api/v1/certificate/{issuer}/{domain}
Authorization: Bearer {token}
Or by name (for named certificates):
DELETE /api/v1/certificate/{name}
Authorization: Bearer {token}
To revoke during deletion, append ?revoke=true:
DELETE /api/v1/certificate/{name}?revoke=true
Authorization: Bearer {token}
Get Certificate Metadata
Get certificate metadata without the actual certificate data:
GET /api/v1/certificate/metadata?issuer={issuer}&domain={domain}
Authorization: Bearer {token}
Response:
{
"domain": "example.com",
"issuer": "letsencrypt",
"san": ["www.example.com", "api.example.com"],
"expires": "2025-10-14 12:00:00",
"fingerprint": "SHA256:...",
"serial": "03:04:05:06:07:08",
"issuer_cn": "Let's Encrypt Authority X3"
}
Token Operations
Get Token Information
View your current token details:
GET /api/v1/token/self
Authorization: Bearer {token}
Response:
{
"tokenHash": "sha256-hash",
"username": "your-username",
"expires": "2025-11-14 12:00:00",
"duration": "30d",
"scope": ["read", "create", "update", "delete"]
}
Certificate Management
Certificate Renewal
Certificates are automatically renewed based on the renewal_days parameter:
- Single value (e.g.,
"30"): Renewal occurs exactly 30 days before expiration - Range (e.g.,
"20-30"): Renewal occurs randomly between 20-30 days before expiration
Short-lived Certificates: For certificates with a lifetime shorter than the renewal window (e.g., 6-day certificate with renewal_days: "20-30"), the system automatically switches to percentage-based renewal, renewing at 50-75% of the certificate’s lifetime.
Certificate Bundling
When bundle: true is set, the certificate includes the full chain:
-----BEGIN CERTIFICATE-----
[Your Certificate]
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
[Intermediate Certificate]
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
[Root Certificate]
-----END CERTIFICATE-----
When bundle: false, the CA chain is saved as a separate file:
- Certificate:
domain.crt - Private key:
domain.key - CA chain:
domain.ca.crt(configurable viacertificate_ca_file_ext)
Named Certificate Local File Paths
When a certificate has the optional name field set, the local file path uses a flat structure (no issuer subdirectory). This mirrors the KV ring key structure used by the server.
| KV ring key | Local file path | |
|---|---|---|
| Named cert | {prefix}/{owner}/{name} |
{certDir}/{name}{ext} |
| Unnamed cert | {prefix}/{owner}/{issuer}/{domain} |
{certDir}/{issuer}/{domain}{ext} |
Example — a certificate named myapp with default extensions and certificate_dir: /etc/ssl/:
| File | Path |
|---|---|
| Certificate | /etc/ssl/myapp.crt |
| Private key | /etc/ssl/myapp.key |
| CA chain (bundle=false) | /etc/ssl/myapp.ca.crt |
For comparison, an unnamed certificate under issuer letsencrypt with domain example.com would be placed at /etc/ssl/letsencrypt/example.com.crt.
Configuration example:
certificate:
- name: myapp
domain: example.com
issuer: letsencrypt
dns_challenge: cloudflare
Breaking change: In earlier versions, named certificates were stored at
{certDir}/{issuer}/{name}/{domain}{ext}. They are now stored at the flat path{certDir}/{name}{ext}. On the next client run after upgrading, the files will be re-created at the new location. Remove the old files manually once you have verified the new paths are in place.
ACME Profiles
Some Certificate Authorities support custom certificate profiles via the ACME protocol (draft-aaron-acme-profiles). This allows requesting certificates with specific Key Usage or Extended Key Usage extensions.
certificate:
- domain: "example.com"
issuer: "letsencrypt"
profile: "shortlived" # CA-specific profile name
Note: Profile support depends on the CA. Contact your CA to confirm support and available profile names.
Challenge Types
DNS Challenge
Requires DNS provider credentials configured on the server:
dns_challenge: "cloudflare" # or route53, gcp, azure, etc.
HTTP Challenge
Requires HTTP accessibility on port 80:
http_challenge: "kvring"
Monitoring
Web UI
Access the web interface at:
- Certificates:
http://acme-manager.example.com:8989/certificates - Tokens:
http://acme-manager.example.com:8989/tokens - Swagger API:
http://acme-manager.example.com:8989/swagger
Metrics
Client metrics are exposed at:
http://localhost:8989/metrics
Common Configuration Options
Certificate Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
domain |
string | required | Primary domain name |
issuer |
string | required | Certificate authority (e.g., “letsencrypt”) |
name |
string | ”” | Optional logical name for the certificate. When set, local files use a flat path ({certDir}/{name}{ext}) instead of {certDir}/{issuer}/{domain}{ext} |
san |
string | ”” | Subject Alternative Names (comma-separated) |
dns_challenge |
string | ”” | DNS provider for DNS-01 challenge |
http_challenge |
string | ”” | HTTP-01 challenge method |
days |
int | 90 | Certificate validity period in days (if CA supports it) |
renewal_days |
string | “20-30” | Days before expiration to renew |
bundle |
bool | false | Include CA chain in certificate. When false, CA chain saved as separate file |
key_type |
string | “ec256” | Private key type (RSA2048, RSA4096, EC256, EC384) |
labels |
string | ”” | Custom labels for the certificate (key=value,key2=value2) |
profile |
string | ”” | ACME profile for custom certificate issuance (requires CA support) |
Client Deployment Options
| Parameter | Type | Default | Description |
|---|---|---|---|
cert_days |
int | 90 | Default certificate validity period in days |
cert_days_renewal |
string | “20-30” | Default renewal window (days before expiration) |
certificate_deploy |
bool | false | Deploy certificates locally |
certificate_backup |
bool | false | Backup certificates and private keys to Vault |
certificate_dir |
string | ”” | Directory for certificate deployment |
certificate_dir_perm |
octal | 0700 | Directory permissions |
certificate_file_perm |
octal | 0600 | Certificate file permissions |
certificate_keyfile_perm |
octal | 0600 | Private key file permissions |
certificate_file_ext |
string | “.crt” | Certificate file extension |
certificate_keyfile_ext |
string | “.key” | Private key file extension |
certificate_ca_file_ext |
string | “.ca.crt” | CA chain file extension (used when bundle=false) |
certificate_keyfile_no_generate |
bool | false | Do not generate private key, use existing local key file |
certificate_timeout |
int | 180 | Timeout in seconds for certificate operations |
cmd_enabled |
bool | false | Enable command execution |
pre_cmd_run |
string | ”” | Command to run before deployment |
pre_cmd_timeout |
int | 60 | Pre-command timeout in seconds |
post_cmd_run |
string | ”” | Command to run after deployment |
post_cmd_timeout |
int | 60 | Post-command timeout in seconds |
revoke_on_update |
bool | false | Revoke old certificate on update |
revoke_on_delete |
bool | false | Revoke certificate on delete |
delay_before_delete |
string | ”” | Duration to wait before deleting (e.g., “24h”) |
http_client_retry_max |
int | 4 | Maximum number of retry attempts for requests to the server |
http_client_retry_wait_min |
int | 1 | Minimum wait time in seconds between retries |
http_client_retry_wait_max |
int | 30 | Maximum wait time in seconds between retries |
http_client_retry_status_code |
[]int | [] | Additional HTTP status codes to trigger a retry (e.g. [429, 503]) |
http_client_debug |
bool | false | Log full HTTP request and response bodies for debugging |
Best Practices
Security
- Protect your tokens: Store tokens securely and never commit them to version control
- Use minimal scopes: Request only the permissions you need
- Rotate tokens regularly: Set appropriate expiration times
- Secure certificate directories: Use restrictive file permissions (0600 for keys)
Certificate Management
- Use certificate bundling: Enable
bundle: truefor compatibility - Set appropriate renewal windows: Use ranges like “20-30” to distribute load
- Monitor expiration: Set up alerts for certificates nearing expiration
- Test in staging: Use Let’s Encrypt staging environment for testing
Client Deployment
- Run as a service: Use systemd or similar to ensure the client runs continuously
- Monitor logs: Regularly check client logs for errors
- Test commands: Verify post-deployment commands work correctly
- Use configuration management: Deploy client configs via Ansible/Puppet/Chef
Troubleshooting
Certificate Not Deploying
- Check client logs for errors
- Verify token has correct permissions
- Ensure certificate exists on server
- Check file system permissions
Command Execution Fails
- Verify command path and syntax
- Check command timeout value
- Ensure user has permission to run command
- Test command manually first
API Authentication Errors
- Verify token is not expired
- Check token scope includes required permission
- Ensure correct Authorization header format
- Contact administrator if token is invalid
Rate Limit Errors (HTTP 429)
If you receive a 429 Too Many Requests response:
{"error": "rate limit exceeded for example.com, 5/5 requests in current window, retry after 847s"}
- Check the
Retry-Afterheader for when you can retry - Wait for the specified time before making another request
- Rate limiting applies to certificate create and update operations
- Contact your administrator if you need a higher rate limit (per-token override is possible)
Support
For issues and questions:
- Check server logs:
journalctl -u acme-manager - Review Prometheus metrics
- Contact your system administrator
- Review the administrator documentation
Additional Resources
Version: 0.7.3+ Last Updated: January 2026