API는 인터넷으로 연결된 최신 응용 프로그램에서 생명선 역할을 합니다. 매 1/1000초마다 API는 모바일 응용 프로그램으로부터의 요청, 즉 사진에 '좋아요' 추천하기, 음식 배달 주문하기를 비롯하여, IoT 장치에 차 문 열기, 세탁 사이클 시작하기, 주인이 방금 5km 달리기를 마침 등을 지시하는 것을 포함하여 수많은 작업을 수행합니다.

API는 또한 무단 조치 또는 데이터 탈취를 위한 광범위한 공격의 대상이 되기도 합니다. Gartner의 데이터에 따르면 "2019년에는 40%였던 웹 기반 응용 프로그램이 UI보다는 노출된 API의 형태로 공격을 받는 부분이 더 늘어나 2021년경에는 90%에 이를 것"이라고 합니다. Gartner에서는 "2022년경에는 API 남용이 빈번하지 않았던 공격 벡터에서 가장 빈번한 공격 벡터로 옮겨가 기업의 웹 응용 프로그램 데이터 유출의 원인이 될 것"이라고도 예상했습니다[1][2]. Cloudflare의 네트워크를 통과하는 요청은 초당 1800만 개 정도이고 그중 50%는 바로 API로 향하며 이 요청 중 대부분은 악의적인 요청으로 판단되어 차단됩니다.

이러한 위협에 대응하기 위해 Cloudflare는 강력한 클라이언트 인증서 기반의 ID와 스키마 기반의 유효성 검사를 통해 API의 보안을 간소화하고 있습니다. 현재 이러한 기능은 새로운 "API Shield" 내의 모든 요금제에서 무료로 이용할 수 있습니다. 또한 현재의 보안 혜택은 gRPC 기반의 API까지 확장되는데, 해당 API는 JSON이 아니라 프로토콜 버퍼 등의 바이너리 형식을 사용하며, 고객들의 선호도가 계속 높아지고 있습니다.

새 기능에 대해 계속 읽어보실 수도 있고, 첫 API Shield 규칙 구성 방법에 대한 예를 알아보시려면 "적용 예시" 단락으로 건너뛰셔도 됩니다.

능동적 보안 모델 및 클라이언트 인증서

"능동적 보안" 모델은 알려진 행동과 ID만을 허용하고 나머지는 모두 차단하는 모델입니다. 이는 전통적인 "소극적 보안" 모델과는 정반대의 성격입니다. 소극적 모델은 문제가 될 수 있는 IP, ASN, 국가로부터의 요청 또는 문제가 될 수 있는 서명(SQL 삽입 시도 등)이 된 요청을 제외하고는 모든 진입을 허용하는 웹 애플리케이션 방화벽(WAF)으로 실행됩니다.

API를 대상으로 능동적 보안 모델을 실행하는 것은 자격 증명 스터핑 공격과 기타 자동화 스캐닝 툴의 노이즈를 제거하는 가장 직접적인 방식입니다. 능동적 모델로 가는 첫 번째 단계는 상호 TLS 인증과 같은 강력한 인증을 배포하는 것입니다. 상호 TLS 인증은 비밀번호를 재사용하거나 공유하는 행위에 취약하지 않습니다.

2014년에Universal SSL로 서버 인증서 발급을 간소화했듯이, API Shield도 클라이언트 인증서 발급 프로세스를 Cloudflare Dashboard에서 몇 개의 버튼만 클릭하면 되도록 줄여줍니다. 완전히 호스팅된 개인 공개 키 인프라(PKI)를 제공하기 때문에, 자체 인증 기관(CA)을 운영하고 보호하는 일에서 벗어나 응용 프로그램과 기능에 집중할 수 있습니다.

스키마 유효성 검사를 통한 유효한 요청 강제 적용

개발자가 문제 없는 클라이언트(SSL 인증서 소유)만이 API와 연동한다고 확신할 수 있게 되면, 능동적 보안 모델 구현을 위한 그 다음 단계는 그러한 클라이언트가 유효한 요청을 하는지 확인하는 것입니다. 기기에서 클라이언트 인증서를 추출하고 다른 곳에서 재사용하는 것은 어렵기는 하지만 불가능하지는 않습니다. 그러므로 API가 의도한대로 호출되는지 확인하는 작업도 중요합니다.

불필요한 입력을 포함한 요청은 API 개발자가 예상하지 못했을 수도 있고 응용 프로그램이 곧바로 처리한다면 문제로 이어질 수도 있으므로 가능하면 이를 제외해야 합니다. API 스키마 유효성 검사는 예상하는 내용에 대한 규칙을 포함하는 계약이나 "스키마"에 대해 API 요청, 즉 URL 다음에 오는 쿼리 매개변수와 POST 본문의 내용을 매칭하는 방식으로 작동합니다. 유효성 검사가 실패할 경우 API 호출이 차단되어 유효하지 않은 요청이나 악의적인 페이로드로부터 원본이 보호됩니다.

스키마 유효성 검사는 현재 JSON 페이로드를 대상으로 비공개 베타 버전을 진행 중이며, 로드맵의 gRPC/protocol 버퍼를 함께 지원하고 있습니다. 베타 버전에 참여하시려면 "API Schema Validation Beta(API 스키마 유효성 검사 베타)" 제목으로 지원서를 작성해주세요. 스키마 유효성 검사는 베타 종료 후에 API Shield 사용자 인터페이스의 일부로 제공할 예정입니다.

적용 예시

IoT 장치와 모바일 응용 프로그램을 구동하는 API가 어떻게 보안을 확보하는지 예시를 보여드리기 위해 클라이언트 인증서와 스키마 유효성 검사를 사용해 API Shield 예시를 구축합니다.

온도는 외부 적외선 온도 센서가 장착된 Raspberry Pi 3 Model B+ 데모로 보여주는 IoT 장치로 기록하고, POST 요청을 통해서 Cloudflare가 보호하는 API로 전송됩니다. 그런 다음 GET 요청으로 온도가 검색되어 iOS용 Swift에 내장된 모바일 응용 프로그램에 표시됩니다.

두 경우 모두 API는 실제로 Cloudflare Workers®와 Workers KV를 사용해 구축했지만, 인터넷으로 액세스할 수 있는 모든 엔드포인트로 교체할 수 있습니다.

1. API 구성

안전하게 API와 통신하기 위하여 IoT 장치와 모바일 응용 프로그램을 구성하기 전에, API 엔드포인트를 부트스트랩해야 합니다. 예를 단순화하고 추가 사용자 정의를 허용하기 위해 Cloudflare Worker로서의 API를 구현했습니다(코드는 To-Do List 튜토리얼에서 가져옴).

이 특정한 예시에서는 소스 IP 주소를 키로 사용해 온도를 Workers KV에 저장하지만, 이는 지문과 같은 클라이언트 인증서에 있는 값으로 쉽게 교체할 수 있습니다. 아래 코드는 POST가 이루어질 때 KV에 온도와 타임스탬프를 저장하고, GET 요청이 들어오면 최근의 온도 5개를 반환합니다.

const defaultData = { temperatures: [] }

const getCache = key => TEMPERATURES.get(key)
const setCache = (key, data) => TEMPERATURES.put(key, data)

async function addTemperature(request) {

    // pull previously recorded temperatures for this client
    const ip = request.headers.get('CF-Connecting-IP')
    const cacheKey = `data-${ip}`
    let data
    const cache = await getCache(cacheKey)
    if (!cache) {
        await setCache(cacheKey, JSON.stringify(defaultData))
        data = defaultData
    } else {
        data = JSON.parse(cache)
    }

    // append the recorded temperatures with the submitted reading (assuming it has both temperature and a timestamp)
    try {
        const body = await request.text()
        const val = JSON.parse(body)

        if (val.temperature && val.time) {
            data.temperatures.push(val)
            await setCache(cacheKey, JSON.stringify(data))
            return new Response("", { status: 201 })
        } else {
            return new Response("Unable to parse temperature and/or timestamp from JSON POST body", { status: 400 })
        }
    } catch (err) {
        return new Response(err, { status: 500 })
    }
}

function compareTimestamps(a,b) {
    return -1 * (Date.parse(a.time) - Date.parse(b.time))
}

// return the 5 most recent temperature measurements
async function getTemperatures(request) {
    const ip = request.headers.get('CF-Connecting-IP')
    const cacheKey = `data-${ip}`

    const cache = await getCache(cacheKey)
    if (!cache) {
        return new Response(JSON.stringify(defaultData), { status: 200, headers: { 'content-type': 'application/json' } })
    } else {
        data = JSON.parse(cache)
        const retval = JSON.stringify(data.temperatures.sort(compareTimestamps).splice(0,5))
        return new Response(retval, { status: 200, headers: { 'content-type': 'application/json' } })
    }
}

async function handleRequest(request) {

    if (request.method === 'POST') {
        return addTemperature(request)
    } else {
        return getTemperatures(request)
    }

}

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

상호 TLS 인증을 추가하기 전에 임의의 온도 판독값을 POST 하는 것을 테스트합니다.

$ TEMPERATURE=$(echo $((361 + RANDOM %11)) | awk '{printf("%.2f",$1/10.0)}')
$ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

$ echo -e "$TEMPERATURE\n$TIMESTAMP"
36.30
2020-09-28T02:57:49Z

$ curl -v -H "Content-Type: application/json" -d '{"temperature":'''$TEMPERATURE''', "time": "'''$TIMESTAMP'''"}' https://shield.upinatoms.com/temps 2>&1 | grep "< HTTP/2"
< HTTP/2 201 

다음은 제출된 이전 4개와, 해당 온도에 대한 후속 판독값입니다.

$ curl -s https://shield.upinatoms.com/temps | jq .
[
  {
    "temperature": 36.3,
    "time": "2020-09-28T02:57:49Z"
  },
  {
    "temperature": 36.7,
    "time": "2020-09-28T02:54:56Z"
  },
  {
    "temperature": 36.2,
    "time": "2020-09-28T02:33:08Z"
  },
    {
    "temperature": 36.5,
    "time": "2020-09-28T02:29:22Z"
  },
  {
    "temperature": 36.9,
    "time": "2020-09-28T02:27:19Z"
  } 
]

2. 클라이언트 인증서 발급

Cloudflare API가 있으면 유효한 클라이언트 인증서를 요청하기 위해 API를 잠글 수 있습니다. 하지만 그 전에 해당 인증서를 생성해야 합니다. 인증서를 생성하려면, SSL/TLS → Cloudflare Dashboard의 클라이언트 인증서 탭에서 “인증서 생성”을 클릭하거나, API 호출로 해당 프로세스를 자동화할 수 있습니다.

대부분의 개발자가 각자 자신의 개인 키와 CSR을 생성하고 API를 통해 이에 대한 서명을 요청할 것이므로, 여기에 그 프로세스를 작성해 놓았습니다. Cloudflare의 PKI 툴킷 CFSSL을 사용해 먼저 iOS 응용 프로그램에 대한 부트스트랩 인증서를 생성하고 나서, IoT 장치에 대한 인증서를 생성합니다.

$ cat <<'EOF' | tee -a csr.json
{
    "hosts": [
        "ios-bootstrap.devices.upinatoms.com"
    ],
    "CN": "ios-bootstrap.devices.upinatoms.com",
    "key": {
        "algo": "rsa",
        "size": 2048
    },
    "names": [{
        "C": "US",
        "L": "Austin",
        "O": "Temperature Testers, Inc.",
        "OU": "Tech Operations",
        "ST": "Texas"
    }]
}
EOF

$ cfssl genkey csr.json | cfssljson -bare certificate
2020/09/27 21:28:46 [INFO] generate received request
2020/09/27 21:28:46 [INFO] received CSR
2020/09/27 21:28:46 [INFO] generating key: rsa-2048
2020/09/27 21:28:47 [INFO] encoded CSR

$ mv certificate-key.pem ios-key.pem
$ mv certificate.csr ios.csr

// and do the same for the IoT sensor
$ sed -i.bak 's/ios-bootstrap/sensor-001/g' csr.json
$ cfssl genkey csr.json | cfssljson -bare certificate
...
$ mv certificate-key.pem sensor-key.pem
$ mv certificate.csr sensor.csr
IoT 장치, iOS 응용 프로그램을 위한 개인 키 및 CSR 생성하기
// we need to replace actual newlines in the CSR with ‘\n’ before POST’ing
$ CSR=$(cat ios.csr | perl -pe 's/\n/\\n/g')
$ request_body=$(< <(cat <<EOF
{
  "validity_days": 3650,
  "csr":"$CSR"
}
EOF
))

// save the response so we can view it and then extract the certificate
$ curl -H 'X-Auth-Email: YOUR_EMAIL' -H 'X-Auth-Key: YOUR_API_KEY' -H 'Content-Type: application/json' -d “$request_body” https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/client_certificates > response.json

$ cat response.json | jq .
{
  "success": true,
  "errors": [],
  "messages": [],
  "result": {
    "id": "7bf7f70c-7600-42e1-81c4-e4c0da9aa515",
    "certificate_authority": {
      "id": "8f5606d9-5133-4e53-b062-a2e5da51be5e",
      "name": "Cloudflare Managed CA for account 11cbe197c050c9e422aaa103cfe30ed8"
    },
    "certificate": "-----BEGIN CERTIFICATE-----\nMIIEkzCCA...\n-----END CERTIFICATE-----\n",
    "csr": "-----BEGIN CERTIFICATE REQUEST-----\nMIIDITCCA...\n-----END CERTIFICATE REQUEST-----\n",
    "ski": "eb2a48a19802a705c0e8a39489a71bd586638fdf",
    "serial_number": "133270673305904147240315902291726509220894288063",
    "signature": "SHA256WithRSA",
    "common_name": "ios-bootstrap.devices.upinatoms.com",
    "organization": "Temperature Testers, Inc.",
    "organizational_unit": "Tech Operations",
    "country": "US",
    "state": "Texas",
    "location": "Austin",
    "expires_on": "2030-09-26T02:41:00Z",
    "issued_on": "2020-09-28T02:41:00Z",
    "fingerprint_sha256": "84b045d498f53a59bef53358441a3957de81261211fc9b6d46b0bf5880bdaf25",
    "validity_days": 3650
  }
}

$ cat response.json | jq .result.certificate | perl -npe 's/\\n/\n/g; s/"//g' > ios.pem

// now ask that the second client certificate signing request be signed
$ CSR=$(cat sensor.csr | perl -pe 's/\n/\\n/g')
$ request_body=$(< <(cat <<EOF
{
  "validity_days": 3650,
  "csr":"$CSR"
}
EOF
))

$ curl -H 'X-Auth-Email: YOUR_EMAIL' -H 'X-Auth-Key: YOUR_API_KEY' -H 'Content-Type: application/json' -d "$request_body" https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/client_certificates | perl -npe 's/\\n/\n/g; s/"//g' > sensor.pem
Cloudflare에 사용자의 영역에 발행된 개인 CA로 CSR에 서명 요청하기

3. API Shield 규칙 생성

인증서가 있으면 API 엔드포인트가 인증서의 사용을 요청하도록 구성할 수 있습니다. 아래는 규칙을 생성하는 방법에 대한 적용 예시입니다.

이 단계는 어떤 호스트가 인증서(shield.upinatoms.com 등)를 요구하는지 지정하고 API Shield 규칙을 만드는 작업으로 구성됩니다.

4. IoT 장치 통신

Cloudflare API 엔드포인트와의 안전한 통신이 가능한 IoT 장치를 준비하려면, API 엔드포인트에 POST 요청 시 사용할 수 있도록 해당 장치에 인증서를 포함하고 Cloudflare의 응용 프로그램을 지정해야 합니다.

개인 키와 인증서를 /etc/ssl/private/sensor-key.pem 및 /etc/ssl/certs/sensor.pem에 안전하게 복사하고 샘플 스크립트가 다음의 파일을 지정하도록 수정합니다.

import requests
import json
from datetime import datetime

def readSensor():

    # Takes a reading from a temperature sensor and store it to temp_measurement 

    dateTimeObj = datetime.now()
    timestampStr = dateTimeObj.strftime(‘%Y-%m-%dT%H:%M:%SZ’)

    measurement = {'temperature':str(36.5),'time':timestampStr}
    return measurement

def main():

    print("Cloudflare API Shield [IoT device demonstration]")

    temperature = readSensor()
    payload = json.dumps(temperature)
    
    url = 'https://shield.upinatoms.com/temps'
    json_headers = {'Content-Type': 'application/json'}
    cert_file = ('/etc/ssl/certs/sensor.pem', '/etc/ssl/private/sensor-key.pem')
    
    r = requests.post(url, headers = json_headers, data = payload, cert = cert_file)
    
    print("Request body: ", r.request.body)
    print("Response status code: %d" % r.status_code)

해당 스크립트가 https://shield.upinatoms.com/temps에 연동을 시도하면, Cloudflare는 ClientCertificate 전송을 요청합니다. 스크립트는 SSL/TLS 핸드셰이크를 완료하는 데 필요한 sensor-key.pem을 가지고 있음을 증명하기 전에 sensor.pem의 내용을 전송합니다.

클라이언트 인증서를 전송하지 못하거나, API 요청에서 불필요한 필드를 포함하려는 시도를 한다면, 스키마 유효성 검사가 실패하고(구성이 표시되지 않음) 요청이 거절됩니다.

Cloudflare API Shield [IoT device demonstration]
Request body:  {"temperature": "36.5", "time": "2020-09-28T15:52:19Z"}
Response status code: 403

그 대신 유효한 인증서가 제시되고 페이로드가 이전에 업로드된 스키마를 따른다면, 스크립트가 API에 최신 온도 판독값을 POST합니다.

Cloudflare API Shield [IoT device demonstration]
Request body:  {"temperature": "36.5", "time": "2020-09-28T15:56:45Z"}
Response status code: 201

5. 모바일 응용 프로그램(iOS) 통신

API 엔드포인트로 온도 요청이 전송되었으므로, 이제 클라이언트 인증서 중 하나를 사용해 모바일 응용 프로그램에서 이를 안전하게 읽을 수 있습니다.

간략함을 위해 PKCS#12 파일로써 "부트스트랩" 인증서와 키를 응용 프로그램 번들 내에 포함할 것입니다. 실제 배포에서 이 부트스트랩 인증서는 사용자의 자격 증명과 더불어 고유한 사용자 인증서를 반환할 수 있는 API 엔드포인트를 인증하기 위해서만 사용해야 합니다. 기업 사용자는 추가 제어 및 지속적인 옵션을 위한 MDM을 사용하여 인증서를 배포하기를 바랄 것입니다.

인증서 및 개인 키 패키지

바이너리 PKCS#12 파일에 부트스트랩 인증서와 개인 키를 결합하고 난 후에 부트스트랩 인증서와 개인 키를 추가합니다. 그러면 바이너리 파일이 Cloudflare iOS 응용 프로그램 번들에 추가됩니다.

$ openssl pkcs12 -export -out bootstrap-cert.pfx -inkey ios-key.pem -in ios.pem
Enter Export Password:
Verifying - Enter Export Password:

사용자의 iOS 응용 프로그램에 인증서 번들 추가하기

XCode에서 File(파일) → Add Files To "[Project Name]"("[프로젝트명]"에 파일 추가)를 클릭하고 .pfx 파일을 선택합니다. "Add to target(대상에 추가)"에 체크하고 확인합니다.

URLSession 코드가 클라이언트 인증서를 사용하도록 수정하기

이 글은 PKCS#11 클래스와URLSessionDelegate의 사용을 통한 멋진 시연을 제공해 상호 TLS 인증을 요청하는 API와의 연동 시 이를 완료하기 위해 사용자의 응용 프로그램을 수정하는 내용입니다.

앞으로의 전망

앞으로 몇 개월 동안 API 트래픽을 보호하기 위해 설계한 여러 추가 기능과 함께 API Shield를 확장할 계획입니다. 자신의 PKI를 사용하기를 원하는 고객을 위해Cloudflare Access일부로 현재 이용 가능한 자체 CA를 가져올 수 있는 기능을 제공할 예정입니다.

스키마 유효성 검사 베타 버전의 피드백을 받는 대로 모든 고객이 보편적으로 이용할 수 있는 기능을 제공하게 되기를 기대하고 있습니다. 베타 버전을 체험 중이고 공유할만한 아이디어가 있으시면, 여러분의 피드백을 들려주세요.

인증서와 스키마 유효성 검사 외에도, 우리는 추가 API 보안 기능과 API에 대해 더 깊은 이해를 도울 수 있는 심도있는 분석을 제공하게 된 것을 자랑스럽게 생각합니다. 사용하고 싶은 기능이 있으면 아래 댓글난에 적어주세요!

1: "2019년에는 40%였던 웹 활성화 응용 프로그램이 UI보다는 노출된 API의 형태로의 공격을 받는 부분이 더 늘어나 2021년경에는 90%에 이를 것이다." 출처: Gartner “Gartner's API Strategy Maturity Model(Gartner의 API 전략 완성 모델”, Saniye Alaybeyi, Mark O'Neill, 2019년 10월 21일. (Gartner 구독 필요)

2: "2022년경에는 API 남용이 빈번하지 않았던 공격 벡터에서 가장 빈번한 공격 벡터로 옮겨가 기업 웹 응용 프로그램의 데이터 유출 원인이 될 것이다." 출처: Gartner “Cool Vendors in API Strategy(API 전략에서 문제 없는 벤더들)”, Shameen Pillai, Paolo Malinverno, Mark O'Neill, Jeremy D'Hoinne, 2020년 5월 18일. (Gartner 구독 필요)