Ssl Certificates

Jabali Panel uses Certbot and Let’s Encrypt to manage SSL/TLS certificates for domains and the panel itself, with automatic renewal via systemd timers.

Certificate Issuance

Issue Domain Certificate

jabali ssl:issue <domain> [--force]

Issues a new certificate from Let’s Encrypt using webroot validation. The certificate includes: - Primary domain (e.g., example.com) - www subdomain (e.g., www.example.com) - Mail subdomain (e.g., mail.example.com) for Stalwart

Use --force to overwrite an existing certificate. Certificates are stored in /etc/letsencrypt/live/{domain}/ with symlinks to actual certificate files.

Panel Certificate

jabali ssl:panel [--json]

Displays the control panel’s SSL certificate status, expiration date, and issuer.

Issue Panel Certificate

jabali ssl:panel-issue [--json]

Issues a new certificate for the control panel itself (FrankenPHP running on port 8443). The panel certificate is separate from domain certificates and is critical for panel access.

Certificate Renewal

Renew Certificate

jabali renew <domain>

Manually renews an existing certificate before expiration. Certbot checks certificate age and skips renewal if not due.

Check Certificate Status

jabali ssl:check [domain] [--issue-only] [--renew-only]

Validates all domain certificates or checks a specific domain: - --issue-only — only check for missing certificates (exit with error if any domain lacks a cert) - --renew-only — only check for expiring certificates (90+ days old)

With no options, performs all checks (missing and expiring certificates).

List Certificates

jabali ssl:list [--json]

Shows all installed certificates with domain names, issue dates, and expiration dates.

Certificate Details

jabali ssl:status <domain>

Displays full certificate information including: - Common name (CN) - Subject alternative names (SAN) - Issue date - Expiration date - Certificate chain - Issuer details

Automatic Renewal

Certbot runs automatic renewal via systemd timer (certbot.timer). The timer runs daily and checks for certificates expiring within 30 days.

Renewal process: 1. Certbot validates domain ownership via webroot (places token in domain’s public directory) 2. Let’s Encrypt verifies the token via HTTP 3. If validation succeeds, new certificate is issued 4. Certificates are stored in /etc/letsencrypt/live/{domain}/ 5. Nginx vhost config is updated with new cert paths 6. Nginx is reloaded (no downtime)

Webroot Validation

Certificate validation uses webroot mode: Certbot places a challenge token in the domain’s public web directory (usually /home/{user}/{domain}/public/.well-known/acme-challenge/). Let’s Encrypt fetches the token via HTTP to verify domain ownership.

For webroot validation to succeed: - Domain DNS must resolve to the server - HTTP (port 80) must be accessible - The .well-known/acme-challenge/ directory must be writable by the web server - Nginx must serve files from the webroot without redirecting to HTTPS

Integration with Panel

The panel: - Detects new domains and automatically requests certificates - Monitors certificate expiration and schedules renewal - Stores certificate metadata in the database (expiration dates, issuer) - Displays certificate status on the Domains page - Alerts administrators to expiring or missing certificates - Auto-deploys certificates to Nginx vhost configurations

Database Storage

Certificates are NOT stored in the database; only metadata is: - domains.ssl_issued_at — timestamp when certificate was issued - domains.ssl_expires_at — timestamp when certificate expires - domains.ssl_status — status (active, pending, failed, expired)

Actual certificate files are in /etc/letsencrypt/live/{domain}/: - privkey.pem — private key - cert.pem — certificate - chain.pem — certificate chain - fullchain.pem — certificate + chain combined

Nginx Configuration

Vhost files at /etc/nginx/sites-available/{domain}.conf reference certificates:

listen 443 ssl http2;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

When certificates are renewed, Nginx is reloaded to use the new certificate paths.

Troubleshooting

Certificate Issuance Failed

Check Let’s Encrypt validation: 1. Verify domain DNS resolves: dig example.com 2. Verify HTTP is accessible: curl http://example.com/.well-known/acme-challenge/test 3. Check Certbot logs: certbot logs or /var/log/letsencrypt/ 4. Verify domain is added to system: jabali domain:list

Certificate Expired

Certificates that expire are typically renewed automatically. If renewal fails: 1. Check renewal logs: certbot renew --dry-run 2. Manually renew: jabali ssl:renew <domain> 3. Check Let’s Encrypt rate limits (max 50 certs per domain per week)

Webroot Not Accessible

If Let’s Encrypt cannot access the challenge token: 1. Verify nginx is serving the domain: curl -I http://example.com 2. Check .well-known/acme-challenge/ is writable: ls -la /home/{user}/{domain}/public/.well-known/acme-challenge/ 3. Verify nginx config doesn’t redirect HTTP to HTTPS before challenge validation 4. Check web server user can write to challenge directory

Panel Certificate Issues

Panel certificate is critical for control panel access. If it expires: 1. Check status: jabali ssl:panel 2. Issue new cert: jabali ssl:panel-issue 3. Restart FrankenPHP: systemctl restart frankenphp

Security

  • Private keys are stored with restrictive permissions (readable only by root)
  • Certificates are signed by Let’s Encrypt, a trusted Certificate Authority
  • Validation ensures only domain owners can request certificates
  • Automatic renewal prevents certificate expiration
  • Certificate transparency logs record all issued certificates (public record)

Cost

Let’s Encrypt certificates are free. No fees or subscriptions apply.