오류가 발생했을 때의 처리를 fail-open과 fail-closed로 정리했습니다

· 8분 읽기
목차

AI 에이전트가 만들어 보내 준 해설 자료를 바탕으로, 오류 때 통과시킬지 차단할지를 정하는 fail-open과 fail-closed, 함수 반환값 검사에 관한 내용을 정리했습니다. 그간 경험상 그리 해 왔던 부분인데 표준처럼 존재하는 게 좋았습니다. 표준 내용을 먼저 살펴보고, 마지막에 제가 설계하며 고민하던 것과 비교해 봅니다.

오류 때의 통과와 차단

요청을 검사하는 기능이 정상적으로 판정할 수 있다면, 그 결과에 따라 통과시키거나 차단하면 됩니다. fail-open과 fail-closed는 오류가 발생해 그 판정을 할 수 없을 때의 처리를 가리킵니다. fail-open은 검사 대상 요청을 통과시키고, fail-closed는 차단합니다. 규칙에 맞아서 허용한 것과, 규칙을 검사하지 못했지만 허용한 것은 구분해야 합니다.

이 가운데 오류 때 덜 안전한 상태를 선택하는 설계를 다루는 약점 분류가 CWE-636입니다. CWE는 소프트웨어와 하드웨어의 약점을 분류한 목록이며, CWE-636의 이름은 Not Failing Securely (‘Failing Open’)입니다. 정의는 다음과 같습니다.

제품에 오류나 실패가 발생했을 때, 설계상 사용 가능한 다른 선택보다 덜 안전한 상태로 전환하도록 되어 있습니다. 가장 약한 암호 알고리즘을 선택하거나 가장 관대한 접근 제어 제한을 사용하는 경우가 이에 해당합니다.

CWE-636, Description을 번역했습니다.

문서에는 네트워크 스위치 사례가 나옵니다. 스위치는 트래픽을 해당 인터페이스에만 보내야 하지만, ARP 정보를 인터페이스에 대응시키는 테이블이 넘치면 허브처럼 동작할 수 있습니다. 그러면 트래픽을 여러 인터페이스로 브로드캐스트하여 도청자도 받을 수 있게 됩니다. 통신 기능은 계속되지만, 전송 대상을 제한하던 보안 특성은 유지되지 않습니다.

CWE-636은 이 사례의 완화책으로 인터페이스별 ARP 항목 수 제한을 제시합니다. 테이블이 넘쳐 트래픽이 모든 인터페이스로 퍼지는 일을 막으면서, 다른 인터페이스는 정상적으로 동작하게 두는 방법입니다. 오류 때 취할 조치를 설정할 수 있게 하더라도 안전한 기본값을 써야 한다고 덧붙입니다. 따라서 통과와 차단을 고를 때는 기능을 계속할 수 있는지만이 아니라, 그 상태에서 무엇이 허용되는지도 봐야 합니다.

robots.txt의 응답별 처리

웹을 자동으로 탐색하는 프로그램인 크롤러에 접근 가능한 리소스를 사이트가 알리는 파일이 robots.txt이고, 그 처리 방법을 정한 문서가 RFC 9309입니다. 이 표준은 파일을 가져오지 못한 경우를 응답에 따라 나누고, 어떤 경우에는 접근을 허용하며 어떤 경우에는 차단을 요구합니다.

400-499 응답은 robots.txt를 이용할 수 없는 상태인 Unavailable로 분류합니다. 404만을 가리키는 것이 아니라 400번대 전체가 해당합니다.

서버 상태 코드가 크롤러에 robots.txt를 이용할 수 없음을 나타내면, 크롤러는 서버의 어떤 리소스에도 접근할 수 있습니다(MAY).

RFC 9309, 2.3.1.3을 번역했습니다.

서버나 네트워크 오류로 파일에 도달할 수 없는 Unreachable은 다르게 처리합니다. HTTP에서 500-599 응답이 서버 오류에 해당합니다.

서버나 네트워크 오류로 robots.txt에 도달할 수 없다면, robots.txt가 정의되지 않은 상태라는 뜻이며 크롤러는 전면적인 접근 금지를 가정해야 합니다(MUST).

RFC 9309, 2.3.1.4를 번역했습니다.

400번대에서는 접근을 허용할 수 있고, 500번대에서는 접근을 금지해야 합니다. MAY는 허용되는 선택이고 MUST는 반드시 따라야 하는 요구이므로, 둘 다 단순히 권장 사항으로 읽으면 안 됩니다. 파일을 이용할 수 없다는 응답과 서버 오류 때문에 규칙을 알 수 없다는 상황을 서로 다르게 취급한 것입니다.

차단을 계속 유지해야 하는지도 정해 둡니다. 같은 2.3.1.4절은 robots.txt가 상당히 오랫동안 정의되지 않은 경우, 예를 들어 30일 동안 그런 상태가 이어지면 Unavailable로 간주하거나 캐시된 사본을 계속 사용할 수 있다고 합니다. 30일에 반드시 차단을 해제하라는 요구는 아닙니다. 오류가 오래 이어질 때 적용할 선택을 별도로 둔 것입니다.

HTTP 400, 500에 대한 조치 사례는 딱이었습니다. 하나의 표준 안에서 오류의 종류와 지속 기간에 따라 처리하는 모습을 볼 수 있습니다.

차단 이유와 오류 기록

fail-closed로 만들었다면 오류 때 요청은 차단됩니다. 그러나 차단 건수만으로는 규칙을 위반한 요청이 많았는지, 검사 기능이 판정하지 못했는지 알 수 없습니다. 요청을 막는 조치와 오류가 발생했다는 사실을 알게 하는 처리는 별도로 필요합니다.

오류를 감지하고도 처리하거나 알리지 않으면 차단 건수만 남습니다. 이 경우를 다루는 약점이 CWE-390이며, 정의는 다음과 같습니다.

제품이 특정 오류를 감지하지만, 그 오류를 처리하기 위한 조치를 취하지 않습니다.

CWE-390, Description을 번역했습니다.

완화책에는 문제를 수정하고 재시도하기, 사용자에게 오류를 알리고 프로그램을 계속하기, 사용자에게 알린 뒤 프로그램을 종료하고 정리하기가 나옵니다. 오류를 감지한 다음 반드시 전체 프로그램을 종료해야 한다는 뜻은 아닙니다. 계속 동작하더라도 오류를 알리는 선택이 함께 있습니다.

보안상 중요한 사건을 충분히 기록하지 않는 약점은 CWE-778입니다. 이 문서는 관리자가 공격을 감지하고 오류를 진단하며 공격에서 복구할 수 있을 만큼 데이터를 기록하라고 합니다. 동시에 지나치게 많은 기록도 같은 문제를 일으킬 수 있다고 경고합니다. 요청의 차단 여부만 남기기보다, 규칙 판정에 따른 차단과 오류에 따른 차단을 구분할 정보가 필요합니다.

해설 자료에서는 이 내용을 검사 기능 바깥의 기록과도 비교하는 방법으로 구체화했습니다. 검사 기능이 남긴 이유별 결과와, 호출하는 쪽에서 관찰한 요청 및 응답을 함께 보는 것입니다. 검사 기능이 오류를 감지하지 못하면 자체 오류 기록도 남지 않을 수 있기 때문입니다. 이 구체적인 집계 방법은 표준의 문장 자체가 아니라 해설에서 정리한 방법입니다. 그 근거가 된 CWE-636은 덜 안전한 상태로 동작하면서 관리자가 잘못 안심하게 되는 결과를 지적하고, CWE-778은 진단에 필요한 기록을 요구합니다.

정상 결과와 실패 표시

오류 때의 조치를 실행하려면, 호출하는 쪽에서 먼저 오류를 구분할 수 있어야 합니다. 이 구분이 함수 반환값에서 어긋나는 경우를 다루는 약점이 CWE-253, Incorrect Check of Function Return Value입니다.

제품이 함수의 반환값을 잘못 검사하여 오류나 예외 상황을 감지하지 못합니다.

CWE-253, Description을 번역했습니다.

저는 해설 내용을 두고 토론하면서 “응답값에 -1을 쓰는 것만으로는 실제 -1과 구분할 순 없잖아”라고 반론했습니다. 실패를 -1로 표시하더라도 정상적인 계산 결과에 -1이 포함된다면, 그 숫자만 비교해서 성공과 실패를 나눌 수는 없습니다.

오류 번호를 보관하는 변수인 errno를 설명하는 errno(3)은 이 경우를 직접 다룹니다. 일부 호출에서는 -1이 성공한 경우에도 유효한 반환값입니다. 이런 호출은 실행 전에 errno를 0으로 설정하고, 반환값이 오류일 가능성을 나타낼 때 errno가 0이 아닌지 확인해 구분합니다.

그래서 errno는 반환값이 오류일 수 있다고 나타낼 때만 확인합니다. 같은 매뉴얼은 호출의 반환값이 오류를 나타낼 때만 errno가 의미 있으며, 성공한 함수도 errno를 변경할 수 있다고 명시합니다. 호출하는 함수가 정한 반환값과 오류 표시 방법을 함께 따라야 합니다.

문자열을 long 정수로 바꾸는 strtol도 정상값과 실패 표시가 겹치는 사례입니다. strtol(3)의 범위 검사 설명은 다음과 같습니다.

strtol()은 성공과 실패 양쪽에서 0, LONG_MAX, LONG_MIN을 정상적으로 반환할 수 있으므로(strtoll()에서는 LLONG_MAX, LLONG_MIN), 호출 프로그램은 호출 전에 errno를 0으로 설정하고 호출 후 errno == ERANGE인지 검사하여 오류 발생 여부를 판단해야 합니다.

long과 long long은 C의 정수형이고, strtoll은 long long 정수로 변환하는 함수입니다. LONG_MIN과 LONG_MAX는 long의 최솟값과 최댓값이며, LLONG_MIN과 LLONG_MAX는 long long의 최솟값과 최댓값입니다. ERANGE는 결과가 범위를 벗어났음을 나타내는 오류 코드입니다.

이 검사는 범위를 벗어난 오류를 구분합니다. 숫자를 하나도 읽지 못한 경우에는 변환이 끝난 위치를 받는 endptr도 확인해야 합니다. 매뉴얼의 예제는 endptr가 입력 문자열의 시작 위치와 같은지 검사하여 이 경우를 구분합니다. 결과가 0이라는 사실만으로 숫자 0을 읽었다고 판단하지 않는 것입니다.

실패를 나타내는 값의 선택은 fail-closed 설계와도 연결됩니다. crypt는 비밀번호로부터 저장용 해시 문자열을 계산하는 함수입니다. libxcrypt 매뉴얼은 오류 때 만드는 무효 해시가 13자보다 짧고, *로 시작하며, 입력 인자 setting과 같지 않다고 설명합니다. setting은 해시 방법과 매개변수를 전달하는 값으로, 저장된 해시를 넣어 사용할 수 있습니다. 매뉴얼은 errno를 설정하고 setting과 다른 무효 해시를 반환하는 동작을 오래된 프로그램이 오류 때 fail-closed하도록 고른 것이라고 밝힙니다. 실패 표시가 저장된 값과 일치하여 인증 성공으로 처리되는 일을 피하려는 설계입니다.

여기까지는 함수가 반환값으로 실패를 알리는 경우입니다. 제 반론은 외부에서 프로그램을 호출하는 문맥에서 나온 것이었고, 함수 내부의 -1뿐 아니라 프로그램의 종료 코드와, 오류 설명을 출력하는 표준 오류 스트림인 stderr를 생각했습니다. 함수가 호출자에게 돌려주는 값과 프로세스가 자신을 실행한 부모 프로세스에 알리는 종료 상태는 구분해야 합니다. exit(3)은 종료 상태가 부모 프로세스에 전달된다고 설명하고, stdin(3)은 stderr를 진단 및 오류 메시지를 출력하는 스트림으로 설명합니다.

strtol 매뉴얼의 예제에서도 이 둘이 함께 나옵니다. 변환 오류를 확인하면 perror로 오류 설명을 출력하고 exit(EXIT_FAILURE)로 종료합니다. 호출하는 쪽은 프로그램이 정한 종료 상태로 성공과 실패를 판단하고 오류 설명도 함께 확인해야 합니다. 설명 문자열이 있다는 사실 하나만으로 모든 프로그램의 성공 여부를 판단하는 식으로 일반화할 수는 없습니다.

설계하며 고민하던 처리 방식

제가 설계할 때 고민하던 것도 운영 관점의 처리였습니다. “조용히 넘어가게 할 거냐”, “로그로 남기고 인지시킬 거냐”, 그냥 기능을 차단할지, “차단 스위치를 두고 확인 후 다시 동작하게 할 거냐”를 놓고 여러 번 고민했습니다.

앞의 개념과 비교하면, 조용히 통과시키는 것은 fail-open입니다. 로그를 남기면서 통과시키는 것도 조치하는 방향은 같습니다. 다만 로그를 운영자가 확인할 수 있다면 오류를 알아차릴 수 있습니다. CWE-390이 제시한 오류를 알리고 계속하는 처리, CWE-778이 요구하는 진단 가능한 기록과 연결됩니다. 기록을 남겼다는 사실과 사람이 실제로 알림을 받는다는 것은 구분해야 합니다.

고민하던 처리조치하는 방향알아차리기와 재개
조용히 통과fail-open별도 알림 없음
로그로 남기고 인지fail-open진단 기록과 확인 경로
기능 차단fail-closed별도 기록 및 알림 설계 필요
차단 후 확인하고 재개확인 전 차단상태 변화 알림과 재개 판단

요청을 통과할지 차단할지와 오류를 알리는지에 따라 조용히 통과, 로그로 남기고 인지, 기능 차단, 차단 후 확인하고 재개의 네 가지로 나눈 표

마지막 고민에 연결할 패턴으로 에이전트가 찾아 넣은 것이 서킷 브레이커입니다. 실패가 기준을 넘으면 원격 서비스나 리소스에 대한 호출을 일시 차단하는 패턴입니다. Microsoft의 서킷 브레이커 설명은 요청을 계속 재시도하는 대신 상태를 나누어 처리합니다.

Closed는 요청을 보내면서 최근 실패 수를 세는 상태입니다. 기준을 넘으면 Open으로 바뀌어 요청을 즉시 실패 처리하고 예외를 반환합니다. 일정 시간이 지난 뒤의 Half-Open은 제한된 수의 요청만 보내 성공 여부를 확인하는 상태입니다. 확인 요청이 성공하면 Closed로 돌아가고, 실패하면 다시 Open으로 전환합니다. 서킷 브레이커의 Closed는 요청을 보내고 Open은 차단하므로, fail-closed와 fail-open의 closed/open과 뜻이 반대입니다.

이 문서에는 상태가 바뀔 때 이벤트를 발생시키면 상태 감시나 관리자 알림에 도움이 된다는 설명도 있습니다. 복구 시간이 크게 달라지는 경우에는 관리자가 수동으로 차단을 해제하고 실패 횟수를 초기화할 수 있도록 하는 Manual override도 제시합니다. 자동 확인을 통한 재개와 사람이 확인한 뒤의 수동 해제를 모두 다룹니다. RFC 9309가 오래 지속된 오류에 대해 다른 처리를 허용한 것과도 비교할 수 있지만, RFC의 기간 조건과 서킷 브레이커의 확인 요청은 서로 다른 재개 조건입니다.

경험으로 고민하던 처리를 표준과 비교하니, 통과시킬지 차단할지라는 조치하는 방향과 오류를 알게 할지라는 알아차리기를 따로 설명할 수 있습니다. 기능을 차단하는 것으로 요청 처리는 정해져도, 오류를 누가 알게 하고 무엇을 확인한 뒤 다시 동작시킬지는 남습니다. 제가 “차단 스위치를 두고 확인 후 다시 동작하게 할 거냐”까지 고민하던 것은 차단 이후의 운영도 함께 정하려던 것이었습니다.