6 minute read

AI로 AD 운영을 자동화할 수 있을까? — MCP 활용 실험

ADFS 편에 이어, 이번엔 Active Directory 일상 운영 작업을 MCP로 자동화한 실험임.
로컬 테스트 환경 기준이며, 보안 고려사항을 포함함.


ADFS보다 AD가 더 잘 맞는 이유

이전 포스팅에서 ADFS 운영 자동화를 다뤘는데, 솔직히 AD 쪽이 MCP와 더 잘 맞음.

구분 ADFS AD
작업 빈도 낮음 (설정 변경 간헐적) 높음 (매일 반복)
자연어 질의 적합성 보통 매우 높음
Helpdesk 자동화 가능성 낮음 높음

ADFS는 설정 변경이 드문 반면, AD는 계정 잠금 해제, 신규 입사자 계정 생성, 비밀번호 만료 확인 같은 매일 반복되는 작업이 넘침.
“계정 잠겼어요” 티켓 하나 처리하는 데 매번 ADUC 열고 찾고 클릭하는 과정, AI한테 맡길 수 있으면 어떨까?


아키텍처 구성

ADFS 편과 동일한 구조임. DC(도메인 컨트롤러)에 Python을 설치할 필요 없이, 개발 PC에서 PowerShell Remoting으로 원격 실행함.

[개발 PC]                              [도메인 컨트롤러]
  Claude Desktop                         Windows Server
       │                                      │
  MCP Client                            PowerShell
       │                                      │
  MCP Server (Python/FastMCP) ── WinRM ──▶ Get-ADUser
                                            Unlock-ADAccount
                                            New-ADUser
                                            Get-ADGroupMember 등

준비 환경

항목 내용
개발 PC Windows 10/11, Python 3.10+, Claude Desktop
도메인 컨트롤러 Windows Server, RSAT 설치, PowerShell Remoting 활성화
라이브러리 fastmcp, pywinrm

구현: AD 운영 자동화 MCP Server

1. 설치

pip install fastmcp pywinrm

2. MCP Server 코드 (ad_mcp_server.py)

import subprocess
import os
from fastmcp import FastMCP

mcp = FastMCP("AD Ops Server")

DC_SERVER = os.getenv("DC_SERVER", "dc01.bwcorp.com")
DC_USER   = os.getenv("DC_USER",   "BWCORP\\Administrator")
DC_PASS   = os.getenv("DC_PASS",   "P@ssw0rd")


def run_ps_remote(script: str) -> str:
    """PowerShell Remoting으로 DC에서 명령 실행"""
    cmd = [
        "powershell", "-NonInteractive", "-Command",
        f"""
        $pw   = ConvertTo-SecureString '{DC_PASS}' -AsPlainText -Force
        $cred = New-Object System.Management.Automation.PSCredential('{DC_USER}', $pw)
        Invoke-Command -ComputerName '{DC_SERVER}' -Credential $cred -ScriptBlock {{
            Import-Module ActiveDirectory -ErrorAction SilentlyContinue
            {script}
        }}
        """
    ]
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
        if result.returncode != 0:
            return f"[오류] {result.stderr.strip()}"
        return result.stdout.strip() or "(결과 없음)"
    except subprocess.TimeoutExpired:
        return "[오류] 명령 실행 시간 초과 (30초)"
    except Exception as e:
        return f"[오류] {str(e)}"


# ── Helpdesk 자동화 ─────────────────────────────

@mcp.tool()
def get_user_status(upn: str) -> str:
    """사용자 계정 상태를 조회함 (잠금, 만료, 비밀번호 등).

    Args:
        upn: 사용자 UPN 또는 sAMAccountName (예: user@bwcorp.com 또는 jongmin)
    """
    script = f"""
        $u = Get-ADUser -Filter {{UserPrincipalName -eq '{upn}' -or SamAccountName -eq '{upn}'}} `
             -Properties LockedOut, Enabled, PasswordExpired, PasswordLastSet,
                         PasswordNeverExpires, LastLogonDate, Department, Title
        if (-not $u) {{ "'{upn}' 계정을 찾을 수 없음."; return }}

        [PSCustomObject]@{{
            이름              = $u.DisplayName
            UPN               = $u.UserPrincipalName
            활성화여부        = $u.Enabled
            계정잠금          = $u.LockedOut
            비밀번호만료      = $u.PasswordExpired
            마지막비밀번호변경 = $u.PasswordLastSet
            영구비밀번호      = $u.PasswordNeverExpires
            마지막로그온      = $u.LastLogonDate
            부서              = $u.Department
            직책              = $u.Title
        }} | Format-List | Out-String
    """
    return run_ps_remote(script)


@mcp.tool()
def unlock_account(upn: str) -> str:
    """잠긴 사용자 계정을 해제함.

    Args:
        upn: 사용자 UPN 또는 sAMAccountName
    """
    script = f"""
        $u = Get-ADUser -Filter {{UserPrincipalName -eq '{upn}' -or SamAccountName -eq '{upn}'}}
        if (-not $u) {{ "'{upn}' 계정을 찾을 수 없음."; return }}
        if (-not (Get-ADUser $u -Properties LockedOut).LockedOut) {{
            "$($u.SamAccountName) 계정은 잠겨있지 않습니다."; return
        }}
        Unlock-ADAccount -Identity $u
        "✅ $($u.DisplayName) ($($u.SamAccountName)) 계정 잠금을 해제했습니다."
    """
    return run_ps_remote(script)


@mcp.tool()
def get_locked_accounts() -> str:
    """현재 잠긴 계정 전체 목록을 조회함."""
    script = """
        Search-ADAccount -LockedOut |
        Select-Object Name, SamAccountName, LastLogonDate |
        Format-Table -AutoSize | Out-String
    """
    return run_ps_remote(script)


@mcp.tool()
def get_expiring_passwords(days: int = 7) -> str:
    """비밀번호 만료 임박 사용자를 조회함.

    Args:
        days: 만료 기준 일수 (기본값 7일)
    """
    script = f"""
        $maxAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge.Days
        $threshold = (Get-Date).AddDays({days})

        Get-ADUser -Filter {{Enabled -eq $true -and PasswordNeverExpires -eq $false}} `
            -Properties PasswordLastSet, EmailAddress |
        Where-Object {{
            $_.PasswordLastSet -ne $null -and
            $_.PasswordLastSet.AddDays($maxAge) -le $threshold
        }} |
        Select-Object DisplayName, SamAccountName, EmailAddress,
            @{{N='만료예정일';E={{$_.PasswordLastSet.AddDays($maxAge).ToString('yyyy-MM-dd')}}}} |
        Format-Table -AutoSize | Out-String
    """
    return run_ps_remote(script)


# ── 계정 생성 / 관리 ────────────────────────────

@mcp.tool()
def create_user(
    name: str,
    sam: str,
    upn_prefix: str,
    department: str,
    title: str,
    ou: str = "OU=Users,DC=bwcorp,DC=com"
) -> str:
    """신규 사용자 계정을 생성함.

    Args:
        name: 표시 이름 (예: 박종민)
        sam: sAMAccountName (예: jongmin.park)
        upn_prefix: UPN 앞부분 (예: jongmin.park → jongmin.park@bwcorp.com)
        department: 부서명
        title: 직책
        ou: 생성할 OU 경로 (기본값: Users OU)
    """
    script = f"""
        $exists = Get-ADUser -Filter {{SamAccountName -eq '{sam}'}} -ErrorAction SilentlyContinue
        if ($exists) {{ "'{sam}' 계정이 이미 존재합니다."; return }}

        $pw = ConvertTo-SecureString "Welcome1!" -AsPlainText -Force
        New-ADUser `
            -Name            '{name}' `
            -SamAccountName  '{sam}' `
            -UserPrincipalName '{upn_prefix}@bwcorp.com' `
            -DisplayName     '{name}' `
            -Department      '{department}' `
            -Title           '{title}' `
            -Path            '{ou}' `
            -AccountPassword $pw `
            -ChangePasswordAtLogon $true `
            -Enabled         $true

        "✅ 계정 생성 완료: {name} ({sam}@bwcorp.com) / 초기 비밀번호: Welcome1! (최초 로그인 시 변경 필요)"
    """
    return run_ps_remote(script)


@mcp.tool()
def get_user_groups(upn: str) -> str:
    """사용자가 속한 그룹 목록을 조회함.

    Args:
        upn: 사용자 UPN 또는 sAMAccountName
    """
    script = f"""
        $u = Get-ADUser -Filter {{UserPrincipalName -eq '{upn}' -or SamAccountName -eq '{upn}'}}
        if (-not $u) {{ "'{upn}' 계정을 찾을 수 없음."; return }}

        Get-ADPrincipalGroupMembership $u |
        Select-Object Name, GroupScope, GroupCategory |
        Sort-Object Name |
        Format-Table -AutoSize | Out-String
    """
    return run_ps_remote(script)


# ── 보안 감사 ───────────────────────────────────

@mcp.tool()
def get_inactive_accounts(days: int = 90) -> str:
    """지정 일수 이상 로그인하지 않은 계정을 조회함.

    Args:
        days: 비활성 기준 일수 (기본값 90일)
    """
    script = f"""
        $cutoff = (Get-Date).AddDays(-{days})
        Get-ADUser -Filter {{Enabled -eq $true}} -Properties LastLogonDate |
        Where-Object {{ $_.LastLogonDate -lt $cutoff -or $_.LastLogonDate -eq $null }} |
        Select-Object DisplayName, SamAccountName,
            @{{N='마지막로그온';E={{
                if ($_.LastLogonDate) {{$_.LastLogonDate.ToString('yyyy-MM-dd')}} else {{'없음'}}
            }}}} |
        Sort-Object 마지막로그온 |
        Format-Table -AutoSize | Out-String
    """
    return run_ps_remote(script)


@mcp.tool()
def get_admin_accounts() -> str:
    """관리자 권한을 가진 계정 목록을 조회함 (Domain Admins, Administrators)."""
    script = """
        $adminGroups = @("Domain Admins", "Administrators", "Enterprise Admins")
        foreach ($group in $adminGroups) {
            Write-Output "=== $group ==="
            Get-ADGroupMember -Identity $group -Recursive |
            Where-Object { $_.objectClass -eq 'user' } |
            Get-ADUser -Properties LastLogonDate, Enabled |
            Select-Object Name, SamAccountName, Enabled,
                @{N='마지막로그온';E={
                    if ($_.LastLogonDate) {$_.LastLogonDate.ToString('yyyy-MM-dd')} else {'없음'}
                }} |
            Format-Table -AutoSize | Out-String
        }
    """
    return run_ps_remote(script)


if __name__ == "__main__":
    mcp.run()

3. Claude Desktop 연결 설정

%APPDATA%\Claude\claude_desktop_config.json에 추가합니다.

{
  "mcpServers": {
    "ad-ops": {
      "command": "python",
      "args": ["C:/path/to/ad_mcp_server.py"]
    }
  }
}

4. 실행 결과

Claude Desktop에서 아래처럼 자연어로 질의가 가능합니다.

"user@bwcorp.com 계정 잠겼는지 확인해줘"
"지금 잠긴 계정 전부 보여줘"
"비밀번호 7일 이내 만료되는 사람 알려줘"
"90일 이상 로그인 안 한 계정 뽑아줘"
"관리자 권한 가진 계정 전체 조회해줘"
Claude — AD Ops Server 연결됨
jongmin.park 계정 상태 확인해줘
JP
C
ad-ops get_user_status("jongmin.park")
DC: dc01.bwcorp.com · 조회: 2026-04-07 21:22:11
이름박종민 UPNjongmin.park@bwcorp.com 활성화여부True 계정잠금True ← 잠김 비밀번호만료False 마지막변경2026-01-15 09:32:04 마지막로그온2026-04-07 18:45:22 부서IT인프라팀 직책ADFS 엔지니어
계정이 잠긴 상태입니다. 비밀번호는 유효하고 계정은 활성화되어 있으니 잠금만 해제하면 됩니다. 바로 해제할까?
응 해제해줘
JP
C
ad-ops unlock_account("jongmin.park")
✅ 박종민 (jongmin.park) 계정 잠금을 해제했습니다.
잠금 해제 완료입니다. 사용자에게 다시 로그인해보도록 안내하면 됨.
메시지를 입력하세요...

⚠️ 보안 고려사항

AD MCP는 ADFS보다 훨씬 더 신중하게 접근해야 합니다.
읽기(Read)는 비교적 안전하지만, 쓰기(Write) 작업은 다름.

작업별 위험도

Tool 작업 유형 위험도 비고
get_user_status 읽기 낮음 정보 유출 주의
get_locked_accounts 읽기 낮음 계정 목록 외부 전송
get_inactive_accounts 읽기 낮음 동일
get_admin_accounts 읽기 중간 관리자 목록 노출 위험
unlock_account 쓰기 높음 AI가 실제 계정 변경
create_user 쓰기 높음 AI가 계정 생성

반드시 고려할 사항

1. 쓰기 작업은 승인 절차 추가 권장

AI가 unlock_account를 바로 실행하는 게 편리하지만, 운영 환경에서는 “해제할까?” → 사람이 확인 → 실행 흐름을 유지하는 것이 안전합니다.

2. Claude API 경유 데이터 주의

읽기 결과(계정 목록, 관리자 목록 등)가 Anthropic 서버로 전송됨.
운영 환경에서는 Ollama 같은 로컬 LLM 대체를 검토해야 함.

3. 서비스 계정 권한 최소화

MCP Server가 사용하는 DC 접속 계정에 최소 권한만 부여함.

# 읽기 전용 운영이라면 Domain Users + Read 권한으로 충분
# 잠금 해제가 필요하다면 Account Operators 정도로 제한
# Domain Admins 계정을 MCP에 쓰는 것은 절대 비권장

폐쇄망 환경이라면 — Ollama 대안

[폐쇄망 내부]
  Ollama (로컬 LLM)
       │
  MCP Server (Python)  ── WinRM ──▶ DC (도메인 컨트롤러)

  → 외부 통신 없음, 계정 정보 내부에서만 처리

쓰기 작업이 포함된 AD 자동화는 특히 폐쇄망 + 로컬 LLM 조합이 현실적임.
이 구성은 Mac mini M4 + Ollama 환경이 준비되면 별도로 다뤄볼 예정임.


마치며

이번 실험으로 느낀 점은, AD 운영에서 반복 작업의 비중이 생각보다 크다는 것임.

계정 잠금 해제 한 건이야 1분도 안 걸리지만, 하루에 수십 건씩 들어오는 환경이라면 얘기가 달라짐. AI가 1차 확인을 해주고, 담당자가 최종 승인만 하는 구조로 가면 운영 부담을 꽤 줄일 수 있음.

다만 쓰기 작업에 대한 보안 설계는 선택이 아닌 필수입니다.
편의성과 안전성 사이에서 어디에 선을 그을지, 각 조직의 정책에 맞게 설계하시길 권장함.


Comments