#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 neededConfiguration
Variables d'environnement
# 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=30Model SQLAlchemy
# 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
# /root/.certbot/duckdns.ini
dns_duckdns_token = abc123xyz...
dns_duckdns_propagation_seconds = 60Endpoints API
| Méthode | Route | Réponse | Auth |
|---|---|---|---|
| GET | /api/v1/ssl/status | 200 {certs: [{domain, expiry, days_left, status}]} | Admin |
| GET | /api/v1/ssl/certs/{domain} | 200 détail cert | Admin |
| POST | /api/v1/ssl/renew?domain=api-rgz.duckdns.org | 202 {job_id} | Admin |
| POST | /api/v1/ssl/renew-all | 202 {job_id} | Admin |
Commandes Utiles
# 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-20admin-rgz.duckdns.org: Valide jusqu'au 2026-05-20access-rgz.duckdns.org: Valide jusqu'au 2026-05-20grafana-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