Installation Guide

September 6, 2026 · View on GitHub

This guide covers all methods of installing and deploying CertMate.


Prerequisites

  • Python 3.12
  • pip (Python package manager)
  • Docker (optional, for containerized deployment)

Method 1: Direct Installation

1. Clone the Repository

git clone https://github.com/fabriziosalmi/certmate.git
cd certmate

2. Create Virtual Environment

python3 -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

3. Install Dependencies

pip install -r requirements.txt

4. Configure Environment

Create a .env file:

cp .env.example .env
# Edit .env with your settings

5. Start the Application

python app.py

Method 2: Docker Installation

git clone https://github.com/fabriziosalmi/certmate.git
cd certmate
docker-compose up -d

Using Docker Build

git clone https://github.com/fabriziosalmi/certmate.git
cd certmate
docker build -t certmate .
docker run -p 8000:8000 --env-file .env -v ./certificates:/app/certificates certmate

For advanced Docker deployment including multi-platform builds, see the Docker Guide.


System Dependencies

Ubuntu / Debian

sudo apt update
sudo apt install python3-dev python3-venv build-essential libssl-dev libffi-dev

CentOS / RHEL / Rocky

sudo yum install python3-devel gcc openssl-devel libffi-devel

macOS

brew install python3 openssl libffi

DNS Provider Setup

After installation, configure your DNS provider credentials. See the DNS Providers Guide for detailed setup instructions for every supported provider.

Quick setup for common providers:

Cloudflare

  1. Go to Cloudflare Dashboard
  2. Create a new API token with Zone:DNS:Edit permissions
  3. Add the token in CertMate Settings

AWS Route53

  1. Create IAM user with Route53 permissions
  2. Generate access keys
  3. Add credentials in CertMate Settings

Azure DNS

  1. Create a Service Principal
  2. Assign DNS Zone Contributor role
  3. Configure subscription details in CertMate Settings

Google Cloud DNS

  1. Create a Service Account with DNS Administrator role
  2. Download JSON key file
  3. Upload in CertMate Settings

Environment Variables

# API Authentication (auto-generated if neither is set)
# Option A: inline value
API_BEARER_TOKEN=your_secure_token_here
# Option B: path to a file containing the token (takes precedence over API_BEARER_TOKEN)
API_BEARER_TOKEN_FILE=/run/secrets/api_bearer_token

# Flask session secret key (auto-generated if neither is set)
# Option A: inline value
SECRET_KEY=your_flask_secret_key
# Option B: path to a file containing the key (takes precedence over SECRET_KEY)
SECRET_KEY_FILE=/run/secrets/secret_key

# Reverse proxy — set to 'true' when CertMate is fronted by Nginx,
# HAProxy, Traefik, Cloudflare, etc. Without this, request.remote_addr
# resolves to the proxy's IP for every request, which collapses per-
# client rate-limiting into a single bucket. See the "Behind a reverse
# proxy" section under Production Deployment for details.
BEHIND_PROXY=true

# Backup encryption at rest (optional, recommended).
# When set, unified backups are written as encrypted .zip.enc files
# (PBKDF2-SHA256 key derivation + Fernet/AES) instead of cleartext .zip.
# A disaster-recovery backup (include_secrets=true) embeds every
# certificate private key and every credential, so without this an
# exfiltrated copy is a full compromise; the default share-safe backup
# carries neither. The same passphrase
# must be present to restore. Deliberately env-only: a passphrase stored
# in settings.json would itself end up inside plaintext backups.
CERTMATE_BACKUP_PASSPHRASE=choose-a-long-random-passphrase

# DNS Providers (choose one or multiple)
CLOUDFLARE_TOKEN=your_cloudflare_token
AWS_ACCESS_KEY_ID=your_aws_access_key
AWS_SECRET_ACCESS_KEY=your_aws_secret_key
AZURE_SUBSCRIPTION_ID=your_azure_subscription
AZURE_TENANT_ID=your_azure_tenant
AZURE_CLIENT_ID=your_azure_client
AZURE_CLIENT_SECRET=your_azure_secret
GOOGLE_PROJECT_ID=your_gcp_project
POWERDNS_API_URL=https://your-powerdns:8081
POWERDNS_API_KEY=your_powerdns_key

Resolution Order

VariablePrecedence
API_BEARER_TOKEN_FILEHighest — if set, API_BEARER_TOKEN is never read
API_BEARER_TOKENUsed only when API_BEARER_TOKEN_FILE is absent
(generated)Fallback when neither is set or the value fails validation
SECRET_KEY_FILEHighest — if set, SECRET_KEY is never read
SECRET_KEYUsed only when SECRET_KEY_FILE is absent
(generated + persisted)Written to data/.secret_key so sessions survive restarts

Docker Secrets tip: Use API_BEARER_TOKEN_FILE=/run/secrets/api_bearer_token and SECRET_KEY_FILE=/run/secrets/secret_key with Docker Swarm or Kubernetes secrets to avoid putting sensitive values in environment variables.


Production Deployment

Behind a reverse proxy

If CertMate sits behind a reverse proxy (Nginx, HAProxy, Traefik, Cloudflare, Kubernetes Ingress) — which is the recommended way to run it for TLS termination — set BEHIND_PROXY=true in the container environment. This enables Werkzeug's ProxyFix middleware so the following trust the X-Forwarded-* headers from your proxy:

  • request.remote_addr resolves to the original client IP instead of the proxy's IP. Rate limiting, audit-log entries, and the "invalid API token attempt from X" warnings all become per-client instead of per-proxy.
  • The proxy's scheme / host / prefix headers are honored, which keeps generated URLs and cookie scopes correct.
# docker-compose.yml snippet
services:
  certmate:
    image: fabriziosalmi/certmate:latest
    environment:
      BEHIND_PROXY: "true"
    volumes:
      - ./data:/app/data

When NOT to enable it. If you expose CertMate directly to the network with no proxy in front, leave BEHIND_PROXY unset. With it set, anyone who can reach the listener could spoof X-Forwarded-For and bypass per-client rate limits. The proxy is the trust boundary.

Your proxy must of course forward the headers. Nginx example:

proxy_set_header Host              $host;
proxy_set_header X-Real-IP         $remote_addr;
proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

The bundled nginx profile

docker-compose.yml ships an optional nginx service (--profile nginx) that terminates TLS in front of CertMate. Its configuration is not tracked as nginx.conf — that file is yours and gitignored — but as nginx.conf.example, the same pattern as .env.example / .env:

cp nginx.conf.example nginx.conf
# edit nginx.conf: replace every <your-domain> in ssl_certificate / ssl_certificate_key
docker compose --profile nginx up -d

Copy before up: if nginx.conf does not exist on the host when the bind mount is created, Docker creates it as an empty directory and nginx fails to start. Upgrading from a release where nginx.conf was tracked: your edited copy stays in place (git now ignores it); diff it against the new nginx.conf.example after a pull to pick up upstream changes.

Example: Zion (Rust TLS gateway + WAF)

Zion is a high-performance Rust TLS reverse proxy with a built-in WAF — a good fit in front of CertMate when you want TLS 1.3 termination and request filtering at the edge. CertMate stays on plain HTTP on the internal network; Zion terminates TLS and forwards.

zion.toml:

[server]
listen_http  = "0.0.0.0:8080"
listen_https = "0.0.0.0:8443"

[tls]
cert_path = "/etc/ssl/zion/tls.crt"   # your cert, or use Zion's ACME (--features acme)
key_path  = "/etc/ssl/zion/tls.key"
min_version = "1.3"
alpn = ["h2", "http/1.1"]

[upstream.backend]
url = "http://certmate:8000"

# Catch-all to the backend. The explicit "/" route is harmless and documents
# intent; recent Zion also auto-registers "/" for a root catch-all.
[[route]]
path = "/"
upstream = "backend"

[[route]]
path = "/{*rest}"
upstream = "backend"

docker-compose.yml:

services:
  certmate:
    image: certmate:latest          # the published image, or your local build
    environment:
      BEHIND_PROXY: "true"          # trust Zion's X-Forwarded-* headers
    expose:
      - "8000"                       # internal only; not published to the host
    volumes:
      - ./data:/app/data

  zion:
    image: zion:latest              # the published image, or your local build
    depends_on:
      - certmate
    environment:
      ZION_CONFIG: /etc/zion/zion.toml
    volumes:
      - ./zion.toml:/etc/zion/zion.toml:ro
      - ./certs:/etc/ssl/zion:ro
    ports:
      - "443:8443"                   # host 443 -> Zion's HTTPS listener
      - "80:8080"                    # host 80  -> Zion's HTTP listener

Keep BEHIND_PROXY=true on the CertMate service: Zion appends X-Forwarded-For, so this makes per-client rate limiting, audit entries and the auth-failure warnings resolve to the real client IP rather than Zion's.

/metrics is served by Zion, not proxied. Zion exposes its own Prometheus endpoint at /metrics (zion_* series for the proxy), which shadows CertMate's /metrics. Scrape them separately: Zion's /metrics from inside the host / cluster network (Zion gates it to private source IPs and returns 403 to public clients), and CertMate's certmate_* directly against the internal certmate:8000 with an admin Bearer token (see monitoring/ for the dashboard and scrape config).

Confining outbound traffic (egress hardening)

CertMate makes outbound connections to ACME Certificate Authorities, DNS provider APIs, object storage, and notification webhooks over HTTP(S), plus SMTP for email notifications. You can confine and audit the HTTP(S) traffic by routing it through a forward proxy and denying CertMate any other route to the internet.

CertMate's HTTP(S) clients (requests, certbot, webhook delivery via urllib, boto3) honor the standard HTTP_PROXY / HTTPS_PROXY / NO_PROXY environment variables, so no code changes are needed. SMTP is the exception: email notifications use smtplib, which opens a direct TCP connection and does not consult the HTTP-proxy variables. On a locked-down egress network, allow your SMTP relay's host:port directly (a firewall / NetworkPolicy rule), or use a webhook notification channel instead of email.

Example with Secure Proxy Manager, a self-hosted Squid-based forward proxy with a WAF, a DNS sinkhole, and — since v3.9.0 — a first-class default-deny egress allowlist (only explicitly approved destinations are reachable; everything else is refused):

services:
  certmate:
    image: certmate:latest
    environment:
      HTTP_PROXY:  "http://proxy:3128"
      HTTPS_PROXY: "http://proxy:3128"
      NO_PROXY:    "localhost,127.0.0.1"
    networks:
      - egress            # CertMate can reach ONLY the proxy on this network
  # The Secure Proxy Manager stack provides the `proxy` service on :3128.
  # Attach that proxy to BOTH the `egress` network (so CertMate can reach it)
  # AND a second, non-internal network (so the proxy itself reaches the
  # internet). The proxy is then CertMate's only route out.
networks:
  egress:
    internal: true        # no gateway: CertMate has no direct internet

Putting CertMate on an internal network (no gateway) shared with the proxy makes the proxy its only path out. Outbound becomes a single auditable choke point: allow the destinations CertMate actually needs (your CA, DNS provider, object storage, notification endpoints), deny the rest; raw-IP-literal destinations are blocked at the proxy rather than trusted blindly. If you use DNS-alias / CNAME delegation, also allow cloudflare-dns.com — CertMate resolves those CNAMEs over DoH.

With Secure Proxy Manager v3.9.0+ this is a built-in mode rather than a manual ACL exercise: enable Default-deny egress in Settings and populate the Egress Allowlist with the destinations CertMate needs. Each entry is a domain or an IP/CIDR (auto-classified), and the /api/egress-allowlist endpoint lets you manage the list from IaC. A representative starter allowlist:

  • your ACME CA's API host — e.g. acme-v02.api.letsencrypt.org (plus acme-staging-v02.api.letsencrypt.org if you issue against staging), or the endpoint of whichever CA you configured;
  • your DNS provider's API host — varies by provider (for example api.cloudflare.com);
  • your object-storage endpoint, if off-site backup is enabled;
  • your notification host — the webhook, Gotify, ntfy, or Telegram endpoint, if used;
  • cloudflare-dns.com, if you use DNS-alias / CNAME delegation (resolved over DoH, as above).

Everything else is refused at the proxy, so a misconfiguration or a compromised dependency cannot quietly exfiltrate to an arbitrary host. For HTTPS the allowlist matches on the host of the CONNECT request, so it works without TLS interception.

  • Kubernetes: an egress default-deny NetworkPolicy that permits traffic only to the proxy Service, plus the HTTP(S)_PROXY env on the Deployment.
  • systemd: Environment=HTTPS_PROXY=... in the unit, plus host firewall rules that restrict egress to the proxy.

HTTPS content inspection (optional, advanced). A forward proxy sees only the SNI / host / IP of an HTTPS connection — destination allow-deny works on that without decryption. If you additionally enable TLS interception (SSL-bump) on the proxy to inspect outbound content, CertMate must trust the proxy's interception CA: point REQUESTS_CA_BUNDLE (and SSL_CERT_FILE) at a bundle that includes it, or add it to the container's system trust store. Exclude (splice) the ACME endpoints from interception — you do not want to MITM the connection to your Certificate Authority, and some endpoints pin certificates. This is not mutual TLS; it is one-way trust of a private CA.

Storage location for the data directory

CertMate uses standard Python blocking file I/O for everything under data/ (settings, certificates, audit log, scheduler SQLite store). Local disk is strongly recommended.

If you mount data/ on a network filesystem (NFS, SMB), be aware:

  • A frozen NFS server can hang Python file reads indefinitely with no built-in timeout. The renewal worker, the audit log writer, and the /health probe will all block on the same underlying mount.
  • SQLite's WAL journal mode requires lock semantics that NFS does not always provide. CertMate logs a warning if it had to fall back to a weaker journal mode; correctness is preserved, but concurrency drops.

If NFS is unavoidable, mount with soft,timeo=30,retrans=3 (or your distro's equivalent) so I/O fails fast instead of hanging on a stalled server, and check data/logs/certmate.log for the WAL-fallback line after first start.

Using Gunicorn

gunicorn --bind 0.0.0.0:8000 --workers 1 --threads 8 --timeout 300 app:app

Using systemd

Create /etc/systemd/system/certmate.service:

[Unit]
Description=CertMate SSL Certificate Manager
After=network.target

[Service]
Type=simple
User=certmate
WorkingDirectory=/opt/certmate
Environment=PATH=/opt/certmate/venv/bin
Environment=GUNICORN_TIMEOUT=300
ExecStart=/opt/certmate/venv/bin/gunicorn --bind 0.0.0.0:8000 --workers 1 --threads 8 --timeout ${GUNICORN_TIMEOUT} app:app
Restart=always

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl enable certmate
sudo systemctl start certmate

Using Docker in Production

version: '3.8'
services:
  certmate:
    build: .
    ports:
      - "8000:8000"
    environment:
      - API_BEARER_TOKEN=${API_BEARER_TOKEN}
      - CLOUDFLARE_TOKEN=${CLOUDFLARE_TOKEN}
    volumes:
      - ./certificates:/app/certificates
      - ./data:/app/data
    restart: unless-stopped

Troubleshooting

DNS Plugin Version Conflicts

Install from the requirements file CertMate ships. That set is resolved, built and booted in CI on every run; a list assembled by hand is not.

pip install -r requirements.txt            # every bundled provider
pip install -r requirements-minimal.txt    # certbot + Cloudflare only
pip install -r requirements-extended.txt   # more providers, on top of minimal
pip install -r requirements-aws.txt        # Route53, on top of either

This page used to publish its own pinned list, and it had drifted to certbot==4.1.1 in all five languages while the project is pinned to 2.10.0 — the 5.x migration is still a plan (issue #103), not a release.

Correcting those numbers is not enough, which is why the list is gone rather than fixed. What holds the stack together is not the plugin versions but cryptography, pyopenssl, josepy and acme holding each other in place: newer pyOpenSSL drops OpenSSL.crypto.X509Extension, which acme evaluates at import. Assembled by hand, certbot dies before it can issue anything — measured four times, adding one pin at a time. See SECURITY.md, "Known dependency constraint".

certbot-dns-powerdns needs its own environment: it requires dns-lexicon<=3.5.6 while the Linode, OVH, RFC2136, DNSMadeEasy and NS1 plugins require >=3.14.1, and pip cannot satisfy both.

Manual Dependency Installation

If one provider fails to install, add it on top of a working base rather than assembling one by hand:

pip install -r requirements-minimal.txt    # the working base, first
pip install -r requirements-aws.txt        # Route53 + boto3
pip install -r requirements-gcp.txt        # Google Cloud DNS
pip install -r requirements-azure.txt      # Azure DNS

Each file carries the versions its plugin needs, and CI resolves every combination on every run — including the check that certbot does not move off its pin.

Validation Commands

# Check certbot plugins
certbot plugins --text

# Verify service is running
curl -X GET http://localhost:8000/api/health

Support

If you encounter issues:

  1. Check the logs for specific errors
  2. Verify your DNS provider credentials
  3. See the DNS Providers Guide for provider-specific troubleshooting
  4. See the Testing Guide for running diagnostics

← Back to DocumentationDNS Providers →Docker →