필수 요청 헤더

/v1/health를 제외한 모든 요청은 다음 네 가지 헤더를 포함해야 해요. 하나라도 누락되거나 유효하지 않으면 서버가 HTTP 401로 거부해요.

HeaderFormatDescription
KH-Keykh_live_[A-Z0-9]{32}공개 키 식별자. 시크릿이 아니므로 로그에 노출돼도 무방해요.
KH-TimestampUnix 초 단위 (10자리)현재 타임스탬프. 허용 범위 +-300초.
KH-Noncebase64url 22-44자요청마다 한 번만 사용되는 값. 600초간 캐시된 뒤 다시 사용하실 수 있어요.
KH-Signature64 hexHMAC-SHA256(secret, signing_string), 16진수 인코딩.

서명 문자열 구성

서명 대상 문자열은 다섯 개 구성 요소를 줄바꿈(\n)으로 결합해 만들어요.

signing_string = METHOD + "\n"
               + PATH    + "\n"
               + TIMESTAMP + "\n"
               + NONCE   + "\n"
               + SHA256_HEX(BODY)

signature = HEX( HMAC_SHA256(secret, signing_string) )

PATH는 쿼리 문자열을 포함한 전체 요청 경로이며, 호스트나 프래그먼트는 포함하지 않아요. BODY는 원시 본문 바이트이고, 본문이 없는 GET/DELETE에서는 빈 문자열이며 그 SHA256은 잘 알려진 상수 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855에요.

레퍼런스 구현

PHP:

$method = 'POST';
$path   = '/v1/orders';
$body   = json_encode(['product_id' => 42, 'billing_cycle' => 'monthly']);
$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, $KH_SECRET);

$ch = curl_init('https://www.kernelhost.com/cp/kh_reseller_api/v1/orders');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_POSTFIELDS     => $body,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        "KH-Key: {$KH_KEY}",
        "KH-Timestamp: {$ts}",
        "KH-Nonce: {$nonce}",
        "KH-Signature: {$sig}",
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
]);
$res = curl_exec($ch);

Node.js:

import crypto from 'crypto';

const method = 'POST';
const path   = '/v1/orders';
const body   = JSON.stringify({ product_id: 42, billing_cycle: 'monthly' });
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', KH_SECRET).update(signing).digest('hex');

const res = await fetch('https://www.kernelhost.com/cp/kh_reseller_api/v1/orders', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'KH-Key': KH_KEY,
        'KH-Timestamp': ts,
        'KH-Nonce': nonce,
        'KH-Signature': sig,
        'Idempotency-Key': crypto.randomBytes(16).toString('hex'),
    },
    body,
});

재전송 방어

성공적으로 서명된 요청은 절대 재사용하실 수 없어요. 서버는 모든 논스를 600초간 데이터베이스에 저장하며, 동일한 논스로 들어온 두 번째 요청은 replay_detected 사유로 거부돼요. 마찬가지로 타임스탬프가 서버 시간에서 300초 이상 벗어난 요청도 거부돼요.

권한 범위 모델

키마다 명시적인 권한 범위 목록을 가져요. 각 라우트는 필요한 권한 범위를 검사하며, 부족하면 HTTP 403 forbidden_scope가 반환돼요. 쓰기 및 민감한 권한 범위는 키 생성 시 명시적으로 활성화해야 해요.

  • read:products, read:orders, read:services, read:billing, read:webhooks
  • read:credentials (서비스 자격증명 조회 (root 비밀번호, FTP, VNC). 호출마다 별도의 credentials.read 감사 항목이 생성돼요.)
  • write:orders (주문 처리 및 결제.)
  • write:services (서비스 액션 실행 (시작/정지/재부팅/재설치/해지).)
  • write:webhooks (웹훅 URL 설정.)