Files
homelab-optimized/docs/infrastructure/ssl-tls-management.md
Gitea Mirror Bot de73d60a93
Some checks failed
Documentation / Deploy to GitHub Pages (push) Has been cancelled
Documentation / Build Docusaurus (push) Has been cancelled
Sanitized mirror from private repository - 2026-04-05 12:13:06 UTC
2026-04-05 12:13:06 +00:00

6.1 KiB

SSL/TLS Certificate Management

Managing SSL certificates for the homelab infrastructure


Overview

The homelab uses Nginx Proxy Manager (NPM) as the primary certificate authority, with Let's Encrypt providing free SSL certificates.


Certificate Authorities

Primary: Let's Encrypt

  • Provider: Let's Encrypt
  • Validation: HTTP-01 (automatic via NPM)
  • Renewal: Automatic at 90 days
  • Domains: *.vish.local, *.vish.gg

Secondary: Self-Signed

  • Use: Internal services (non-public)
  • Tool: OpenSSL
  • Regeneration: As needed

Certificate Locations

Nginx Proxy Manager

/opt/docker/npm/data/
├── letsencrypt/
│   └── accounts/
│       └── acme-v02.api.letsencrypt.org/
└── ssl/
    └── <domain>/
        ├── fullchain.pem
        ├── privkey.pem
        └── bundle.crt

Services with Own Certs

  • Authentik: /opt/authentik/ssl/
  • Matrix: /etc/matrix-synapse/ssl/
  • PostgreSQL: /etc/ssl/private/

Adding New Certificates

  1. Access NPM: http://calypso.vish.local:81
  2. Navigate to SSL CertificatesAdd SSL Certificate
  3. Enter domain names:
    • service.vish.local (internal)
    • service.vish.gg (public)
  4. Enable Force SSL
  5. Click Save

Via CLI (Automation)

# Using certbot directly
certbot certonly --webroot \
  -w /var/www/html \
  -d service.vish.local \
  --agree-tos \
  --email admin@vish.local

Certificate Renewal

Automatic (Default)

  • NPM auto-renews 7 days before expiration
  • No action required
  • Check logs: NPM → Logs

Manual Renewal

# Force renewal via NPM
docker exec nginx-proxy-manager npm --root /etc/npm \
  force-renew

# Or via API
curl -X POST http://npm/api/nginx/certificates/<id>/renew

Ansible Playbook

ansible-playbook ansible/automation/playbooks/certificate_renewal.yml

Certificate Status

Check Expiration

# Via NPM
# Navigate to SSL Certificates tab

# Via openssl
echo | openssl s_client -connect service.vish.local:443 2>/dev/null | openssl x509 -noout -dates

# Via script
cd /opt/npm/letsencrypt/live/
for cert in */; do
  echo "$cert: $(openssl x509 -enddate -noout -in "$cert/cert.pem" | cut -d= -f2)"
done

Certificate Dashboard

Domain Expiry Status Renews
vish.gg +85 days Active Auto
*.vish.local +85 days Active Auto

Common Issues

Rate Limiting

Problem: Too many certificate requests

Solution:

  • Wait 1 hour (Let's Encrypt limit)
  • Use staging environment for testing
  • Request multiple domains in one cert

DNS Validation Failure

Problem: ACME challenge fails

Solution:

  • Verify DNS A record points to public IP
  • Check firewall allows port 80
  • Ensure no CNAME conflicts

Mixed Content Warnings

Problem: HTTP resources on HTTPS page

Solution:

  • Update service config to use HTTPS URLs
  • For internal services, use HTTP (NPM handles SSL)
  • Check browser console for details

Certificate Mismatch

Problem: Wrong certificate served

Solution:

  1. Check NPM proxy host settings
  2. Verify certificate is assigned
  3. Clear browser cache
  4. Check for multiple certificates

Internal Services (Self-Signed)

Creating Self-Signed Cert

# Create directory
mkdir -p /opt/service/ssl

# Generate certificate
openssl req -x509 -nodes -days 365 \
  -newkey rsa:2048 \
  -keyout /opt/service/ssl/key.pem \
  -out /opt/service/ssl/cert.pem \
  -addext "subjectAltName=DNS:service.local,DNS:service"

# Set permissions
chmod 600 /opt/service/ssl/key.pem

Adding to Trust Store

# Linux (Ubuntu/Debian)
sudo cp /opt/service/ssl/cert.pem /usr/local/share/ca-certificates/service.crt
sudo update-ca-certificates

# macOS
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /opt/service/ssl/cert.pem

Matrix/Synapse Certificates

Custom Certificate Setup

# docker-compose.yml
services:
  synapse:
    environment:
      - SYNAPSE_TLS_CERT_FILE=/ssl/tls.crt
      - SYNAPSE_TLS_KEY_FILE=/ssl/tls.key
    volumes:
      - ./ssl:/ssl:ro

Federation Certificates

# Add to TLS certificates
/usr/local/bin/REDACTED_APP_PASSWORD \
  --server-name vish.local \
  --tls-cert /opt/npm/ssl/vish.gg/fullchain.pem \
  --tls-key /opt/npm/ssl/vish.gg/privkey.pem

Security Best Practices

Key Permissions

# Private keys should be readable only by root
chmod 600 /path/to/privkey.pem
chown root:root /path/to/privkey.pem

Cipher Suites

Configure in NPM under Settings → SSL → Advanced:

ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA256

HSTS

Enable in NPM:

  • Settings → SSL → Force HSTS
  • Preload recommended

Backup

Backup Certificates

# Backup NPM certificates
tar -czf backups/ssl-$(date +%Y%m%d).tar.gz \
  /opt/docker/npm/data/letsencrypt/ \
  /opt/docker/npm/data/ssl/

Restore

# Restore
tar -xzf backups/ssl-20240101.tar.gz -C /

# Restart NPM
docker-compose -f /opt/docker/npm/docker-compose.yml restart

Monitoring

Expiration Alerts

Configure in Prometheus/Alertmanager:

groups:
- name: certificates
  rules:
  - alert: REDACTED_APP_PASSWORD
    expr: (certify_not_after - time()) < (86400 * 30)
    for: 1h
    labels:
      severity: warning
    annotations:
      summary: "Certificate expiring soon"

Useful Commands

# Check all certificates
docker exec nginx-proxy-manager npm --root /etc/npm list

# Force renewal
docker exec nginx-proxy-manager npm --root /etc/npm force-renew

# Manual ACME challenge
docker exec -it nginx-proxy-manager sh
cd /etc/letsencrypt/renewal-hooks/deploy/

# Verify certificate
openssl s_client -connect vish.gg:443 -servername vish.gg