En-têtes de requête requis
Chaque requête (sauf /v1/health) doit porter ces quatre en-têtes. Si l'un est manquant ou invalide, le serveur rejette avec HTTP 401.
| Header | Format | Signification |
|---|---|---|
KH-Key | kh_live_[A-Z0-9]{32} | Identifiant de clé publique. N'est pas un secret, peut apparaître dans les journaux. |
KH-Timestamp | Secondes Unix (10 chiffres) | Horodatage actuel. Fenêtre de tolérance de +-300s. |
KH-Nonce | 22-44 caractères base64url | Valeur à usage unique par requête. Mise en cache pendant 600s, puis réutilisable. |
KH-Signature | 64 hex | HMAC-SHA256(secret, signing_string), encodé en hexadécimal. |
Construire la chaîne à signer
La chaîne à signer est composée de cinq éléments joints par un saut de ligne (\n).
signing_string = METHOD + "\n"
+ PATH + "\n"
+ TIMESTAMP + "\n"
+ NONCE + "\n"
+ SHA256_HEX(BODY)
signature = HEX( HMAC_SHA256(secret, signing_string) )
PATH désigne le chemin à partir de /v1, par exemple /v1/orders et non /cp/kernelhost_api/v1/orders. Une éventuelle chaîne de requête fait partie de PATH (par exemple /v1/services?limit=10&offset=0). BODY est le corps brut de la requête ; pour les requêtes sans corps, BODY est une chaîne vide dont le SHA256 est la constante bien connue e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
Implémentations de référence
PHP:
$method = 'POST';
$path = '/v1/orders';
$body = json_encode(['product_id' => 42, 'billing_cycle' => 'monthly', 'hostname' => 'web01.example.com']);
$idem = 'order-web01-' . date('Ymd');
$ts = (string) time();
$nonce = bin2hex(random_bytes(16));
$signing = "{$method}\n{$path}\n{$ts}\n{$nonce}\n" . hash('sha256', $body);
$sig = hash_hmac('sha256', $signing, getenv('KH_SECRET'));
$ch = curl_init('https://www.kernelhost.com/cp/kernelhost_api/v1/orders');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'KH-Key: ' . getenv('KH_KEY'),
"KH-Timestamp: {$ts}",
"KH-Nonce: {$nonce}",
"KH-Signature: {$sig}",
"Idempotency-Key: {$idem}",
],
]);
$res = curl_exec($ch);
Node.js:
import crypto from 'node:crypto';
const method = 'POST';
const path = '/v1/orders';
const body = JSON.stringify({ product_id: 42, billing_cycle: 'monthly', hostname: 'web01.example.com' });
const idem = 'order-web01-' + new Date().toISOString().slice(0, 10).replaceAll('-', '');
const ts = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(16).toString('hex');
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
const signing = `${method}\n${path}\n${ts}\n${nonce}\n${bodyHash}`;
const sig = crypto.createHmac('sha256', process.env.KH_SECRET).update(signing).digest('hex');
const res = await fetch('https://www.kernelhost.com/cp/kernelhost_api/v1/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'KH-Key': process.env.KH_KEY,
'KH-Timestamp': ts,
'KH-Nonce': nonce,
'KH-Signature': sig,
'Idempotency-Key': idem,
},
body,
});
Protection anti-rejeu
Une requête correctement signée NE peut PAS être rejouée. Le serveur stocke chaque nonce pendant 600s en base de données ; une deuxième requête portant le même nonce est rejetée avec replay_detected. De même, une requête dont l'horodatage s'écarte de plus de 300s de l'heure serveur est rejetée.
Modèle de permissions
Chaque clé possède une liste de permissions explicite. Les routes vérifient la permission requise ; si elle manque, HTTP 403 forbidden_scope. Les permissions d'écriture et sensibles doivent être activées explicitement à la création de la clé.
read:products,read:orders,read:services,read:billing,read:webhooksread:credentials(Lire les identifiants de service (nom d'hôte, adresses IP, nom d'utilisateur, mot de passe). Chaque appel génère une entrée d'audit supplémentaire credentials.read.)write:orders(Passer et payer des commandes.)write:services(Exécuter des actions de service (start, stop, reboot, ainsi que la résiliation en fin de période de facturation et son annulation).)write:webhooks(Définir l'URL du webhook.)

