이 글은 오래된 전에 작성된 글입니다. 따라서 최신 버전의 기술에 알맞지 않거나 오류를 유발할 수 있습니다.
저자는 이 글에 대한 질문을 받지 않을 것입니다. 하지만 이 글이 리뉴얼 되면 이 글에 대한 질문을 하거나
토론을 할 수도 있습니다.
이 글은
월간 마이크로소프트웨어(일명 마소) 2002년 10월호 닷넷 칼럼에 기고한 글입니다. Windows 2000을 기준으로 작성된 글이므로 현재 많이 사용되는 Windows 2003과 다소 차이가 있을 수 있습니다.
Windows 2003에서의 바뀐 사항 혹은 고려사항은 별도의 회색 박스에서 필자주 형태로 내용을 추가 하였습니다. 글을 읽으실 때 2002년 10월의 즈음에 작성된 것임을 기억하시면 혼동을 줄이실 수 있을 겁니다. 이 글과 관련한 질문은 피드백에 남겨주시거나 메일 주시면 곧바로 답변해 드리겠습니다.
주) 이 글은 마소 편집부에 제가 발송한 원고를 약간 편집하여 올린 글입니다. 따라서 마소에 실린 글과는 약간의 차이가 있을 수 있습니다.
IIS & ASP.NET Security
Subtitle : ASP.NET과 IIS, 그리고 Windows 2000의 보안 관계
예제코드(이달의 디스켓 내용) 다운로드
최근 빌게이츠의 닷넷 관련 발언에 대해 파문이 일었었다(필자주: 2002년 10월의 즈음임). 빌게이츠가 닷넷의 마이서비스 닷넷(My Service .NET)과 관련하여 보안문제로 인해 고객들의 사용이 없었으며 이로 인해 닷넷 빌딩블록 서비스중 하나로서 마이서비스 닷넷이 실패했다고 발언한 내용을 국내에서 빌게이츠가 닷넷이 실패했다고 보도함으로써 일어난 에피소드이다. 빌게이츠는 이 ‘닷넷 실패’와 더불어 언급한 것이 보안에 대해 집중투자하겠다는 의사를 밝혔으며 이 말은 조금씩 현실로 나타나고 있다. 필자가 항상 읽어보는 MSDN Magazine의 최신호는 몇몇 고정 칼럼을 제외하고는 모두 보안에 대해 다루고있다. MSDN Online을 찾아가 보아도 보안관련 글들이 많이 눈에 띈다. 예전에는 찾기 어려웠던 CryptoAPI (암호화, 해쉬 관련 API)에 대한 글들도 이젠 어렵지 않게 찾을 수 있게 되었다. 본지 9월호의 New About 에서도 보안에 대한 내용이 실렸으며 여기에서 ASP.NET의 보안 모델과 CAS(Code Access Security)에 대해서도 언급이 되었다. 이번 컬럼에서는 지난호에서 소개된 ASP.NET의 보안모델을 보다 깊고 심층적으로 분석해 보고자 한다. 이를 위해서 Windows 2000의 기본적인 보안에 대해서 언급하지 않을 수 없으므로 간단하게나마 몇마디 하고난 후에 IIS 5.0 및 ASP의 보안설정을 살펴볼 것이다. 그리고나서 ASP.NET의 보안모델을 정리하고자 한다. 지난호의 New About와 중복되는 부분은 언급을 피할 것임을 미리 알려두는 바이다. 또한 이 칼럼에서 다루는 보안은 암호화에 관련된 것이 아님을 밝히며 주로 인증과 권한에 대한 것들임을 명시하는 바이다. 독자들의 혼동이 없기를 바란다.
Windwos 2000과 IIS의 보안 상식(?)
ASP나 ASP.NET의 보안을 이해하기 위해 필요한 Windows 2000의 보안 상식(?) 몇가지를 알아보자. Windows 보안을 공부해본 독자들은 알겠지만 대개 이 개념과 용어에 질려서 중도하차 한 적이 몇 번 있을 것이다. 필자도 예외는 아니다. 따라서 이 칼럼에서는 꼭 필요한 개념만 몇 가지를 설명하도록 하겠다. 이와 더불어 대개 독자들이 혼동하거나 잘 이해하지 못하고 있는 IIS의 보안 설정 역시 몇 가지 알아보도록 한다.
Access Token
액세스 토큰이라 함은 프로세스 혹은 쓰레드의 보안 문맥(security context)을 기술하는 Windows의 커널 객체를 말한다. 사용자(사람)이 컴퓨터에 로그온할때는 계정(account)을 사용하게 된다. 그리고 이 사용자가 어떤 프로그램을 수행시키면 이 프로그램은 그 사용자의 계정으로 작동하게 된다. 즉, joe라는 계정을 사용하는 사용자가 notepad.exe를 수행시켰다면 이 notepad.exe는 joe라는 계정하에서 작동한다는 말이다. 이는 joe라는 계정이 갖는 각종 권한을 notepad.exe도 갖는다는 말과 같은 말이다. 이 때 notepad.exe 프로세스가 어떤 계정하에서 수행중이며 어떤 특권을 갖고 있는지, 그리고 특별한 권한 명세가 없을 때 사용되는 디폴트 액세스 권한은 어떤 것인지에 대한 정보를 가지고 있는 것이 액세스 토큰인 것이다. 액세스 토큰은 보안 정보에 대한 일종의 포인터 성격이 강하다. 예를 들어, 현재 프로세스가 어떤 계정 하에서 수행되는지 알고 싶다면 맨 먼저 해야 할 일은 액세스 토큰을 구하는 일이다. 액세스 토큰을 구했다면 이제 이 액세스 토큰에 대한 SID를 구해야 하고 SID로부터 계정 이름을 알아내면 된다. 화면 1은 Windows.NET 서버의 작업 관리자를 보여주고 있다. 이 화면에서 각 프로세스가 어떤 계정 하에서 작동하는가를 파악할 수 있다.
화면 1. Windows.NET 서버의 작업 관리자
액세스 토큰은 다시 프라이머리 토큰(primary token)과 가장 토큰(impersonation token)으로 나누어볼 수 있다. 프라이머리 토큰은 프로세스가 최초로 수행될 때 생성된 액세스 토큰을 말하며 가장(impersonation)을 수행하지 않은 모든 쓰레드는 프라이머리 토큰하에서 작동되게 된다. 가장 토큰은 가장(Impersonation)에 대한 이해를 요구하므로 잠시 후에 다루기로 하자.
Impersonation
가장(impersonation)이란 용어는 Visual Studio .NET에 포함된 한글판 MSDN 라이브러리에서 사용되는 용어이다. 책들에 따라서 인격화 혹은 흉내내기 라는 용어가 사용되기도 한다. 개인적인 의견으로는 어정쩡하고 직관적이지 않는 가장 이나 인격화 같은 용어보다는 직관적인 흉내내기 라는 용어가 마음에 들지만 MSDN을 자주 참고할 독자들을 위해 ‘가장’ 이라는 용어를 사용하겠다.
가장(Impersonation)은 말 그대로 계정을 흉내낸다는 말이다. 대개의 데스크톱 어플리케이션(오피스, 노트패드 등등)은 가장을 필요로하지 않는다. 가장이 필요한 어플리케이션은 대개 서버측 어플리케이션으로서 클라이언트가 서버에 대해 권한이 있는가를 검사하는데 사용되어 진다. 즉, 서버가 클라이언트로부터 파일 액세스와 같은 서비스 요청을 받았을 때 클라이언트가 이러한 서비스에 대한 권한을 갖고 있는가를 검사할 때 사용된다는 말이다.
그런데 이러한 권한 검사에서 왜 가장을 사용할까? 이 질문에 대해 필자는 “가장을 사용하지 않는 경우에는 어떻게 권한 검사를 하는가?” 라고 반문을 하고 싶다. 가장을 사용하지 않는다면 서버는 클라이언트의 액세스 토큰이 액세스 하고자 하는 서버 자원(이 경우 파일)에 대한 권한이 있는가를 검사해야 할 것이다. 이러한 권한 검사를 위해 서버는 파일의 ACL(Access Control List)를 읽어야 하고 클라이언트의 액세스 토큰과 ACL를 비교 검사하여 권한이 있고 없음을 결정해야 할 것이다. 하지만 이러한 작업은 이미 Windows 커널 내에서 수행하는 작업 아닌가? Windows 커널은 컴퓨터의 자원을 액세스할 때 이러한 보안 검사를 수행하도록 되어 있다. 다만 주의할 점은 서버 자원의 ACL과 서버 프로그램의 권한을 비교하는 것이 아니라 서버 자원의 ACL과 클라이언트 계정의 권한을 비교해야 한다는 점에서 다르다. 이때 서버 프로그램이 클라이언트 계정인 것 처럼 흉내를 내면 어떨까? 서버 프로그램이 자신을 클라이언트 계정인 것처럼 흉내낸 다음 파일을 액세스 한다면 Windows 커널은 클라이언트 계정의 권한과 파일에 주어진 ACL을 비교하여 보안 검사를 수행하게 될 것이다. 이처럼 가장은 서버측에서 클라이언트들의 권한 검사를 보다 손쉽게 해주는 방법으로 생각하면 될 것이다.
FTP를 예로 들어 보자. FTP에 접속한 사용자는 자신의 계정과 암호를 밝힐 것이다. FTP 서버는 이 계정과 암호를 이용해 가장을 수행한다. 그리고 FTP 클라이언트가 다운로드할 파일을 읽으려고 시도한다. 만약 사용자가 제시한 계정이 해당 파일에 대한 읽기 권한이 없다면 오류가 발생할 것이고 FTP 서버는 이 오류 내용을 클라이언트에게 넘겨주면 된다.
Windows 에서 가장을 수행하는 방법은 여러가지가 있다. 그 중에 많이 사용되는 방법은 LogonUser 라는 API를 호출하는 것이다. 이 API 함수는 사용자 계정, 암호, 도메인 등을 매개변수로 취하기 때문에 사용하기 편리하다. LogonUser 함수에 대한 상세한 내용은 MSDN을 참고하기 바란다. LogonUser 가 호출되면 이 API를 호출한 쓰레드는 가장 토큰(impersonation token)을 받게 된다. 가장 토큰은 프라이머리 토큰과는 별개의 것으로 LogonUser를 호출한 쓰레드만이 이 가장 토큰의 지배를 받게 되는 것이다. 그리고 가장 토큰을 갖는 쓰레드는 언제든지 RevertToSelf 라는 함수를 호출하여 가장을 중지할 수 있다.
IIS의 인증
액세스 토큰과 가장에 대한 내용을 알았으므로 이제 IIS의 보안 설정에 대해서 알아보자. IIS의 보안 설정은 크게 인증 설정과 권한 관리로 나누어 볼 수 있는데, 인증은 IIS에 의해 관리되는 반면 권한 설정은 Windows의 파일 보안을 따르고 있다고 볼 수 있다. 권한 검사를 위해서는 먼저 인증이 수행되어야 하기 때문에(당연 !) IIS는 인터넷을 통해 접속한 클라이언트(대개의 경우 웹브라우저 일 것이다)를 어떻게 인증할 것인가에 대한 설정을 가지고 있다.
인증 설정은 인터넷 서비스 관리자를 통해 수행할 수 있다. 인터넷 서비스 관리자에서 폴더 혹은 파일을 선택하고 등록 정보 대화상자를 열면 디렉터리 보안 혹은 파일 보안 탭을 찾을 수 있다. 여기서 다시 “익명 액세스 및 인증 제어”를 선택하면 화면 2와 같은 인증 방법 대화 상자를 볼 수 있다. 4가지의 인증 방법이 나오는데 각각의 인증방법에 대한 상세한 설명은 참고 문헌을 살펴보기 바란다. 필자가 이 대화상자에서 주목하고 싶은 것은 익명 액세스 일 때와 인증된 액세스 일 때의 차이점이다.
익명 액세스라 함은 자신이 누구인가를 밝히지 않는 것을 말한다. 대부분의 경우 웹 브라우저는 익명으로 IIS에 접근하게 된다. 익명 액세스가 요청되면 IIS는 클라이언트를 인터넷 게스트 계정이라 불리는 IUSR_XXX 계정(XXX는 컴퓨터의 이름으로 대체된다)으로 가장(impersonation)한다. 물론 이 가장은 브라우저의 요청(request)를 처리하는 쓰레드에 한한 것으로 전체 프로세스에 적용되는 것은 아니다. IUSR_XXX 계정은 IIS 설치와 더불어 생성되는 로컬 계정(도메인 계정이 아님에 유의한다)으로서 제한된 읽기 권한만을 가지고 있다. 화면 1을 살펴보면 inetinfo.exe 프로세스의 계정이 SYSTEM으로 되어 있는 것을 알 수 있다. Inetinfo.exe 프로세스는 IIS의 핵심적인 프로세스로서 HTTP 포트인 80 포트를 리스닝하면서 웹 브라우저의 HTTP 요청을 읽어 파일(HTML, 이미지, 등등)을 다운로드 하거나 ASP, ASP.NET으로 클라이언트의 요청을 포워드 해주는 역할을 담당한다. 따라서 이 프로세스는 다수의 저수준 시스템 자원을 액세스해야 하며 그에 따른 권한을 충분히 갖추고 있어야 한다. 그래서 이 프로세스는 무소불위의 권한을 갖는 SYSTEM 계정을 프로세스 계정(프라이머리 토큰)으로 가지고 있다. 하지만 익명 사용자가 이 SYSTEM 계정하에서 작동된다면 보안상의 문제를 야기할 수 있므로 IIS는 익명 사용자인 경우에 가장을 통해 시스템을 보호하는 것이다.
필자주)
IIS 5.0에서 HTTP 포트를 리스닝하는 프로세스는 inetinfo.exe 였다. 하지만 Windows 2003의 IIS 6.0 부터는 HTTP 포트를 리스닝하는 것은 커널의 HTTP.SYS 드라이버이다. 그리고 이 드라이버가 HTTP 요청(request)을 수신하면 작업 프로세스인 w3wp.exe에게 곧바로 HTTP 요청을 포워드 한다. IIS 6.0에서 inetinfo.exe 프로세스가 수행하는 역할은 IIS 메타 베이스에 대한 관리로 축소되어 있다. 따라서 IIS의 프라이머리 토큰은 w3wp.exe 프로세스의 프로세스 계정으로 결정된다.
화면 2. IIS 5.0의 보안 설정 다이얼로그(Windows 2000)
다시 화면 2로 되돌아가서 인증된 액세스에 대해 살펴보자. 인증된 액세스는 3가지의 인증방법을 사용하는데 이들은 인증 방법이 다른 것이지 결과는 모두 동일하다고 볼 수 있다(실제로는 약간 차이가 난다. 상세한 차이점은 참고문헌을 살펴보기 바란다). 즉, 인증된 액세스라 함은 사용자가 제시한 인증정보를 이용해 쓰레드를 가장(impersonation)하겠다는 말이다. 이때 사용자가 제시하는 인증정보를 서버와 주고 받을 때 ‘기본인증’, ‘다이제스트 인증’, ‘통합(NTLM) 인증’의 방법을 사용하겠다는 뜻이다. 여기서 한가지 주의할 점은 IIS는 보안 수준이 낮은 쪽을 먼저 적용한다는 것이다. 즉, 어떤 자원을 접근할 때 그 자원이 익명 액세스를 허용한다면 추가적인 인증절차를 시도 조차 하지 않는다. 또 한가지 알아두어야 할 사항은 기본인증, 다이제스트 인증, 통합 인증이 모두 선택되어 있다면 IIS와 브라우저는 어떤 인증 방식을 사용할 것인가를 협상하게 된다. 브라우저에 따라서 통합 인증이나 다이제스트 인증을 전혀 지원하지 않을 수도 있기 때문이다.
IIS가 브라우저에게 인증을 요구할 때는 브라우저가 요구한 파일이 익명 액세스를 허용하지 않을 때 이다. 화면 2에서는 익명 액세스와 Windows 통합 인증이 활성화 되어 있다. 따라서 브라우저가 AAA.htm 파일을 요구했다면 IIS는 먼저 익명 액세스(IUSR_XXX)로 AAA.htm 파일을 액세스하려 할 것이다. 만일 IUSR_XXX 계정으로 이 파일을 액세스 하지 못한다면(AAA.htm 파일의 보안 설정에서 IUSR_XXX가 읽기 권한이 없을 것이다) IIS는 브라우저로 하여금 인증 받을 것을 강요한다. 즉, 사용자의 계정, 암호 및 도메인을 입력하도록 요구할 것이며(네트워크 암호 입력 대화상자가 나타난다) 이는 화면 2에서 설정된 Windows 통합 인증을 사용하겠다는 말이다. 일단 사용자가 계정, 암호, 도메인을 입력하면 IIS는 이러한 정보를 이용하여 계정의 유효성을 확인하고 인증 작업이 성공적이라면 이 계정으로 스스로를 가장한다. 여기서도 역시 가장은 브라우저의 요청을 처리하는 쓰레드 레벨에 국한될 뿐이다. 그리고 다시 파일을 액세스하려고 시도할 것이다.
IIS의 웹 어플리케이션 보호와 보안
IIS는 웹 어플리케이션 보호를 지원한다. 웹 어플리케이션 보호라 함은 어플리케이션을 별도의 프로세스 공간에서 수행하도록 함으로써 어플리케이션에서 발생할 수 있는 오류(다운, 무한루프 등등)가 다른 웹 어플리케이션에 영향을 주지 못하도록 한다는 것이다. IIS에서 지원하는 웹 어플리케이션 보호는 3가지 레벨로서 낮음, 보통, 높음이 있다. ‘낮음’이라 함은 웹 어플리케이션이 IIS의 프로세스 공간에서 수행됨을 의미한다. 즉, 웹 어플리케이션은 inetinfo.exe 프로세스 공간에서 수행되며 만약 웹 어플리케이션이 잘못된 동작을 일으켜 프로세스를 다운시킨다면 inetinfo.exe 자체가 다운된다는 말이 된다. 이는 한 어플리케이션의 오작동이 다른 어플리케이션에도 영향을 미치며 심지어 전체 웹 사이트를 다운시킬 수 있음을 의미한다(더 이상 80 포트를 리스닝하지 않는다!). ‘높음’이라 함은 웹 어플리케이션이 IIS와는 별도의 프로세스 공간에서 수행됨을 의미한다. 응용 프로그램 보호가 ‘높음’인 웹 어플리케이션은 dllhost.exe라 불리는 프로세스에 의해 호스트되며 dllhost.exe 프로세스는 웹 어플리케이션마다 1개씩 생성되어 수행된다. Inetinfo.exe가 수신한 브라우저의 요청(HTTP Request)는 Named pipe를 통해 dllhost.exe에 전달되며 만약 어플리케이션이 오작동을 일으키더라도 영향을 주는 프로세스는 dllhost.exe 하나 뿐이므로 inetinfo.exe 프로세스나 다른 dllhost.exe 프로세스는 아무런 영향을 받지 않을 것이다. 하지만 응용 프로그램 보호가 ‘높음’이 되면 ‘낮음’에 비해 성능 손해가 크다. Inetinfo.exe가 수신한 HTTP Request를 별도의 프로세스인 dllhost.exe로 전송하기 위해서는 추가적인 오버헤드가 발생하기 때문이다. 또한 웹 어플리케이션이 여러 개 라면 dllhost.exe가 여러 개 수행되므로 시스템의 자원 소비도 늘어나기 때문이다. ‘보통’이라 함은 낮음과 높음의 중간적인 상태로서 ‘높음’의 단점을 보완해 보고자 Windows 2000 부터 등장한 보호 레벨이다. 응용 프로그램 보호 수준이 ‘보통’인 어플리케이션은 별도의 dllhost.exe 프로세스에서 호스팅되는 것은 변하지 않는다. 하지만 이 프로세스는 모든 ‘보통’ 레벨의 웹 어플리케이션을 모두 호스팅한다. 웹 어플리케이션 마다 하나씩 생성되는 dllhost.exe 프로세스의 오버헤드를 줄이면서 WWW 서비스의 중요한 프로세스인 inetinfo.exe가 중단되는 것을 막기위함이다. 물론 이 호스팅 프로세스인 dllhost.exe가 다운되면 ‘보통’ 보호 수준의 웹 어플리케이션은 모두 다운됨은 물론이다. 필자가 낮음, 보통, 높음으로 설명했지만 많은 문헌이나 MSDN에서는 낮음 수준의 웹 어플리케이션을 In-proc 어플리케이션이라 부르며 보통/높음 수준을 통칭하여 out-of-proc 어플리케이션이라고 하는 것이 일반적이다(그림 1).
그림 1. IIS 5.0의 In-proc/Out-of-proc 어플리케이션과 보안설정
그렇다면 in-proc과 out-of-proc 어플리케이션이 IIS 보안과 무슨관계가 있는 것인가? In-proc 어플리케이션은 inetinfo.exe 프로세스에서 호스팅된다. 따라서 프로세스 계정은 inetinfo.exe 프로세스의 계정을 말하는 것이다. 그리고 그 계정은 로컬 컴퓨터에 모든 권한을 쥐고 있는 SYSTEM 계정이다. 반면 dllhost.exe에 의해 호스팅되는 out-of-proc 어플리케이션은 디폴트로 dllhost.exe의 프로세스 계정인 IWAM_XXX 계정을 프로세스 계정으로 갖는다. 만약 in-proc 어플리케이션 상에서 어떤 ASP(ASP.NET이 아님에 유의하자) 페이지가 익명 액세스를 허용한다면 이 페이지는 IUSR_XXX라는 쓰레드 계정과 SYSTEM라는 프로세스 계정하에서 수행될 것이다. 반면 out-of-proc 어플리케이션이라면 IUSR_XXX라는 쓰레드 계정과 IWAM_XXX라는 프로세스 계정을 갖는다.
필자주)IIS 6.0 (Windows 2003)에서는 IIS 6.0이 IIS 5.0 호환 모드로 작동하지 않는 한 in-proc/out-of-proc 어플리케이션 이란 개념이 없다. 대신 어플리케이션 풀(Application Pool) 개념이 도입되어 있다. 각 웹 어플리케이션은 어플리케이션 풀에 할당되고 어플리케이션 풀 마다 작업 프로세스인 w3wp.exe 프로세스가 할당된다. w3wp.exe 프로세스는 서로 독립적으로 작동하는 프로세스이므로 서로다른 어플리케이션 풀에서 작동중인 웹 어플리케이션은 서로 영향을 미칠 수 없다. 즉, 한 어플리케이션 풀(w3wp.exe 프로세스)이 다운되더라도 다른 어플리케이션 풀은 지속적으로 서비스가 가능하다는 이야기이다. 또한 HTTP 80 포트를 리스닝하는 주체는 커널의 HTTP.SYS 드라이버이므로 작업 프로세스의 일부가 다운되더라도 HTTP 서비스 자체가 중단하는 경우는 없다.
각 어플리케이션 풀의 w3wp.exe 프로세스는 별도의 프로세스 계정을 설정할 수 있다. 강력한 권한을 갖는 SYSTEM 계정 부터 시작해서 가장 적은 권한을 갖는 NETWORK_SERVICE 계정으로 설정을 수행할 수 있다. 따라서 IIS 6.0의 경우, IWAM_XXX 계정은 사용되지 않는다고 보아도 무방하다.
Inetinfo.exe는 HTTP 80 포트를 리스닝한다. 뿐만 아니라 IIS가 수행되기 위해 필요한 각종 시스템 자원(레지스터리, 시스템 파일등등)을 액세스해야 한다. 게다가 이 프로세스는 Windows 서비스로 등록되어 있다. 따라서 이 프로세스가 왜 SYSTEM 계정하에서 수행되어야 하는가를 이해할 수 있을 것이다. 그렇다면 dllhost.exe는 ? “응용 프로그램 보호” 라는 제목을 생각하면 이 프로세스가 왜 SYSTEM 계정이 아닌가를 이해할 수 있을 것이다. 이 웹 어플리케이션을 수행하는데 필요한 제한된 권한만을 갖고 있는 계정으로서 어플리케이션을 보호하기 위한 방법으로 사용되는 것이다.
여기서 한가지 의문을 갖어보자. In-proc 어플리케이션은 계정에 의한 응용 프로그램 보호가 안된다는 말인가? 익명 사용자가 ASP 페이지나 ISAPI 익스텐션(extension)을 사용하면 이 요청을 처리하는 쓰레드는 IUSR_XXX라는 인터넷 게스트 계정하에서 작동된다고 했지 않았는가? 그리고 이 계정은 말 그대로 게스트 계정으로 그 권한은 매우 미미하지 않은가? 그렇게 생각하기 쉽지만 사실은 전혀 그렇지 않다. 쓰레드가 IUSR_XXX 계정하에서 작동하기 위해서는 가장(impersonation)을 수행해야 하는데 가장을 할 수 있는가 없는가는 쓰레드 계정보다는 프로세스 계정의 영향을 받기 때문이다. 일례로 가장을 중지하기 위해서는 RevertToSelf 라는 API를 호출하면 된다. 이 API는 가장을 중단할 권한이 호출자에게 있는가를 검사하는데, 이 검사의 대상이 쓰레드가 아닌 프로세스이다. 즉, 프로세스 계정이 RevertToSelf를 호출할 권한이 있는가를 살핀다는 것이다. In-proc 서버의 경우 프로세스 계정이 SYSTEM이므로 익명 사용자도 RevertToSelf를 호출할 수 있으며 RevertToSelf 호출 후에는 쓰레드는 SYSTEM 계정하에서 작동할 것이며 이제는 로컬 시스템의 모든 자원을 액세스할 수 있는 권한을 쥐는 것과 같다. 반면 out-of-proc을 고려해 보자. 이 경우도 마찬가지로 익명 사용자라도 RevertToSelf를 호출 할 수 있다는 점은 같다. 하지만 RevertToSelf를 호출한 후에 쓰레드가 갖는 계정은 IWAM_XXX 계정일 뿐이다. 앞서 언급했듯이 이 계정은 상당히 제한된 권한만을 가지고 있을 뿐이다. 응용 프로그램 보호와 보안에 대해 이제 조금이나마 감을 잡았으리라 생각된다.
요약 그리고 테스트
IIS의 보안 설정을 요약해보자. IIS의 기본적인 보안 정책은 Windows의 파일 시스템의 보안설정을 근거로 하고 있으며 이 보안설정은 Windows의 NTLM 혹은 Active Directory에 근거한 인증이 필요하다. 기본(Basic) 인증, 다이제스트(Digest) 인증, NTLM 인증 중 하나를 거친 웹 사용자는 IIS의 작업 쓰레드로 하여금 사용자를 흉내내도록(impersonate) 하며 사용자를 흉내낸 쓰레드가 파일을 액세스 함으로써 주어진 권한을 검사하는 것이다. 만일 클라이언트가 익명 사용자로서 IIS에 접근한다면 작업 쓰레드는 자신을 인터넷 게스트 계정인 IUSR_XXX 계정으로서 가장할 것이며 이 계정에 대해 접근 권한이 있는 파일만을 액세스할 수 있을 것이다.
만약 독자들이 IIS를 사용하면서 원인모를 Access Denied 오류나 웹브라우저에서 뜬금없이 나타나는 인증 대화 상자를 경험했다면 지금까지 필자가 다루었던 내용을 세심히 살펴보고 웹 컨텐츠들이 포함된 디렉토리나 파일들이 IUSR_XXX 계정에 대해 권한이 있는가를 살펴보는 것이 좋을 것이다.
지금까지 언급한 내용을 ASP를 이용하여 테스트해 보자. ASP.NET으로 테스트하는 것은 잠시만 미루자. 테스트를 위해서는 현재 프로세스의 프라이머리 토큰 혹은 가장 토큰을 읽어야 하고 이를 위해서는 WIN32 API인 OpenThreadToken을 호출해야 한다. 하지만 ASP는 WIN32 API를 호출할 능력이 없다. 따라서 액세스 토큰을 읽거나 가장을 수행하는 COM 컴포넌트를 작성하고 이 COM 컴포넌트를 ASP에서 호출하도록 구성한다. 컬럼 성격상 COM 컴포넌트 작성법이나 액세스 토큰을 읽고 계정 정보를 표시하는 방법에 대한 설명은 생략하도록 하겠다. 다만 필자가 작성한 SecurityUtil 컴포넌트의 메소드와 프로퍼티를 간단히 설명하도록 하겠다. SecurityUtil 컴포넌트의 전체 소스는 이달의 디스켓을 참고하자.
SecurityUtil 컴포넌트는 2개의 프로퍼티와 3개의 메소드를 가지고 있다. ProcessUserName 프로퍼티는 프라이머리 액세스 토큰으로부터 계정 이름을 읽어 반환하는 프로퍼티로서 프로세스 계정이 무엇인가를 알려주며 ThreadUserName 프로퍼티는 현재 쓰레드의 액세스 토큰으로부터 계정 이름을 읽어 반환하는 프로퍼티이다. 만약 쓰레드가 어떤 가장도 하고 있지 않다면 단순히 프로세스 계정을 반환한다. 따라서 ThreadUserName 프로퍼티는 현재 쓰레드가 어떤 계정 하에서 수행중인가를 나타낸다고 할 수 있다. RevertToSelf 메소드는 쓰레드가 어떤 가장을 수행하고 있다면 이것을 중지하도록 하며, Impersonate 메소드는 매개변수로 주어진 계정으로 가장을 수행한다. Refresh 메소드는 ProcessUserName 프로퍼티와 ThreadUserName 프로퍼티를 최신정보로 업데이트 해준다.
이제 테스트에 사용한 코드를 살펴보자. 리스트 1은 간단한 ASP 페이지로서 SecurityUtil 컴포넌트의 ProcessUserName 프로퍼티와 ThreadUserName 프로퍼티를 사용하여 프로세스 계정과 쓰레드 계정을 보여준다. 이 페이지를 응용 프로그램 보호가 ‘높음’ 인, 즉 Out-of-proc 어플리케이션으로 구성해서 수행해 보자. 수행결과는 화면 3과 같다. 설명한 대로 프로세스 계정은 IWAM_XXXX 이며 쓰레드 계정은 익명 사용자인 IUSR_XXXX 가 되었다. 필자 컴퓨터의 이름이 SIMPLE 이기 때문에 XXX 대신 SIMPLE이 들어갔음에 유의하자. 만약 이 웹 어플리케이션이 In-proc 어플리케이션으로 구성되었다면 프로세스 계정이 IWAM_XXXX가 아닌 SYSTEM이 되었을 것이다.
이제 이 ASP 페이지의 보안 속성에서 익명 사용자 액세스를 제거하자. 그리고 이 페이지를 다시 열어 본다. 브라우저는 사용자에게 인증을 요구하는 대화상자를 나타낼 것이다. 로컬 계정의 아이디 그리고 암호를 입력하면 수행결과는 IUSR_XXXX 대신 사용자가 입력한 계정이 나타날 것이다. 이는 쓰레드가 인증된 사용자로 가장을 수행함을 보여준다. 이 테스트에서 주의할 점은 로컬 컴퓨터에서 브라우저를 사용하면 인증 대화상자를 볼 수 없는데, 이유는 IIS가 인증을 요구할 때 브라우저는 기본적으로 현재 로그온된 사용자 정보를 IIS에게 보낸다. 이 인증 과정이 실패할 경우에만 인증 대화상자가 나타난다. 따라서 로컬 컴퓨터 상에서 테스트를 하는 경우 인증 대화상자가 나타나지 않을 수도 있다. 독자들은 놀라지 말기를…
<%@ Language="VBScript" %>
<%
Dim SecurityUtil
Dim ProcessUser, ThreadUser
On Error Resume NEXT
Set SecurityUtil = CreateObject("SecurityUtil.TokenInfo")
ProcessUser = SecurityUtil.ProcessUserName
ThreadUser = SecurityUtil.ThreadUserName
%>
<html>
<head>
<title>ASP Examples Home Page</title>
<link rel="stylesheet" href="/asp/main.css">
</head>
<body>
<h2>Process/Thread Account Information</h2>
<h3>Default Account Info</h3>
<table border="1" cellpadding="0" cellspacing="0">
<tr><td width=200>Process Account</td><td><% = ProcessUser %></td></tr>
<tr><td>Thread Account</td><td><% = ThreadUser %></td></tr>
</table>
</body>
</html>
리스트 1. IIS & ASP 계정 관련 테스트 코드 (Security.asp)
화면 3. 수행결과 1
필자주)
IIS 6.0 에서 테스트하는 경우 수행 결과는 Process Account는 어플리케이션 풀의 계정이 나타날 것이다. 즉, Security.asp 페이지가 수행되는 어플리케이션 풀의 계정이 NETWORK SERVICE(디폴트) 라면 Process Account 값이 NT AUTHORITY\NETWORK SERVICE 로 표시된다는 것이다. 혹은 어플리케이션 풀의 계정 설정이 SYSTEM으로 되어 있다면 Process Account 값은 NT AUTHORITY\SYSTEM 으로 표시될 것이다.
화면 4. 수행결과 2
화면 4는 간단한 코드를 좀더 추가하여 테스트한 결과다(필자주: 예제 코드는 추가된 코드가 포함되어 있다). SecurityUtil 컴포넌트의 RevertToSelf 메소드를 호출하여 가장을 중단하였으며 그 결과를 표시한 것과 ASP 페이지에서 임의의 계정으로 가장을 할 수 있음을 보인 것이다. 이 테스트에서 주의할 곳은 가장을 수행한 마지막 테스트인데, 가장을 수행하기 위해서는 프로세스 계정은 “운영체제 일부로 활동” 이라는 특권(privileage)을 갖어야 한다. 즉, 이 경우에 IWAM_XXXX 계정이 “운영체 일부로 활동”이라는 특권을 갖어야 하는 것이다. 하지만 기본 설치 사항으로 IWAM_XXXX 계정은 이 특권을 갖지 않는다. IWAM_XXXX 계정이 이 특권을 갖도록 하기 위해서는 “로컬 보안 정책” MMC에서 IWAM_XXXX 계정에게 이 특권을 주어야만 화면 4와 같은 결과를 볼 수 있을 것이다. 만약 이 특권이 없다면 가장은 실패하게 된다.
필자주)Windows XP 이상에서는 "운영체제 일부로 활동" 이라는 특권이 불필요하다. 때문에 Security.asp 예제 코드는 Windows XP와 Windows 2003에서 항상 가장(impersonation)이 성공한다. Windows 2003에서는 클라이언트를 인증한 후 가장하기 위해서는 별도의 특권이 필요하며 이 특권을 가진 계정은 SYSTEM, NETWORK SERVICE, LOCAL SERVICE, Administrators 그룹내의 계정들이다.
Windows 2003에서 Security.asp를 수행하면 수행 결과가 다르게 나타날 것이다. IIS 6.0 어플리케이션 풀 설정에 계정을 바꾸지 않았다면 프로세스 계정은 NETWORK SERVICE이 될 것이며 가장(impersonation)을 취소한 ReverToSelf 호출 이후에 나타나는 쓰레드 계정 역시 NETWORK SERVICE가 될 것이다.
ASP.NET 보안 모델
지금까지 IIS의 보안 모델에 살펴보았다. ASP.NET의 보안 모델은 IIS의 보안모델과 관계가 깊다. 그 때문에 이렇게까지 자세히 IIS 보안모델을 살펴보았던 것이다. 이제 ASP.NET의 보안 모델을 살펴보자.
ASP.NET 페이지의 수행 과정
보안 모델에 앞서 한가지 이해해야 할 사항이 있다. 그것은 ASP.NET의 대표적인 웹 페이지인 aspx 파일이 수행되는 과정에 대한 것이다. IIS가 웹 브라우저로부터 aspx 파일에 대한 요청을 수신했다고 가정해 보자. IIS는 주어진 URL로부터 어떤 웹 어플리케이션에 해당되는지를 먼저 파악한다. 그리고 그 웹 어플리케이션의 응용 프로그램매핑을 살피게 된다. 응용 프로그램매핑이란 요청된 파일의 확장자에 의해 적절한 ISAPI 익스텐션이 수행되도록 하는 것을 말한다. 화면 5는 IIS 관리자에 설정된 응용 프로그램 매핑을 보여주고 있다. 확장자가 aspx 인 경우 닷넷 프레임워크의 aspnet_isapi.dll 에 연결되어 있다. 이제 IIS는 클라이언트로부터 수신한 HTTP Request를 aspnet_isapi.dll로 넘긴다. aspnet_isapi.dll은 그다지 많은 일을 하지 않는다. IIS로부터 넘겨받은 HTTP Request를 실제적인 처리를 수행할 ASP.NET 작업 프로세스(worker process)로 넘기는 일을 주로 수행한다. 만약 이 작업 프로세스가 이미 수행중이 아니라면 이 프로세스를 시작시키는 역할 역시 aspnet_isapi.dll 의 역할이다. 이제 작업 프로세스는 요청된 HTTP Request를 처리하기 위한 일련의 작업 aspx 파싱, 컴파일, 수행 등등의 작업을 수행한다. 수행이 끝나면 최종결과는 다시 aspnet_isapi.dll로 넘겨지며 이 결과는 다시 IIS를 통해 클라이언트(브라우저)로 넘어가게 된다.
화면 5. ASPX 파일에 대한 응용 프로그램 매핑
ASP.NET의 작업 프로세스는 aspnet_wp.exe라 불리는 프로세스로서 모든 ASP.NET의 페이지와 웹 서비스들은 이 프로세스 상에서 처리되고 수행된다. 따라서 이 프로세스가 모든 ASP.NET 웹 어플리케이션을 호스팅하게 되는 것이다. 이쯤에서 의문을 갖을 법도 한 것은 IIS의 응용 프로그램 보호와 aspnet_wp.exe 프로세스와의 관계에 대한 것이다. 응용 프로그램 보호 수준이 ‘낮음’인 ASP.NET 웹 어플리케이션은 어디에서 수행되는 것인가? Inetinfo.exe 인가 아니면 aspnet_wp.exe인가? 혹은 응용 프로그램 보호 수준이 ‘높음’인 ASP.NET 어플리케이션은 dllhost.exe 인가 아니면 aspnet_wp.exe 인가? 이 질문에 대한 답은 “항상 aspnet_wp.exe 에서 수행된다” 이다. 그렇다면 IIS의 응용 프로그램 보호 수준과 ASP.NET과의 관계는? “아무런 관계 없음” 이다. 즉, IIS의 응용 프로그램 보호 수준과 아무런 관계 없이 ASP.NET 웹 어플리케이션들은 항상 aspnet_wp.exe에서 작동되며 In-proc 이니 Out-of-proc 이니 하는 것들은 ASP.NET 이전의 기술들, ASP 와 ISAPI 익스텐션들과만 관계가 있을 뿐이다. ASP에 기반을 둔 개발자들이 ASP.NET에서 혼동하는 첫번째 요소가 바로 이 점이며 이제는 혼동하지 않기를 바란다.
필자주)IIS 6.0 에서는 응용 프로그램 보호가 어플리케이션 풀(Application Pool)이라는 새로운 개념을 통해서 수행된다. 어플리케이션 풀은 어플리케이션이 수행될 프로세스, 리사이클링 설정, 웹 가든 설정 등 웹 어플리케이션이 수행될 환경을 구성하는 단위이다. 각 웹 어플리케이션은 1개의 어플리케이션 풀에 할당되며 어플리케이션 풀에 할당된 웹 어플리케이션은 해당 어플리케이션 풀이 지정하는 설정에 의해 수행된다.
어플리케이션 풀은 여러 개 생성될 수 있으며, 또한 1개의 어플리케이션 풀이 여러 웹 어플리케이션을 호스팅 할 수 있다. 예를 들어 AppPool1 에 WebApp1, WebApp2가 할당되었고 AppPool2에 WebApp3 가 할당되었다고 가정해 보자. 어플리케이션 풀은 웹 가든(Web Garden)이 설정되지 않은 상황에서는 풀마다 하나의 작업 프로세스(w3wp.exe 프로세스)가 하나씩 할당되므로 WepApp1과 WepApp2는 동일 프로세스상에서 수행되며 WebApp3은 별도의 프로세스에서 수행된다.
이러한 어플리케이션 풀 개념은 ASP 니 ASP.NET 이니 웹 어플리케이션 기술을 가리지 않는다. IIS 6.0의 모든 웹 어플리케이션들(그것이 ASP.NET 이든 아니든 관계 없이)은 어플리케이션 풀의 작업 프로세스 상에서 수행됨을 기억해야 한다.
ASP.NET 작업 프로세스와 보안
앞서 언급한대로 ASP.NET 웹 어플리케이션들은 모두 ASP.NET 작업 프로세스에서 호스팅된다고 하였다. 그렇다면 이들 여러 웹 어플리케이션들이 하나의 프로세스에서 동시에 작동할 때 한 웹 어플리케이션이 다른 어플리케이션에 영향을 미칠 수도 있지 않을까? 전혀 그렇지 않다. 비록 한 프로세스 내에서 호스팅되고 있다 할지라도 이들 웹 어플리케이션은 닷넷의 새로운 개념인 어플리케이션 도메인(Application Domain)에 의해 완전히 독립되어 있으며 한 어플리케이션이 다른 어플리케이션의 메모리를 액세스하거나 직접적으로 코드를 호출할 수 없다. 이것은 IIS의 응용 프로그램 보호에서 ‘보통’ 수준과 비슷하다고 생각하면 될 것이다. 하지만 이 모델은 몇가지 치명적인 단점을 가지고 있다. 첫째로 어느 웹 어플리케이션의 오작동으로 인해 aspnet_wp.exe 프로세스가 다운되면 전체 ASP.NET 웹 어플리케이션이 모두 다운되고 만다. 둘째로 특정 웹 어플리케이션만을 종료할 방법이 없다. 하나의 프로세스에서 여러 웹 어플리케이션이 호스팅 되고 있으므로 특정 웹 어플리케이션만을 선택하여 종료시키기 어려운 것이다. 하지만 이러한 문제점은 Windows 2000과 Windows XP에만 국한된다. Windows .NET(필자주: Windows 2003이 정식 발표되기전에 코드 네임 비슷하게 Windows .NET 서버라는 이름이 사용되었었다)에 포함된 IIS 6.0은 aspnet_wp.exe를 아예 사용하지 않는다. IIS 6.0에 대한 사항은 조금 뒤에 다루기로 하자.
그림 2. IIS 5.0 프로세스 & Out-of-proc 프로세스 & ASP.NET 작업 프로세스
그림 2는 IIS 프로세스(inetinfo.exe)와 Out-of-proc 프로세스, 그리고 ASP.NET 작업 프로세스의 관계를 보여주고 있다. 그림에도 나와있듯이 aspnet_wp.exe 프로세스 역시 프로세스 계정을 갖게 된다. 이 프로세스는 디폴트로서 ASPNET 계정을 사용한다. 이는 Out-of-proc 프로세스가 IWAM_XXX 계정 하에서 수행되는 것과 비슷하게 볼 수 있다. 이 작업 프로세스의 계정은 바꿀 수 있는데 닷넷 프레임워크 폴더(대개 C:\Winnt\Microsoft.NET\Framework\버전\Config 폴더)의 machine.config 파일의 processModel 노드의 값을 수정하면 된다. 대개의 경우, ASPNET 계정으로 대부분의 작업을 수행할 수 있으며 일부 권한이 필요한 작업을 수행해야 한다면 작업 프로세스의 계정을 바꾸는 것 보다 웹 어플리케이션 내에서 가장(impersonation)을 수행하는 것이 바람직하다고 할 수 있다.
필자주)그림 3은 IIS 6.0의 어플리케이션 풀, 작업 프로세스, 웹 어플리케이션 및 작업 프로세스의 계정을 보여주고 있다. HTTP Request는 커널모드 드라이버인 HTTP.SYS에 의해 리스닝 된다. HTTP 요청이 HTTP.SYS에 의해 수신되면 이 드라이버는 적절한 프로세스에게 해당 Request를 포워드 해준다.
작업 프로세스는 어플리케이션 풀 마다 최소 1개씩 생성된다. 그리고 어플리케이션 풀의 설정은 작업 프로세스에서 사용될 계정을 지정할 수 있다. 그림 3에서 어플리케이션 풀 #1의 프로세스 계정은 NETWORK SERVICE(디폴트 값임)이며, 어플리케이션 풀 #2의 계정은 SYSTEM임을 확인해 두자.
각 어플리케이션 풀의 작업 프로세스는 어플리케이션 풀에 할당된 웹 어플리케이션들을 1개 이상 호스팅 한다. 그림 3에서 어플리케이션 풀 #1은 2개의 ASP.NET 웹 어플리케이션을, 어플리케이션 풀 #2는 1개의 ASP.NET 웹 어플리케이션을, 어플리케이션 풀 #3는 ASP 웹 어플리케이션 1개와 기타 JSP 웹 어플리케이션등을 호스팅하고 있다. 물론 1개의 어플리케이션 풀에 ASP.NET, ASP, 기타 다른 스크립트 웹 어플리케이션이 동시에 호스팅 될 수 있다. 하나의 어플리케이션 풀에 여러 ASP.NET 웹 어플리케이션이 호스팅 되는 것은 Windows 2000(IIS 5.0)에서 aspnet_wp.exe 프로세스에서 여러 웹 어플리케이션이 호스팅 되는 것과 비슷하게 생각하면 되겠다.
그림3. IIS 6.0의 작업 프로세스와 웹 어플리케이션 보호
ASP.NET 인증
이제 기본 사항들을 모두 정리했으니 ASP.NET의 보안모델에 대해 상세히 알아보자. ASP.NET도 ASP나 ISAPI 와 마찬가지로 IIS에 의해 작동되므로 IIS의 기본 보안 모델과 관계가 있다. 하지만 ASP.NET 만의 고유한 특성 역시 제공하고 있다. ASP.NET가 제공하는 보안 모델 역시 크게 인증 부분과 권한 부분으로 나누어 볼 수 있으며 인증 모델은 Windows, Forms, Passport, Custom 의 네 가지를 제공하고 권한은 ASP의 권한 검사와 동일한 파일 권한에 의한 보안과 역할 기반 보안(Role-Based Security)으로 다시 나누어 볼 수 있겠다. 인증 부분부터 차근차근 알아보도록 하자. 앞서 IIS 인증에서 기본적인 내용을 다루었으므로 이번에는 테스트 중심으로 ASP.NET의 인증을 살펴보도록 하겠다. 여기서 살펴볼 내용은 Windows 인증과 Form 인증이다. Passport 인증은 아직까지 널리 보편화 되지 않았고 어플리케이션에서 사용하기 위해서는 라이센스를 받아야 하므로 이 칼럼에서는 다루지 않겠다(필자주: 아직도 라이센스가 필요한가는 잘 모르겠다). Custom 인증은 Form 인증과 같은 인증 모듈을 독자적으로 개발하여 적용할 수 있는 방법을 말한다. Custom 인증 역시 이 컬럼의 범위를 벗어나므로 여기서 다루지 않을 것이다. 독자들의 넓은 이해를 바란다.
.NET에서 Windows 보안 시스템에 접근
테스트를 하기 위해서는 .NET에서 액세스 토큰을 접근하는 방법부터 알아야 한다. 앞서 작성한 SecurityUtil 컴포넌트를 COM Interop를 통해 사용할 수도 있지만 다른 접근방법을 알아보자. 닷넷이 Windows를 기반으로 하고 있음에도 불구하고 모든 Windows 프로그래밍 환경을 커버하고 있지는 않다. 모든 WIN32 API를 닷넷 프레임워크가 대체하고 있지는 않다는 말이다. 이 때문에 닷넷 프레임워크는 WIN32 API 나 기존의 COM 컴포넌트를 사용할 수 있도록 PInvoke(Platform Invoke) 나 COM Interop를 지원하고 있음은 필자의 컬럼(마소 2002년 6월호)에서 밝힌바 있다. 닷넷 환경에서 액세스 토큰을 얻기 위한 클래스는 제공되지 않는다. 액세스 토큰이라는 개념은 운영체제에 매우 의존적인 것이다. 예를 들어 프라이머리 액세스 토큰을 읽기 위해 제공되는 OpenProcessToken 이라는 API는 Windows 95/98/Me에서는 제공되지 않는다. 하지만 닷넷환경은 다양한 Windows 버전들 뿐만 아니라 FreeBSD 와 같은 다른 운영체제 상(필자주: 말그대로 작동만 한다)에서도 작동한다. 이런 맥락에서 닷넷 프레임워크가 액세스 토큰을 제어하는 메소드들을 가지고 있지 않다고 생각하면 될 것이다.
만약 프로그램 내에서 액세스 토큰과 같은 Windows 보안 시스템에 접근하기 위해서는 닷넷 프레임워크가 제공하는 몇몇 클래스와 WIN32 API를 섞어 사용해야 한다. 앞서 예로 든 프라이머리 액세스 토큰을 읽기 위해서는 OpenProcessToken API를 호출하는 PInvoke를 사용해야 하지만 현재 쓰레드의 쓰레드 계정을 알아내기 위해서는 WindowsIdentity 클래스의 스태틱 메소드인 GetCurrent() 메소드를 사용하면 된다. 비슷한 예로서, WindowsIdentity 클래스는 Impersonate() 메소드를 가지고 있지만 WindowsIdentity 클래스의 인스턴스를 만들기 위해서는 액세스 토큰이 필요하고 이 액세스 토큰을 만들기 위해서는 LogonUser 라는 WIN32 API를 호출해야 한다.
필자는 ASP.NET에서 보안 정보를 읽기 위한 몇몇 WIN32 API를 묶어 유틸리티 클래스를 만들었다. 이 클래스의 코드는 리스트 2에 일부 나타나 있다. 전체 코드는 이달의 디스켓을 참고하기 바란다. Win32SecurityAPI 클래스는 프로세스 토큰(프라이머리 액세스 토큰)을 읽기 위한 GetProcessToken 메소드, 가장(impersonation)을 위한 액세스 토큰 생성을 위한 LogonUser 메소드등을 가지고 있다. 이들 메소드들을 사용하여 프로세스 계정을 나타내는 코드는 다음과 같다.
string result;
System.Diagnostics.Process proc =
System.Diagnostics.Process.GetCurrentProcess();
try {
// 프로세스의 액세스 토큰을 읽는다.
IntPtr procToken = Win32SecurityAPI.GetProcessToken(proc.Handle);
// WindowsIdentity 클래스를 이용하여 현재 쓰레드의 사용자 이름을 알아낸다.
WindowsIdentity procUser = new WindowsIdentity(procToken);
result = procUser.Name;
Win32SecurityAPI.CloseHandle(procToken);
}
catch (Win32Exception e) {
result = "다음 이유로 알 수 없음 : " + e.Message;
}
쓰레드 계정을 읽는 방법은 매우 쉽다. WindowsIdentity 클래스의 GetCurrent() 메소드를 호출하면 현재 쓰레드의 액세스 토큰으로부터 계정명을 읽을 수 있다. 만일 가장이 수행중이라면 가장 액세스 토큰으로부터 계정명을 읽을 것이며 그렇지 않다면 GetCurrent() 메소드가 반환하는 것은 프라이머리 액세스 토큰, 즉 프로세스 계정일 것이다.
string threadUserName = WindowsIdentity.GetCurrent().Name;
이러한 코드를 바탕으로 예제 웹 어플리케이션을 작성하고 ASP.NET의 Windows 인증, Form 인증에서 테스트를 수행해 보도록 하겠다.
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
namespace Security
{
/// <summary>
/// WIN32 토큰과 관련된 Access Mask 상수들
/// </summary>
[Flags]
public enum TokenAccess
{
TOKEN_ASSIGN_PRIMARY = 0x0001,
TOKEN_DUPLICATE = 0x0002,
TOKEN_IMPERSONATE = 0x0004,
TOKEN_QUERY = 0x0008,
// 생략 ......
}
/// <summary>
/// LogonUser API에서 사용하는 로그온 타입
/// </summary>
public enum LogonType
{
LOGON32_LOGON_INTERACTIVE = 2,
LOGON32_LOGON_NETWORK = 3,
LOGON32_LOGON_BATCH = 4,
// 생략 ......
}
// ...... 중략 ......
/// <summary>
/// WIN32 Security 관련 API 들에 대한 모음
/// </summary>
public class Win32SecurityAPI
{
[DllImport("advapi32.dll", EntryPoint="LogonUser", SetLastError=true)]
private static extern bool _LogonUser(string lpszUsername,
string lpszDomain, string lpszPassword,
int dwLogonType, int dwLogonProvider, out int phToken);
/// <summary>
/// 주어진 사용자 ID로 로그온하여 액세스 토큰을 반환한다.
/// </summary>
/// <param name="userName">사용자 ID</param>
/// <param name="password">암호</param>
/// <param name="domainName">도메인 이름</param>
/// <param name="logonType">로그온 종류</param>
/// <param name="logonProvider">로그온 프로바이더</param>
/// <returns></returns>
public static IntPtr LogonUser(string userName, string password,
string domainName, LogonType logonType,
LogonProvider logonProvider)
{
int token = 0;
bool logonSuccess = _LogonUser(userName, domainName, password,
(int)logonType, (int)logonProvider, out token);
if (logonSuccess)
return new IntPtr(token);
int retval = Marshal.GetLastWin32Error();
throw new Win32Exception(retval);
}
[DllImport("advapi32.dll",
EntryPoint="OpenProcessToken", SetLastError=true)]
private static extern bool _OpenProcessToken(
IntPtr handle, int desigedAccess, out int phToken);
/// <summary>
/// 프로세스 토큰을 반환한다.
/// </summary>
/// <param name="handle">토큰을 얻고자 하는 프로세스의 핸들</param>
/// <returns>프로세스 토큰 핸들</returns>
public static IntPtr GetProcessToken(IntPtr handle)
{
int desiredAccess = (int)TokenAccess.TOKEN_QUERY;
return GetProcessToken(handle, desiredAccess);
}
/// <summary>
/// 프로세스 토큰을 반환한다.
/// </summary>
/// <param name="handle">토큰을 얻고자 하는 프로세스의 핸들</param>
/// <param name="desiredAccess">토큰 액세스 목록</param>
/// <returns>프로세스 토큰</returns>
public static IntPtr GetProcessToken(IntPtr handle, int desiredAccess)
{
int token = 0;
bool openSuccess = _OpenProcessToken(
handle, desiredAccess, out token);
if (openSuccess)
return new IntPtr(token);
throw new Win32Exception(Marshal.GetLastWin32Error());
}
// ...... 생략 .....
}
}
리스트 2. 보안 API를 위한 Win32SecurityAPI 클래스
Windows 인증
ASP.NET에서 제공하는 네 가지 인증 모드 중 디폴트는 Windows 인증이다. 이 Windows 인증은 기존 ASP 인증과 다를 바가 없다. 즉, IIS에서 디렉터리나 파일레벨에서 설정된 인증 사항이 그대로 ASP.NET에도 적용되는 사항이다. 예를 들어 보자. IIS가 Default.aspx 파일에 대한 처리 요청을 수신하면 IIS는 이 파일의 인증 사항을 메타베이스를 통해 읽는다. 만일 익명 사용자가 이 파일을 액세스 할 수 있다면 별다른 처리 없이 aspnet_isapi.dll 에게 HTTP Request를 넘겨주게 될 것이다. 만일 이 파일이 익명 액세스를 허용하지 않는다면(필자주 : IUSR_XXX 계정으로 default.aspx를 읽을 수 없다면) IIS 브라우저에게 인증을 요구하게 된다. 인증 방법은 기본 인증, 다이제스트 인증 혹은 NTLM 중 하나를 선택하게 될 것이다.
ASP.NET의 Windows 인증은 몇 가지 주의할 사항이 있다. 첫째로 ASP.NET 웹 어플리케이션의 web.config 파일에 인증이 명시되지 않으면 machine.config 파일의 인증 설정을 따른다. 그리고 이 기본 인증 설정은 다음과 같이 Windows 로 설정되어 있다.
<authentication mode="Windows">
이러한 기본 설정은 우리가 ASP에서 테스트 해 보았던 것과 다르게 작동함에 유의하자. 이달의 디스켓에 포함된 Default.aspx는 앞서 설명한 Win32SecurityAPI 클래스를 이용하여 프로세스 계정과 쓰레드 계정을 표시해 준다. 그리고 이 Default.aspx에 대한 IIS의 보안 설정이 익명 액세스를 허용하도록 되어 있다고 가정해 보자(필자주: IIS 관리자에서 폴더나 Default.aspx 파일에 대해 익명 액세스를 허용하자). ASP에서의 테스트를 기억해 보면 프로세스 계정은 ASPNET 이 될 것이고(그림 2 참조), 쓰레드 계정은 IUSR_XXXX 가 되어야 할 것이다. 하지만 수행결과는 전혀 그렇지 않다. 수행 결과인 화면 6을 보면 프로세스 계정과 쓰레드 계정이 모두 ASPNET 으로 되어 있다. 왜 일까?
ASP.NET은 설정 파일(configuration)의 <identity> 태그에 따라서 클라이언트를 가장할 것인가를 결정하도록 되어 있고 machine.config 파일의 기본 설정은 다음과 같이 impersonate 값이 false 로 되어 있다. 그래서 쓰레드 계정이 IUSR_XXXX가 아닌 프로세스 계정으로 설정되어 있는 것이다.
<identity impersonate="false" userName="" password="" />
화면 6. ASP.NET 테스트 결과 1 (IIS 5.x 수행 결과)
필자주)Windows 2000 은 IIS 5.0이 Windows XP에는 IIS 5.1이 포함되어 있다. 5.0과 5.1은 기본적으로 차이가 없다고 보면 된다. 화면 6 결과에서 SIMPLE\ASPNET 계정은 닷넷이 설치되면서 생성되는 계정이며 앞에 붙은 SIMPLE은 컴퓨터 이름이다.
Windows 2003에서 수행결과는 ASPNET 계정이 아닌 NT AUTHORITY\NETWORK SERVICE 계정으로 출력될 것이다. Windows 2000에서는 ASP.NET을 위한 작업 프로세스로 aspnet_wp.exe 프로세스가 사용되고 이 프로세스가 사용하는 계정이 ASPNET 계정이지만 Windows 2003에서는 어플리케이션 풀에 설정된 사용자 계정이 사용되고 디폴트 계정이 NETWORK SERVICE 계정이다.
익명 액세스를 허용하지 않고 인증을 받게 되면 어떻게 될까? IIS 설정에서 Default.aspx의 익명 액세스를 disable 하고 이 페이지를 다시 시도하면 인증 대화 상자가 나타날 것이다(로컬 컴퓨터에서 테스트하면 나타나지 않을 수도 있다). 인증을 성공적으로 마치더라도 결과는 화면 6과 전혀 다르지 않다.
그렇다면 기존의 ASP나 ISAPI와 호환이 되도록 하는 방법은 무엇인가? 그것은 웹 어플리케이션의 web.config 파일에 <identity> 태그를 다음과 같이 추가하는 것이다. 그리고 난 후 다시 Default.aspx를 접근하면 화면 7과 같은 결과를 얻게 될 것이다.
<identity impersonate="true">
화면 7에서는 프로세스 계정을 읽는 것이 실패했다. 그 이유는 현재 쓰레드 계정인 IUSR_XXXX 계정이 OpenProcessToken API를 호출할 권한이 없기 때문이다. 대개 프로세스 토큰을 얻기 위해서는 가장(Impersonation)을 중단하는 것이 좋다. 가장을 수행한 쓰레드가 프로세스 토큰을 액세스할 권한이 없을 수도 있기 때문이다. 따라서 현재 쓰레드 토큰을 읽어 저장해 두고, 가장을 중단(필자주: ReverToSelf API 함수를 호출하면 된다) 한 후에 프로세스 토큰을 읽는다. 그리고 나서 저장해둔 쓰레드 토큰으로 다시 가장을 하는 것이 일반적인 방법인 것이다. 앞서 테스트에 사용한 SecurityUtil 컴포넌트는 이러한 방식으로 코딩이 되어 있지만 ASP.NET 코드는 그렇지 못했기 때문에 문제가 발생한 것이다. 오류가 발생하지 않도록 하는 것은 Default.aspx.cs 에 몇 줄을 추가하는 것이므로 독자들에게 숙제로 남기겠다.
화면 7. ASP.NET 테스트 결과 2 (IIS 5.x 및 IIS 6.0 테스트 결과)
화면 7은 익명 액세스를 허용한 경우이다. 만일 익명 액세스를 허용하지 않는다면 IIS는 브라우저에게 인증을 요구할 것이고 사용자가 제공한 인증 정보를 통해 가장이 수행될 것이다. 결과적으로 쓰레드 계정은 IUSR_XXXX가 아닌 사용자가 제공한 계정이 될 것이다. 이것은 ASP 나 ISAPI와 동일하게 적용된다.
ASP.NET 에서는 지금까지 설명한 내용 외에도 추가적인 액세스 토큰 설정 방법이 있다. 이 방법은 인증 방법에 관계 없이 쓰레드가 하나의 계정하에서 작동하도록 하는 방법이다. 즉, 익명 액세스를 사용하건 아니면 NTLM 인증을 사용하건 쓰레드 계정을 단일화 한다는 것이다. 이와 같이 사용하기 위해서는 web.config의 <identity> 설정에 가장을 수행할 구체적인 계정 이름과 암호를 설정하면 된다.
<identity impersonate="true" userName="ksyu" password="pwd" />
web.config에 위와 같은 설정을 추가하고 페이지를 보려고 하면 Windows 2000에서는 오류가 발생할 것이다. 오류가 발생하는 이유는 ASP.NET이 가장을 하기 위해서 호출하는 LogonUser API 때문에 발생하는 것이다. 앞서 언급한 대로 이 API를 호출하기 위해서는 프로세스 계정(현재 쓰레드 계정이 아니다!)이 “운영체제의 일부로 활동” 이라는 특권을 가지고 있어야 하는데 ASP.NET 작업 프로세스의 계정인 ASPNET은 이 특권을 가지고 있지 않다. 이 때문에 액세스 토큰을 만들 수 없다는 오류가 발생한다. 반면 Windows XP와 Windows .NET은 LogonUser를 호출하기 위해 이 특권을 요구하지 않는다. 이 오류를 해결하는 방법은 두 가지가 있다. machine.config 파일을 수정하여 작업 프로세스가 ASPNET이 아닌 SYSTEM이나 administrator와 같이 특권을 갖도록 하는 방법과 ASPNET 계정에 “운영체제의 일부로 활동” 특권을 주는 방법이다. 보다 안전한 웹 사이트를 위해서는 후자의 방법을 택하는 것이 좋다. “운영체제의 일부로 활동” 특권을 주기 위해서는 로컬 보안 설정 관리자에서 사용자 권한 할당을 사용하면 된다. 특권을 할당했으면 IIS를 재시작해야만 한다. iisreset 커맨드나 서비스 관리자로 IIS를 재시작한 후 다시 시도하면 화면 9와 같은 결과를 얻을 것이다.
화면 8. 로컬 보안 설정 MMC
화면 9. ASP.NET 테스트 결과 3
화면 9는 익명 액세스를 사용하거나 NTLM 인증을 사용하더라도 항상 동일하게 나타나는 결과 화면이다. ASP.NET은 쓰레드 계정을 항상 <identity>에 명시된 계정으로 가장을 수행할 것이다. 화면 9에서 프로세스 계정 정보가 잘 나타나는 이유는 <identity>에 명시된 계정 ksyu가 프로세스 토큰을 읽을 권한이 있기 때문이다. 만약 권한이 없는 사용자가 <identity>에 명시되었다면 프로세스 계정 정보는 화면 7과 처럼 오류가 나타날 것이다.
ASP.NET의 Windows 인증은 기존 ASP를 사용할 때의 IIS의 인증과 호환되면서 추가적인 기능이 포함되어 있다. 필요에 따라 적절하게 설정을 수행하면 인트라넷에서 Active Directory와 더불어 Single-Sign-In을 구현하는데 매우 편리한 방법일 것이다. 하지만 인터넷 환경이나 Active Directory를 사용하지 않는 인트라넷에서는 그다지 많이 사용되지 않는다. 하지만 ASP.NET 어플리케이션을 개발하다가 종종 등장하는 ‘권한 없음’ 오류에 대처하는데 지금까지 설명한 프로세스 계정, 쓰레드 계정 지식을 적용한다면 쉽게 해결할 수 있는 문제들이 많이 있을 것이라고 필자는 장담할 수 있다.
Form 인증
ASP .NET 어플리케이션에서 가장 유용하고 많이 사용될 수 있는 인증 방식이 바로 Form 인증이다. web.config 파일의 <authentication> 태그의 mode 어트리뷰트를 Forms로 설정하면 Form 인증이 활성화 된다. Form 인증 방식의 기본 원리는 인증 티켓을 브라우저 쿠키에 저장하고 이 인증 티켓을 서버측에서 검사하는 방식이다. 인증 티켓은 FormsAuthenticationTicket 클래스에 의해 표현되며 이 티켓의 여러 속성들은 web.config 파일의 설정에 따라 초기화 된다(물론 코드에 의해 제어도 가능하다). 인증 티켓은 web.config 설정에 따라 적절히 암호화되어 브라우저 쿠키로 저장되며 Form 인증 검사 모듈은 브라우저의 request에 유효한 인증 티켓이 있는가를 검사하는 것이다. 만약 인증 티켓이 존재하지 않는다면 ASP .NET은 사용자를 web.config에 명시된 로그인 페이지로 리다이렉트를 수행하며 이 페이지에서 사용자에게 ID 및 암호 등 인증에 필요한 정보를 입력 받게 된다. 입력된 인증 정보를 통해 어플리케이션은 사용자를 인증하게 되고 인증이 성공하면 FormsAuthentication 클래스를 통해 인증 티켓을 만들고 이것을 쿠키로서 브라우저에 내려 보낸다. 복잡하지 않다 !
Form 인증은 ASP .NET의 중요한 특징 중 하나이며 많은 자료나 서적에서 사용법을 설명하기 때문에 구체적인 사용법이나 코딩 방법을 여기서 굳이 다시 설명하지 않겠다. 다만, Form 인증이 사용될 때 웹 어플리케이션의 프로세스 계정이나 쓰레드 계정은 어떻게 되는지, Form 인증과 IIS의 보안 설정은 어떤 관계가 있는지를 테스트를 통해 살펴보기로 하자.
테스트를 위해 web.config 파일에서 Forms 인증 설정을 다음과 같이 수행한다. 이 설정은 Form 인증을 사용하며 인증되지 않은 사용자를 거부할 것을 명시하고 있다. 인증되지 않은 사용자는 디폴트 설정에 의해 Login.aspx 로 리다이렉트 될 것이며 이 페이지에서 인증을 받도록 할 것이다. Login.aspx가 아닌 다른 페이지로 설정하는 것은 MSDN을 참조하기 바란다.
<authentication mode="Forms" />
<!-- identity 가 존재한다면 지우자 -->
<authorization>
<deny users="?" />
</authorization>
그리고 Login.aspx 페이지를 만들자. 이 페이지는 단순한 테스트를 위한 것이므로 로그인 버튼 하나만을 둘 것이다. 그리고 이 버튼이 눌려지면 FormsAuthentication.RedirectFromLoginPage 메소드를 호출하여 인증 티켓을 만든다. 인증에 사용된 ID는 하드 코드된 값으로 “formsUser”가 사용되었다. 다음 코드는 Login.aspx.cs 의 일부이며 전체 코드는 이달의 디스켓을 참고하기 바란다.
private void btnLogin_Click(object sender, System.EventArgs e)
{
// 간단한 테스트를 위해 임으로 인증을 수행했다.
FormsAuthentication.RedirectFromLoginPage("formsUser", false);
}
Login.aspx와 Default.aspx의 IIS 보안 설정은 디폴트(익명 액세스와 NTLM 인증 선택. 화면 2 참조) 설정대로 둔다.
이제 브라우저가 Default.aspx 를 접근하고자 했을 때 어떤 일들이 일어나는가 차근차근 알아보자. 브라우저가 Default.aspx를 요청하면 이 요청은 IIS에 의해 수신될 것이다. IIS는 메타베이스의 Default.aspx 보안 설정을 확인하고 이것이 익명 액세스를 허용하므로 곧바로 aspnet_isapi.dll를 통해 작업 프로세스인 aspnet_wp.exe로 브라우저의 요청을 넘긴다.(필자주: 다시 말하지만 Windows 2003에서는 HTTP Request는 HTTP.SYS가 수신하고 이것이 작업 프로세스인 w3wp.exe로 전달된다. 이 프로세스에서 aspnet_isapi.dll에 의해 request가 ASP.NET 엔진에 전달된다) ASP .NET 엔진은 web.config 파일에 의해 Form 인증이 수행 중이라는 것을 알게 될 것이고 Form 인증을 수행한다. 인증 모듈은 인증 티켓을 검사하고 인증 티켓이 없으므로 브라우저에게 Login.aspx로 리다이렉트 할 것을 지시한다. 이제 브라우저는 Login.aspx로 리다이렉트 되어 인증을 수행하고 다시 Default.aspx로 리다이렉트 될 것이다. 이 리다이렉트 과정에서 브라우저는 인증 티켓을 쿠키로서 보관하게 된다는 점이 중요한 포인트 이다(왜 Transfer가 아닌 Redirect인가에 대한 답이 될 것이다). 이제 인증 모듈은 인증 티켓을 브라우저가 제시했으므로 인증 티켓의 유효성을 검사하고 Default.aspx를 수행할 것이다.
이러한 Form 인증 과정에서 프로세스 계정과 쓰레드 계정은 어떻게 될까? Form 인증이라 할지라도 프로세스 계정은 Windows 인증과 다를 바가 없을 것이다. 즉, ASP .NET의 인증 방법에 관계 없이 프로세스 계정은 여전히 ASPNET 계정, 혹은 machine.config의 설정 사항을 따를 것이라는 것에는 의심이 없다. 그렇다면 Form 인증 시 쓰레드 계정은 어떻게 될까? 결론부터 이야기 하자면 Windows 인증과 크게 다르지 않다는 점이다. 먼저 우리가 테스트 환경에서 web.config의 <identity> 태그의 impersonate를 명시하지 않았으므로 ASP .NET은 가장을 하지 않는다. 따라서 쓰레드의 액세스 토큰은 프로세스의 액세스 토큰이 되며 쓰레드 계정은 프로세스 계정과 같다. 따라서 이 테스트의 결과는 화면 6과 같다. 만약 web.config에 <identity impersonate=”true” />를 삽입해 넣는다면 프로세스 계정은 ASPNET 이고 쓰레드 계정은 IUSR_XXX 가 될 것이다 (화면 7). 이는 Default.aspx가 익명 액세스를 허용했을 때의 결과이다.
만일 Default.aspx가 익명 액세스를 허용하지 않고 NTLM 인증만을 사용한다면 어떻게 될까? 이 경우, 웹 페이지를 액세스하기 위해서는 두 가지 인증이 모두 사용된다. IIS의 인증과 ASP .NET의 Form 인증이 순서대로 일어난다. IIS가 브라우저의 요청을 먼저 처리하기 때문에 NTLM 인증을 위해 네트워크 암호 입력 대화상자가 먼저 나타나게 되고(다시 한번 이야기 하지만 로컬 컴퓨터에서 테스트하면 나타나지 않을 수도 있다) ASP .NET의 Form 인증에 의해 login.aspx로 리다이렉트 될 것이다. 즉, 이중 인증이 필요하다는 이야기가 된다. 이 때문에 Form 인증을 사용하는 경우에는 익명 액세스를 사용하는 것이 일반적이고, IUSR_XXX 계정이 아닌 계정 하에서 쓰레드가 작업을 해야 한다면 web.config의 <identity> 태그에서 사용할 구체적인 계정을 적어 주는 것이 좋다. 이때 ASPNET 계정이 aspnet_wp.exe 프로세스의 계정으로 사용된다면 “운영체제 일부로 활동” 특권이 주어져야 함을 물론이다(Windows 2000의 경우에만).
또하나의 도전, CLR 쓰레드 계정
Form 인증과 더불어 한 가지 새로운 개념을 이해해야 할 필요가 있다. 소위 CLR(Common Language Runtime) 쓰레드라는 것이 그것인데, 명칭상 쓰레드를 지칭하는 것 같은데 CLR 쓰레드는 무엇을 의미할까? CLR 쓰레드는 지금까지 필자가 언급해온 쓰레드와는 약간 다른 개념이다. 구분을 위해 지금까지 단순히 쓰레드라고 지칭했던 것을 WIN32 쓰레드로 구분하기로 한다. CLR 쓰레드라 함은 WIN32 쓰레드와는 달리 닷넷에서 쓰레드 정보를 구분하기 위해 사용되는 개념으로서 몇 개의 CLR 쓰레드가 하나의 WIN32 쓰레드에서 수행될 수 있음을 의미한다. 실제로 이러한 작업은 CLR에 의해 수행되며, 어떤 CLR 쓰레드가 WIN32 쓰레드에서 수행되도록 할 것인가에 대한 제어는 프로그래머에 의해 조정될 수 없다. 간단히 말해 CLR 쓰레드는 닷넷 CLR에만 존재하며 오직 CLR에 의해서만 제어되는 개념적인 쓰레드라 할 수 있다. 99.9%의 경우 개발자는 CLR 쓰레드에 대해 인지할 필요가 없으며 WIN32 쓰레드와 동등 혹은 동일한 것으로 파악해도 아무런 무리가 없다. 실제로 MSDN을 뒤져보아도 CLR 쓰레드에 대한 문서는 전혀 없으며 MSDN Magazine 같은 잡지에 극히 일부만 언급되는 내용이 CLR 쓰레드다.
프로그래밍적인 관점에서 System.Threading.Thread 클래스는 CLR 쓰레드를 나타내고 있다고 보아야 한다. 하지만 99.99%의 경우 이 클래스를 WIN32 쓰레드로 보아도 무방하다. 한 프로세스 내에서 여러 어플리케이션 도메인이 수행되는 상황에서 하나의 WIN32 쓰레드가 두 개 이상의 Thread 클래스 객체에 의해 표현되는 경우가 발생하기도 한다. Form 인증과 앞으로 설명할 역할 기반의 권한 검사에서 CLR 쓰레드의 개념을 이해하는 것이 많은 도움이 된다.
필자가 전혀 몰라도 무방할 CLR 쓰레드에 대해 언급하는 이유는 이렇다. Windows 인증 혹은 Form 인증을 사용할 때 Page.User 프로퍼티가 나타내는 계정 때문이다. Page.User 프로퍼티는 현재 페이지를 액세스하는 사용자의 인증정보(사용자 계정)을 나타내는 프로퍼티인데, 이 프로퍼티의 값을 해석하는데 혼동이 올 소지가 다분하기 때문이다. 백문이 불여일견이고 백견(白見)이 불여일Run 이다. 실제로 테스트를 해보자. Windows 인증 하에서 impersonate가 true로 설정되어 있다고 가정해 보자. 그리고 페이지에 익명 액세스가 허용되어 있다. 이 때 프로세스 계정은 ASPNET 이고 쓰레드 계정(정확하게 WIN32 쓰레드 계정)은 IUSR_XXX 이다. 그렇다면 Page.User 프로퍼티는 IUSR_XXX 계정을 지칭하고 있을까? 다음과 같이 코드를 추가하여 Page.User 프로퍼티의 정보를 출력해 보자.
if (Page.User.Identity.IsAuthenticated) {
lblAuthUserName.Text = Page.User.Identity.Name;
}
else {
lblAuthUserName.Text = "인증되지 않음";
}
출력결과는 화면 10과 같다. 프로세스 계정은 권한 문제 때문에 표시할 수 없고 쓰레드 계정은 IUSR_XXX 이다. 하지만 Page.User가 나타내는 계정은 “없음” 이다. 이것이 무슨 이슈인가하고 의아해 할지도 모르겠지만 Page.User 프로퍼티는 실제로는 System.Threading.Thread.CurrentPrincipal 프로퍼티와 같은 객체이기 때문이다. 이는 곧 현재 쓰레드의 계정이 “없음”에 해당하는 것이다. 하지만 쓰레드의 액세스 토큰은 IUSR_SIMPLE 이라고 표시되지 않았는가? 액세스 토큰은 WIN32 쓰레드에 해당되는 이야기 이다. CLR 쓰레드의 계정(Principal)은 액세스 토큰과 무관하다. 비록 WIN32 쓰레드가 IUSR_XXX의 액세스 토큰하에서 수행된다 할지라도 CLR 쓰레드는 인증되지 않은 상태로도 있을 수 있다는 얘기 이다. 화면 10의 결과는 논리적인 의미와 매우 일치되는 결과이다. Default.aspx가 익명 액세스를 허용하기 때문에 CLR 쓰레드는 익명의, 즉 인증되지 않은 사용자임을 나타낸다. 하지만 Default.aspx 파일에 접근하기 위해서 WIN32 쓰레드는 반드시 WinNT 계정을 필요로 하므로 익명 사용자에 걸맞는 제한된 권한만을 갖는 IUSR_XXXX 계정으로 가장을 하는 것이다. 만약 익명 액세스를 허용하지 않고 NTLM 인증을 수행하면 어떻게 될까? 이렇게 되면 CLR 쓰레드 계정과 WIN32 쓰레드의 계정은 일치하게 된다(화면 11).
화면 10. ASP.NET 테스트 결과 4
화면 11. ASP.NET 테스트 결과 5
Form 인증이 사용되면 CLR 쓰레드의 계정은 더욱 극명하게 나타난다. 화면 12는 Form 인증을 사용하고 impersonate가 false 이며 익명 액세스를 허용한 경우의 결과이다. 익명 액세스가 허용되었으나 web.config에서 impersonate를 허용하지 않았으므로 WIN32 쓰레드는 프로세스 계정인 ASPNET 계정 하에서 수행된다. 하지만 CLR 쓰레드의 계정은 Form 인증의 결과안 formUser 임에 주의해야 한다. 화면 12 같은 경우 Default.aspx에서 웹 서버 상의 어떤 파일을 액세스하고자 한다면 그 파일은 ASPNET 계정에 대해 권한이 있어야만 한다. 파일 액세스는 WIN32 쓰레드 계정과 관계가 있기 때문이다. 하지만 조금 있다 설명할 URL 권한 검사나 역할 기반 권한 검사에서는 CLR 쓰레드의 계정이 사용됨에 유의해야 할 것이다.
화면 12. ASP.NET 테스트 결과 6
CLR 쓰레드의 계정 설정은 System.Threading.Thread.CurrentPrincipal 프로퍼티를 통해 수행될 수 있다. ASP .NET은 웹 어플리케이션에서 설정된 인증 방법에 따라서 이 프로퍼티에 Principal 객체를 만들어 준다. 특이한 점은 이 프로퍼티의 값을 코드에 의해 임의로 수정할 수 있다는 점이다.
CLR 쓰레드의 등장으로 독자들이 혼란에 빠졌을지도 모르겠다. 이제 차분히 정리를 해보자. WIN32 쓰레드는 운영체제 레벨에서 제공되는 쓰레드로서 파일 액세스나 기타 운영체제에서 제공하는 자원에 접근하기 위해서는 WIN32 쓰레드가 어떤 액세스 토큰(쓰레드 계정)을 갖고 있는가가 중요하다. 반면 CLR 쓰레드는 소프트웨어적이며 개념적인 쓰레드로서 운영체제가 아닌 닷넷 수준에서 제공하는 보안 처리를 하는데 CLR 쓰레드의 계정이 사용되는 것이다. 일례로 Windows 98/Me는 멀티 스레딩을 제공하지만 (WIN32 쓰레드가 존재하지만) 각 쓰레드의 액세스 토큰은 존재하지 않는다. 반면 Windows 98/Me 에서도 닷넷 CLR은 작동하며 CLR 쓰레드는 계정 정보를 갖을 수 있다.
ASP.NET 권한 검사
지금까지 많은 지면을 할애하여 인증에 대해 살펴보았다. 운영체제 적인 입장에서 프로세스 계정과 WIN32 쓰레드 계정을 살펴보았고 CLR 쓰레드의 계정 설정 역시 살펴보았다. 이렇게 설정된 인증 정보를 통해 ASP.NET은 3가지 수준의 권한 검사를 수행할 수 있다. 첫째는 파일 수준의 권한 검사이며 두번째는 URL 권한 검사, 세번째는 역할 기반 권한 검사이다.
파일 수준의 권한 검사는 ASP.NET 만의 고유한 기능이라기 보다는 IIS와 연계되는 서버측 기술들(ASP, ISAPI)이 공통적으로 제공하는 권한 검사라고 볼 수 있다. 즉, 특정 페이지를 브라우저가 요청했을 때 그 페이지 파일에 대한 권한이 있는지를 검사하는 것으로서 ASP.NET이 수행하는 가장(impersonation)과 WIN32 쓰레드 계정과 밀접하게 연관된다. 예를 들어 Form 인증을 사용하고 impersonate가 true로 설정되어 있을 때, aaa.aspx를 브라우저가 접근하려한다면 두 가지 사항이 점검 대상이 된다. 첫째로 IIS의 메타베이스 설정과 aaa.aspx에 설정된 Windows 파일 보안이다. IIS 보안 설정이 이 파일에 대해 익명 액세스를 허용할 때, IUSR_XXXX 계정이 aaa.aspx 파일에 읽기 권한이 없다면 오류가 발생할 것이다. IIS 설정이 이 파일에 대해 익명 액세스를 허용하지 않는다면 사용자는 Windows 아이디와 암호를 네트워크 암호 대화 상자에 입력해야 할 것이고 IIS는 이 계정으로 가장(impersonation)을 수행한다. 그리고 나서 이 가장된 계정이 aaa.aspx 파일에 읽기 권한이 있는가 확인해야 할 것이다. 반면 Form 인증에서 수행한 인증 정보는 aaa.aspx를 접근할 수 있는 가와는 전혀 무관한 설정이다. Form 인증에서 제공한 계정은 WIN32 쓰레드 계정이 아닌 CLR 쓰레드 계정과 관계가 있기 때문이다.
따라서 Form 인증을 사용할 때 원인 모를 액세스 거부 오류가 발생하거나 뜬금없이 네트워크 암호 대화 상자가 브라우저에 나타난다면 ASPNET 계정(필자주: IIS 6.0에서는 IIS_WPG 사용자 그룹을 사용하여 파일 권한을 주면 된다)이나 IUSR_XXXX 계정이 오류를 발생시킨 aspx 파일 혹은 디렉토리에 읽기 권한이 있는가를 검사해 보아야 한다. 또, ASP.NET 웹 페이지 내에서 System.IO 네임스페이스의 클래스를 이용해 파일을 읽거나 쓰려고 할 때 역시 그 파일에 대해서 ASPNET 혹은 IUSR_XXXX 계정이 적절한 권한이 있는가 살펴보아야 할 것이다. 독자들이 필자에게 가끔씩 하는 질문 중 하나가 COM+ 컴포넌트를 ASP.NET 에서 사용할 때 레지스트리 관련 오류가 발생한다는 것이였다. 이 오류는 닷넷으로 작성한 COM+ 컴포넌트가 최초로 액세스될 때 등록되지 않은 COM+가 발견되면 스스로 COM+ 카탈로그에 등록하려고 하는데 이때 ASPNET 계정이나 IUSR_XXXX 계정이 레지스트리에 쓰기 권한이 없기 때문에 발생하는 오류이다. 이 오류를 해결하기 위해서는 web.config 파일의 <identity> 태그에서 권한이 있는 사용자로 가장을 하거나, machine.config 파일을 수정하여 aspnet_wp.exe 프로세스가 ASPNET 이 아닌 다른 계정으로 수행되도록 하던가, 자동 등록이 발생하지 않도록 미리 수동으로 등록을 해주어야 한다.
URL 권한 검사는 브라우저가 요청한 URL에 대해 사용자 별로 권한을 주거나 주지 않을 수 있음을 말한다. URL 권한 검사 설정은 web.config 파일의 <location> 태그를 통해 권한을 설정할 URL을 명시하고 <authorization> 태그를 통해 어떤 사용자 계정 혹은 역할(role)에게 액세스를 허용할 것인지를 결정한다. 이 때 권한 검사에 사용되는 계정은 CLR 쓰레드의 계정임에 유의하자. URL 권한 검사를 위한 설정이나 구체적인 적용 방법은 여러 ASP.NET 관련 서적에서 상세히 다루고 있고 MSDN에도 충분한 설명이 있으므로 더 이상 다루지 않겠다.
역할 기반 권한 검사는 닷넷 CLR에서 제공하는 닷넷 만의 독특한 보안 기능으로서 ASP.NET 뿐만 아니라 WinForm 어플리케이션, 콘솔 어플리케이션 등 모든 닷넷 어플리케이션에서 적용할 수 있는 보안 메커니즘이다. 역할 기반 권한 검사는 어떤 메소드 혹은 코드 블록을 수행하는데 필요한 권한을 역할에 따라 결정하겠다는 말이다. 예를 들어 데이터베이스에서 데이터를 읽어 오는 작업을 수행하는 메소드가 있다고 가정해 보자. 이 메소드는 관리자 만이 호출할 수 있으며 일반 사용자는 접근을 허용하지 않아야 한다. 이 요구 사항을 만족시키기 위해 흔히 사용하는 접근 방법은 이 메소드의 도입부에서 이 메소드를 호출한 사용자 정보를 읽고 이 정보를 바탕으로 액세스 허용 여부를 결정하는 것이다. 이러한 권한 검사 코드는 필연적으로 if 와 같은 조건문을 사용하기 마련이고 권한 설정이 복잡하거나 다양한 역할에 대해 권한 검사를 해야 한다면 이러한 조건문의 개수는 기하 급수적으로 늘어나기 마련이다. 하지만 닷넷에서는 이러한 역할 기반 권한 검사에 대한 인프라를 갖추고 있다. 다음 코드가 이러한 역할 기반의 권한 검사를 해준다.
[PrincipalPermission(SecurityAction.Demand, Role="administrators")]
static void DoDatabaseAccess()
{
// 데이터베이스 조회 코드
// administrators 역할을 가진 사용자(principal) 만이 이 함수를 호출할 수 있다.
// 여기서 말하는 사용자란 Thread.CurrentPrincipal이 나타내는 사용자를 말함에 유의하자.
}
위 코드에서 주의해서 볼 부분은 PrincipalPermission 특성(attribute) 이다. 이 특성은 선언적(declarative)인 보안 검사를 할 수 있도록 해주는 것으로서 호출자가 administrators 역할을 갖지 않으면 DoDatabaseAccess 메소드 호출은 SecurityException 예외를 발생시킬 것이다. 역할 기반 권한 검사는 이 칼럼에서 그 내용을 모두 다 다룰 수 없으므로 상세한 내용은 MSDN 도움말을 참고하기 바란다.
역할 기반 권한 검사에서 우리가 주목해야 할 것은 권한 검사를 수행할 대상이 무엇이냐는 것이다. PrincipalPermission 특성의 이름이 암시하듯이 검사의 대상은 CLR 쓰레드의 계정(principal)이다. 즉, 역할 기반 권한 검사는 WIN32 쓰레드의 액세스 토큰과는 전혀 무관하며 오로지 Thread 클래스의 CurrentPrincipal 프로퍼티가 반환하는 계정 정보에 의해서만 역할 기반 권한 검사가 수행된다는 것이다(다만 WIN32의 쓰레드 토큰이 CLR 쓰레드 계정정보를 생성하는데 사용되기도 한다). 이점을 독자들은 주지해야 할 것이다.
ASP.NET이 제공하는 보안 모델에서 권한 검사는 다양하고 풍부하지만 여전히 실제 어플리케이션에서 적용하기에는 어려운 점들이 있다. 예를 들어 보자. 어떤 웹 페이지는 데이터를 조회/추가/수정/삭제를 할 수 있다. 그러나 이 페이지는 사용자에 따라서 서로 다른 권한을 주고자 한다. 관리자는 조회/추가/수정/삭제를 할 수 있으며 파워 유저는 조회/수정을 그리고 일반 사용자는 조회만이 가능하게 하고 싶다. 이 문제를 해결하는 방법으로 파일 기반 권한 검사나 URL 기반 권한 검사는 적절하지 않다. 모든 사용자가 이 웹 페이지를 열 권한은 있어야 하기 때문이다. 따라서 역할 기반의 권한 검사가 적절할 것처럼 보이지만 역할 기반 권한 검사는 약간의 한계가 있다. 첫째로 호출하기 전에는 권한이 있는지 없는지 알기 어렵다. 이 경우 일반 사용자가 수정 버튼을 클릭해 보고서야 자신이 권한이 없다는 것을 알게 된다. 일반 사용자가 이 페이지를 열었을 때 아예 추가/수정/삭제 버튼이 Disable 하도록 하기 어렵다는 것이다. 물론 이 기능이 불가능하다는 것은 아니다. 하지만 앞서 본 코드 처럼 선언적으로 보안 검사를 하면 소스 내에 하드 코드가 되기 때문에 수정이나 관리가 어려워진다. 프레임워크에서 제공하는 다른 클래스를 이용하면 프로그램적으로 검사가 가능하지만 이 역시 개발자가 많은 코드를 작성해야 한다. 대부분의 경우 사용자가 어떤 웹 페이지에 어떤 권한이 있는가는 데이터베이스에 별도로 저장되기 마련이고 권한 검사가 필요할 때 이 데이터를 읽어서 처리하는 것이 대부분이다. 이런 점에서 봤을 때 닷넷에서 제공하는 역할 기반 권한 검사 보다는 어플리케이션 레벨에서 권한 검사 코드를 작성하는 것이 일반적이라 할 수 있겠다.
IIS 6.0과 보안
Windows 2000 서버 제품군의 다음 버전인 Windows .NET의 RC(Release Candidate) 버전이 출시되었다(필자주: 이 글은 2002년 9월에 쓰여진 글임을 기억하라). Windows .NET 서버는 .NET 프레임워크를 기본으로 탑재하고 있으며 IIS 6.0과 COM+ 1.5이 제공되며 Windows XP와 같은 UI를 갖고 있다. Windows .NET에 포함된 IIS 6.0은 기존 버전에 비해 다양한 기능들이 추가되었으며 이 기능들은 ASP.NET과 IIS의 통합, 보안 강화, 웹 서비스의 안정성 증대, 성능 개선 등으로 축약할 수 있다. 여기서 이들에 대한 모든 내용을 다 살펴볼 수 없으므로 보안에 관련된 부분 몇가지만 언급하도록 하겠다.
IIS 6.0에서 크게 바뀐 점은 IIS 6.0이 제공하는 프로세스 모델과 보안 기능이다. IIS 6.0은 설치되면 ASP, ISAPI, ASP.NET 등 서버측에서 수행(execution)을 요구하는 어떤 스크립트나 코드도 수행되지 않도록 되어 있다. 서버측 수행이 필요하다면 관리자는 이들을 명시적으로 Enable 시켜주어야 한다. 이는 사용하지 않더라도 Windows와 함께 디폴트로 설치된 IIS가 코드레드 류의 바이러스에게 공격을 받는 것을 방지하기 위함이다. 어찌되었건 IIS 6.0이 최초로 설치된 후, 디폴트로 제공되는 서비스는 정적인 htm 파일이나 이미지 파일들 뿐이다.
IIS 6.0은 HTTP 80 포트를 inetinfo.exe가 리스닝 하지 않는다. 이제 80 포트는 Windows 커널(HTTP.SYS)이 리스닝하게 된다. inetinfo.exe 프로세스는 이제 메타베이스를 관리하는 작업만을 수행하며 inetinfo.exe 프로세스에서 서비스 되는 웹 어플리케이션은 더 이상 존재하지 않는다. 이제 모든 웹 어플리케이션은 IIS 작업 프로세스 w3wp.exe 상에서 작동되며 작업 프로세스는 설정에 따라 2개 이상 수행될 수 있다. 이는 ASP.NET 웹 어플리케이션에도 해당되며 더 이상 aspnet_wp.exe는 존재하지 않는다. 결과적으로 In-proc 이나 Out-of-Proc 이니 하는 용어도 사라져 버렸다. 다만 어떤 웹 어플리케이션을 어떤 IIS 작업 프로세스(w3wp.exe)에서 수행시킬것인 가를 결정하는 어플리케이션 풀(application pool) 개념이 등장했다. 어플리케이션 풀은 하나 혹은 그 이상의 웹 어플리케이션을 호스팅 할 수 있으며 어플리케이션 풀은 2개 이상의 작업 프로세스에 부하를 분산시킬 수도 있다(WebGarden). 이러한 변화 때문에 Windows .NET 서버에서는 aspnet_isapi.dll의 역할은 크게 줄어들었으며(단순히 CLR에게 HTTP 메시지를 전달하는 정도), aspnet_wp.exe 프로세스도 ASPNET 계정도 존재하지 않는다(존재는 하지만 사용하지 않는다). ASP.NET 어플리케이션이 IIS 작업 프로세스에 의해 호스팅 되므로 어플리케이션 프로세스 계정은 이 프로세스의 계정을 따르게 된다. IIS 작업 프로세스 계정은 미리 정의된 SYSTEM, NETWORK_SERVICE, LOCAL_SERVICE 계정 이나 임의의 계정으로 설정될 수 있다(화면 13).
화면 13. IIS 6.0의 작업 프로세스 계정 설정
필자주)IIS 6.0 에서 작업 프로세스의 프로세스 계정으로 설정될 수 있는 계정은 Local System, Local Service, Network Service 세개가 디폴트로 제공된다. 이외의 계정을 사용하고자 한다면 해당 계정을
IIS_WPG 로컬 그룹에 포함시켜야 한다. 그리고 다양한 파일 시스템 권한 및 특권은 모두 IIS_WPG 그룹에 대해 주어져 있다.
예를 들어, WebAppUser라는 계정을 작업 프로세스의 계정으로 사용하고자 한다면 이 계정을 IIS_WPG 그룹에 포함시키고 어플리케이션 풀의 Identity 설정에서 ID/PWD를 설정하면 별다른 문제 없이 웹 어플리케이션들이 작동할 것이다.
간략하게나마 IIS 6.0의 보안 사항을 지금까지 이 컬럼에서 다루어온 내용을 위주로 설명하였다. WIN32 쓰레드 계정이나 CLR 쓰레드 계정은 Windows 2000과 동일하다. IIS 6.0은 ASP.NET 을 통합 함으로서 진정한 닷넷을 위한 웹 서버의 역할을 한다고 할 수 있겠다. IIS 6.0의 새로운 기능들에 대해서는 다음 기회에 상세히 다룰 것을 약속하는 바이다.
보안 오류를 자신 있게 해결하자
지금까지 ASP.NET의 보안에 관련된 여러 가지 사항을 살펴보았다. 일반적인 내용들 보다는 그 뒤에 숨겨진 원리나 작동 방식에 주안점을 두었고 이러한 내용들이 실제 문제를 해결하는데 도움이 되리라 생각한다. IIS에 기반한 많은 웹 개발자들이 전혀 예상하지 못한 보안 오류에 부딪히곤 하는데 대부분의 문제들은 IIS가 어떤 식으로 작동하는 지를 정확히 이해하지 못했거나 ASP.NET의 다양한, 아니 다양하다 못해 혼동 스럽고 복잡하기까지 한 보안 사항을 상세히 이해하지 못했기 때문에 해결이 어려웠던 것이 사실이다. 이제 이 칼럼의 내용이 다른 ASP.NET 관련 서적과 함께 보안 오류를 해결하고 더 나아가 보다 안전한 웹 어플리케이션을 계획하고 설계하는데 도움이 되었으면 하는 바람이다.
참고문헌
Authentication in ASP.NET: .NET Security Guidance
Jeff Kercher, Edward Jezierski, MSDN online Technical Article
http://msdn.microsoft.com/library/en-us/dnbda/html/bdadotnetarch16.asp
ASP.NET Security : An Introductory Guide to Building and Deploying More Secure Sites with ASP.NET and IIS, Part I
Jeff Prosise, MSDN Magazine, April 2002
ASP.NET Security : An Introductory Guide to Building and Deploying More Secure Sites with ASP.NET and IIS, Part II
Jeff Prosise, MSDN Magazine, May 2002
Security Briefs : Managed Security Context in ASP.NET
Keith Brown, MSDN Magazine, January 2002
Security Briefs : ASP.NET Security Issues
Keith Brown, MSDN Magazine, November 2001
MSDN Online
좀 길줄 알았는데 마니 길어진 것 같습니다. 원본 문서가 원래 긴데다가 여기 저기 Windows 2003/IIS 6.0에 대한 사항을 적다보니 마니 길어 졌네요. 다 읽으셨다면... 수고 많으셨습니다~~~