En 2026, il n'y a aucune excuse pour servir du trafic en HTTP simple. Chaque PaaS doit gérer les certificats SSL automatiquement -- les utilisateurs ne devraient pas avoir à penser au TLS du tout. Mais « automatique » couvre un spectre. À une extrémité, vous avez l'intégration ACME basique qui provisionne des certificats depuis Let's Encrypt. À l'autre extrémité, vous avez des clients entreprise qui apportent leurs propres certificats, signés par leurs propres autorités de certification, avec des clés privées qui doivent être chiffrées au repos.
sh0 gère tout le spectre. Voici l'histoire de comment nous avons construit la gestion des certificats SSL, allant de l'ACME zéro-configuration aux uploads de certificats personnalisés avec stockage de clés privées chiffrées en AES-256-GCM -- le tout orchestré à travers le même reverse proxy Caddy que nous avons dompté dans l'Article 5.
Caddy et ACME : le chemin heureux
La fonctionnalité phare de Caddy est le HTTPS automatique. Quand Caddy reçoit une route pour myapp.example.com, il automatiquement :
- Écoute sur le port 80 pour le challenge ACME HTTP-01
- Demande un certificat à Let's Encrypt (ou ZeroSSL, son CA par défaut)
- Installe le certificat et commence à servir en HTTPS sur le port 443
- Renouvelle le certificat automatiquement avant expiration
Pour sh0, cela signifie que l'expérience SSL par défaut ne nécessite aucune configuration de l'utilisateur. Déployez une application, pointez votre DNS vers le serveur, et le HTTPS fonctionne. La seule chose que Caddy a besoin de nous est une adresse e-mail ACME pour les notifications d'expiration de certificats.
La configuration Caddy que nous générons inclut l'émetteur ACME :
rustfn build_tls_automation(email: &str) -> CaddyTlsAutomation {
CaddyTlsAutomation {
policies: vec![CaddyTlsPolicy {
issuers: vec![CaddyIssuer {
module: "acme".to_string(),
email: email.to_string(),
}],
subjects: None, // s'applique à tous les domaines par défaut
}],
}
}Ce seul bloc de configuration active le HTTPS automatique pour chaque domaine routé à travers Caddy. Pas de gestion de certificats par domaine, pas de tâches cron de renouvellement, pas d'alertes d'expiration. Caddy gère tout.
Configuration de l'e-mail ACME à l'exécution
L'e-mail ACME n'est pas juste un nice-to-have -- Let's Encrypt l'utilise pour envoyer des notifications critiques sur les problèmes de certificats. Nous l'avons rendu configurable à l'exécution via le tableau de bord, pas seulement au démarrage.
L'implémentation traverse toute la stack :
Backend : Un endpoint POST /settings/acme-email stocke l'e-mail dans la base de données (table settings, clé acme_email) et met à jour le ProxyManager à l'exécution :
rustpub async fn set_acme_email(
State(state): State<AppState>,
Json(req): Json<SetAcmeEmailRequest>,
) -> Result<Json<ApiResponse>> {
// Persister en base de données
Setting::upsert(&state.pool, "acme_email", &req.email).await?;
// Mettre à jour le proxy à l'exécution -- reconstruit et recharge la config Caddy
state.proxy.set_email(&req.email).await?;
Ok(Json(ApiResponse::success("E-mail ACME mis à jour")))
}ProxyManager : Le champ email est enveloppé dans un RwLock<String>, permettant les mises à jour à l'exécution sans redémarrer le serveur.
Démarrage : La fonction main charge l'e-mail ACME depuis la base de données si le flag CLI --acme-email est vide, utilisant le paramètre de base de données comme repli.
Configuration DNS pour les déploiements auto-hébergés
sh0 est un logiciel auto-hébergé. Les utilisateurs le font tourner sur leurs propres serveurs, avec leurs propres domaines. La configuration DNS est de la responsabilité de l'utilisateur, mais nous le guidons.
Le panneau de gestion des domaines du tableau de bord affiche l'adresse IP réelle du serveur et fournit des instructions claires :
- Créer un enregistrement A pointant le domaine vers l'IP du serveur
- Attendre la propagation DNS
- Caddy gère le reste (challenge ACME, provisionnement du certificat, HTTPS)
Le problème Cloudflare
Un pourcentage significatif des utilisateurs de sh0 utilisent Cloudflare pour le DNS. Cela crée un défi de configuration subtil que nous adressons directement dans le tableau de bord.
Mode DNS uniquement (nuage gris) : Le trafic va directement au serveur. Caddy gère le TLS de bout en bout via ACME. C'est le cas simple.
Mode proxy (nuage orange) : Cloudflare termine le TLS en périphérie. Le paramètre SSL/TLS de Cloudflare doit être « Full (Strict) » pour garantir le chiffrement de bout en bout.
Nous avons ajouté des conseils spécifiques à Cloudflare dans le modal de configuration DNS :
Vous utilisez Cloudflare ? Pour le SSL direct de Caddy, utilisez « DNS only » (icône de nuage gris). Si vous préférez le proxy Cloudflare (nuage orange), configurez votre mode SSL/TLS sur « Full (Strict) » dans le tableau de bord Cloudflare.
Ce seul paragraphe a empêché une catégorie de demandes de support que nous anticipions par expérience.
Certificats SSL personnalisés : la voie entreprise
L'ACME automatique couvre 90 % des cas d'utilisation. Mais les clients entreprise ont souvent des exigences que l'ACME ne peut pas satisfaire : autorités de certification internes, certificats Extended Validation, certificats wildcard, ou certificats devant être émis par un CA spécifique.
sh0 supporte tout cela via les uploads de certificats personnalisés.
Le modèle de données
Le schéma de base de données introduit deux nouvelles tables : certificates et domain_certificates. Les certificats stockent le PEM, les métadonnées (CN, SANs, empreinte, dates de validité), et la clé privée chiffrée en AES-256-GCM.
Génération de CSR
Pour les utilisateurs qui ont besoin que leur CA émette un certificat, sh0 génère la Certificate Signing Request côté serveur en utilisant la crate rcgen. L'utilisateur télécharge le CSR PEM, le soumet à son CA, reçoit le certificat signé et l'uploade dans sh0.
Chiffrement des clés privées
Les clés privées sont les joyaux de la couronne de tout déploiement TLS. sh0 les chiffre au repos avec AES-256-GCM :
rust// Chiffrer la clé privée avant stockage en base de données
let (encrypted, nonce) = sh0_auth::crypto::encrypt(
key_pem.as_bytes(),
&state.master_key,
)?;
// Écrire le PEM déchiffré sur disque avec permissions restreintes (pour que Caddy puisse le lire)
let key_path = state.certs_dir.join(format!("{}.key", cert_id));
let mut file = File::create(&key_path)?;
file.write_all(&decrypted_key)?;
// Unix : restreindre à lecture/écriture propriétaire uniquement
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&key_path, Permissions::from_mode(0o600))?;
}La base de données ne stocke que la clé chiffrée et son nonce. Le fichier PEM déchiffré sur disque (que Caddy lit) a des permissions 0o600 -- lisible uniquement par le propriétaire du processus sh0.
Commutation du mode SSL par domaine
Chaque domaine dans sh0 a un ssl_mode qui détermine comment son certificat TLS est géré :
auto(défaut) : l'intégration ACME de Caddy provisionne et renouvelle les certificats automatiquementcustom: Caddy charge le certificat et la clé uploadés depuis le disque
Le builder de configuration Caddy gère les deux modes en une seule passe de génération de configuration, en excluant les domaines avec certificats personnalisés de la politique ACME automatique.
Synchronisation des certificats au démarrage
Quand sh0 redémarre, l'état custom_certs en mémoire est vide et les fichiers PEM sur disque peuvent ne pas exister. La routine de démarrage restaure tout depuis la base de données : déchiffre les clés privées, écrit les fichiers PEM sur disque pour Caddy, et met à jour le ProxyManager.
Considérations de sécurité
La gestion des certificats SSL est critique pour la sécurité. Nous avons appliqué plusieurs mesures de défense en profondeur :
Chiffrement au repos. Les clés privées sont stockées en base de données chiffrées avec AES-256-GCM. La clé maître est dérivée du mot de passe admin via Argon2.
Permissions de fichiers. Les fichiers PEM sur disque ont des permissions 0o600 (lecture/écriture propriétaire uniquement).
Pas de clé privée dans les réponses API. Le endpoint de détail de certificat retourne les métadonnées mais jamais la clé privée. Une fois uploadée, la clé privée est en écriture seule du point de vue de l'API.
Déduplication par empreinte. L'empreinte SHA-256 est stockée comme champ unique, empêchant le même certificat d'être uploadé deux fois.
Suppression en cascade. Quand un certificat est supprimé, tous les domaines associés reviennent au mode ACME. Pas de références orphelines.
Leçons apprises
Laisser le reverse proxy gérer ACME. Implémenter ACME directement en Rust aurait pris des semaines et introduit des bugs subtils. L'implémentation ACME de Caddy est éprouvée et gère des cas limites que nous n'aurions jamais anticipés.
Chiffrer les clés privées même sur le même serveur. Cela semble redondant, mais la défense en profondeur compte. Une sauvegarde de base de données qui fuit ne divulguera pas les clés privées si elles sont chiffrées au repos.
Guider les utilisateurs à travers le DNS, ne pas l'automatiser. La configuration DNS est la seule chose que nous ne pouvons pas faire pour les utilisateurs auto-hébergés. Nous montrons des instructions claires et laissons le challenge ACME de Caddy être la vraie vérification.
Ceci est la Partie 8 de la série « Comment nous avons construit sh0.dev ». sh0 est une plateforme PaaS construite entièrement par un CEO à Abidjan et un CTO IA, avec zéro ingénieur humain.