공개된 REST API 서버를 운영하고 있다면 보안도 고려해야 합니다.
API(Application Programming Interface)는 소프트웨어 애플리케이션 구현에 꼭 필요한 요소입니다. IT 서비스에서 클라이언트가 서버 쪽 백엔드와 통신할 수 있는 방법은 여러 가지가 있겠지만, API 통신이 대표적인 방법이라고 할 수 있습니다.
로그인, 상품 정보 조회, 구매 등 필요한 로직을 서버 백엔드에 구현해두고 클라이언트가 그 기능을 API를 통해 호출하는 구조를 사용하는데, API는 데이터베이스, 웹 서버, WAS, 메시지큐, 스토리지 등 중요한 데이터가 저장되고 처리되는 컴포넌트와 통신하며 데이터를 추출하기도, 저장하기도 하기 때문에 API의 보안은 매우 중요합니다.
특히, 내부 시스템이나 내부 네트워크가 아닌 인터넷을 통해 호출하는 REST API와 같은 경우는 더더욱 보안성을 갖추어야 합니다. 만약 여러분이 인터넷상에 공개된 REST API 서버를 운영하고 있다면, 고민해야 할 보안의 종류는 여러 가지가 있습니다.
인가된 사용자(클라이언트)만 사용할 수 있는 API 인증(Authentication)
중간자 공격(man in the middle)으로 인한 권한 탈취 방어
API 요청과 응답이 인터넷 구간에서 변조되었는지 확인하는 메시지 무결성(integrity) 과정
인가된 API 사용자가 어느 기능까지 사용 가능한지 자격을 부여하는 API 인가(Authorization)
일반 웹 서버와 마찬가지로 외부의 정상적이지 않은 L7 레이어 공격을 막을 웹 방화벽(WAF)
Dos, DDoS와 같이 무분별한 공격성 API 트래픽을 막기 위한 rate control과 rate limit
일반 API 클라이언트가 아닌 악성 Bot이 API 응답 정보를 scrapping 하려는 요청에 대한 방어
이 글에서는 API를 사용할 수 있는 사용자(혹은 클라이언트)의 정상적인 인증(Authentication) 여부를 확인하는 과정과 API의 메시지가 전달하는 중간에 변조되지 않았다는 무결성이 확인하는 방법에 대해 다룹니다.
API를 호출한 요청이 정상적인 권한이 있는 호출인지를 확인하는 과정입니다. 즉, API 사용 권한 여부를 확인하는 과정이며 API 키, API 토큰, 제3의 서비스 제공자가 인증을 대신해주는 OAuth 2.0 등의 방식을 사용합니다.
API 키(Key)의 사용
API 인증에서 가장 단순한 방법은 API Key를 권한이 있는 사용자에게만 나누어주고, 모든 API 요청에 사용하도록 하는 방법입니다. API Key가 외부에 노출되면 누구나 API를 사용할 수 있기 때문에 매우 중요한 트랜잭션이 있는 민감한 서비스에서는 사용하지 않습니다.
GET /membership/member/gildong/email?api_key=thisisanapikey
누구나 사용할 수 있는 오픈 API를 제공하는 업체들도 개발자 등록을 해야만 저 key를 나누어주는 경우가 있습니다. 이는 API 보안의 목적이기보다는 API 호출을 누가 하고 있는지 key 값을 통해 구분하고 Key 별로 단위 시간당 API 호출 횟수를 제한하여, 특정 클라이언트가 DDoS 성의 너무 많은 API 호출을 하지 않도록 하는 목적도 가지고 있습니다.
ID/PW 방식으로 일반적인 인증을 한번 거친 클라이언트에게 이에 대한 증표로 토큰(token)을 나누어주고, 추후 로직부터는 ID/PW가 아닌 토큰을 사용하도록 합니다. 토큰과 키의 가장 큰 차이점은 키는 의미 없는 난수로 고유한 값을 사용하지만 토큰 내에는 이를 사용하는 사용자 정보 등 추가적인 정보를 담을 수 있다는 점입니다.
# ID/PW 정보를 base64 인코딩 후, HTTPS를 사용하여 로그인 수행
GET https://auth.example.com/logon/logon.jsp
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
# 로그인 성공 시, 인증 서버로 부터 Token을 전달받음
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0b2tlbiBleGFtcGxlIiwibmFtZSI6IlRlY2hUcmlwIiwiaWF0IjoxNTc2NzUwNTU5LCJleHAiOjE1NzY3NTA2MTl9.mlWUufG8X1Zx8Ysw9VGHC9i-c82z_ht-SOtLCxWLlyg
# 다음 호출부터는 인증된 클라이언트라는 것을 요청 헤더에 token값을 포함하여 증명
GET https://api.example.com/membership/access
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0b2tlbiBleGFtcGxlIiwibmFtZSI6IlRlY2hUcmlwIiwiaWF0IjoxNTc2NzUwNTU5LCJleHAiOjE1NzY3NTA2MTl9.mlWUufG8X1Zx8Ysw9VGHC9i-c82z_ht-SOtLCxWLlyg
위 예제를 살펴보면. 첫 번째 요청은 로그인 과정이고 헤더에 Authorization 값을 넣고 인증 헤더 방식(scheme)에는 'Basic'을 사용하였습니다. Basic scheme은 HTTP 프로토콜이 사용하는 인증 방식이며 ID/PW 값을 Base64 인코딩하여 전달하는 방식입니다. 인코딩은 암호화가 아니기 때문에 Base64 디코딩하면 원래의 ID/PW 값을 알 수 있습니다. 따라서 암호화하여 HTTPS와 같은 보안 채널을 통해서만 서버에 전달해야 합니다.
로그인 성공 이후, 인증 서버에게 받은 토큰 값을 클라이언트의 안전한 영역에 저장해두었다가 다음 요청부터 Authorization 헤더와 인증 scheme을 'Bearer'로 변경하여 전달하였습니다. Bearer 지시자는 OAuth 2.0 Framework에서 정의한 Bearer 보안 토큰을 전달하는 방식입니다.
위 예제에서 인정 서버가 돌려준 토큰은 JWT(JSON Web Token) 형식의 토큰입니다. 난수와 같이 보이지만 저 Token에는 다음 의미가 실려있습니다.
{
"alg": "HS256",
"typ": "JWT"
}
{ "sub": "token example",
"name": "TechTrip",
"iat": 1576750559,
"exp": 1576750619
}
alg(alogorithm): 토큰을 생성할 때 사용한 서명 알고리즘, 예제에서는 HS256 (HMAC with SHA-256).
typ(type): 자신이 JWT 임을 알려줌.
sub(subject), name: API를 호출하면서 서버에 별도로 전달할 값.
iat: 1576750550 -> 토큰이 만들어진(issued at) 시간의 epoch time 값 (이보다 이전에 사용할 수 없다)
exp: 1576751610 -> 토큰이 만료되는(expiration) 시간의 epoch time 값 (이보다 이후에 사용할 수 없다)
JWT의 각 필드 값들을 claim이라고 합니다. 위의 claim 외에도 클라이언트와 서버가 사전에 약속한 custom claim도 사용 가능합니다. 서버는 토큰에 포함된 내용을 지속적으로 확인하며 로직을 수행합니다. 만약 API 호출이 iat 이전에 왔거나, exp 이후에 왔다면 403 Forbidden 등의 응답으로 올바르지 않은 호출이라는 것을 클라이언트에 알려줍니다.
API를 호출하며 포함한 쿼리 파라미터 혹은 요청 헤더, URI 등의 값들이 변경되지 않았는지를 확인하는 과정입니다. 무결성이 왜 필요한지 아래 예제를 통해 이해해보도록 하겠습니다.
#원래의 API 호출
GET /membership/member/gildong/email?api_key=thisisanapikey
#key를 복제한 제삼자의 API 호출
GET /membership/member/gildong/address?api_key=thisisanapikey
원래의 API 호출은 특정 회원의 이메일 주소를 물어보는 REST API였습니다. 두 번째 API 호출은 URI 상의 변경이 제삼자에 의해 발생하였습니다. 정상적인 api_key를 사용했기 때문에 서버의 입장에서는 올바른 API 호출이라고 인식하고, gildong 회원의 집 주소를 api key를 탈취한 자에게 알려줄 것입니다. API 경로별로 사전에 다른 key를 사용하면 해결될 문제이지만 일반적으로 API KEY는 URL별로 따로 만들면 복잡도가 증가합니다.
메시지 무결성이란 메시지 내용 변경이 중도에 없었다는 것을 증명하는 과정입니다. 이를 위해 자주 사용하는 방식이 MAC(Message Authentication Code, 메시지 인증 코드)이며, 대표적으로 문자열의 해시값을 사용한 HMAC(Hash based MAC, 해시 기반 메시지 인증 코드)이 많이 사용됩니다.
HMAC에서는 해시값을 만들 비밀키(secret key)가 필요합니다. 아래 예제는 API 개발자가 API를 제공하는 업체의 개발자 등록을 하면, App(applicaiton) ID와 비밀키 한 쌍을 부여받는 시나리오입니다.
API 개발자 등록 서버는 고유한 App ID와 비밀키를 개발자에게 제공하고, 자신의 키 저장소에 이 키를 동시에 보관합니다. (* 서버가 클라이언트에 키를 안전하게 전송하는 방법은 여러 방법이 있는데, 그중 대표적인 방법인 Diffie-Hellman 알고리즘입니다)
API 개발자는 부여받은 비밀키를 사용하여 메시지 해시값을 생성하고, 이를 API 호출에 실어 보냅니다. 해시값은 API를 호출하는 주소(URI), 비밀키, 해시 알고리즘(SHA-256 혹은 MD5)을 매개 변수로 생성됩니다. 여기에 추가로 이 서명을 사용할 수 있는 기간을 명시하는 timestamp 값을 보냅니다.
위와 같은 값을 전달받은 API 서버는 App ID, timestamp가 유효한 값인지, HMAC은 올바른 값이 맞는지 서버가 키 저장소에 보관한 App ID와 한 쌍의 개발자 키를 사용하여 해시값을 분석합니다. 서버에 요청된 쿼리 파라미터(query parameter)를 포함한 URI 원본의 해시를 만들어 클라이언트가 보낸 HMAC과 동일하다면 API 로직을 처리하고, 다르다면 메시지 변조가 일어났음을 감지하고 403 Forbidden 응답을 보냅니다.
timestamp 값은 API 호출의 유효한 시간을 정하는 것인데, API 호출 정보가 만약에라도 외부에 유출되었을 것을 대비하여, 유효 시간이 지나면 API 호출 자체가 무효화되도록 설정하는 것입니다. 이 값도 HMAC의 매개 변수로 넣는다면 역시 이 값을 마음대로 변조할 수 없게 됩니다.
위 구조에서 한 가지 명심해야 할 것은, 모든 API 요청에 대해 서버가 HMAC 계산을 하고, timestamp가 지났는지를 확인하는 과정을 수행하는 서버의 리소스가 예상보다 심각하게 소비될 수도 있다는 점입니다. 따라서 위 구조를 모든 API에 적용하지 않고 어떤 API는 key 방식으로, 어떤 API는 token으로, 또 어느 API는 메시지 무결성까지 확인할지를 결정하여, API 중요도에 맞는 보안 강도를 다르게 가져가는 정책이 필요합니다.
또한 API 서버 자체의 부하를 감소하고, 빠른 API 응답을 위해서 캐싱(caching) 기술을 사용하는 것도 고려해야 합니다. API의 대부분 로직이 데이터베이스에서 질의한 데이터를 가져오는 것이라면 데이터베이스 앞단에 캐싱 레이어를 Redis 혹은 Memcashed로 구성하여 빠르게 가져오거나 XML이나 JSON 형태의 API 결과 자체를 API 서버의 Nginx, HAProxy 혹은 인터넷상의 CDN에서 캐시 하는 방법도 있습니다.
지금까지 알려드린 대로 API는 인증, 인가, 무결성 보장, 추가적인 보안, 성능 그리고 끊임없는 모니터링과 트래픽 집계 등이 모두 중요합니다. 이 모든 로직을 API 서버에서 모두 구현하고 적용하는 것은 큰 부담이기 때문에 API Gateway를 도입하여 이 부분의 처리를 단순화하는 방법도 많이 선호하고 있습니다.