인증이 누구인지를 확인하는 일이라면, 그 뒤에 남는 질문은 그 사람이 무엇을 해도 되느냐다. 이 질문에 답하는 것이 인가이고, 인가를 어떻게 설계하느냐가 서비스의 안전과 관리 편의를 함께 좌우한다. 사용자 하나하나에 할 수 있는 일을 일일이 붙이면 처음에는 단순하지만, 사용자와 기능이 늘수록 감당할 수 없이 복잡해진다. 권한과 역할의 설계는 이 복잡함을 다스리는 방법이다.
이번 편은 권한을 사용자에게 직접 붙이는 방식의 한계가 무엇인지, 역할이라는 묶음이 그 한계를 어떻게 푸는지, 최소 권한의 원칙이 왜 방어의 바탕인지, 역할의 계층을 어떻게 다루는지, 판단의 규칙을 왜 한곳에 모아야 하는지, 그리고 권한 설계에서 흔히 저지르는 실수가 무엇인지를 다룬다. 인가를 촘촘히 설계하는 것이 권한 상승을 막는 근본이다.
권한을 직접 붙이는 것의 한계
가장 단순한 인가는 각 사용자에게 할 수 있는 일을 하나하나 붙이는 것이다. 이 사용자는 이 글을 수정할 수 있고 저 화면에 들어갈 수 있다는 식으로, 사용자마다 허용된 동작의 목록을 둔다. 사용자가 적고 기능이 단순할 때는 이 방식이 직관적이고 문제없이 돌아간다.
그러나 사용자와 기능이 늘면 이 방식은 감당하기 어려워진다. 사용자 수만큼 권한 목록이 생기고, 새 기능이 생길 때마다 그 권한을 누구에게 줄지 하나하나 정해야 한다. 비슷한 일을 하는 사용자가 여럿이어도 각자에게 같은 권한을 따로 붙여야 하므로, 같은 설정이 수없이 반복된다.
반복은 실수를 부른다. 어떤 사용자에게는 권한을 주고 다른 사용자에게는 빠뜨리는 일이 생기고, 권한 규칙이 바뀌면 관련된 모든 사용자를 찾아 일일이 고쳐야 한다. 하나라도 빠뜨리면 권한이 어긋난 사용자가 남는다. 직접 붙이는 방식은 규모가 커질수록 이런 어긋남을 피하기 어렵다.
더 큰 문제는 전체를 파악하기 어려워진다는 것이다. 누가 무엇을 할 수 있는지가 사용자마다 흩어져 있으면, 지금 이 기능에 누가 접근할 수 있는지를 한눈에 알 수 없다. 권한의 전체 그림이 보이지 않으면, 어디에 구멍이 있는지도 알기 어렵다. 이 파악의 어려움이 직접 붙이는 방식의 근본적인 한계다.
역할이라는 묶음
이 한계를 푸는 방법이 역할이다. 비슷한 일을 하는 사용자들이 공통으로 필요로 하는 권한을 하나의 묶음으로 만들고, 그 묶음을 역할이라 부른다. 사용자에게는 개별 권한을 일일이 붙이는 대신 역할을 부여하고, 그 역할에 담긴 권한을 사용자가 함께 갖게 한다. 권한과 사용자 사이에 역할이라는 중간층을 두는 것이다.
이 중간층이 반복을 없앤다. 같은 일을 하는 사용자들에게 같은 역할을 부여하면, 그 역할에 담긴 권한이 한꺼번에 적용된다. 새 사용자가 그 일을 맡게 되면 역할 하나를 부여하는 것으로 필요한 권한이 모두 갖추어진다. 사용자마다 권한을 따로 붙이던 수고가 역할 부여 하나로 줄어든다.
규칙이 바뀔 때도 한곳만 고치면 된다. 어떤 역할이 할 수 있는 일이 달라지면, 그 역할의 권한 묶음만 고치면 그 역할을 가진 모든 사용자에게 반영된다. 사용자를 하나하나 찾아 고칠 필요가 없으므로, 규칙 변경이 빠르고 빠뜨림이 없다. 역할이 권한 관리의 단위가 되는 것이다.
전체를 파악하기도 쉬워진다. 어떤 기능에 누가 접근할 수 있는지는, 그 권한을 가진 역할이 무엇이고 그 역할을 누가 가졌는지를 보면 알 수 있다. 사용자마다 흩어져 있던 권한이 역할로 모이므로, 권한의 전체 그림이 역할 단위로 정리되어 드러난다. 이 파악의 용이함이 방어의 점검을 쉽게 만든다.
역할과 권한을 나누어 보기
역할을 쓸 때 흔히 흐려지는 구분이 역할과 권한의 차이다. 권한은 개별 동작을 할 수 있는 자격이고, 역할은 그런 권한들의 묶음이다. 코드에서 판단할 때는 역할이 아니라 권한을 기준으로 삼는 것이 좋다. 이 사용자가 특정 역할인지를 묻기보다, 이 동작을 할 권한이 있는지를 묻는 것이다.
권한을 기준으로 판단하면 역할의 구성이 바뀌어도 코드가 흔들리지 않는다. 어떤 동작에 어떤 역할이 필요한지를 코드에 박아 두면, 역할을 새로 나누거나 합칠 때 그 코드를 모두 찾아 고쳐야 한다. 반면 그 동작에 필요한 권한을 기준으로 두면, 역할을 어떻게 바꾸든 그 권한을 어느 역할에 담을지만 조정하면 된다.
이 분리는 역할의 설계에 유연함을 준다. 서비스가 자라며 역할을 더 잘게 나누거나 새 역할을 만들 때, 권한 자체는 그대로 두고 그 권한을 어느 역할에 담을지만 다시 짜면 된다. 판단의 기준인 권한과 관리의 단위인 역할을 나누어 두면, 둘을 서로 독립적으로 바꿀 수 있다.
한 사용자가 여러 역할을 함께 갖는 경우도 이 분리로 자연스럽게 다루어진다. 여러 역할을 가진 사용자는 그 역할들에 담긴 권한을 모두 갖게 되고, 판단은 여전히 권한 단위로 이루어진다. 역할이 겹치더라도 권한을 기준으로 보면 그 사용자가 무엇을 할 수 있는지가 분명하다. 역할은 권한을 모으는 편의일 뿐, 판단의 근거는 권한이다.
최소 권한의 원칙
권한을 설계하는 바탕에는 최소 권한의 원칙이 있다. 각 사용자와 역할에는 그 일을 하는 데 꼭 필요한 만큼의 권한만 주고, 그 이상은 주지 않는 것이다. 필요 이상의 권한은 쓰이지 않더라도 위험으로 남는다. 그 계정이 도용되었을 때, 넘겨줄 수 있는 것이 필요한 만큼으로 제한되기 때문이다.
넉넉하게 권한을 주는 편이 편하다는 유혹은 늘 있다. 나중에 필요할지 모른다며 미리 넓게 주면 당장은 문제가 없어 보인다. 그러나 넓게 준 권한은 대개 그대로 잊히고, 필요 없는 권한이 계정에 쌓인다. 그 계정이 뚫리는 순간, 잊힌 권한들이 모두 공격자의 손에 들어간다. 최소 권한은 이 잠재된 위험을 처음부터 줄인다.
최소 권한은 권한 상승의 피해도 줄인다. 어떤 사용자가 권한을 넘어 접근하는 데 성공하더라도, 애초에 각 권한이 좁게 나뉘어 있으면 한 번에 넘길 수 있는 범위가 제한된다. 넓은 권한 하나를 뚫는 것과 좁은 권한 여럿을 하나씩 뚫는 것은 난이도가 다르다. 권한을 잘게 나누고 좁게 주는 것이 피해를 국한한다.
이 원칙은 특별한 권한일수록 엄격히 지켜야 한다. 관리 기능처럼 큰 영향을 미치는 권한은 정말 필요한 소수에게만 주고, 그 부여를 신중히 다룬다. 강한 권한이 넓게 퍼져 있으면 그중 하나만 뚫려도 큰 피해가 나므로, 강한 권한일수록 가진 사람의 수를 줄이는 것이 안전하다.
역할의 계층과 상속
역할이 여럿이 되면, 어떤 역할이 다른 역할의 권한을 포함하는 관계가 생긴다. 상위 역할이 하위 역할의 권한을 모두 갖고 그 위에 더 가진 형태다. 이런 계층을 두면 상위 역할에 하위의 권한을 일일이 다시 담지 않아도 되어, 권한 묶음이 간결해진다.
계층은 권한의 관계를 자연스럽게 드러낸다. 넓은 책임을 진 역할이 좁은 책임의 권한을 포함하는 구조는 현실의 조직과도 닮아 이해하기 쉽다. 상위 역할을 가진 사용자는 하위의 일도 할 수 있으므로, 권한을 중복해 부여할 필요가 없다. 계층이 권한의 상속을 통해 관리를 덜어 준다.
다만 계층을 지나치게 깊게 만들면 오히려 파악이 어려워진다. 여러 단계로 상속이 이어지면, 특정 역할이 실제로 어떤 권한을 갖는지 따라가기가 복잡해진다. 그래서 계층은 이해할 수 있는 깊이로 얕게 유지하고, 상속이 복잡하게 얽히지 않도록 한다. 편의를 위한 계층이 파악을 해치면 본말이 뒤바뀐 것이다.
계층이 최소 권한과 충돌하지 않도록 주의해야 한다. 상위 역할이 하위의 권한을 모두 포함하다 보면, 상위 역할에 실제로는 필요 없는 권한까지 딸려 오기 쉽다. 그래서 계층을 두더라도 각 역할이 정말 그 권한들을 필요로 하는지 점검하고, 상속 때문에 넘치는 권한이 생기지 않는지 살펴야 한다.
판단의 규칙을 한곳에 모으기
인가의 판단은 여러 곳에서 이루어지지만, 그 판단의 규칙은 한곳에 모으는 것이 안전하다. 어떤 동작에 어떤 권한이 필요한지, 소유권을 어떻게 확인하는지 같은 규칙이 코드 여기저기에 흩어져 있으면, 어느 하나가 빠지거나 서로 어긋나기 쉽다. 규칙을 한곳에 모으면 이런 흩어짐이 사라진다.
규칙을 모아 두면 새 기능을 붙일 때 그 규칙을 불러 쓰기만 하면 된다. 소유권 확인이나 권한 검사 같은 판단을 공통의 자리에 두고, 각 기능은 그 자리를 거쳐 판단을 받는다. 판단 로직을 기능마다 새로 쓰지 않으므로, 어느 기능에서 검사를 빠뜨리는 실수가 줄어든다. 공통의 문지기를 두고 모든 요청이 그 문을 거치게 하는 것이다.
규칙이 바뀔 때도 한곳만 고치면 전체에 반영된다. 권한 판단의 기준이 달라지면, 흩어진 코드를 모두 찾을 필요 없이 모아 둔 규칙만 고친다. 이 한곳이 인가의 정본이 되어, 판단이 언제나 일관되게 이루어진다. 규칙을 모으는 것은 관리의 편의이자 어긋남을 막는 방어다.
규칙을 모으더라도 판단 자체는 각 요청의 맥락에서 구체적으로 이루어져야 한다. 그냥 권한이 있는지가 아니라, 이 사용자가 바로 이 자원에 이 동작을 해도 되는지를 그 자리의 정보로 판정한다. 모아 둔 규칙은 판단의 기준을 제공하고, 실제 판정은 각 요청의 자원과 사용자를 대입해 이루어진다. 기준의 통일과 판정의 구체성이 함께 가야 한다.
속성에 따른 세밀한 판단
역할만으로는 담기 어려운 판단도 있다. 같은 역할이라도 상황에 따라 허용 여부가 갈리는 경우다. 자기 것만 다룰 수 있게 하거나, 특정 조건을 만족할 때만 허용하는 판단은 역할이라는 묶음만으로는 표현하기 어렵다. 이런 경우에는 자원과 사용자의 속성을 함께 보고 판단한다.
소유권 확인이 대표적이다. 글을 수정하는 동작은 그 글의 작성자에게만 허용되는데, 이는 사용자의 역할이 아니라 그 사용자와 그 글의 관계에 달려 있다. 그래서 이 판단은 요청자와 자원의 속성을 대조해, 그 자원이 그 사용자의 것인지를 그 자리에서 확인하는 방식으로 이루어진다.
조건에 따른 판단도 있다. 특정 시간에만, 특정 상태의 자원에만, 특정 범위 안에서만 허용하는 규칙은 역할의 소속만으로는 표현되지 않는다. 이런 규칙은 판단 시점에 관련 속성을 함께 살펴, 조건이 맞을 때만 허용한다. 역할이 큰 틀의 권한을 정하고, 속성에 따른 판단이 그 안에서 세밀한 경계를 긋는다.
세밀한 판단을 더할수록 규칙이 복잡해지므로, 이해할 수 있는 선에서 다루어야 한다. 조건이 지나치게 얽히면 어떤 경우에 허용되는지 파악하기 어렵고, 파악하기 어려운 규칙에는 구멍이 숨기 쉽다. 그래서 역할로 다룰 수 있는 것은 역할로 두고, 속성에 따른 판단은 꼭 필요한 곳에만 더해 규칙을 명료하게 유지한다.
권한 설계의 함정
가장 흔한 함정은 넉넉하게 권한을 주고 방치하는 것이다. 나중을 위해 넓게 준 권한이 쓰이지 않은 채 쌓이면, 그 계정이 뚫렸을 때 넘어갈 것이 늘어난다. 그래서 권한은 최소로 주고, 주기적으로 각 역할과 사용자의 권한을 점검해 필요 없어진 것을 거두어야 한다. 권한은 주는 것만큼 거두는 것도 관리의 일부다.
역할이 시간이 지나며 부풀어 오르는 것도 경계해야 한다. 이런저런 필요에 따라 역할에 권한을 계속 더하다 보면, 원래 좁던 역할이 어느새 넓은 권한을 가진 역할이 된다. 부푼 역할은 그것을 가진 모든 사용자에게 넓은 권한을 퍼뜨린다. 그래서 역할에 권한을 더할 때는 그것이 그 역할의 본래 책임에 맞는지 따지고, 맞지 않으면 새 역할을 두는 편이 낫다.
화면에서만 권한을 가리고 서버에서 확인하지 않는 것도 되풀이되는 함정이다. 버튼을 숨기는 것은 인가가 아니라 눈속임일 뿐이며, 요청은 화면과 무관하게 직접 보낼 수 있다. 인가의 판단은 반드시 서버가 요청을 처리하기 직전에 이루어져야 하고, 화면의 가림은 그 위에 더해지는 편의일 뿐이다.
클라이언트가 보낸 권한 주장을 그대로 믿는 것도 위험하다. 요청에 담긴 역할이나 권한 정보를 검증 없이 신뢰하면, 그 정보를 조작해 권한을 넘어설 수 있다. 사용자가 어떤 권한을 갖는지는 서버가 자신이 쥔 정보로 판단해야 하며, 클라이언트의 주장은 판단의 근거가 아니라 검증의 대상이다. 이 원칙을 지켜야 권한 상승의 문이 닫힌다.
정리하면 권한을 사용자에게 직접 붙이는 방식은 규모가 커지면 감당하기 어려우므로, 권한을 묶은 역할을 중간층으로 두어 관리한다. 판단은 역할이 아니라 권한을 기준으로 삼고, 최소 권한의 원칙 아래 필요한 만큼만 주며, 계층은 얕게 유지하고 규칙은 한곳에 모은다. 역할로 담기 어려운 것은 속성에 따른 세밀한 판단으로 다루되 규칙을 명료하게 지킨다. 다음 편에서는 사용자의 인증 상태가 엉뚱한 요청에 악용되는 것을 막는 사이트 간 요청 위조 방어를 들여다본다.