Skip to content

#50 — Gestionnaire Certificats SSL

EN COURS

Priorité: 🟠 HAUTE · Type: TYPE C · Conteneur: rgz-beat · Code: app/tasks/ssl_renew.pyDépendances: #10 rgz-beat


Description

Renouvellement automatique des certificats Let's Encrypt via certbot avec plugin DNS DuckDNS. Quatre domaines couverts : api-rgz.duckdns.org, admin-rgz.duckdns.org, access-rgz.duckdns.org, grafana-rgz.duckdns.org. Les certificats sont actuellement valides jusqu'au 2026-05-20. Une Celery task s'exécute quotidiennement (03:00 UTC) pour vérifier si renouvellement nécessaire (expiration <30j). En cas de succès, Traefik recharge la config automatiquement via file watcher.

Image utilisée: infinityofspace/certbot_dns_duckdns (note : underscore, pas trait d'union — LL#46). Les clés API DuckDNS sont injectées via env var DUCKDNS_TOKEN. Les certificats sont stockés dans /etc/letsencrypt/live/ avec liens symboliques vers la dernière version.

Status V1: Partiellement déployé. Le renouvellement automatique est en place mais n'a pas encore été testé en cas d'expiration réelle (prévue mai 2026).

Architecture Interne

1. Déploiement initial (already done):
   └─> Manuel une fois :
       ├─> certbot certonly --dns-duckdns \
       │   --dns-duckdns-credentials /root/.certbot/duckdns.ini \
       │   --dns-duckdns-propagation-seconds 60 \
       │   -d api-rgz.duckdns.org,admin-rgz.duckdns.org,\
       │      access-rgz.duckdns.org,grafana-rgz.duckdns.org \
       │   --agree-tos \
       │   --non-interactive

       ├─> Résultat: certs dans /etc/letsencrypt/live/api-rgz.duckdns.org/
       │   ├─ privkey.pem   (clé privée RSA 2048)
       │   ├─ cert.pem      (certificat leaf)
       │   ├─ chain.pem     (chaîne CA intermédiaire)
       │   └─ fullchain.pem (cert + chain)

       └─> Traefik config: utilise fullchain.pem + privkey.pem

2. Monitoring quotidien (daily 03:00 UTC, Celery task):
   └─> rgz.ssl.renew
       ├─> Schedule: "0 3 * * *" (daily)
       ├─> Queue: rgz.maintenance
       ├─> Timeout: 300s (5min)
       └─> Task:
           ├─> FOR EACH domain IN [api, admin, access, grafana]:
           │   ├─> openssl x509 -in /etc/letsencrypt/live/{domain}/cert.pem -enddate
           │   ├─> Parse expiry date
           │   ├─> Calc days_to_expiry
           │   ├─> IF days_to_expiry <= 30:
           │   │   ├─> certbot renew --non-interactive --quiet
           │   │   ├─> Result: OK ou FAIL
           │   │   ├─> Si OK: signal Traefik (file watcher détecte changement)
           │   │   └─> Si FAIL: retry 3x, puis SMS admin
           │   │
           │   ├─ ELIF days_to_expiry <= 15:
           │   │   └─> SMS reminder: "🔒 Cert {domain} expire dans {days} j"
           │   │
           │   └─> ELSE (days > 30): OK, next check

           └─> LOG: immutable_logs "ssl_cert_check" (pour #45 audit)

3. Renewal process (automatique ou manuel):
   ├─> certbot renew command:
   │   └─> certbot --authenticator dns-duckdns \
   │       --dns-duckdns-credentials /root/.certbot/duckdns.ini \
   │       --non-interactive \
   │       --renew-hook "systemctl reload traefik" (if needed)

   ├─> Étapes:
   │   ├─ 1. Validate domain via DNS challenge (TXT record DuckDNS)
   │   ├─ 2. Request cert from Let's Encrypt
   │   ├─ 3. Install new cert in /etc/letsencrypt/
   │   ├─ 4. Symlink update (live → archive/XXX)
   │   └─ 5. Traefik auto-reload via file watcher

4. DuckDNS integration:
   ├─> Plugin: certbot-dns-duckdns (pip install certbot-dns-duckdns)
   ├─> Image Docker: infinityofspace/certbot_dns_duckdns (underscore!)
   ├─> Config file: /root/.certbot/duckdns.ini
   │   ├─ dns_duckdns_token = {DUCKDNS_TOKEN}
   │   └─ dns_duckdns_propagation_seconds = 60
   ├─> Env var: DUCKDNS_TOKEN (secret)
   ├─> Workflow:
   │   ├─ Certbot → plugin DuckDNS
   │   ├─ Plugin updates TXT record via API DuckDNS
   │   ├─ DNS propagation: ~60s
   │   ├─ Let's Encrypt validates TXT
   │   └─ Cert issued

   └─> Pour 4 domaines (api, admin, access, grafana):
       ├─ 1 certificat wildcard? Non → 4 certificats séparés
       ├─ Ou 1 certificat multi-SAN: api,admin,access,grafana? Possible mais complexe
       └─ Actuellement: 1 cert par domaine (simpler)

5. Traefik auto-reload:
   └─> File watcher: Traefik surveille /etc/letsencrypt/live/*/cert.pem
       ├─> Si fichier modifié (certbot renew update)
       ├─> Traefik détecte changement
       ├─> Reload TLS certs sans restart (hot reload)
       └─> Services continuent sans interruption

6. Alertes et monitoring:
   ├─> SMS si expiry <15j (reminder)
   ├─> SMS si renewal FAIL (action requise)
   ├─> Prometheus metric: ssl_cert_days_to_expiry{domain}
   ├─> Grafana dashboard: SSL Cert Expiry
   │   ├─ Graph: days_to_expiry over time
   │   ├─ Alert: <30j (yellow), <7j (red), <0 (critical)
   │   └─ Table: domain, issuer, valid_from, valid_until
   └─> ELK logs: ssl_renew task execution + result

7. Backup et recovery:
   └─> Certs archivés automatiquement par certbot:
       └─> /etc/letsencrypt/archive/api-rgz.duckdns.org/
           ├─ cert1.pem (ancien)
           ├─ cert2.pem (ancien)
           ├─ cert3.pem (actuel)
           └─ privkey1.pem, privkey2.pem, privkey3.pem (symlinks dans live/)
   └─> Recovery: utiliser version antérieure si rollback needed

Configuration

Variables d'environnement

env
# SSL Cert Manager
SSL_RENEW_ENABLED=true                # Activation
SSL_RENEW_SCHEDULE=0 3 * * *          # daily 03:00 UTC
SSL_RENEW_TIMEOUT=300                 # 5min
SSL_RENEW_CHECK_DAYS=30               # Alert si expiry <30j
SSL_RENEW_REMINDER_DAYS=15            # SMS reminder si <15j
SSL_CERT_PATH=/etc/letsencrypt/live   # Chemin certs

# DuckDNS
DUCKDNS_TOKEN=abc123xyz...            # API token DuckDNS (secret!)
DUCKDNS_DOMAINS=api-rgz,admin-rgz,access-rgz,grafana-rgz  # Sans .duckdns.org
DUCKDNS_PROPAGATION_SECONDS=60        # DNS propagation timeout

# Certbot
CERTBOT_EMAIL=admin@rgz.local         # Email Let's Encrypt
CERTBOT_AGREE_TOS=true
CERTBOT_RENEW_BEFORE_DAYS=30

Model SQLAlchemy

python
# app/models/monitoring.py

class SslCertificate(Base):
    __tablename__ = "ssl_certificates"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
    domain = Column(String(100), unique=True, nullable=False)  # api-rgz.duckdns.org
    issuer = Column(String(100))  # Let's Encrypt
    valid_from = Column(DateTime, nullable=False)
    valid_until = Column(DateTime, nullable=False)
    fingerprint = Column(String(64), nullable=False)  # SHA-256 hexdigest

    last_renewal_attempt_at = Column(DateTime, nullable=True)
    last_renewal_success_at = Column(DateTime, nullable=True)
    renewal_status = Column(String(20))  # success, failed, pending

    cert_file_path = Column(String(200))  # /etc/letsencrypt/live/{domain}/cert.pem
    key_file_path = Column(String(200))   # privkey.pem

    created_at = Column(DateTime, default=utcnow, nullable=False)
    updated_at = Column(DateTime, default=utcnow, onupdate=utcnow)

class SslRenewalLog(Base):
    __tablename__ = "ssl_renewal_logs"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
    domain = Column(String(100), nullable=False)
    renewal_date = Column(DateTime, nullable=False)
    status = Column(String(20))  # success, failed
    error_message = Column(Text, nullable=True)
    old_expiry = Column(DateTime, nullable=True)
    new_expiry = Column(DateTime, nullable=True)

    created_at = Column(DateTime, default=utcnow, nullable=False)

Fichier DuckDNS Config

ini
# /root/.certbot/duckdns.ini
dns_duckdns_token = abc123xyz...
dns_duckdns_propagation_seconds = 60

Endpoints API

MéthodeRouteRéponseAuth
GET/api/v1/ssl/status200 {certs: [{domain, expiry, days_left, status}]}Admin
GET/api/v1/ssl/certs/{domain}200 détail certAdmin
POST/api/v1/ssl/renew?domain=api-rgz.duckdns.org202 {job_id}Admin
POST/api/v1/ssl/renew-all202 {job_id}Admin

Commandes Utiles

bash
# Vérifier expiry d'un certificat
openssl x509 -in /etc/letsencrypt/live/api-rgz.duckdns.org/cert.pem -enddate -noout
# Output: notAfter=May 20 14:30:45 2026 GMT

# Voir tous les certs
ls -la /etc/letsencrypt/live/

# Test manual renewal (dry-run, pas vraiment renouveller)
certbot renew --dns-duckdns \
  --dns-duckdns-credentials /root/.certbot/duckdns.ini \
  --dry-run --non-interactive

# Test manual renewal (réel)
certbot renew --dns-duckdns \
  --dns-duckdns-credentials /root/.certbot/duckdns.ini \
  --non-interactive

# Voir status renewal
certbot renew --dns-duckdns --status

# Vérifier détail certificat (OpenSSL)
openssl x509 -in /etc/letsencrypt/live/api-rgz.duckdns.org/fullchain.pem \
  -text -noout | grep -A2 "Validity"

# API endpoint: vérifier status SSL
curl -X GET http://localhost:8000/api/v1/ssl/status \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq

# Forcer renewal manuel (ne pas attendre 03:00)
curl -X POST http://localhost:8000/api/v1/ssl/renew-all \
  -H "Authorization: Bearer $ADMIN_TOKEN"

# Logs Traefik: vérifier si reload OK
docker logs rgz-traefik | tail -50 | grep -i "cert\|tls"

Implémentation TODO

  • [x] Configuration initiale certbot (4 domaines, 2026-05-20)
  • [x] Integration certbot-dns-duckdns (image infinityofspace)
  • [ ] Celery task: rgz.ssl.renew (daily 03:00)
  • [ ] Fonction check_cert_expiry() (openssl x509 -enddate)
  • [ ] Fonction renew_cert() (certbot renew command)
  • [ ] Fonction send_expiry_alert() (SMS admin <15j)
  • [ ] Endpoint GET /api/v1/ssl/status (vérifier certs)
  • [ ] Endpoint POST /api/v1/ssl/renew-all (manual)
  • [ ] Prometheus metrics: ssl_cert_days_to_expiry
  • [ ] Grafana dashboard: SSL Expiry monitoring
  • [ ] Tests: test renewal (dry-run), vérify dates, verify Traefik reload
  • [ ] Documentation: "Como renovar certificados", emergency rollback
  • [ ] Intégration #45 logs immuables: logguer ssl renewal
  • [ ] Intégration #49 compliance-check: vérif SSL valide

Status V1 (Février 2026)

PARTIELLEMENT DÉPLOYÉ

Certificats actuels:

  • api-rgz.duckdns.org: Valide jusqu'au 2026-05-20
  • admin-rgz.duckdns.org: Valide jusqu'au 2026-05-20
  • access-rgz.duckdns.org: Valide jusqu'au 2026-05-20
  • grafana-rgz.duckdns.org: Valide jusqu'au 2026-05-20

Tâche restante:

  • Implémenter Celery task pour renouvellement automatique (actuellement sur roadmap, sera automatisé avant mai 2026)
  • Test du renouvellement en cas d'expiration réelle (prévu en Q2 2026)

Dernière mise à jour: 2026-02-21

PROJET MOSAÏQUE — 81 outils, 22 conteneurs, 500+ revendeurs WiFi Zone