Administration and configuration guide for Ultimate Certificate Manager.
UCM stores data in:
- Database --
/opt/ucm/data/ucm.db(SQLite, default) or PostgreSQL viaDATABASE_URL - Data Directory --
/opt/ucm/data/(certificates, keys, backups) - Config --
/etc/ucm/ucm.env(DEB/RPM) or environment variables (Docker) - Logs --
/var/log/ucm/(DEB/RPM) or stdout (Docker)
| Variable | Default | Description |
|---|---|---|
UCM_SECRET_KEY |
(generated) | Session signing key |
UCM_HOST |
0.0.0.0 |
Bind address |
UCM_PORT |
8443 |
HTTPS port |
UCM_DATA_DIR |
/opt/ucm/data |
Data storage |
UCM_LOG_LEVEL |
INFO |
Logging verbosity |
UCM_HTTPS_CERT |
(auto) | Server certificate |
UCM_HTTPS_KEY |
(auto) | Server private key |
DATABASE_URL |
(unset → SQLite) | SQLAlchemy URL. Set to postgresql://user:pass@host:5432/dbname to use PostgreSQL. When unset, UCM uses SQLite at UCM_DATA_DIR/ucm.db. |
# Check status
sudo systemctl status ucm
# Restart service
sudo systemctl restart ucm
# View logs
sudo journalctl -u ucm -f
# Enable on boot
sudo systemctl enable ucmThe service runs as user ucm with restricted permissions (NoNewPrivileges, ProtectSystem=strict).
Logs are rotated automatically via logrotate:
- Location:
/etc/logrotate.d/ucm - Rotation: Daily, 14 copies kept
- Compression: gzip
See LOG_ROTATION.md for details.
UCM auto-generates a self-signed certificate on first run.
Replace with trusted certificate:
- Go to Settings > Security tab
- Select certificate from your CA
- Click Apply HTTPS Certificate
- Restart service:
sudo systemctl restart ucm
Or via files:
sudo cp /path/to/cert.pem /opt/ucm/data/https_cert.pem
sudo cp /path/to/key.pem /opt/ucm/data/https_key.pem
sudo chown ucm:ucm /opt/ucm/data/https_*.pem
sudo systemctl restart ucmConfigure in Settings > Security:
| Setting | Default | Description |
|---|---|---|
| Session Timeout | 24h | Auto-logout after inactivity |
| Max Sessions | 5 | Per-user session limit |
| Require 2FA | No | Force MFA for all users |
UCM enforces 0o700 on its session directory at boot. If the directory is group- or world-readable, the service refuses to start:
RuntimeError: Refusing to boot: session dir <path> has perms 0o755, expected 0o700
Default paths:
| Install method | Path |
|---|---|
| DEB / RPM (systemd) | /var/lib/ucm/sessions/ |
Source / /opt/ucm |
/opt/ucm/data/sessions/ |
| Docker | /app/data/sessions/ (inside container) |
DEB/RPM post-install scripts and the Docker entrypoint already set the right perms. After a manual cp -r or migration that lost ownership:
sudo chown -R ucm:ucm /var/lib/ucm/sessions
sudo chmod 0700 /var/lib/ucm/sessions
sudo systemctl restart ucmThe same recommendation applies to any directory holding TLS keys or HSM PINs (/etc/ucm/, /var/lib/ucm/keys/).
If you terminate TLS on a reverse proxy (Nginx, Traefik, HAProxy, NPM) and forward client info to UCM via headers, declare the proxy CIDR(s) in Settings → Security → Trusted proxies:
security.trusted_proxies = 10.0.0.5/32, 192.168.10.0/24
This affects:
- Audit log IP —
X-Forwarded-Foris honoured only when the request comes from a trusted CIDR; otherwise the directremote_addris used. Spoofed XFF from untrusted networks is ignored. - mTLS / EST / SCEP — proxy-injected
X-SSL-Client-*headers are only accepted from trusted CIDRs. Direct deployments (UCM terminates TLS itself) are unaffected.
Enable/disable in Settings > Security:
- Password -- Standard username/password
- 2FA TOTP -- Time-based one-time password
- WebAuthn -- Hardware security keys
- mTLS -- Client certificate authentication
mTLS client certificates can be enrolled from the Account → mTLS tab. Once enrolled, certificates are fully managed by UCM:
- User Certificates page (
/user-certificates) — Dedicated page to list, export, revoke, and delete all mTLS client certificates - Export — Download as PEM (with key and chain) or PKCS12 (password-protected)
- Revoke — Revoke with reason (key compromise, superseded, etc.)
- RBAC — Viewers see only their own certificates; operators and admins see all
Configure SSO under Settings → SSO. UCM supports three SSO providers:
LDAP / Active Directory:
- Server URL, bind DN, search base, user/group filters
- Group-to-role mapping (map AD groups to UCM roles)
- Test connection before saving
OAuth2 (Azure AD, Google, GitHub):
- Client ID, Client Secret, Authorization/Token/UserInfo URLs
- Callback URL:
https://your-server:8443/api/v2/auth/sso/oauth2/callback - Role claim mapping from token attributes
SAML 2.0:
- IdP Metadata URL or manual XML upload
- Entity ID, ACS URL, certificate configuration
- Attribute mapping for username, email, roles
Role mapping resolution: when a user belongs to several groups that each match a mapping entry, the highest-privilege role wins (admin > operator > auditor > viewer); the order of the entries does not matter. This lets you map a broad access group to a low role and grant higher privileges through narrower groups. Users with no matching entry get the provider's default role at account creation; on later logins their stored role is kept unless Sync role on each login is enabled.
Important: After configuring SSO, test with a non-admin account first. Keep at least one local admin account as fallback.
Configure SMTP settings under Settings → Email to enable email notifications:
- SMTP Host/Port — Mail server address and port
- Credentials — Username and password (if required)
- Encryption — None, STARTTLS, or SSL/TLS
- From Address — Sender email for all notifications
- Content Type — HTML, Plain Text, or Both
- Alert Recipients — One or more email addresses for expiry alerts
Use the Test button to send a test email and verify connectivity.
Customize the notification email template via the built-in editor:
- Navigate to Settings → Email → Email Template
- Click Edit Template to open the floating editor window
- Switch between HTML and Plain Text tabs
- Edit the template source on the left, see the live preview on the right
- Available variables:
{{title}},{{content}},{{datetime}},{{instance_url}},{{logo}},{{title_color}} - Click Save to apply, or Reset to Default to restore the UCM default template
When SMTP is configured, enable automatic certificate expiry alerts:
- Toggle notifications on/off
- Select warning thresholds (90, 60, 30, 14, 7, 3, 1 days before expiry)
- Check Now triggers an immediate scan of all certificates
Via UI:
- Go to Settings > Backup tab
- Click Create Backup
- Enter encryption password
- Download
.ucmbkpfile
Via command line:
sudo systemctl stop ucm
sudo cp /opt/ucm/data/ucm.db ~/ucm-backup-$(date +%Y%m%d).db
sudo systemctl start ucmVia UI:
- Go to Settings > Backup tab
- Click Restore Backup
- Upload
.ucmbkpfile - Enter encryption password
- All certificates and private keys
- CA hierarchy
- Users and settings
- Audit logs
- Templates
UCM supports two database backends:
- SQLite (default) — zero-config, file-based, suitable for single-node deployments
- PostgreSQL 13+ — recommended for high availability, multi-instance, or when you already operate a managed PG cluster
The active backend is selected by the DATABASE_URL environment variable (or /etc/ucm/ucm.env on DEB/RPM):
- Unset → SQLite at
UCM_DATA_DIR/ucm.db postgresql://user:pass@host:5432/dbname→ PostgreSQL
Settings → Database shows the current backend, size, table count, and exposes:
- Test connection — validate a
DATABASE_URLbefore switching - Switch backend — persist
DATABASE_URLto/etc/ucm/ucm.envand restart (DEB/RPM) - Migrate data — copy all rows from the current backend to the target, then restart
The migration is bidirectional (SQLite ↔ PostgreSQL) and:
- Backs up the source first (
/opt/ucm/data/backups/db_migration/) - Creates the schema on the target via SQLAlchemy
- Disables FK checks during bulk load
- Intersects source/target columns (legacy columns are skipped with a warning)
- Resets PostgreSQL sequences after load
Safety checks (fail fast, source untouched):
- Test connection rejects PostgreSQL servers older than 13 (UCM minimum supported version).
- Migrate refuses if the target already contains UCM data (rows in
users,cas, orcertificates). Reset the target first:- PostgreSQL:
psql ... -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;' - SQLite: delete the target
.dbfile
- PostgreSQL:
- If a migration fails mid-way, the source is untouched and a backup is available under
/opt/ucm/data/backups/db_migration/. Reset the target before retrying.
⚠ Docker installs cannot persist
/etc/ucm/ucm.envfrom inside the container. After running Migrate on Docker, the API returns the target URL — setDATABASE_URLin yourdocker-compose.ymlordocker run -eand restart the container manually.
Admin lockout fix (v2.141). Switching the active backend from PostgreSQL back to SQLite (or vice versa) no longer locks out the admin account. The bcrypt password hash is preserved across the swap and the in-process SQLAlchemy session pool is rebuilt before the next login attempt. Earlier releases could leave a stale connection pool pointing at the old backend, causing
Invalid credentialson first login after the swap.
Backups now backend-aware (v2.141). The Backup action and
/api/v2/system/backupautomatically dispatch topg_dump -Fcwhen PostgreSQL is the active backend (custom format, suitable forpg_restore). SQLite continues to use file-copy snapshots. Restore handles both formats transparently.
Location: /opt/ucm/data/ucm.db
Vacuum database:
sudo systemctl stop ucm
sqlite3 /opt/ucm/data/ucm.db "VACUUM;"
sudo systemctl start ucmExport database:
sqlite3 /opt/ucm/data/ucm.db ".dump" > ucm_dump.sqlUCM supports PostgreSQL 13+ as a drop-in replacement for SQLite. The schema is created automatically on first start.
Recommended PostgreSQL setup:
CREATE USER ucm WITH PASSWORD 'strong-password';
CREATE DATABASE ucm OWNER ucm;
GRANT ALL PRIVILEGES ON DATABASE ucm TO ucm;Activate PostgreSQL (DEB/RPM):
echo 'DATABASE_URL=postgresql://ucm:strong-password@db.example.com:5432/ucm' | sudo tee -a /etc/ucm/ucm.env
sudo systemctl restart ucmActivate PostgreSQL (Docker):
# docker-compose.yml
services:
ucm:
image: neyslim/ultimate-ca-manager:latest
environment:
DATABASE_URL: postgresql://ucm:strong-password@db:5432/ucmBackup PostgreSQL:
pg_dump -U ucm -h db.example.com ucm > ucm-pg-backup.sqlRestore PostgreSQL:
psql -U ucm -h db.example.com ucm < ucm-pg-backup.sqlℹ The
psycopg2-binarydriver is bundled with the DEB/RPM packages and the Docker image. No extra install step is needed.
| Table | Purpose |
|---|---|
users |
User accounts |
certificates |
Certificate records |
certificate_authorities |
CA records |
audit_logs |
Activity audit trail |
settings |
Application settings |
templates |
Certificate templates |
acme_accounts |
ACME client accounts |
scep_requests |
SCEP enrollment requests |
Public CDP (HTTP, typically port 8080):
http://your-server:8080/cdp/<ca_refid>.crl
http://your-server:8080/cdp/<ca_refid>-delta.crl # when delta CRL is enabled
Named URLs (opt-in, per CA): when a CA is created with Use CA name in
protocol URLs (Advanced), CDP/AIA paths use an immutable slug derived from the
CA name (/cdp/my-issuing-ca.crl) instead of the random id — easier to
configure manually in relying products. Both forms always resolve; the slug
cannot be changed after creation and reveals the CA name in URLs.
Management UI: CA → CRL / CDP tab (enable CDP, optional delta, regeneration interval).
CDP/OCSP/AIA clients often expect plain HTTP on port 80. UCM runs unprivileged, so it cannot bind ports below 1024 by itself. Three options:
-
Systemd capability — let the service bind 80 directly:
# /etc/ucm/ucm.env HTTP_PROTOCOL_PORT=80sudo systemctl edit ucm # add: # [Service] # AmbientCapabilities=CAP_NET_BIND_SERVICE sudo systemctl restart ucm
-
Reverse proxy — keep UCM on 8080 and forward
:80 → :8080for the protocol paths (/cdp/,/ocsp,/aia/) with nginx/HAProxy/Caddy. -
Docker — map the port at run time:
-p 80:8080.
Whichever option you pick, set the CDP/OCSP/AIA URLs on the CA so issued certificates embed the port-80 form of the URL.
Regenerate via API (requires write:crl):
curl -k -X POST -H "Authorization: Bearer $TOKEN" \
https://your-server:8443/api/v2/crl/<ca_id>/regenerate
curl -k -X POST -H "Authorization: Bearer $TOKEN" \
https://your-server:8443/api/v2/crl/<ca_id>/delta/regenerate| Extension / rule | Behaviour | RFC |
|---|---|---|
| Authority Key Identifier | Identifies the signing CA key: SKI of the issuing CA certificate (fallback: hash of the CA public key if SKI is absent). Not a copy of the CA certificate's own AKI (that points at the parent for intermediates). | §5.2.1 (#202) |
| CRL Number | Monotonic, non-critical; full and delta CRLs share one number sequence | §5.2.3 |
| Delta CRL Indicator | Critical on delta CRLs; BaseCRLNumber = last complete CRL |
§5.2.4 |
| Freshest CRL | Non-critical on complete CRLs when delta+CDP are configured; points at the delta CDP URL | §5.2.6 |
| Serials | Entries with serials >159 bits are skipped (logged); no silent truncation | §4.1.2.2 |
| Offline CA | Cannot regenerate while ca.offline |
ops |
Intermediate CAs: clients that match CRL.AKI to the intermediate's SKI will reject a CRL that incorrectly carried the parent's key id. After #202, regenerate CRLs for intermediates so published CDP objects pick up the corrected AKI.
Verify with OpenSSL (after fetching PEM from GET /api/v2/crl/<ca_id> or CDP):
openssl crl -in ca.crl -inform PEM -text -noout | grep -A2 'Authority Key Identifier'
openssl x509 -in intermediate.pem -noout -text | grep -A1 'Subject Key Identifier'
# keyIdentifier bytes must match the intermediate SKI, not the parentSince #204: IDP omitted on both full and delta (§5.2.4 parity), FreshestCRL guarded when CDP is missing, and reasonCode hygiene (unspecified omitted; removeFromCRL on delta only).
RFC 5280 profile (issuing CA + CRL):
- CRL Authority Key Identifier identifies the signing CA Subject Key Identifier (§5.2.1).
- Base and delta CRLs both omit IssuingDistributionPoint (§5.2.4).
- FreshestCRL points at the delta URL when CDP + delta CRL are enabled (§5.2.6).
- Reason
unspecifiedis omitted;removeFromCRLappears only on delta CRLs (§5.3.1). - Optional revoke field
invalidity_dateis emitted as CRL entryinvalidityDate(§5.3.2). - Lifting a certificateHold (unhold) regenerates the full CRL; with delta CRL enabled, UCM first emits a delta entry with reason
removeFromCRL.
Certificate issuance profile:
- CSR-supplied SKI/AKI extensions are ignored; SKI comes from the subject public key and AKI from the issuing CA’s SKI.
- Intermediate CAs inherit parent AIA caIssuers (and OCSP) when the parent has AIA/OCSP configured.
Lab scripts (repo root):
python3 scripts/lab_crl_openssl_verify.pypython3 scripts/lab_rfc5280_cert_crl_profile.py
OCSP responder runs automatically:
- URL:
https://your-server:8443/ocsp - Signing: Uses issuing CA's certificate
- Caching: 5-minute response cache
View ACME accounts and orders in the ACME page, or via API:
curl -k -b cookies.txt https://localhost:8443/api/v2/acme/accountsUCM can select an alternate chain advertised by the CA via Link: rel="alternate" (RFC 8555 §7.4.2). This choice is driven by the preferred_chain field on ACME client accounts.
What to put in preferred_chain
- The trust anchor CN at the bottom of the target chain (e.g.
ISRG Root X1for Let's Encrypt gen-Y). - Case-insensitive comparison on the last certificate's CN in the PEM: match on subject CN or issuer CN (useful when the CA "shortens" the chain and omits the root).
Constraints / semantics
- Maximum length: 255 characters.
- Empty value (
"") or unset: UCM keeps the CA's default chain (unchanged behavior).
Scope (client + proxy)
- The same registry (
AcmeClientAccount) backs both the ACME client and the ACME proxy.
API (example)
- Update via
PATCH /api/v2/acme/client/accounts/<id>(requireswrite:acme):
curl -k -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"preferred_chain":"ISRG Root X1"}' \
https://localhost:8443/api/v2/acme/client/accounts/<id>View pending SCEP requests in the SCEP page, or via API:
curl -k -b cookies.txt https://localhost:8443/api/v2/scep/requests?status=pendingEST (RFC 7030) is configured under Operations → EST:
- Enable EST and select the issuing CA
- Authentication — Configure client authentication (HTTP Basic or TLS mutual auth)
- Endpoint —
https://your-server:8443/.well-known/est/ - Operations — Simple enroll, re-enroll, CA certs distribution
- Monitor EST requests in the Operations page
Configure certificate discovery under Operations → Discovery:
- Scan Profiles — Create profiles with target hosts/CIDR ranges, port lists, and scan options
- Scheduling — Enable scheduled scans with configurable intervals
- Results — View discovered certificates, filter by status, expiry, issuer
- Quick Scan — One-off scans without creating a profile
- SNI support — Enable SNI for virtual host scanning
Configure under Settings → Windows Autoenrollment. Policy discovery (XCEP) and issuance (WSTEP) work out of the box for username/password and certificate-based binding. The Kerberos binding, used for silent GPO autoenrollment, needs one extra server-side dependency that is deliberately not installed by default: pyspnego's kerberos extra pulls in gssapi, a C extension that requires the system's Kerberos development headers to build. Installing it by default would break pip install -r requirements.txt on hosts without a compiler or libkrb5-dev, so it's opt-in.
If the Kerberos section shows "Kerberos library not available", install the extra in the same Python environment UCM runs in, then restart the service:
# Debian/Ubuntu
apt-get install -y libkrb5-dev build-essential python3-dev
pip install pyspnego[kerberos]
# RHEL/Rocky/Alma
dnf install -y krb5-devel gcc python3-devel
pip install pyspnego[kerberos]For a DEB/RPM install, run pip install inside /opt/ucm/venv (e.g. /opt/ucm/venv/bin/pip install pyspnego[kerberos]) as the ucm user. For Docker, add the same apt-get/pip install lines to a custom image built from neyslim/ultimate-ca-manager — the extra isn't baked into the published image either.
systemctl restart ucmOnce installed, gssapi/spnego are importable and the warning clears without any further configuration. This mirrors how requests-kerberos is handled for the MS CA WinRM/Kerberos admin channel (also excluded from the default install for the same reason).
Admin reset:
- Go to Users page
- Select user
- Click Reset Password
- Set temporary password
- User must change on next login
| Role | Certificates | CAs | Users | Settings |
|---|---|---|---|---|
| Admin | Full | Full | Full | Full |
| Operator | Full | Full | Read | Read |
| Auditor | Read | Read | None | None |
| Viewer | Read (limited) | Read | None | None |
Users can create API keys for automation:
- Go to Account > API Keys
- Click Generate Key
- Copy key (shown only once)
- Use in
X-API-Keyheader
curl -k https://localhost:8443/api/health
# Or remotely: curl -k https://your-server-fqdn:8443/api/healthKey metrics to monitor:
- Certificate expiration dates
- CA validity periods
- Disk space for data directory
- Database size
All actions are logged:
- Go to Audit page
- Filter by action, user, date
- Export CSV for compliance
UCM includes a full report scheduler that can automatically generate and email reports on a recurring basis.
Prerequisite: Email delivery must be configured first. Go to Settings > Email and configure your SMTP server before enabling scheduled reports.
| Report Type | Description |
|---|---|
expiring_certificates |
Certificates expiring within N days |
revoked_certificates |
All revoked certificates with reason and date |
ca_hierarchy |
CA tree with issued certificate counts |
audit_summary |
Audit log activity grouped by action type |
compliance_status |
Policy compliance across all certificates |
certificate_inventory |
Full certificate inventory with metadata |
- Go to Reports page
- Click the schedule icon next to any report type
- Configure:
- Enabled — Toggle schedule on/off
- Frequency —
daily,weekly, ormonthly - Time — Execution time in
HH:MMformat (24-hour, server timezone) - Day of Week — For weekly:
0(Monday) through6(Sunday) - Day of Month — For monthly:
1through28 - Format — Output format:
csv,json, orpdf - Recipients — Email addresses to receive the report (max 50)
- Click Save
A weekly expiring certificates report sent every Monday at 8 AM:
Report Type: Expiring Certificates
Frequency: Weekly
Time: 08:00
Day of Week: Monday
Format: CSV
Recipients: admin@example.com, security@example.com
Before relying on a schedule, send a test report:
- Configure the schedule as desired
- Click Send Test on the report row
- A one-time report is generated and emailed to all configured recipients
- Verify the email arrives and the content is correct
- Reports not sending — Check SMTP configuration in Settings > Email
- Empty reports — Verify the report type has data (e.g., no expiring certs if all are valid)
- Wrong timezone — Report time uses server timezone; check system clock
- Recipient limit — Maximum 50 email addresses per report schedule
See UPGRADE.md for version-specific migration steps.
# Check logs
sudo journalctl -u ucm -n 100
# Check permissions
ls -la /opt/ucm/data/
sudo chown -R ucm:ucm /opt/ucm/data/
# Check port
sudo netstat -tlpn | grep 8443# Find locking process
fuser /opt/ucm/data/ucm.db
# Stop service and fix
sudo systemctl stop ucm
sqlite3 /opt/ucm/data/ucm.db "PRAGMA integrity_check;"
sudo systemctl start ucm