이야기를 디버깅하는 개발자.

개발과 창작 사이에서, 사람들이 자기만의 이야기를 만들 수 있는 도구와 기록을 만듭니다.

roslyn.dev 자세히보기

개발 노트/ASP.NET Core

OIDC와 OPA로 접근 제어를 분리해보는 닷넷 설계 기록

Roslyn 2026. 7. 5. 20:41
반응형

서비스를 만들다 보면 로그인보다 더 오래 고민하게 되는 부분이 있습니다. 바로 “이 사용자가 어디까지 할 수 있는가”를 판단하는 일입니다.

처음에는 닷넷 코드 안에 Admin, Manager, User 같은 역할을 기준으로 조건문을 넣으면 충분해 보입니다. 하지만 서비스가 조금씩 커지고, 조직이나 기능 단위의 규칙이 늘어나면 접근 제어는 금방 복잡해집니다.

저는 이 문제를 보면서 인증과 인가를 조금 더 분리해서 생각해보고 싶었습니다. 사용자가 누구인지 확인하는 일은 OIDC가 맡고, 그 사용자가 특정 자원에 접근할 수 있는지 판단하는 일은 OPA가 맡는 구조입니다. 닷넷 애플리케이션은 두 세계를 연결하는 얇은 통로가 됩니다.

OIDC는 사용자가 누구인지 확인합니다
OIDC, 즉 OpenID Connect는 OAuth 2.0 위에 인증 계층을 얹은 표준입니다. 쉽게 말하면 “이 사용자가 누구인지”를 애플리케이션이 신뢰할 수 있는 방식으로 전달받기 위한 구조입니다.

닷넷에서는 ASP.NET Core의 OpenID Connect 인증 미들웨어를 사용하면 비교적 익숙한 방식으로 붙일 수 있습니다. 예를 들어 Microsoft Entra ID, Keycloak, Auth0, Duende IdentityServer 같은 OIDC Provider를 통해 로그인하고, 애플리케이션은 ID Token이나 Access Token에 담긴 클레임을 읽습니다.

여기서 중요한 것은 OIDC가 모든 접근 제어 문제를 해결해주지는 않는다는 점입니다. OIDC는 사용자의 식별자, 이메일, 조직, 역할, 그룹 같은 정보를 전달해줄 수 있습니다. 하지만 “이 사용자가 이 문서의 수정 버튼을 눌러도 되는가”, “이 프로젝트의 결제 정보를 볼 수 있는가” 같은 판단은 결국 서비스의 정책에 가깝습니다.

처음에는 이 판단을 컨트롤러나 서비스 코드 안에 직접 넣게 됩니다. 문제는 시간이 지나면서 이 규칙들이 흩어진다는 점입니다. 어떤 규칙은 컨트롤러에 있고, 어떤 규칙은 서비스 레이어에 있고, 어떤 규칙은 데이터 조회 조건에 숨어 있게 됩니다. 나중에 접근 정책을 바꾸려면 코드를 따라다니며 확인해야 합니다.

OPA는 접근 판단을 밖으로 꺼냅니다
OPA, Open Policy Agent는 정책을 코드 바깥에서 판단할 수 있게 해주는 정책 엔진입니다. 정책은 Rego라는 선언형 언어로 작성합니다. 애플리케이션은 OPA에게 “이 입력값에서 접근을 허용해도 되는가”를 물어보고, OPA는 정책에 따라 결과를 돌려줍니다.

이 구조의 장점은 접근 제어 규칙이 애플리케이션 코드 안에 흩어지지 않는다는 데 있습니다. 닷넷 서비스는 사용자의 클레임, 요청 경로, HTTP 메서드, 대상 리소스의 소유자 정보 같은 데이터를 모아 OPA에 전달합니다. OPA는 그 데이터를 기준으로 허용 여부를 판단합니다.

예를 들어 이런 흐름을 생각할 수 있습니다.

사용자가 OIDC Provider를 통해 로그인합니다.
ASP.NET Core 애플리케이션은 토큰을 검증하고 사용자 클레임을 확보합니다.
요청이 들어오면 닷넷 미들웨어나 AuthorizationHandler가 접근 판단에 필요한 정보를 구성합니다.
닷넷 애플리케이션은 OPA의 REST API로 정책 판단을 요청합니다.
OPA가 허용 여부와 필요한 사유를 응답합니다.
닷넷 애플리케이션은 그 결과에 따라 요청을 계속 처리하거나 차단합니다.
이렇게 보면 OPA는 로그인 시스템이 아니라 판단 시스템에 가깝습니다. OIDC가 “누구인가”를 알려주고, OPA는 “그래서 지금 이 행동을 해도 되는가”를 판단합니다.

닷넷에서는 어디에 붙일 수 있을까
ASP.NET Core에서는 몇 가지 위치에서 OPA를 붙일 수 있습니다. 가장 단순한 방식은 커스텀 미들웨어를 만드는 것입니다. 요청이 컨트롤러에 도달하기 전에 사용자 클레임과 요청 정보를 모아 OPA에 질의하고, 허용되지 않으면 403 응답을 반환합니다.

조금 더 닷넷다운 방식으로는 IAuthorizationHandler를 구현할 수 있습니다. 이 경우 기존의 [Authorize] 특성과 정책 기반 권한 부여 흐름을 유지하면서, 실제 판단만 OPA에 위임할 수 있습니다. 이미 ASP.NET Core Authorization을 사용하고 있다면 이 방식이 구조적으로 더 자연스럽습니다.

입력값은 대략 이런 형태가 될 수 있습니다.

{
  "user": {
    "sub": "user-123",
    "roles": ["writer"],
    "tenant_id": "tenant-a"
  },
  "request": {
    "method": "POST",
    "path": "/api/articles/100/publish"
  },
  "resource": {
    "owner_id": "user-123",
    "status": "draft"
  }
}
OPA 정책은 이 입력값을 보고 판단합니다. 예를 들어 글의 소유자이고, 상태가 초안이며, 사용자의 역할이 작성자라면 발행을 허용할 수 있습니다. 반대로 소유자가 아니거나 이미 발행된 글이라면 차단할 수 있습니다.

이 방식은 역할 기반 접근 제어만으로 부족할 때 특히 유용합니다. 실제 서비스에서는 역할보다 맥락이 더 중요할 때가 많습니다. 같은 작성자라도 자기 글은 수정할 수 있지만 다른 사람의 글은 수정할 수 없고, 같은 관리자라도 특정 테넌트 안에서만 권한을 가져야 할 수 있습니다.

운영 관점에서 조심해야 할 부분
물론 OPA를 붙인다고 해서 구조가 자동으로 단순해지는 것은 아닙니다. 오히려 작은 서비스에서는 과한 선택이 될 수도 있습니다. 정책 엔진이 하나 더 생긴다는 것은 배포, 모니터링, 장애 대응 지점도 하나 늘어난다는 뜻입니다.

그래서 처음부터 모든 권한을 OPA로 옮기기보다는, 바뀔 가능성이 높고 코드 안에 흩어지기 쉬운 정책부터 분리하는 편이 현실적입니다. 예를 들어 관리자 메뉴 접근처럼 단순한 규칙은 닷넷의 기본 Authorization만으로도 충분할 수 있습니다. 반면 테넌트, 리소스 소유자, 상태, 요청 행위가 함께 얽히는 규칙은 OPA로 분리했을 때 장점이 생깁니다.

또 하나 중요한 점은 OPA에 전달하는 입력값의 기준을 잘 정리하는 일입니다. 정책 엔진은 입력값이 정확할 때만 좋은 판단을 할 수 있습니다. 사용자 클레임을 그대로 믿을 것인지, 데이터베이스에서 리소스 정보를 다시 조회할 것인지, 토큰에 들어 있는 역할과 내부 권한 테이블이 다를 때 무엇을 우선할 것인지 같은 기준이 필요합니다.

성능도 고려해야 합니다. 모든 요청마다 OPA를 호출하면 네트워크 비용이 생깁니다. 사이드카로 붙일지, 별도 정책 서버로 둘지, 일부 결과를 캐시할지, 실패했을 때 기본적으로 차단할지 허용할지 같은 운영 기준도 함께 정해야 합니다.

제가 이 구조에 관심을 두는 이유
제가 OIDC와 OPA 조합에 관심을 두는 이유는 최신 기술을 더 붙이고 싶어서라기보다, 역할과 정책이 커지는 순간을 미리 상상하게 되었기 때문입니다. 혼자 서비스를 만들고 운영하다 보면 기능을 빨리 붙이는 것만큼이나 나중에 고치기 쉬운 구조가 중요해집니다.

특히 닷넷 기반의 서비스는 인증과 권한 부여 기능이 이미 잘 정리되어 있습니다. 그래서 모든 것을 바깥으로 밀어내기보다, 닷넷이 잘하는 부분은 그대로 두고, 자주 바뀌는 정책 판단만 분리하는 방식이 더 현실적으로 느껴집니다.

OIDC는 신원을 확인하는 안정적인 입구가 되고, OPA는 정책을 기록하고 검증하는 별도의 공간이 됩니다. 닷넷 애플리케이션은 그 사이에서 요청의 맥락을 정리해 전달합니다. 이 구분이 명확해지면 나중에 정책을 바꿀 때도 “코드 어디에 조건문을 넣었더라” 하고 헤매는 시간을 줄일 수 있습니다.

마무리
접근 제어는 처음에는 단순한 권한 체크처럼 보이지만, 서비스가 오래 갈수록 운영의 언어에 가까워집니다. 누가, 어떤 상황에서, 어떤 자원에, 어떤 행동을 할 수 있는지를 정리하는 일이기 때문입니다.

OPA와 OIDC를 닷넷에 붙이는 구조는 모든 서비스의 정답은 아닙니다. 다만 인증과 정책 판단을 분리해두면, 나중에 서비스가 커졌을 때 규칙을 더 차분하게 다룰 수 있습니다. 지금 제 기준에서는 이것이 “멋진 아키텍처”라기보다, 혼자 운영하는 서비스에서 복잡도를 한곳에 모아두기 위한 실험에 가깝습니다.

아직 이 구조를 어디까지 적용하는 것이 적당한지는 더 실험해봐야 합니다. 다만 이번 기록은 닷넷 서비스에서 접근 제어를 바라볼 때, 로그인과 권한 판단을 같은 덩어리로 보지 않기 위한 기준점으로 남겨두고 싶습니다.

반응형