이해 가능성

시스템의 “이해 가능성”의 정의는 관련된 기술적 배경지식을 가진 사람이 정확하고 확실하게 다음의 사항을 판단할 수 있는 특성이라고 할 수 있습니다.

  • 시스템의 기능적 동작
  • 보안과 가용성 등을 포함한 시스템의 불변성

이해 가능성이 중요한 이유

  • 보안 취약점이나 회복성에 장애 가능성을 줄일 수 있다.

    시스템을 다루는 엔지니어의 이해도가 낮으면 실수할 확률은 높아지기 때문입니다.

  • 효율적인 장애 대처가 가능하다.

    시스템 이해도가 높다면 피해를 빠르고 정확하게 평가하고, 신속하게 장애의 확산을 막아 근본 원인을 수정할 수 있습니다.

  • 시스템의 보안 상태를 정비함으로 확신이 증가한다.

    이해하기 어려운 시스템은 보안 상태를 확신할 수 없거나 불가능하다.

시스템 불변성

시스템의 환경이 제대로 동작하든 그렇지 않든 항상 참인 특성을 말합니다.

여기선 좁은 의미로 시스템 불변성은 시스템 보안의 모든 동작과 특성을 의미합니다.

불변성을 쉽게 풀어말하면 “나쁜 일이 시스템이 정상이든 아니든 절대 일어나지 않는다.” 라고 할 수 있습니다.

안전한 시스템을 위해 “불변성”을 유지해야하며, 따라서 시스템 분석의 주요 목표도 “불변성”이 유지되는지 확인하는 것입니다.

만일 시스템이 불변성을 위반하는 동작을 수행한다면 시스템이 보안에 약하거나 취약점이 있다고 보면 됩니다.

간단한 예시

불변성에 대한 개념이 어려우니 간단한 예시를 들어 설명해보겠습니다.

시스템 불변성 : “인증받지 않은 사용자는 절대 권한을 얻을 수 없다.”

  • 불변성이 올바르게 유지된 경우
    1. 악의적인 입력으로 SQL Injection이 들어옴.
    2. 서버가 “잘못된 요청”이라고 응답하고, 로그에 남기지만 관리자 권한은 그대로 보호됨.
  • 잘못된 경우
    1. 악의적인 입력으로 SQL Injection이 들어옴.
    2. 관리자 권한으로 인식되어 공격자에게 데이터를 넘겨줌. = 불변성 위반

시스템 불변성으로 유지해야되는 속성들 예시

  • 인증을 받았으며 적절한 권한을 승인받은 사용자만 시스템의 영구 데이터 스토어에 접근할 수 있다.
  • 시스템의 영구 데이터 스토어에 저장된 민감한 데이터에 대한 모든 작업은 시스템 감사 정책에 따라 감사 로그에 기록한다.
  • 시스템의 백엔드가 수신하는 쿼리의 수는 시스템의 프런트엔드가 받은 쿼리의 수에 비례하여 확장한다.
  • 시스템은 미리 지정된 시스템에서만 RPC 요청을 수신할 수 있으며 미리 지정된 시스템이만 RPC 요청을 보낼 수 있다.

불변성 분석하기

“불변성 위배에 따른 위험도” vs “이를 유지하고 검증하는데 필요한 노력”의 균형을 고려해야합니다.

불변성을 분석하는 몇가지 방법을 소개합니다.

  1. 코드 분석
  • 소스 코드를 직접 분석하여 불변성을 위배할 수 있는 버그를 찾는 등의 작업이 포함됩니다.
  • 예상치 못한 버그들이 있을 수 있어 높은 수준의 확신을 얻을 수는 없습니다.
  1. 형식 추론 방식
  • 시스템과 형식화된 로직에 모델링되어 있고 시스템이 해당 속성을 유지하고 있음을 논리적으로 증며하는 경우
  • 마이크로커널이나 복잡한 암호화 라이브러리 코드같은 특정 상황에서는 적절한 선택
  • 대용량 애플리케이션 소프트웨어 개발 프로젝트에는 적합하지 않습니다.

불변성과 멘탈 모델

멘탈 모델은 사람들 각자가 해당 시스템이 어떻게 작동하는지 머릿속에 갖고 있는 심리적/인지적 틀입니다.

멘탈 모델이 유용한 이유는 복잡한 시스템에 대한 이해를 간소화할 수 있기 때문입니다.

따라서, 가능하다면 시스템이 극한 상황이나 예외적인 상황에 빠졌을 때, 멘탈 모델을 유용하게 사용할 수 있도록 시스템을 설계해야합니다.

이상적으로는 대형 시스템에 새로 추가되는 컴포넌트를 설계할 땐, 사람들이 이미 만들어 둔 멘탈 모델로 구성하는 편이 좋습니다.

즉, 시스템은 최대한 직관적으로 구성되어야 한다는 것입니다.

이해 가능한 시스템 설계

단순하고 간단한 시스템이면 이해하기 쉽지만, 최근 시스템들의 복잡성은 자연적이며 필연적입니다.

이해 가능한 시스템을 위해 중요한 점은 관리되지 않은 복잡성을 최소화 하는 것입니다.

따라서, 이해 가능한 시스템을 설계하기 위해선 복잡성을 이해 가능성의 관점에서 관리해야합니다.

이해 가능한 시스템 설계 방법

복잡한 시스템을 이해하려면 대형 멘탈 모델을 내재화하고 유지해야하지만, 인간인 이상 쉽지 않습니다.

따라서, 이 복잡한 시스템을 이해 가능하게 만드는 방법으로 바로 떠오르는 생각은 “작고 간단하게 나누기”입니다.

각 컴포넌트들은 간단하니 멘탈 모델을 만들기 쉽습니다.

하지만… 엄청나게 많아진 컴포넌트들을 모두 관계지어 이해하려면 그것도 쉽지 않고 컴포넌트로 나누어졌으니 각각의 보안 특성과 불변성들도 관리해야하는 등, 현실적으로 쉽지 않습니다.

해결법 : 보안과 신뢰성 요구사항의 중앙 집중식 책임

이 문제를 해결하는 방법은 하나의 컴포넌트에서 모든 보안과 신뢰성 요구사항을 책임지는 것입니다.

  • 이렇게 되면 보안 관련 문제를 단 한곳에서만 확인하면 되고,
  • 각 애플리케이션 코드가 보안 요구사항을 잘못 구현하거나 구현을 놓칠 가능성을 제거합니다.

시스템 아키텍쳐

복잡성을 관리하는 핵심은 시스템을 계층과 컴포넌트로 나누는 것입니다.

그 다음은 어떻게 컴포넌트와 계층으로 분리할 것인지 주의깊게 고민하는 것 입니다.

각 컴포넌트들은 아래와 같은 특성을 가져야합니다.

  • 일관적
  • 직관적
  • 최소 권한의 원칙 준수

지금부턴 이러한 컴포넌트들을 이해 가능한 시스템으로 설계하는 방법에 대해 알아보겠습니다.

이해 가능한 인터페이스 명세

호출자에 대해 가정하는 부분이 적을 수록 좋으며 가정을 해야한다면 그 가정이 명확한 편이 좋습니다.

큰 범주로 구별하면 “구조화된 인터페이스”, “일관적인 객체 모델”, “멱등성을 가진 동작” 등은 시스템의 이해 가능성에 큰 영향을 미칩니다.

해석해야 할 의미가 적어지도록 인터페이스를 추리자.

자유 형식의 JSON 문자열로 구현한 API는 구현 코드와 핵심 비즈니스 로직을 살펴보지 않으면 이해하기가 어렵습니다.

즉, 자유 형식의 JSON API는 직접 해석해야할 의미가 많아 시스템의 이해 가능성을 떨어뜨립니다.

따라서, 인터페이스를 명시적으로 노출할 수 있는 여러 방법으로 이해 가능성을 높이는 것이 좋습니다.

  • OpenAPI
  • gRPC
  • Thrift
  • etc…

인터페이스에 공통 객체 모델을 사용하자

각 리소스를 별개가 아닌 하나의 멘탈 모델을 사용하면 각 리소스들 뿐만아니라 시스템의 큰 부분까지도 쉽게 이해할 수 있습니다.

평소에 이해하고 있는 모델을 재사용했기 때문입니다.

공통 객체 모델을 사용하면 다음과 같은 이점이 있습니다.

  • 시스템의 각 속성이 미리 정의된 기본 속성(불변성)을 만족하는 것을 보장할 수 있다.
  • 시스템은 모든 타입의 객체에 대한 범위, 어노테이트, 참조, 그룹을 표준화된 방법으로 제공할 수 있다.
  • 모든 종류 객체에 대한 작업을 일관된 동작으로 실행할 수 있다. 엔지니어는 필요에 따라 커스텀 객체 타입을 생성할 수 있으며 내장 타입에 사용하는 것과 같은 멘탈 모델로 커스텀 객체 타입을 이해할 수 있다.

최대한 멱등적으로 동작하도록 설계하자

어떤 인터페이스가 어떤 상황에서든 동일한 결과를 반환한다면 그 인터페이스는 멱등적으로 동작한다고 할 수 있으며 “신뢰”할 수 있습니다.

하지만, 멱등적이지 않다면 엔지니어의 멘탈 모델에도 영향을 미쳐 인터페이스를 신뢰할 수 없게되고 잘못된 결과로 이어집니다.

이해할 수 있는 신원, 인증 그리고 접근 제어

모든 시스템은 누가 어떤 리소스(특히, 민감한 리소스)에 접근할 수 있는지 식별할 수 있어야 합니다.

신원과 자격 증명

신원은 엔티티와 관련된 속성이나 식별자의 집합을 의미합니다.

자격 증명은 주어진 엔티티에 대한 신원을 검증합니다. 방법은 간단한 비밀번호, X.509 인증서, OAuth2 토큰 등 다양합니다.

자격 증명은 주로 사전에 정의된 인증 프로토콜을 이용해 전송합니다.

신원과 자격 증명으로 사람 뿐만아니라 모든 엔티티를 식별할 수 있어야 합니다.

안전한 신원 관리와 자격 증명 방법

신원이 유의미하게 안전하려면 다음과 같이 관리되어야 합니다.

  • 이해가 가능한 식별자를 가져야 한다. 보통 unique한 ID 값은 랜덤한 값을 생성하지만, 사람이 이해하기 쉬운 값을 사용하는 것이 좋습니다. 직접 정책을 세우고 관리하는 것은 사람이기 때문에 이해하기 어려운 값이라면 실수할 가능성이 높아집니다.
  • 스푸핑에 견고해야 한다. 쉽게 정보가 탈취되지 않도록 TLS를 사용하거나 하드웨어 수준의 보안을 위한 TPM을 사용하여 스푸핑을 방지할 수 있습니다.
  • 식별자를 재사용해서는 안된다. 식별자를 재사용하면 권한이 제한되어야 하는 엔티티가 해당 식별자를 그대로 사용할 수 있게 되므로 보안 위험이 증가합니다.

따라서, 이해 가능성을 확보하기 위한 가장 기본적인 단계는 “시스템 내의 모든 활성 엔티티에 의미있는 식별자를 부여하는 것” 입니다.

인증과 전송 보안

인증과 전송 보안의 경우 전문 지식이 필요한 복잡한 분야로 모든 엔지니어가 이와 관련된 모든 주제를 깊이 있게 이해하기를 기대할 수는 없습니다.

따라서, 추상화된 API를 제공하고 이를 집중적으로 관리하도록 하여 이해 가능성을 높이는 것이 좋습니다.

접근 제어와 프레임워크

접근 제어 정책은 어떤 워크로드 신원이 고객을 대신해 데이터를 조회할지 결정할 수 있는 충분한 표현력을 가지고 있어야합니다.

이런 접근 정책을 프레임워크로 제공함으로서 엔지니어들의 시스템 이해 가능성을 높이는 것이 좋습니다.

프레임워크는 선언적인 접근 제어 정책을 정의하고 일관성을 보장합니다.

보안 경계

“보안 경계”는 TCB와 ‘그 외 나머지’사이의 인터페이스를 일컫는 말입니다.

여기서 신뢰 컴퓨팅 기반 즉, TCB(Trusted Computing Base)는 “제대로 동작하는 것만으로도 보안 정책이 적용되어 있음을 확실하기에 충분하거나 나아가 장애 시 보안 정책의 위반을 유발할 수 있는 일련의 컴포넌트(하드웨어, 소프트웨어, 사람 등)” 을 의미합니다.

다시 말해, TCB 외부 엔티티가 임의로 또는 악의적인 방식으로 오동작하더라도 보안 정책을 반드시 준수해야 한다는 것입니다.

보안 경계 범위 설정

어떤 TCB를 구성할 것인지는 고려하는 보안 정책에 따라 달라집니다. 따라서 각 계층에서 해당 보안 정책을 준수하기 위해 필요한 TCB를 생각해보는 것이 중요합니다.

또한 TCB는 코드의 양과 복잡도를 증가시키므로 TCB 범위를 최대한 작게 유지하고 실질적으로 보안 정책을 준수하는 데 필요하지 않은 컴포넌트는 최대한 TCB에서 배제하는 것이 중요합니다.

TCB를 작게 유지하면 다음과 같은 이점이 있습니다.

  • 단위 TCB의 이해 가능성을 높여 불변성을 유지하기 위한 유지보수 비용을 줄일 수 있습니다.
  • 공격이나 장애가 퍼져나가는 피해 범위를 제한할 수 있습니다.

보안 경계 설정의 예시

  • OS의 경우

“사용자 신원”을 기준으로 각자의 프로세스를 구분하는 보안 정책을 제공합니다.

  • 네트워크 애플리케이션 서버의 경우

애플리케이션 코드 내부에 정의된 자신만의 보안 정책을 사용합니다.

작은 TCB와 강력한 보안 경계

애플리케이션을 마이크로서비스로 분리하면 보안을 향상시킬 수 있습니다.

msa_example

그림에서 공격자가 시스템A의 취약점을 파고들어 시스템A 서버에 접근했고 가정해봅시다.

하지만, 시스템B는 그 자신만의 보안 경계를 가지고 있어 시스템A의 취약점을 다시 이용한 공격자의 접근을 방지할 수 있습니다.

위협이 관리되어야 하는 범위까지가 보안 경계이다

만약, 배송 주문 시스템이 있다고 가정했을 때 FE에서 사용자만의 정보가아닌 모든 사용자들의 정보를 가져온다고 하면, 이는 공격 대상이 되므로, FE까지 TCB로서 관리되어야 합니다.

이 경우 해결방법은 다음과 같을 것입니다.

  • FE와 BE는 서로 신뢰히지 않는 컴포넌트로 분리하여 외부 인증 실행
  • 웹 오리진 기반으로 경계를 구분, 기능 별로 FE를 구분하여 한쪽이 vunerability가 발생하더라도 다른 쪽에는 영향을 미치지 않도록 합니다.

시스템 설계

이번 절에선 모듈, 라이브러리, API 같이 보다 작은 소프트웨어 컴포넌트를 기준으로 불변성을 이해하는 소프트웨어 구조화 기법에 대해 알아보겠습니다.

애플리케이션 프레임워크 활용하기

애플리케이션엔 불변성을 지키기위한 다양한 코드 레벨 컴포넌트들이 존재합니다.

인증 & 승인 프레임워크, RPC 프레임워크, 오케스트레이션 프레임워크, 모니터링 프레임워크, 소프트웨어 릴리즈 프레임워크 등등 각각의 프레임워크는 유연하지만, 관리하기가 쉽지 않습니다.

예를 들어, 어떤 API에 RPC 프레임워크와 인증 프레임워크를 적용해 만들었지만, 승인 프레임워크가 빠져 보안 위험이 발생할 수 있습니다.

이런 복잡성을 해결하기 위한 방법으로, 이 모든 것을 고수준에서 관리하는 “애플리케이션 프레임워크”를 사용하는 것이 좋습니다.

“애플리케이션 프레임워크”는 간단하고 안전한 기본 설정을 통해 이런 문제를 해결하며 다양한 기능들을 유연하게 제공합니다.

복잡한 데이터 흐름의 이해 - 먼저 값이 아닌 타입을 검증하자

보안과 관련된 속성의 상당수는 시스템을 관통하는 “값”에 대한 검증에 의존합니다.

“값”을 검증한다는 것은 “값”에 대한 약속된 데이터 형태가 주어진다는 암묵적인 가정이 존재하는 것으로, 오작동을 일으킬 여지가 있습니다.

따라서, “값”을 검증하기 전에 “타입”을 검증하는 것이 중요합니다.

타입을 먼저 검증하면 다음과 같은 이점이 있습니다.

  • 타입과 값을 검증하는 코드가 나뉘어 이해 가능성을 높이는 데 도움이 됩니다.

타입 검증과 TCB

코드 레벨에서의 타입 검증은 TCB처럼 취급될 수 없습니다.

외부에서 리플렉션이나 타입 캐스팅으로 어떻게든 모듈의 내부 상태를 변경할 수 있기 때문입니다.

즉, 코드를 충분히 신뢰할 수 없기 때문이지만, 코드 자체가 엄격하게 관리되고 있다면 TCB로 취급될 수도 있습니다.

주입 취약점 확인

타입 뿐만아니라, 주입되는 값들도 확인되어야 불변한 시스템을 유지할 수 있습니다.

저장된 XSS의 경우 검증되지 않은 HTML 코드가 저장되어 브라우저에서 실행되어 공격자가 원하는 동작을 수행할 수 있습니다.

따라서, safeSql, SafeHtml같은 함수를 사용하여 타입과 값을 검증하는 것이 중요합니다.

API 사용성에 대한 고려

API 사용성은 시스템의 이해 가능성에 큰 영향을 미칩니다.

API 사용성을 향상시키면 다음과 같은 이점이 있습니다.

  • 시스템의 이해 가능성을 높이고 불변성을 유지하기 위한 유지보수 비용을 줄일 수 있습니다.

정리

신뢰성과 보안의 장점은 이해할 수 있는 시스템과 깊고 밀접한 관련이 있습니다.

여기서 신뢰성은 가용성, 내구성, 보안 불변성 등 시스템의 주요 설계 보장을 모두 준수하는 것을 의미합니다.

이해하기 쉬운 시스템을 구축하기 위해 제시하는 핵심 가이드는 명확하고 제한적인 목적을 가진 컴포넌트로 시스템을 구축하라는 것입니다.

지금까지 알아본 다음과 같은 전략으로 이해 가능한 시스템을 구축할 수 있습니다.

  • 범위가 좁고 일관적이며 타입을 가진 인터페이스
  • 일관적이고 신중하게 구현한 인증, 승인 그리고 계정 전략
  • 소프트웨어나 관리자 역할을 수행하는 직원 등 구분없이 활성 엔티티에 신원을 명확하게 할당하기
  • 보안 불변성을 캡슐화하는 애플리케이션 프레임워크 라이브러리와 데이터 타입의 활용으로 컴포넌트가 권장 사례를 일관되게 준수하는 것을 보장.

시스템의 이해 가능성은 시스템에서 가장 중요한 기능이 제대로 동작하지 않을 때 단순한 장애와 장시간 사투를 벌어야 하는 재해 수준의 장애 사이에서 확연한 차이점을 보여줍니다.