수정 이력(6건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- 관련 기사 링크를 한국어 permalink에 맞추는 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 글 맨 앞에 「이 글의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 외부 리뷰(1283건)에 대응해 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참조하십시오.
- 14장의 그림과 설명을 고쳤습니다. 「impersonation scope 안에서 `Task.Run`으로 던지면 그 스레드에는 impersonation token이 붙어 있지 않다」고 적었으나, .NET에서는 반대입니다. `RunImpersonated`는 impersonation token을 `AsyncLocal`에 싣고, `Task.Run`은 호출 시점의 `ExecutionContext`를 잡아 풀의 스레드에서 복원하므로, 던진 처리도 impersonation된 채로 동작합니다. 독자가 실제와 반대 전제를 둘 수 있어, .NET과 Win32 직접 호출에서 깨지는 방식이 정반대라는 점을 표로 정리하고, 위험한 것은 「impersonation이 풀리는 것」이 아니라 「풀리지 않은 채 추적이 안 되는 것」이라고 다시 썼습니다. 21.3도 같은 취지로 고쳤습니다.
- token의 내용을 항목과 의미의 표로 정리하고, SID와 DACL의 풀 스펠을 병기했습니다. 코드 예제를 그대로 베껴 비밀번호를 리터럴로 쓰지 말라는 경고를 넣고, 자격 정보 보관 방법 비교표를 추가했습니다. double hop의 다음 조치 표, impersonation 시작부터 `RevertToSelf`까지의 그림과, impersonation scope 밖으로 I/O가 삐져나가는 그림을 대비해 추가했습니다.
- 최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635306)
이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.
Go Komura (2026). 「Windows impersonation token을 올바르게 다루기 ── 스레드 단위 권한 차용과 안전한 되돌리기」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635306 https://comcomponent.com/ko/blog/windows-impersonation-token/
- DOI(최신 버전)
- 10.5281/zenodo.21635306
- DOI(이 버전)
- 10.5281/zenodo.21635307
1. 먼저 알아 둘 것
Windows 애플리케이션이나 Windows 서비스를 만들다 보면, 「이 처리만 다른 사용자로 실행하고 싶다」는 장면이 있습니다.
예를 들면 다음과 같은 경우입니다.
- Windows 서비스에서 이용자 본인의 권한으로 파일 서버에 접근하고 싶다
- 관리용 애플리케이션에서 특정 사용자에게 보이는 범위만 확인하고 싶다
- Named Pipe, RPC, COM, IIS, ASP.NET Core 등에서 호출자 사용자의 권한으로 일부 처리를 하고 싶다
- 기존 자산 사정으로 처리 단위마다 사용할 Windows 계정을 전환하고 싶다
이때 나오는 것이 impersonation, access token, impersonation token입니다.
다만 여기서 먼저 강조하고 싶은 점이 있습니다.
Windows impersonation은 「관리자가 되는 마법」이 아닙니다. access check에 쓰이는 security context를, 주로 스레드 단위로 전환하는 메커니즘입니다.
이 차이를 잡지 않은 채 구현하면 이런 문제가 생깁니다.
- impersonation했다고 생각했는데 파일 접근이
Access denied가 된다 - 로컬 파일은 읽을 수 있는데 네트워크 공유만 실패한다
Task.Run이나async도중에 어느새 원래 사용자로 돌아가 있다- impersonation한 채로 로그 출력이나 후속 처리까지 돌아가, 권한 경계가 흐려진다
- primary token과 impersonation token을 혼동해 프로세스 시작에 실패한다
- 「사용자가 Administrators에 들어 있는데 왜 쓸 수 없는가」에서 막힌다
이 글에서는 Windows impersonation token을 실무에서 안전하게 다루기 위한 관점을 정리합니다.
공격 기법이나 권한 탈취 이야기가 아닙니다. Windows 애플리케이션, Windows 서비스, .NET 애플리케이션에서 권한 경계를 올바르게 다루기 위한 이야기입니다.
참고로 이 글에 나오는 코드는, 빌드할 수 있는 샘플 전체(라이브러리, Windows에서 실행하는 데모, 인자 검증과 가드 동작의 단위 테스트)로 GitHub에 공개하고 있습니다.
windows-impersonation-token - komurasoft-blog-samples (GitHub)
이 글의 지식 맵
Windows의 가장 토큰은 프로세스 전체를 다른 사용자로 바꾸는 권한 상승이 아니라, 스레드 단위로 액세스 체크의 보안 컨텍스트를 전환하는 메커니즘이며, Identification·Impersonation·Delegation과 같은 가장 수준에 따라 할 수 있는 일이 결정됩니다. Win32 API로 가장하는 경우에는 반드시 RevertToSelf로 되돌려야 하고, .NET의 RunImpersonated/RunImpersonatedAsync는 가장 토큰을 ExecutionContext에 실어 전달하기 때문에, 가장 스코프 안에서 Task.Run으로 fire-and-forget 처리를 던지면 코드상의 스코프를 넘어 가장이 유지된 채로 처리가 계속 실행되는 방식으로 깨집니다. 프로세스를 다른 사용자로 시작하고 싶을 때는 가장 토큰이 아니라 primary 토큰이 필요하며, 네트워크 너머에서 사용자 본인의 권한을 쓰려는 더블 홉 문제에는 Kerberos의 위임 설계가 필요합니다.
flowchart LR
accTitle: Windows의 가장 토큰의 지식 맵
accDescr: 가장 토큰이 가장 수준을 동반하며 RevertToSelf로 안전하게 되돌려야 한다는 것, .NET의 RunImpersonated가 ExecutionContext의 전파를 통해 Task.Run으로 던진 처리를 가장이 유지된 채로 계속 실행할 수 있다는 것, primary 토큰이나 더블 홉 문제와의 관계를 보여주는 그림
impersonation_token["가장 토큰"]
impersonation_level["가장 수준"]
revert_to_self["RevertToSelf"]
runimpersonated_dotnet["WindowsIdentity.RunImpersonated / RunImpersonatedAsync"]
executioncontext_dotnet[".NET ExecutionContext"]
lingering_impersonation_risk["가장 범위를 넘어 지속되는 가장"]
logonuser_api["LogonUser"]
double_hop_problem["더블 홉 문제"]
kerberos_delegation["Kerberos 위임"]
spn["SPN"]
domain_controller["도메인 컨트롤러"]
safeaccesstokenhandle["SafeAccessTokenHandle"]
handle_leak["핸들 누수"]
primary_token["프라이머리 토큰"]
admin_rights["관리자 권한"]
aspnet_core_windows_auth["ASP.NET Core Windows 인증"]
createprocessasuser["CreateProcessAsUser"]
windows_service["Windows 서비스"]
impersonation_token -->|"전제로 한다"| impersonation_level
impersonation_token -.->|"전제로 한다"| revert_to_self
runimpersonated_dotnet -->|"이용한다"| impersonation_token
runimpersonated_dotnet -->|"이용한다"| executioncontext_dotnet
executioncontext_dotnet -.->|"원인이 될 수 있다"| lingering_impersonation_risk
impersonation_token -->|"이용한다"| logonuser_api
impersonation_token -.->|"원인이 될 수 있다"| double_hop_problem
double_hop_problem -.->|"전제로 한다"| kerberos_delegation
double_hop_problem -.->|"전제로 한다"| spn
kerberos_delegation -->|"에서 구성할 수 있다"| domain_controller
revert_to_self -->|"방지한다"| lingering_impersonation_risk
safeaccesstokenhandle -->|"완화한다"| handle_leak
primary_token -.->|"전제로 한다"| impersonation_token
impersonation_token -->|"사용은 비권장"| admin_rights
aspnet_core_windows_auth -.->|"전제로 한다"| runimpersonated_dotnet
primary_token -->|"이용한다"| createprocessasuser
windows_service -->|"이용한다"| primary_token
runimpersonated_dotnet -->|"전제로 한다"| safeaccesstokenhandle
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 18건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
2. access token이란 무엇인가
Windows에서는 사용자나 프로세스의 security context를 나타내기 위해 access token을 사용합니다.
access token에는 대체로 이런 정보가 들어갑니다. 이후 장에서 반복해서 나오는 용어이므로, 한 줄씩 의미를 붙여 둡니다.
| 항목 | 의미 |
|---|---|
| 사용자의 SID | SID는 Security Identifier의 약자로, 사용자나 그룹을 유일하게 나타내는 식별자입니다. access check는 표시 이름이 아니라 이 값으로 이루어집니다 |
| 소속 그룹 | 그 사용자가 속한 그룹의 SID 모음입니다. Administrators에 들어 있는지도 여기에 나타납니다 |
| 권한, Privilege | 「백업 작업」「종료」처럼, 개별 객체의 ACL과는 별도로 주어지는 특권입니다. 사용(유효)인지 사용 안 함(무효)인지 상태를 가집니다 |
| 기본 소유자 | 이 token으로 새로 만든 객체의 소유자가 되는 SID입니다 |
| 기본 DACL | DACL은 Discretionary Access Control List의 약자로, 누구에게 무엇을 허용할지를 늘어놓은 목록입니다. 새로 만든 객체에 기본으로 붙습니다 |
| 제한 SID | restricted token에서 쓰이는 SID 목록입니다. 이 목록에 대해서도 추가 access check가 이루어지므로, 그룹에 속해 있어도 허가되지 않는 경우가 있습니다 |
| integrity level | Low, Medium, High 같은 계층입니다. 낮은 integrity level에서는 높은 수준의 객체에 쓸 수 없습니다. 17장에서 다룹니다 |
| elevation 상태 | UAC가 켜진 환경에서 그 token이 상승되어 있는지 여부입니다. 17장에서 다룹니다 |
| impersonation level | impersonation token만 가지는 값입니다. 식별만 가능한지, 실제로 접근할 수 있는지, 원격으로 위임할 수 있는지가 정해집니다. 7장에서 다룹니다 |
| token 종류 | primary token인지 impersonation token인지의 구분입니다. 6장에서 다룹니다 |
여기서 access check에 효력이 있는 것은 이름이 아니라 SID와 그룹과 integrity level이다라는 점을 잡아 두면, 이후 장에서 「이름은 맞는데 Access denied가 난다」는 이야기가 이해하기 쉬워집니다.
파일, 레지스트리, 서비스, named pipe, 프로세스, 스레드, 이벤트, mutex 등 Windows의 많은 객체는 security descriptor를 가집니다.
어떤 스레드가 보호 대상 객체를 열려고 할 때, Windows는 token 정보와 대상 객체의 ACL을 대조합니다.
조작하는 스레드
↓
어느 security context로 접근할지
↓
token의 사용자, 그룹, 권한을 본다
↓
대상 객체의 ACL과 대조한다
↓
허용 / 거부를 정한다
이 「어느 security context로 접근할지」를 이해하는 것이 impersonation token을 이해하는 입구입니다.
3. 프로세스에는 primary token이 있다
Windows의 각 프로세스에는 보통 primary access token이 있습니다.
예를 들어 어떤 사용자가 데스크톱에서 애플리케이션을 시작하면, 그 프로세스에는 그 사용자의 security context를 나타내는 primary token이 붙습니다.
Windows 서비스라면 서비스 실행 계정의 primary token이 붙습니다.
예를 들면 이런 이미지입니다.
MyService.exe
Primary Token: DOMAIN\svc-app
이 서비스 안의 스레드가 특별히 impersonation하지 않았다면, 파일이나 레지스트리에 접근할 때는 프로세스의 primary token이 쓰입니다.
즉 기본은 이렇습니다.
Thread A
Impersonation Token: 없음
↓
access check에서는 Process의 Primary Token을 사용한다
이 상태에서 C:\Data\foo.txt를 열면, DOMAIN\svc-app에 대한 접근 권한이 있는지 봅니다.
4. impersonation token은 스레드에 붙는다
impersonation이 시작되면 스레드에 impersonation token이 붙습니다. 여기가 중요한 지점으로, impersonation은 기본적으로 「프로세스 전체가 다른 사용자가 된다」기보다 그 스레드가 다른 security context에서 access check를 받는다고 보는 편이 정확합니다.
MyService.exe
Primary Token: DOMAIN\svc-app
Thread A
Impersonation Token: DOMAIN\alice
Thread B
Impersonation Token: 없음
이때 Thread A가 파일을 열면 DOMAIN\alice의 권한으로 access check됩니다.
한편 Thread B는 impersonation하지 않으므로 DOMAIN\svc-app의 권한으로 access check됩니다.
이 차이를 이해하지 않으면 이런 혼란이 생깁니다.
// Thread A에서 impersonation했다고 생각하지만
StartImpersonation(token);
// 그러나 다른 스레드에 처리를 던지고 있다
Task.Run(() =>
{
File.ReadAllText(path);
});
// 바로 되돌려 버린다
RevertToSelf();
이 경우 실제로 파일을 읽는 스레드가 기대한 impersonation 상태로 동작한다고는 할 수 없습니다.
impersonation은 scope, 스레드, 비동기 처리와의 관계를 명확히 한 뒤 다뤄야 합니다.
5. 「impersonation」은 privilege elevation이 아니다
impersonation이라는 말은 조금 강하게 들리지만, 실무에서 중요한 것은 이것을 「privilege elevation」과 혼동하지 않는 일입니다.
impersonation으로 할 수 있는 것은 기본적으로 이런 일입니다.
서버 프로세스의 권한으로 처리한다
↓
일부 처리만 클라이언트 사용자의 권한으로 access check한다
예를 들어 파일 서버의 ACL을 그대로 권한 제어로 쓰고 싶을 때, 서버 애플리케이션이 항상 서비스 계정으로 파일을 읽으면 이용자별 ACL을 반영할 수 없습니다.
그래서 요청 처리의 일부만 호출자 사용자로 impersonation하고 파일 접근을 합니다.
HTTP / RPC / Named Pipe 요청
User: DOMAIN\alice
↓
서버 애플리케이션
Process: DOMAIN\svc-app
↓
파일 접근 부분만 DOMAIN\alice로 impersonation
↓
파일 서버의 ACL에서 허용 / 거부된다
이것은 애플리케이션 고유의 권한 판정이 아니라, Windows의 기존 ACL을 쓰고 싶을 때 도움이 됩니다.
다만 impersonation은 편리한 한편, 설계를 잘못하면 권한 경계가 알아보기 어려워집니다.
- 어느 처리를 누구로서 실행하고 있는가
- 어디서 impersonation을 시작했는가
- 어디서 확실히 되돌리고 있는가
- 어느 로그가 어느 사용자 권한으로 나오고 있는가
- 예외 시에도 돌아가는가
- 비동기 처리의 끝까지 impersonation이 유효한가
이 부분을 코드에서 명확히 하는 것이 중요합니다.
6. primary token과 impersonation token을 나눠 생각한다
Windows token에서 특히 혼동하기 쉬운 것이 primary token과 impersonation token입니다.
대략 이렇게 생각합니다.
| token | 주요 용도 | 대표 예 |
|---|---|---|
| primary token | 프로세스의 security context를 나타낸다 | 프로세스 시작, CreateProcessAsUser |
| impersonation token | 스레드가 다른 security context에서 동작하기 위해 쓴다 | ImpersonateLoggedOnUser, SetThreadToken, Named Pipe의 클라이언트 impersonation |
특히 중요한 것은 프로세스를 시작하고 싶은 경우에는 원칙적으로 primary token이 필요하다는 점입니다.
impersonation token을 가지고 있다고 해서, 그대로 다른 사용자의 프로세스 시작에 쓸 수 있다고는 할 수 없습니다.
전형적으로는 이런 흐름이 됩니다.
클라이언트를 impersonation한다
↓
OpenThreadToken으로 impersonation token을 취득한다
↓
DuplicateTokenEx로 primary token을 만든다
↓
CreateProcessAsUser 등에 넘긴다
반대로 「이 스레드에서 파일 접근만 다른 사용자로 하고 싶다」는 경우에는, 프로세스 시작이 아니라 impersonation token을 쓰는 이야기가 됩니다.
이 둘을 섞으면 API 인자는 맞는데 Access denied나 The parameter is incorrect 같은 오류로 헤매게 됩니다.
7. impersonation level을 이해한다
impersonation token에는 impersonation level이 있습니다.
대표적으로는 4단계가 있습니다.
| impersonation level | 대략적인 의미 |
|---|---|
| Anonymous | 서버는 클라이언트의 식별 정보를 얻지 못한다 |
| Identification | 서버는 클라이언트를 식별할 수 있지만, 그 권한으로 객체에 접근하는 용도에는 쓸 수 없다 |
| Impersonation | 서버는 로컬 시스템 위에서 클라이언트 권한으로 동작할 수 있다 |
| Delegation | 서버는 원격 시스템에 대해서도 클라이언트 권한을 위임할 수 있다 |
실무에서 자주 막히는 것은 Identification과 Impersonation의 차이입니다.
Identification은 이름 그대로, 상대가 누구인지를 알기 위한 수준입니다.
그 사용자의 권한으로 파일을 여는 용도에는 부족합니다.
그래서 이런 일이 일어납니다.
WindowsIdentity.GetCurrent().Name은 기대한 사용자 이름으로 보인다
↓
그런데 파일 접근은 Access denied가 된다
이 경우 이름만 보지 말고 impersonation level도 확인할 필요가 있습니다.
.NET이라면 WindowsIdentity.ImpersonationLevel을 보면 단서가 됩니다.
using System.Security.Principal;
WindowsIdentity identity = WindowsIdentity.GetCurrent();
Console.WriteLine(identity.Name);
Console.WriteLine(identity.ImpersonationLevel);
네트워크 너머 접근에서는 더 주의가 필요합니다.
「Web 서버에서 사용자를 impersonation하고, 그 사용자로서 다른 파일 서버나 DB 서버에 접근한다」는 구성에서는, 이른바 double hop 문제에 부딪치는 경우가 있습니다.
이 경우 단순히 애플리케이션 코드에서 impersonation한다고 해결된다고는 할 수 없습니다. Kerberos, SPN, 위임, constrained delegation, 서비스 계정, 연결 대상의 인증 방식 등을 포함해 설계해야 합니다.
8. impersonation의 기본 형태
Win32 API로 impersonation을 다룰 때의 개념적인 형태는 이렇습니다.
1. impersonation에 쓸 token을 취득한다
2. 그 token으로 현재 스레드를 impersonation한다
3. 필요한 처리만 실행한다
4. 반드시 원래 security context로 되돌린다
5. token handle을 닫는다
코드 형태로는 반드시 try / finally로 합니다.
if (!ImpersonateLoggedOnUser(tokenHandle))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
try
{
// 여기만 impersonation 사용자로서 실행한다
DoWorkAsImpersonatedUser();
}
finally
{
if (!RevertToSelf())
{
// 되돌릴 수 없는 상태는 위험하므로, 적어도 처리를 계속하면 안 된다
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
시간 순으로 보면 이렇게 됩니다.
sequenceDiagram
participant App as 호출 측
participant Th as 처리 스레드
participant Win as Windows
participant FS as 파일
App->>Th: 처리를 요청한다
Th->>Win: ImpersonateLoggedOnUser로 token을 붙인다
Note over Th: 여기부터 이용자 alice로서<br/>access check된다
Th->>FS: 파일을 연다
FS-->>Th: alice의 권한으로 ACL이 판정된다
Th->>Win: RevertToSelf로 token을 뗀다
Note over Th: 여기부터 서비스 계정<br/>svc-app으로 돌아간다
Th-->>App: 결과를 반환한다
impersonation이 효력을 가지는 것은 ImpersonateLoggedOnUser부터 RevertToSelf까지의 구간뿐입니다. 이 구간 밖에서 한 I/O는 impersonation 사용자가 아니라 프로세스 계정으로 판정됩니다. 14장에서 다루는 비동기 처리의 함정은, 실제 I/O가 이 구간 밖으로 삐져나가는 문제라고 생각하면 이해하기 쉬워집니다.
중요한 것은 impersonation의 시작보다 확실히 되돌리는 것입니다. 되돌리는 것을 잊으면 그 스레드에서 후속 처리가 impersonation 사용자인 채로 돌아갑니다.
특히 thread pool을 쓰는 애플리케이션에서는, 하나의 처리라고 생각한 것이 다른 요청이나 다른 처리에 영향을 줄 수 있습니다.
그래서 impersonation은 「시작했다면 되돌린다」가 아니라 작은 scope에 가둔다고 생각해야 합니다.
9. .NET에서는 WindowsIdentity.RunImpersonated를 쓴다
.NET에서는 가능하면 WindowsIdentity.RunImpersonated를 쓰면 impersonation scope를 코드에서 표현하기 쉬워집니다.
SafeAccessTokenHandle을 가지고 있는 경우 다음처럼 쓸 수 있습니다.
using Microsoft.Win32.SafeHandles;
using System.Security.Principal;
static string ReadFileAsUser(SafeAccessTokenHandle token, string path)
{
return WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(path);
});
}
이 형태의 좋은 점은 impersonation하는 범위가 람다식 안에 닫혀 있다는 것입니다.
RunImpersonated(token, () =>
{
// 여기만 impersonation
});
// 여기서부터 밖은 원래 context
파일 접근, 레지스트리 접근, 기존 라이브러리 호출 등 특정 처리만 impersonation하고 싶은 경우에는 이 형태가 읽기 쉽고 안전합니다.
비동기 처리에서는 RunImpersonatedAsync를 씁니다.
using Microsoft.Win32.SafeHandles;
using System.Security.Principal;
static Task WriteFileAsUserAsync(
SafeAccessTokenHandle token,
string path,
string text,
CancellationToken cancellationToken)
{
return WindowsIdentity.RunImpersonatedAsync(token, async () =>
{
await File.WriteAllTextAsync(path, text, cancellationToken);
});
}
피하고 싶은 것은 impersonation scope 안쪽에서 fire-and-forget 태스크를 던지는 형태입니다.
// 좋지 않은 예
WindowsIdentity.RunImpersonated(token, () =>
{
_ = Task.Run(() =>
{
File.WriteAllText(path, text);
});
});
이 코드는 「실제로 파일을 쓰는 처리」가 언제, 어느 실행 context에서 동작하는지 알아보기 어려워집니다.
impersonation해서 하고 싶은 비동기 처리는 RunImpersonatedAsync 안에서 await하고, 처리가 끝난 뒤에 scope를 나가는 형태로 합니다.
10. LogonUser로 token을 얻는 경우
다른 사용자의 자격 정보에서 token을 얻는 대표적인 API로 LogonUser가 있지만, 신중히 다뤄야 하는 API입니다.
LogonUser에는 사용자 이름, 도메인, 비밀번호를 넘깁니다.
즉 애플리케이션 측이 자격 정보를 다루게 됩니다.
실무에서는 이 부근에 주의합니다.
- 비밀번호를 코드나 설정 파일에 평문으로 두지 않는다
- 가능하면 OS의 인증, 서비스 계정, 위임, 기존 Windows 인증을 쓴다
- 비밀 정보는 적절한 Secret Store나 운영 기반에서 관리한다
- token handle은 반드시 닫는다
- 로그에 사용자 이름 이외의 비밀 정보를 내지 않는다
- impersonation하는 범위를 최소화한다
이어서 최소 구성의 예를 싣지만, 그대로 베껴 password에 문자열 리터럴을 넣는 것만은 피하십시오. 소스 코드에 쓴 자격 정보는 리포지토리 이력, 빌드 산출물, 역컴파일 결과 전부에 남습니다. 설정 파일에 평문으로 두는 것도 같은 일입니다. 21.6에서 다시 다룹니다.
그러면 비밀번호를 어디에 둘 것인가. 선택지를 정리해 둡니다.
| 보관 방법 | 쓰는 곳 | 주의점 |
|---|---|---|
| 애초에 비밀번호를 가지지 않는다 | 제1 후보. Windows 인증, 서비스 계정, 위임으로 끝낸다 | 설계 단계에서 정할 필요가 있다. 나중에 빼기는 힘들다 |
| 실행 시에 대화형으로 입력하게 한다 | 관리자가 손에서 실행하는 운영 도구 | 무인 실행이나 서비스에서는 쓸 수 없다. 읽은 문자열은 사용 후 바로 놓는다 |
| Windows Credential Manager | 같은 계정, 같은 PC에서 반복해 쓴다 | 사용자 단위의 보관함입니다. 서비스 계정으로 쓴다면 그 계정으로 등록해야 합니다 |
| DPAPI | 단일 PC 안에서 설정 파일 값을 보호하고 싶다 | ProtectedData의 scope에 주의합니다. CurrentUser면 암호화한 사용자만, LocalMachine이면 그 PC의 다른 사용자도 복호화할 수 있습니다 |
| Key Vault 등의 시크릿 관리 기반 | 여러 서버, CI, 클라우드 연계가 있다 | 기반 측의 인증이 필요해집니다. 가져온 시크릿의 메모리상 취급은 별도로 생각합니다 |
DPAPI를 쓰는 경우의 구체적인 쓰는 법은 다른 글의 설정 파일의 기밀 정보를 안전하게 저장하는 방법에 정리해 두었습니다.
어느 방법이든 공통인 것은, 보관 방법을 고르기 전에 「정말로 애플리케이션이 비밀번호를 받을 필요가 있는가」를 검토하는 일입니다.
최소 구성의 예를 싣습니다.
using Microsoft.Win32.SafeHandles;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Principal;
internal static class NativeMethods
{
private const int LOGON32_LOGON_INTERACTIVE = 2;
private const int LOGON32_PROVIDER_DEFAULT = 0;
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern bool LogonUser(
string lpszUsername,
string? lpszDomain,
string lpszPassword,
int dwLogonType,
int dwLogonProvider,
out SafeAccessTokenHandle phToken);
public static SafeAccessTokenHandle Logon(
string userName,
string? domain,
string password)
{
bool ok = LogonUser(
userName,
domain,
password,
LOGON32_LOGON_INTERACTIVE,
LOGON32_PROVIDER_DEFAULT,
out SafeAccessTokenHandle token);
if (!ok)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
return token;
}
}
public static string ReadFileWithExplicitCredential(
string userName,
string? domain,
string password,
string path)
{
using SafeAccessTokenHandle token = NativeMethods.Logon(userName, domain, password);
return WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(path);
});
}
이 예는 어디까지나 API의 형태를 보이기 위한 것입니다.
실무에서는 「애초에 애플리케이션이 비밀번호를 받는 설계가 괜찮은가」를 먼저 검토합니다.
많은 경우 이런 대안을 생각하는 편이 안전합니다.
| 하고 싶은 일 | 대안 |
|---|---|
| 서비스 전체가 특정 리소스에 접근하고 싶다 | 전용 서비스 계정에 필요 최소한의 ACL을 붙인다 |
| 이용자 본인의 권한으로 파일에 접근하고 싶다 | Windows 인증과 위임 설계를 쓴다 |
| 관리자 권한이 필요한 조작을 하고 싶다 | 서비스 측에 명확한 관리 API를 두고, 앱 측의 인가로 제어한다 |
| 일부 처리만 다른 계정으로 하고 싶다 | impersonation 범위를 메서드 단위로 가두고, 감사 로그를 남긴다 |
11. LogonUser의 로그온 유형에 주의한다
LogonUser로 얻는 token의 성질은 로그온 유형에 따라 달라집니다.
특히 이 차이를 이해하지 않고 복사해 붙이면 기대한 대로 동작하지 않습니다.
| 로그온 유형 | 주의점 |
|---|---|
| Interactive | 대화형 로그온에 가까운 형태. 로컬 조작에 쓰기 쉽지만 실행 환경이나 권한에 의존한다 |
| Network | 네트워크 로그온용. 돌아오는 token이 프로세스 시작에 직접 쓸 수 없는 경우가 있다 |
| NewCredentials | 로컬에서는 현재 자격 정보에 가깝고, 원격 접속 시에 지정 자격 정보를 쓰는 용도로 쓰이는 경우가 있다 |
여기서 말하고 싶은 것은 특정 로그온 유형를 암기하는 일이 아닙니다.
중요한 것은 두 가지입니다.
- 로그온 유형에 따라 로컬 접근, 네트워크 접근, 프로세스 시작에서의 거동이 달라진다
- 돌아온 token이 primary token인지 impersonation token인지를 확인할 필요가 있다
예를 들어 LOGON32_LOGON_NETWORK로 얻은 token을 그대로 CreateProcessAsUser에 넘겨 실패하는 것은 전형적인 혼란입니다.
프로세스 시작이 목적이라면 primary token이 필요합니다.
필요에 따라 DuplicateTokenEx로 primary token을 만드는 설계가 됩니다.
12. RevertToSelf를 가볍게 보지 않는다
Win32 API로 impersonation을 시작한 경우, RevertToSelf로 impersonation을 종료합니다. 이 되돌리기 처리는 단순한 뒷정리가 아니라, 보안 경계를 원래대로 되돌리기 위한 중요한 처리입니다.
나쁜 예입니다.
ImpersonateLoggedOnUser(token);
DoWork();
RevertToSelf();
언뜻 문제없어 보이지만, DoWork()에서 예외가 발생하면 RevertToSelf()가 호출되지 않습니다.
반드시 finally로 합니다.
if (!ImpersonateLoggedOnUser(token))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
try
{
DoWork();
}
finally
{
if (!RevertToSelf())
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
RevertToSelf가 실패한 경우 그대로 처리를 계속하는 것도 위험합니다. 원래 권한으로 되돌리지 못했을 수 있는 상태에서 후속 처리를 이어가면, 의도하지 않은 사용자 권한으로 처리가 계속됩니다. 적어도 그 처리 단위는 실패로 다루고, 안전한 쪽으로 기울입니다.
.NET의 RunImpersonated / RunImpersonatedAsync는 이 되돌리기를 빠뜨리는 일을 피하기 위한 scope 표현으로 유용합니다.
13. impersonation scope는 가능한 한 작게 한다
impersonation에서 가장 중요한 설계 원칙은 필요한 처리만 impersonation하는 것입니다.
나쁜 예입니다.
WindowsIdentity.RunImpersonated(token, () =>
{
ValidateRequest();
LoadConfiguration();
WriteDebugLog();
ReadUserFile();
UpdateDatabase();
SendNotification();
});
이렇게 넓은 범위를 impersonation하면 어느 조작이 어느 권한으로 이루어지는지 알아보기 어려워집니다.
예를 들어 로그를 쓰는 위치에 대한 접근이 impersonation 사용자 권한으로 이루어져, 로그 쓰기에 실패할 수 있습니다. DB 접속이 서비스 계정이 아니라 impersonation 사용자의 자격 정보로 시도될 수 있습니다. 알림 처리나 임시 파일 작성까지 불필요한 권한 context의 영향을 받을 수 있습니다.
좋은 예는 impersonation이 필요한 조작만 잘라내는 형태입니다.
ValidateRequest();
LoadConfiguration();
string content = WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(userFilePath);
});
UpdateDatabase(content);
WriteAuditLog(userName, userFilePath, success: true);
이 형태라면 impersonation이 필요한 것은 File.ReadAllText 부분뿐이라고 알 수 있습니다.
impersonation은 편리하지만, 넓힐수록 읽기 어렵고 사고 나기 쉬워집니다.
14. 비동기 처리에서는 「끝날 때까지 impersonation되어 있는가」를 본다
현대 .NET 애플리케이션에서는 파일, HTTP, DB, 큐, 스토리지 등 많은 처리가 비동기입니다.
그래서 impersonation과 async / await의 조합은 주의가 필요합니다.
기본 방침은 이것뿐입니다.
impersonation이 필요한 비동기 처리는 RunImpersonatedAsync 안에서 await한다
지키지 않으면 어떻게 되는지를 8장의 그림과 같은 형태로 나란히 둡니다. 다만 먼저 잡아 두고 싶은 점이 있습니다. 「다른 스레드로 던지면 impersonation이 풀린다」고는 할 수 없습니다. 어느 메커니즘으로 impersonation했는지에 따라 깨지는 방식이 정반대가 됩니다.
| impersonation 방식 | Task.Run에 던진 처리는 |
일어나는 일 |
|---|---|---|
.NET의 RunImpersonated / RunImpersonatedAsync |
impersonation된 채로 동작한다 | impersonation이 코드상의 scope보다 길게 늘어난다. 던진 쪽은 완료도 예외도 받지 않는다 |
Win32의 ImpersonateLoggedOnUser를 직접 호출한다 |
프로세스 계정으로 동작한다 | 실제 I/O가 impersonation 구간 밖으로 나가 Access denied가 된다 |
.NET 쪽이 그렇게 되는 것은 RunImpersonated가 impersonation token을 AsyncLocal에 싣고 있기 때문입니다. AsyncLocal의 값은 ExecutionContext와 함께 흐르고, Task.Run은 호출 시점의 ExecutionContext를 잡아 풀의 스레드에서 복원합니다. 복원할 때마다 도는 변경 핸들러가 그 스레드에서 ImpersonateLoggedOnUser를 다시 호출하므로, 풀의 스레드에도 같은 impersonation이 붙습니다. 런타임 구현(WindowsIdentity의 s_currentImpersonatedToken과 CurrentImpersonatedTokenChanged)이 그렇게 되어 있습니다.
한편 Win32 token은 스레드에 묶일 뿐, 다른 스레드로 제멋대로 복제되지는 않습니다.
.NET 쪽의 시간 순을 따라가면 이렇게 됩니다.
sequenceDiagram
participant Th as 호출 스레드
participant Pool as thread pool의 다른 스레드
participant FS as 파일
Th->>Th: RunImpersonated로 impersonation을 시작한다
Th->>Pool: Task.Run으로 쓰기를 던진다
Note over Pool: ExecutionContext와 함께<br/>impersonation token도 운반된다
Th->>Th: scope를 빠져 impersonation을 해제한다
Note over Th: 해제되는 것은<br/>이 스레드의 impersonation뿐이다
Pool->>FS: 실제 쓰기는 이 뒤에 돈다
Note over Pool: impersonation 사용자인 채로.<br/>언제 끝나는지 호출 측은 모른다
FS-->>Pool: 결과도 예외도 아무도 받지 않는다
8장의 그림에서는 I/O가 impersonation 구간 안쪽에 들어가 있었습니다. 이 그림에서는 Task.Run에 던진 시점에 처리가 다른 스레드로 옮겨, 실제 쓰기가 코드상의 구간 밖으로 나갑니다. impersonation이 풀리기 때문에 위험한 것이 아니라, 풀리지 않은 채 추적이 안 되기 때문에 위험하다는 것이 이 형태의 문제입니다. 구체적으로는 세 가지가 동시에 일어납니다.
- impersonation 범위를 읽을 수 없게 된다. 「여기부터 여기까지」라고 쓴 scope와, 실제로 impersonation으로 도는 범위가 일치하지 않습니다
- 실패가 삼켜진다. 던진 태스크를 아무도
await하지 않으므로,Access denied가 되어도 예외는 어디에도 나오지 않습니다 - token을 언제 닫아도 되는지 정해지지 않는다.
SafeAccessTokenHandle을using으로 닫으면, 아직 돌고 있는 처리의 밑에서 닫게 됩니다
Win32로 직접 impersonation하고 있는 경우에는 반대로, 던진 처리가 프로세스 계정으로 동작합니다. 테스트 환경에서는 서비스 계정에도 권한이 있어 성공하고, 프로덕션에서 처음 Access denied가 되는 형태로 표면화되기 쉽습니다.
어느 쪽이든 RunImpersonatedAsync 안에서 완료까지 await하면 일어나지 않습니다.
좋은 예입니다.
await WindowsIdentity.RunImpersonatedAsync(token, async () =>
{
await using FileStream stream = File.OpenRead(path);
using var reader = new StreamReader(stream);
string text = await reader.ReadToEndAsync();
await ProcessTextAsync(text);
});
다만 이 형태에서도 생각할 점이 있습니다.
ProcessTextAsync까지 impersonation 사용자로 돌 필요가 있는가, 라는 점입니다.
파일 읽는 부분만 impersonation하면 된다면, 이렇게 나눈 편이 안전합니다.
string text = await WindowsIdentity.RunImpersonatedAsync(token, async () =>
{
return await File.ReadAllTextAsync(path);
});
await ProcessTextAsync(text);
impersonation scope 안에서 await할 수 있다고 해서, 무엇이든 넣어도 되는 것은 아닙니다.
비동기 처리에서도 impersonation 범위는 최소화합니다.
15. ASP.NET Core와 impersonation
ASP.NET Core에서 Windows 인증을 쓰는 경우에도 impersonation 취급에는 주의가 필요합니다.
「Windows 인증으로 로그인했으니 요청 처리 전체가 그 사용자로 돈다」고 단정하면 위험합니다.
일반적으로 애플리케이션 프로세스 자체는 애플리케이션 풀 ID나 서비스 실행 계정으로 동작합니다. 이용자의 Windows ID는 인증 정보로서 얻을 수 있지만, 그대로 전 처리가 이용자 권한이 된다고는 할 수 없습니다.
이용자의 권한으로 특정 액션을 해야 한다면, 명시적으로 RunImpersonated / RunImpersonatedAsync로 scope를 만듭니다.
코드의 이미지는 이렇습니다.
app.MapGet("/download", async (HttpContext context) =>
{
if (context.User.Identity is not WindowsIdentity user)
{
return Results.Unauthorized();
}
string path = GetPathFromRequest(context);
byte[] bytes = await WindowsIdentity.RunImpersonatedAsync(
user.AccessToken,
async () => await File.ReadAllBytesAsync(path));
return Results.File(bytes, "application/octet-stream");
});
이 예에서도 impersonation하고 있는 것은 파일 읽기 부분뿐입니다.
응답 생성, 로그, 애플리케이션 측의 인가 판정까지 전부를 impersonation scope에 넣을 필요가 있는지는 신중히 생각해야 합니다.
16. Access denied를 보는 법
impersonation을 쓴 구현에서 자주 나오는 오류는 Access denied입니다.
이 오류가 났을 때 「impersonation에 실패했다」고만 생각하면 멀리 돌아가게 됩니다.
확인할 관점을 나눕니다.
| 관점 | 확인 내용 |
|---|---|
| 정말로 impersonation되어 있는가 | impersonation scope 안에서 WindowsIdentity.GetCurrent().Name을 확인한다 |
| impersonation level은 충분한가 | Identification이 아니라 필요한 수준이 되어 있는가 |
| 대상 리소스의 ACL은 맞는가 | impersonation 사용자에게 읽기 / 쓰기 권한이 있는가 |
| 로컬인가 원격인가 | 로컬 파일에서는 성공하고 UNC만 실패하지 않았는가 |
| double hop이 아닌가 | Web 서버에서 파일 서버로 이용자 권한으로 가려고 하지 않았는가 |
| impersonation scope를 빠져나오지 않았는가 | 실제 I/O가 scope 밖이나 다른 태스크에서 돌고 있지 않은가 |
| token 종류는 맞는가 | 프로세스 시작에 impersonation token을 넘기지 않았는가 |
| UAC / integrity level의 영향은 없는가 | Administrators 소속이어도 비상승 token이 아닌가 |
특히 이름만 확인하고 안심하지 않는 것이 중요합니다.
WindowsIdentity identity = WindowsIdentity.GetCurrent();
Console.WriteLine(identity.Name);
이 로그는 유용하지만 충분하지 않습니다.
적어도 이 부근도 봅니다.
Console.WriteLine(identity.ImpersonationLevel);
Console.WriteLine(identity.IsAuthenticated);
또한 대상이 네트워크 공유라면, 애플리케이션 코드뿐 아니라 인증 방식, 위임 설정, SPN, 서비스 계정, 파일 서버 측 ACL도 확인합니다.
17. UAC와 「관리자인데 실패한다」문제
Windows에서는 사용자가 Administrators 그룹에 속해 있는 것과, 현재 token이 상승되어 있는 것은 같지 않습니다.
UAC가 켜진 환경에서는 관리자 사용자라도 보통은 제한된 token으로 프로세스가 돌고, 관리자 권한이 필요한 조작에서는 elevation이 필요해집니다.
그래서 이런 일이 일어납니다.
사용자는 Administrators에 속해 있다
↓
그런데 현재 token은 비상승
↓
Program Files나 HKLM에 대한 쓰기에서 Access denied
impersonation에서도 같습니다.
「impersonation 대상 사용자가 관리자이니 쓸 수 있을 것이다」라고 생각하지 말고, 실제로 넘겨진 token이 어떤 상태인지를 확인할 필요가 있습니다.
디버그에서는 이런 관점을 봅니다.
- 소속 그룹
- Privilege의 사용 / 사용 안 함
- integrity level
- elevation 상태
- restricted token인지
- 링크된 상승 token이 있는지
Win32 API에서는 GetTokenInformation을 써서 TokenType, TokenImpersonationLevel, TokenElevationType, TokenIntegrityLevel 등을 확인할 수 있습니다.
다만 실무상의 설계로는 「관리자 사용자를 impersonation해서 무엇이든 한다」보다, 전용 서비스나 관리 API에 필요 최소한의 조작을 가두는 편이 안전합니다.
18. 네트워크 공유와 double hop
impersonation에서 매우 많은 상담이 네트워크 공유에 대한 접근입니다.
클라이언트 PC
↓ Windows 인증
Web 서버 / API 서버
↓ impersonation해서 접근하고 싶다
파일 서버
이 구성에서는 「Web 서버 위에서는 사용자 이름을 얻었는데, 파일 서버로 가면 실패한다」는 일이 있습니다.
이것은 이용자의 자격 정보를 다른 서버로 재위임할 수 있는가라는 문제입니다.
로컬 서버 위에서의 impersonation과, 다른 서버로의 위임은 같지 않습니다.
Impersonation 수준에서는 로컬 조작에는 쓸 수 있어도, 원격 서버에 대해 클라이언트로 행동하기에는 부족한 경우가 있습니다.
네트워크 너머로 이용자 본인의 권한을 쓰고 싶다면 Kerberos 위임, constrained delegation, SPN, 서비스 계정, 인증 방식의 설계가 필요합니다.
여기서 막히기 쉬우므로, 다음 조치의 선택지를 늘어놓습니다.
| 방향 | 무엇을 하는가 | 맞는 경우 | 주의점 |
|---|---|---|---|
| Kerberos constrained delegation | 중계 서버 계정에 「이 서비스에만 위임해도 된다」는 설정을 넣는다 | 중계 서버와 파일 서버가 같은 도메인에 있고, 도메인 관리자의 협력을 얻을 수 있다 | 설정은 앱 측이 아니라 도메인 측입니다. 위임 대상 SPN을 명시적으로 열거합니다 |
| resource-based constrained delegation | 위임을 허용하는 설정을, 위임받는 쪽 즉 파일 서버 측 계정에 둔다 | 도메인을 넘나드는 경우나, 자원 측 관리자가 주도하고 싶은 경우 | Windows Server 2012 이후의 메커니즘입니다. 설정하는 곳이 종래 constrained delegation과 반대가 됩니다 |
| 자격 정보를 명시적으로 넘긴다 | 이용자 본인이 아니라, 용도를 한정한 전용 계정의 자격 정보로 접속한다 | 위임을 설정할 수 없거나, 「이용자 본인일 것」이 업무 요건이 아니다 | 자격 정보 보관이 필요해집니다. 10장의 표를 참조하십시오 |
| 중계하지 않는 설계로 한다 | 파일 서버 접근을 서비스 계정으로 모으고, 인가는 애플리케이션 측에서 한다 | 업무 규칙이 애플리케이션 측에 있다 | ACL이 최종 판단이 아니게 됩니다. 감사 로그 설계가 중요해집니다 |
| 클라이언트에서 직접 가게 한다 | 서버를 거치지 않고, 클라이언트 PC에서 파일 서버로 직접 접근하게 한다 | 화면에서 공유 폴더를 열면 충분한 경우 | 서버 측에서의 일원적인 제어나 로그 수집은 할 수 없게 됩니다 |
판단의 순서로는, 먼저 「정말로 이용자 본인의 Windows 권한으로 파일 서버에 갈 필요가 있는가」를 정합니다. 필요가 있으면 위임 설계로 들어가고, 필요가 없으면 중계하지 않는 설계로 기울이는 분기입니다. 위임 설정은 애플리케이션 개발자만으로는 끝나지 않으므로, 필요하다고 판단한 시점에 일찍 도메인 관리자에게 상담하는 것이 현실적입니다.
한편 업무 요건에 따라서는 이용자 본인의 Windows 권한으로 파일 서버에 갈 필요가 없는 경우도 있습니다.
그 경우에는 이런 설계가 더 단순합니다.
이용자의 인증·인가는 애플리케이션에서 한다
↓
파일 서버에 대한 접근은 전용 서비스 계정으로 한다
↓
조작 로그에 이용자 ID와 대상 파일을 기록한다
이것은 「OS의 ACL을 최종 판단으로 한다」가 아니라 「애플리케이션의 인가를 최종 판단으로 한다」는 설계입니다.
어느 쪽이 맞는지는 업무 요건에 달렸습니다.
다만 어느 방식을 고르고 있는지를 명확히 하지 않으면, impersonation, 위임, ACL, 앱 인가가 섞여 알아보기 어려워집니다.
19. 토큰 핸들의 수명
token은 커널 객체에 대한 handle이므로, 취득했으면 불필요해진 시점에 닫을 필요가 있습니다.
.NET에서는 SafeAccessTokenHandle을 쓰고, using으로 scope를 관리하는 것이 기본입니다.
using SafeAccessTokenHandle token = NativeMethods.Logon(userName, domain, password);
string result = WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(path);
});
좋지 않은 예입니다.
// 좋지 않은 예: token을 전역으로 계속 유지한다
private static SafeAccessTokenHandle? _cachedToken;
token을 장기간 유지하면 이런 문제로 이어집니다.
- handle leak
- 어느 처리가 어느 token을 쓰는지 알 수 없게 된다
- 계정 무효화나 권한 변경과의 정합성이 알아보기 어려워진다
- 인증 정보를 오래 유지하는 설계가 되기 쉽다
- 감사상 설명하기 어렵다
원칙은 이렇습니다.
필요할 때 취득한다
↓
최소 범위에서 쓴다
↓
반드시 닫는다
물론 인증 비용이나 운영 요건에 따라서는 캐시를 검토하는 경우도 있습니다. 다만 그 경우에도 유효 기간, 폐기, 계정 변경, 감사 로그, 권한 변경 시의 취급을 설계에 넣을 필요가 있습니다.
20. 감사 로그에 남겨야 할 것
impersonation을 쓰는 처리에서는 로그 설계도 중요합니다.
최소한 이 표의 정보를 남길 수 있게 해 두면, 나중에 조사하기 쉬워집니다.
| 항목 | 예 |
|---|---|
| 요청한 이용자 | DOMAIN\alice |
| 실행 프로세스의 계정 | DOMAIN\svc-app |
| impersonation한 계정 | DOMAIN\alice 또는 전용 계정 |
| 대상 리소스 | 파일 경로, 공유 이름, 레지스트리 키 등 |
| 조작 | Read, Write, Delete, CreateProcess 등 |
| 결과 | Success, AccessDenied, Timeout, UnexpectedError |
| 오류 코드 | Win32 error code, HRESULT, 예외 종류 |
| impersonation scope | 어느 메서드, 어느 조작 단위로 impersonation했는지 |
다만 로그에 내면 안 되는 것도 있습니다.
- 비밀번호
- access token의 값
- 인증 헤더
- Kerberos 티켓이나 자격 정보 그 자체
- 개인 정보를 포함한 파일 내용
로그의 목적은 「누구의 요청으로, 어느 계정으로서, 무엇을 시도하고, 어떻게 실패 또는 성공했는지」를 나중에 따라갈 수 있게 하는 것입니다.
자격 정보 그 자체를 기록할 필요는 없습니다.
21. 자주 있는 anti-pattern
impersonation 주변에서 자주 보는 위험한 구현을 정리합니다.
21.1 앱 전체를 impersonation한다
WindowsIdentity.RunImpersonated(token, () =>
{
RunEntireApplication();
});
앱 전체를 impersonation하면 어느 조작이 어느 권한으로 실행되는지 알 수 없게 됩니다.
impersonation은 필요한 I/O나 특정 API 호출에 한정합니다.
21.2 finally 없이 되돌린다
ImpersonateLoggedOnUser(token);
DoWork();
RevertToSelf();
예외 시에 돌아가지 않으므로 위험합니다.
반드시 try / finally나 RunImpersonated를 씁니다.
21.3 impersonation 중에 fire-and-forget한다
WindowsIdentity.RunImpersonated(token, () =>
{
_ = Task.Run(DoWorkAsync);
});
처리 완료 전에 impersonation scope를 빠져나갑니다. 이 쓰기 방식이면 던진 태스크는(14장대로) impersonation된 채로 계속 동작하므로, 코드상의 impersonation 구간과 실제 impersonation 구간이 어긋납니다. 게다가 아무도 await하지 않으므로 실패해도 예외가 나오지 않습니다. Win32로 직접 impersonation하고 있는 경우에는 반대로 프로세스 계정으로 동작합니다.
impersonation이 필요하면 RunImpersonatedAsync 안에서 await합니다.
21.4 사용자 이름만으로 성공 판정한다
Console.WriteLine(WindowsIdentity.GetCurrent().Name);
사용자 이름이 기대한 대로여도 impersonation level이나 권한이 부족한 경우가 있습니다.
ImpersonationLevel, 대상 ACL, 로그온 유형, 네트워크 위임도 봅니다.
21.5 관리자 계정을 만능 계정으로 impersonation한다
「이 처리는 실패하면 안 되니 관리자 계정으로 impersonation한다」는 설계는 위험합니다.
필요 최소한의 권한을 가진 전용 계정을 준비하고, 조작 단위를 좁히는 편이 안전합니다.
21.6 비밀번호를 설정 파일에 둔다
{
"UserName": "DOMAIN\\admin",
"Password": "P@ssw0rd!"
}
이것은 피해야 합니다.
자격 정보를 다룬다면 Secret Store, Windows Credential Manager, DPAPI, 클라우드의 Key Vault, 운영 기반의 시크릿 관리 등, 환경에 맞는 메커니즘을 씁니다.
21.7 프로세스 시작과 파일 접근을 같은 이야기로 다룬다
파일 접근만이면 impersonation token으로 충분한 경우가 있습니다.
한편 다른 사용자로서 프로세스를 시작하고 싶다면, primary token, 프로필, 데스크톱, 환경 변수, 세션, 권한 등 다른 논점이 나옵니다.
CreateProcessAsUser나 CreateProcessWithTokenW를 쓰는 경우에는 impersonation과는 별도 설계로 다뤄야 합니다.
22. 테스트의 관점
impersonation 처리는 로컬 관리자 환경만으로 확인하면 놓치는 것이 나옵니다.
최소한 이 정도의 테스트 케이스를 준비합니다.
| 케이스 | 확인할 것 |
|---|---|
| 권한 있는 사용자 | 대상 파일을 읽을 수 있다 / 쓸 수 있다 |
| 권한 없는 사용자 | Access denied로서 올바르게 실패한다 |
| 존재하지 않는 사용자 | 인증 실패로 다룰 수 있다 |
| 비밀번호 오류 | 로그에 비밀 정보를 내지 않고 실패한다 |
| 네트워크 공유 | 로컬과 UNC의 거동 차이를 확인한다 |
| 비동기 처리 | await 뒤에도 기대한 범위에서 동작한다 |
| 예외 발생 | impersonation이 반드시 해제된다 |
| 병렬 요청 | 다른 사용자의 impersonation이 섞이지 않는다 |
| 서비스 실행 | 개발자의 대화형 로그온 환경이 아니라, 실제 서비스 계정으로 동작한다 |
특히 이 둘은 빼놓을 수 없습니다.
성공할 것
실패해야 할 때에 실패할 것
impersonation 처리에서는 성공 케이스뿐 아니라, 권한이 없는 사용자에서 확실히 거부되는 것도 테스트합니다.
거부되어야 할 조작이 성공해 버린다면, impersonation이나 인가의 설계가 틀린 가능성이 있습니다.
23. 구현 체크리스트
구현 전후에 이 체크리스트를 확인합니다.
| 관점 | 확인 내용 |
|---|---|
| 목적 | 왜 impersonation이 필요한지 설명할 수 있는가 |
| 대안 | 서비스 계정이나 앱 인가로는 안 되는지 검토했는가 |
| 범위 | impersonation scope는 최소인가 |
| 되돌리기 | 예외 시에도 확실히 돌아가는가 |
| 비동기 | RunImpersonatedAsync 안에서 완료까지 await하고 있는가 |
| token | primary token과 impersonation token을 혼동하지 않았는가 |
| impersonation level | Identification과 Impersonation / Delegation의 차이를 보고 있는가 |
| 네트워크 | UNC, double hop, Kerberos 위임 필요 여부를 확인했는가 |
| UAC | 관리자 소속과 상승 완료를 혼동하지 않았는가 |
| 비밀 정보 | 비밀번호를 평문 저장하지 않았는가 |
| handle | SafeAccessTokenHandle을 using으로 닫고 있는가 |
| 로그 | 이용자, impersonation 대상, 대상, 결과를 따라갈 수 있는가 |
| 테스트 | 권한 있음 / 없음 / 예외 / 병렬 처리를 확인했는가 |
이 체크리스트에서 걸리는 항목이 많다면, 코드를 쓰기 전에 설계를 다시 보는 편이 좋습니다.
24. 쓰는 곳을 가린다
impersonation token은 강력하지만, 항상 맨 먼저 고를 수단은 아닙니다.
설계로는 용도마다 나눠 생각하면 헤매기 어려워집니다.
24.1 OS의 ACL을 그대로 쓰고 싶은 경우
파일 서버나 공유 폴더의 ACL이 업무 규칙의 중심이고, 애플리케이션도 그 판단에 따라야 한다면, 이용자 본인으로서의 impersonation에 의미가 있습니다.
이용자 본인으로서 접근
↓
Windows의 ACL이 최종 판단
이 경우에는 Windows 인증, impersonation level, 위임, 네트워크 구성까지 포함해 설계합니다.
24.2 애플리케이션 측에서 인가하고 싶은 경우
업무 규칙이 애플리케이션 측에 있고, 파일 서버나 DB가 애플리케이션의 관리하에 있다면, 서비스 계정으로 접근하고 애플리케이션 측에서 인가하는 편이 알아보기 쉬운 경우가 있습니다.
이용자를 인증
↓
애플리케이션에서 인가
↓
서비스 계정으로 리소스에 접근
↓
감사 로그에 이용자 ID를 남긴다
이 방식에서는 impersonation을 쓰지 않는 대신, 애플리케이션의 인가 로직과 감사 로그가 중요해집니다.
24.3 관리 조작을 하고 싶은 경우
관리 조작을 이용자의 token으로 직접 실행하기보다, 관리 조작 전용 서비스나 API를 준비하고, 거기서 인가, 입력 검증, 감사, 롤백을 하는 편이 안전한 경우가 많습니다.
클라이언트
↓
관리 API에 요청
↓
관리 API가 인가
↓
필요 최소한의 권한으로 조작
↓
감사 로그
「일단 관리자를 impersonation한다」는 단기적으로는 편해 보입니다.
그러나 장기적으로는 감사, 장애 조사, 권한 변경, 보안 리뷰에서 괴로워집니다.
25. 정리
Windows impersonation token은 Windows의 권한 관리를 올바르게 쓰기 위한 중요한 메커니즘입니다.
다만 이해하지 않고 쓰면, 성공하는 것처럼 보이면서 위험한 코드가 되기 쉬운 분야이기도 합니다.
잡아 둘 포인트를 늘어놓습니다.
- access token은 사용자, 그룹, 권한 등의 security context를 나타낸다
- 프로세스에는 primary token이 있다
- impersonation token은 주로 스레드에 붙고, access check에 쓰인다
- impersonation은 privilege elevation이 아니다
- primary token과 impersonation token은 용도가 다르다
- impersonation level에 따라, 식별만 가능한지, 실제로 접근할 수 있는지, 원격으로 위임할 수 있는지가 달라진다
- Win32 API로 impersonation한다면 반드시
try/finally에서RevertToSelf한다 - .NET에서는
WindowsIdentity.RunImpersonated/RunImpersonatedAsync로 scope를 작게 표현한다 LogonUser를 쓰는 경우에는 자격 정보 관리와 로그온 유형에 주의한다- 네트워크 공유에서는 impersonation뿐 아니라 Kerberos 위임이나 서비스 계정 설계도 관계한다
- token handle은
SafeAccessTokenHandle과using으로 관리한다 - 성공 케이스뿐 아니라, 거부되어야 할 케이스도 테스트한다
impersonation token 구현에서 중요한 것은 「다른 사용자로 돌았다」는 한 점이 아니라, 이런 것을 설명할 수 있는 상태로 두는 일입니다.
어느 처리를
누구의 요청으로
어느 계정으로서
어느 범위만 실행하고
어디서 원래로 되돌리며
성공과 실패를 어떻게 기록하고 있는가
여기까지 정리되어 있다면 impersonation은 무서운 메커니즘이 아닙니다.
Windows의 ACL, 서비스 계정, 기존 파일 서버, 사내 도메인 자산을 살리기 위한 실무적인 도구로 쓸 수 있게 됩니다.
참고
- 이 글의 샘플 코드 전체(라이브러리, 데모, 단위 테스트)
https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/windows-impersonation-token - Microsoft Learn: Access Tokens
https://learn.microsoft.com/en-us/windows/win32/secauthz/access-tokens - Microsoft Learn: Impersonation Tokens
https://learn.microsoft.com/en-us/windows/win32/secauthz/impersonation-tokens - Microsoft Learn: Impersonation Levels
https://learn.microsoft.com/en-us/windows/win32/secauthz/impersonation-levels - Microsoft Learn:
SECURITY_IMPERSONATION_LEVELenumeration
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ne-winnt-security_impersonation_level - Microsoft Learn:
ImpersonateLoggedOnUserfunction
https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-impersonateloggedonuser - Microsoft Learn:
RevertToSelffunction
https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-reverttoself - Microsoft Learn:
LogonUserfunction
https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonusera - Microsoft Learn:
DuplicateTokenExfunction
https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-duplicatetokenex - Microsoft Learn:
TOKEN_INFORMATION_CLASSenumeration
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ne-winnt-token_information_class - Microsoft Learn:
WindowsIdentity.RunImpersonated
https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsidentity.runimpersonated - Microsoft Learn:
WindowsIdentity.RunImpersonatedAsync
https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsidentity.runimpersonatedasync - .NET 런타임 구현:
WindowsIdentity.cs(impersonation token을 유지하는AsyncLocal과, 스레드 전환 시에 impersonation을 다시 붙이는CurrentImpersonatedTokenChanged)
https://github.com/dotnet/runtime/blob/main/src/libraries/System.Security.Principal.Windows/src/System/Security/Principal/WindowsIdentity.cs - Microsoft Learn: Configure Windows Authentication in ASP.NET Core
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/windowsauth - Microsoft Learn: Kerberos Constrained Delegation Overview
https://learn.microsoft.com/en-us/windows-server/security/kerberos/kerberos-constrained-delegation-overview - Microsoft Learn:
ProtectedDataClass
https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.protecteddata
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows I/O의 심층(제4회) ── 캐시 관리자: WriteFile은 언제 디스크에 도달하는가
Windows의 캐시 관리자를 그림으로 설명하는 연재 제4회입니다. 파일 매핑으로 구현된 캐시, read-ahead와 lazy write, FlushFileBuffers와 FILE_FLAG_NO_BUFFERING의 용도 구분, 전원 차단으로 데이...
Windows I/O의 심층(제3회) ── I/O 완료 포트(IOCP)와 .NET 스레드 풀: async/await의 지하실
I/O 완료 포트(IOCP)를 그림으로 설명하는 연재의 제3회입니다. 완료 큐와 스레드 수 제어를 하나로 묶은 설계, 동시성 값과 LIFO 해제, .NET 스레드 풀과 async/await 계속(continuation)의 실행 스레드까지 정리합니다.
Windows I/O의 심층(제2회) ── 동기 I/O와 비동기 I/O: OVERLAPPED의 진짜 의미
Windows의 동기 I/O와 비동기 I/O(overlapped I/O)를 그림으로 설명하는 연재의 제2회입니다. FILE_FLAG_OVERLAPPED의 의미, 완료 통지의 네 가지 방식, 비동기인데도 동기 완료되는 조건, 취소 절차, .NET과...
Windows I/O 내부 구조(제1회) ── 모든 읽기·쓰기는 IRP가 된다: I/O 시스템의 전체 그림
Windows I/O 시스템을 밑바닥부터 설명하는 연재의 제1회입니다. Object Manager namespace, driver·device·file 세 가지 object, IRP 수명 주기, CloseHandle 뒤에서 일어나는 일까지를 그림...
Windows PC를 폐기하기 전에 해 두어야 할 일 ── 데이터 삭제·계정 해제·백업의 실무 체크리스트
Windows PC를 폐기·양도·매각·리스 반납하기 전에 해 두어야 할 일을 백업, 데이터 삭제, BitLocker, Microsoft 계정, OneDrive, 업무용 계정, 개발자 PC 특유의 비밀 정보, 폐기 증빙의 관점에서 정리합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기존 자산 활용 & 이관 지원
COM / ActiveX / OCX 자산, 네이티브 코드, 32비트 의존성을 유지하면서 단계적인 이관 계획을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- Windows impersonation token이란 무엇입니까?
- impersonation token은 스레드가 다른 security context에서 access check를 받기 위해 스레드에 붙는 token입니다. 프로세스 전체가 다른 사용자가 되는 것이 아니라, impersonation한 스레드만 그 사용자의 권한으로 파일이나 레지스트리 등의 access check를 받습니다. 「관리자가 되는 마법」 즉 privilege elevation이 아니라, 서버 프로세스의 일부 처리만 클라이언트 사용자의 권한으로 access check하게 하는 메커니즘입니다.
- primary token과 impersonation token은 어떻게 다릅니까?
- primary token은 프로세스의 security context를 나타내며, CreateProcessAsUser 같은 프로세스 시작에 사용합니다. impersonation token은 스레드가 다른 security context에서 동작하기 위해 사용하며, ImpersonateLoggedOnUser나 Named Pipe의 클라이언트 impersonation에서 등장합니다. 프로세스를 시작하려면 원칙적으로 primary token이 필요하고, impersonation token만 있는 경우에는 DuplicateTokenEx로 primary token을 만드는 흐름이 됩니다. 혼동하면 Access denied 같은 오류로 헤매게 됩니다.
- impersonation 중인데 Access denied가 나는 이유는 무엇입니까?
- 확인할 관점은 여러 가지입니다. impersonation scope 안에서 WindowsIdentity.GetCurrent()의 Name뿐 아니라 ImpersonationLevel도 확인하고, Identification 수준이 아니라 Impersonation 이상인지 봅니다. 대상 리소스의 ACL, 실제 I/O가 impersonation scope 밖이나 다른 태스크에서 돌고 있지 않은지, 로컬은 성공하고 UNC만 실패하는 double hop 문제가 아닌지, UAC에 의한 비상승 token의 영향도 확인합니다. 이름이 기대한 대로여도 권한이 부족한 경우가 있습니다.
- .NET에서 안전하게 impersonation을 구현하려면 어떻게 하면 됩니까?
- WindowsIdentity.RunImpersonated / RunImpersonatedAsync를 쓰고, impersonation scope를 람다식 안에 가두는 것이 기본입니다. impersonation이 필요한 비동기 처리는 RunImpersonatedAsync 안에서 await하고, impersonation scope 안쪽에서 fire-and-forget 태스크를 던지지 않습니다. Win32 API로 impersonation하는 경우에는 반드시 try/finally에서 RevertToSelf를 호출합니다. impersonation scope는 필요한 I/O만으로 최소화하고, token handle은 SafeAccessTokenHandle과 using으로 관리합니다.