A complete SSL certificate management platform with web dashboard, ACME integration, CA signing, deploy agents, and automated renewal.
- Dashboard — Overview of all certificates with status, expiry timeline, and statistics
- ACME Protocol — Request certificates from Let's Encrypt and other ACME-compatible CAs (DNS-01 and HTTP-01 challenges)
- Self-Signed & CA Signing — Generate self-signed certificates, create internal CAs, and sign certificates with your own CA
- Auto-Renewal — Automatic certificate renewal with configurable per-certificate thresholds
- Deploy Agents — Lightweight Go agents for automated certificate deployment to servers (amd64, arm64, arm, 386)
- Agent Groups — Group agents and share certificates across multiple servers
- DNS Providers — Cloudflare, TransIP, Hetzner, DigitalOcean, Vultr, OVH, AWS Route 53, Google Cloud DNS, and manual validation
- Multi-Tenant — Group-based resource isolation with inter-group sharing
- SSO / OIDC — Single sign-on via Authentik, Keycloak, Entra ID, or any OpenID Connect provider with auto-provisioning and admin group mapping
- Email Notifications — Customizable HTML email templates for certificate events (expiry, renewal, creation, deletion)
- API — Full REST API with API key authentication for scripts, CI/CD, and automation
- Password Reset — Secure token-based password recovery via email
- Security — Private keys and credentials encrypted at rest, JWT authentication, hashed agent tokens, CORS configuration, Swagger disabled in production
| Layer | Technology |
|---|---|
| Frontend | React, TypeScript, Tailwind CSS, Recharts |
| Backend | Python, FastAPI, SQLAlchemy, cryptography |
| Database | PostgreSQL (production) / SQLite (development) |
| Agent | Go (statically linked, zero dependencies) |
| Infrastructure | Docker, Docker Compose, Nginx |
# 1. Clone and configure
cp .env.example .env
# 2. Generate required secrets
python3 -c "import secrets; print('SECRET_KEY=' + secrets.token_urlsafe(64))"
python3 -c "import base64, os; print('ENCRYPTION_KEY=' + base64.urlsafe_b64encode(os.urandom(32)).decode())"
python3 -c "import secrets; print('DB_PASSWORD=' + secrets.token_urlsafe(32))"
# 3. Edit .env with the generated values and your domain
nano .env
# 4. Start the application
docker compose up -d
# 5. Open your browser and create the first admin accountBackend:
cd backend
python3 -m venv venv
source venv/bin/activate
pip3 install -r requirements.txt
# Create a .env for development
cat > .env << EOF
SECRET_KEY=$(python -c "import secrets; print(secrets.token_urlsafe(64))")
CORS_ORIGINS=http://localhost:5173
FRONTEND_URL=http://localhost:5173
DEBUG=true
EOF
uvicorn app.main:app --reload --port 8000Frontend:
cd frontend
npm install
npm run devOpen http://localhost:5173 in your browser.
- Open the application and create the first admin account
- Go to Settings → configure SMTP for email notifications (optional)
- Go to Settings → configure OIDC/SSO for single sign-on (optional)
- Go to Providers and add a DNS provider (e.g. Cloudflare) and/or Certificate Authority
- Go to Certificates → New certificate and request your first ACME certificate
- Go to Self-Signed to generate internal certificates or create a CA
- (Optional) Set up Agents and install the deploy agent on your servers
- (Optional) Go to API to create API keys for scripting and automation
| Variable | Required | Default | Description |
|---|---|---|---|
SECRET_KEY |
Yes | — | JWT signing key. Must be identical across all replicas |
ENCRYPTION_KEY |
Recommended | Auto-generated | Encryption key for private keys and secrets. Must be identical across replicas |
DB_PASSWORD |
Docker only | — | PostgreSQL password (Docker Compose sets up the database automatically) |
DATABASE_URL |
No | sqlite:///./data/certdax.db |
Database connection string. Use PostgreSQL for production |
ACME_CONTACT_EMAIL |
No | admin@example.com |
Contact email for ACME certificate requests |
JWT_EXPIRY_MINUTES |
No | 1440 |
JWT token lifetime in minutes |
RENEWAL_CHECK_HOURS |
No | 12 |
How often to check for certificates needing renewal |
RENEWAL_THRESHOLD_DAYS |
No | 30 |
Default days before expiry to trigger auto-renewal |
CORS_ORIGINS |
Yes | — | Comma-separated list of allowed frontend origins |
API_BASE_URL |
No | Auto-detected | Public backend URL (used in agent install scripts) |
FRONTEND_URL |
Yes | — | Public frontend URL (used in password reset emails) |
AGENT_BINARIES_DIR |
No | agent-dist |
Directory containing agent binaries |
DEBUG |
No | false |
Enable Swagger/OpenAPI docs at /docs and /redoc |
The deploy agent is a statically compiled Go binary that runs on any Linux distribution without dependencies. Available for amd64, arm64, arm and 386 architectures.
# On the target server (as root)
cd agent/
sudo ./install.shThis automatically detects the architecture, copies the binary to /usr/local/bin/certdax-agent, creates the config directory and installs the systemd service.
# Choose the correct binary for your architecture
# Options: certdax-agent-linux-amd64, -arm64, -arm, -386
sudo install -m 755 dist/certdax-agent-linux-amd64 /usr/local/bin/certdax-agent
# Create config directory and configure
sudo mkdir -p /etc/certdax
sudo cp config.example.yaml /etc/certdax/config.yaml
sudo chmod 600 /etc/certdax/config.yaml
sudo nano /etc/certdax/config.yaml
# Install systemd service
sudo cp certdax-agent.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now certdax-agentcertdax-agent --api-url https://certdax.example.com --token YOUR_AGENT_TOKEN
# Or via environment variables
export CERTDAX_API_URL=https://certdax.example.com
export CERTDAX_AGENT_TOKEN=your-token
certdax-agentcd agent/
# Build for all platforms
make all
# Or build for current platform only
make build
# Binaries are in dist/
ls -la dist/{
"api_token": "your-cloudflare-api-token"
}Create an API token in Cloudflare with Zone:DNS:Edit permissions.
{
"login": "your-transip-login",
"private_key": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
}Generate a key pair in the TransIP control panel.
{
"api_token": "your-hetzner-dns-api-token"
}Create an API token in the Hetzner DNS Console.
{
"api_token": "your-digitalocean-api-token"
}Create a personal access token in the DigitalOcean control panel with read/write scope.
{
"api_key": "your-vultr-api-key"
}Create an API key in the Vultr customer portal.
{
"endpoint": "ovh-eu",
"application_key": "your-app-key",
"application_secret": "your-app-secret",
"consumer_key": "your-consumer-key"
}Generate credentials at https://api.ovh.com/createToken/.
{
"access_key_id": "AKIAIOSFODNN7EXAMPLE",
"secret_access_key": "your-secret-access-key",
"region": "us-east-1"
}Create an IAM user with route53:ChangeResourceRecordSets and route53:ListHostedZones permissions.
{
"project_id": "your-gcp-project-id",
"service_account_json": "{...}"
}Create a service account with the DNS Administrator role and export the JSON key.
{}With manual validation, DNS records are shown in the server logs.
By default CertDax listens on port 80 (HTTP). Place a reverse proxy in front for SSL termination. The examples below assume CertDax runs on 127.0.0.1:80.
server {
listen 443 ssl http2;
server_name certdax.example.com;
ssl_certificate /etc/ssl/certs/certdax.pem;
ssl_certificate_key /etc/ssl/private/certdax.key;
location / {
proxy_pass http://127.0.0.1:80;
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;
proxy_read_timeout 300s;
client_max_body_size 10m;
}
}
server {
listen 80;
server_name certdax.example.com;
return 301 https://$host$request_uri;
}Enable the required modules first:
sudo a2enmod proxy proxy_http ssl rewrite headers
sudo systemctl restart apache2<VirtualHost *:443>
ServerName certdax.example.com
SSLEngine On
SSLCertificateFile /etc/ssl/certs/certdax.pem
SSLCertificateKeyFile /etc/ssl/private/certdax.key
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:80/
ProxyPassReverse / http://127.0.0.1:80/
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Port "443"
</VirtualHost>
<VirtualHost *:80>
ServerName certdax.example.com
RewriteEngine On
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
</VirtualHost>frontend https_in
bind *:443 ssl crt /etc/haproxy/certs/certdax.pem
bind *:80
http-request redirect scheme https unless { ssl_fc }
default_backend certdax
backend certdax
option httpchk GET /health
http-request set-header X-Forwarded-Proto https if { ssl_fc }
server certdax 127.0.0.1:80 checkNote: Set
CORS_ORIGINSandFRONTEND_URLin your.envto the public URL (e.g.https://certdax.example.com).
| Endpoint | Method | Description |
|---|---|---|
/api/auth/register |
POST | Create first admin account |
/api/auth/login |
POST | Login |
/api/certificates |
GET | List ACME certificates |
/api/certificates/request |
POST | Request new ACME certificate |
/api/certificates/{id}/renew |
POST | Renew ACME certificate |
/api/providers/cas |
GET | List Certificate Authorities |
/api/providers/dns |
GET/POST | Manage DNS providers |
/api/self-signed |
GET | List self-signed certificates (filter: ?is_ca=true, ?search=) |
/api/self-signed |
POST | Create self-signed or CA-signed certificate |
/api/self-signed/{id} |
GET | Get certificate details (incl. PEM) |
/api/self-signed/{id} |
DELETE | Delete certificate (?force=true to force) |
/api/self-signed/{id}/renew |
POST | Renew certificate (?validity_days=365) |
/api/self-signed/{id}/parsed |
GET | Parsed X.509 certificate details |
/api/self-signed/{id}/download/zip |
GET | Download cert + key as ZIP |
/api/self-signed/{id}/download/pem/{type} |
GET | Download PEM (type: certificate, privatekey, combined, chain, ca) |
/api/self-signed/{id}/download/pfx |
GET | Download as PFX/PKCS#12 |
/api/agents |
GET/POST | Manage deploy agents |
/api/agent-groups |
GET/POST | Manage agent groups |
/api/agent/poll |
GET | Agent: fetch pending deployments |
/api/agent/heartbeat |
POST | Agent: heartbeat |
Create a self-signed certificate:
curl -X POST https://certdax.example.com/api/self-signed \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"common_name": "myserver.local",
"san_domains": ["*.myserver.local"],
"organization": "MyCompany",
"country": "NL",
"key_type": "rsa",
"key_size": 4096,
"validity_days": 365
}'Create a CA certificate:
curl -X POST https://certdax.example.com/api/self-signed \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"common_name": "My Internal CA",
"organization": "MyCompany",
"country": "NL",
"key_type": "rsa",
"key_size": 4096,
"validity_days": 3650,
"is_ca": true
}'Sign a certificate with an existing CA:
curl -X POST https://certdax.example.com/api/self-signed \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"common_name": "app.internal",
"san_domains": ["app.internal", "api.internal"],
"organization": "MyCompany",
"key_type": "rsa",
"key_size": 4096,
"validity_days": 365,
"ca_id": 1
}'Note: Set
ca_idto the ID of a certificate created withis_ca: true. The certificate will be signed by that CA instead of being self-signed. The CA chain is automatically included in ZIP and PFX downloads.
CertDax supports horizontal scaling with multiple backend replicas. The following mechanisms ensure cluster safety:
- Distributed locking — Scheduled tasks (renewal checks, expiry checks) use database-backed locks so only one instance executes them at a time
- Atomic status transitions — Certificate processing uses atomic database updates to prevent race conditions between replicas
- Stateless API — JWT authentication is stateless; any replica can serve any request
- PostgreSQL required — SQLite only supports single-node; use PostgreSQL for multi-node
| Setting | Why |
|---|---|
ENCRYPTION_KEY |
Must be identical across all replicas. Without it, each node generates its own key and encrypted data becomes unreadable across nodes |
SECRET_KEY |
Must be identical across all replicas for JWT validation |
DATABASE_URL |
Must point to a shared PostgreSQL instance |
| Agent binaries | Built into the Docker image (backend/agent-dist/). Copy them from agent/dist/ before building |
# Build and push images
docker compose build
docker tag certdax-backend registry.example.com/certdax-backend:latest
docker tag certdax-frontend registry.example.com/certdax-frontend:latest
docker push registry.example.com/certdax-backend:latest
docker push registry.example.com/certdax-frontend:latest
# Deploy as a stack (scales backend replicas)
docker stack deploy -c docker-compose.yml certdax
docker service scale certdax_backend=3Use the Docker images with a standard deployment. Key points:
- Store
SECRET_KEY,ENCRYPTION_KEY,DB_PASSWORDin a K8s Secret - Use a
Deploymentwith multiple replicas for the backend - Point
DATABASE_URLto a managed PostgreSQL (e.g. CloudSQL, RDS, or an in-cluster instance) - Copy agent binaries into
backend/agent-dist/before building the image
| Mechanism | Details |
|---|---|
| Private key encryption | Fernet (AES-128-CBC + HMAC) at rest |
| Credential encryption | DNS provider and OIDC secrets encrypted at rest |
| Password hashing | bcrypt |
| Agent tokens | SHA-256 hashed |
| API keys | SHA-256 hashed, 25 keys per user limit |
| Authentication | JWT tokens (configurable expiry) + API key fallback |
| CORS | Configurable per environment |
| OpenAPI/Swagger | Disabled in production |
| Container | Non-root user for backend |
| Cluster safety | Database-backed distributed locking for scheduled tasks |
