<!-- canonical: https://docs.axelabs.ai/onboard/ssh-access -->
<!-- source: content/onboard/ssh-access.mdx -->

---
title: SSH 로컬 작업 (Claude Code / Cursor / 기타 AI session)
description: AXE macmini 에 SSH 진입해 로컬 작업하는 표준 절차. 사람이 read 하거나, AI session 에 본 페이지 URL 을 던지면 step-by-step interactive 진행.
playbook: true
---

# SSH 로컬 작업

## AI 요청 프롬프트

```
https://docs.axelabs.ai/onboard/ssh-access 따라 내 머신을 SSH 셋업해줘.

진행:
1. 내 머신 OS 진단 (Windows/macOS/Linux)
2. 기존 환경 확인 (cloudflared 설치, ~/.ssh/id_ed25519 존재, ~/.ssh/config 등)
3. 페이지의 각 Step 명령 실행 + 검증, 매 step 결과 받고 다음
4. 함정 발생 시 페이지 "함정 정리" 표 따라 우회
5. Step 3 server-side 등록 = 운영자 ai (ai@axellc.com) 에 Teams DM 으로 내 public key + email 전달, 회신 받고 다음 진행
```

본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타.

페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감.

## Prereq

- AXE 임직원 (`@axellc.com`) 또는 customer 직원 (해당 customer 도메인) email
- Microsoft Entra SSO 가능 — 본인 회사 IT 가 Entra 직원 등록 완료
- 머신: Windows 10/11 또는 macOS 또는 Linux

## Step 1: cloudflared 설치

### Windows

```powershell
winget install --id Cloudflare.cloudflared
```

설치 후 **새 PowerShell 창** 열어서 검증:

```powershell
cloudflared --version
```

함정: 같은 PowerShell 창 안에서 검증 시 `cloudflared : The term 'cloudflared' is not recognized` — PATH 환경변수가 같은 shell session 에 갱신 안 됨. **새 창** 필수.

### macOS

```bash
brew install cloudflare/cloudflare/cloudflared
```

### Linux

[Cloudflare 공식 install guide](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/).

## Step 2: ed25519 keypair 생성

본인 머신에 SSH key 가 없으면 생성:

```
ssh-keygen -t ed25519 -C "<your-email>@axellc.com"
```

기본 경로 (Enter 로 수락):

- Windows: `C:\Users\<You>\.ssh\id_ed25519`
- macOS / Linux: `~/.ssh/id_ed25519`

passphrase 입력 권장 (key 유출 시 안전장치).

검증 (public key 출력):

```
cat ~/.ssh/id_ed25519.pub
```

출력 예:

```
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIzszmDFVQCu3ViuGwWV2NatlLhYozWalisAtw6xgQWh you@axellc.com
```

## Step 3: server-side 등록 (운영자 ai 처리)

본인 public key 한 줄을 운영자 ai (`ai@axellc.com`) 에게 전달. 방법 2가지:

**A. Teams DM 으로 전달** — 본인 1:1 채팅에 paste. 운영자 ai 가 메시지 받으면 자동 처리.

**B. AI session 자동 forward** — 본인 Claude Code session 이 본 페이지 따라 진행 중이면 자동으로 운영자 ai 에 메시지 발송 (Blueprint MCP `get_teams_message` 패턴 — [B-bp-mcp-teams-admin-consent](/ops/backlog) 완료 후).

운영자 ai 가 수행하는 작업 (참고):

```bash
# (a) public key append to axe user's authorized_keys (codebase 작업용)
#     ⚠️ axe 는 공용 계정 — install 은 destination replace 라 기존 직원 키 전부 삭제됨.
#     반드시 tee -a 로 append. 권한·소유자는 기존 file 의 것이 유지됨 (file 이 이미 600/axe:staff).
echo "<your-pubkey-one-line>" | sudo /usr/bin/tee -a /Users/axe/.ssh/authorized_keys >/dev/null

# (b) SACL 추가 (PAM pam_sacl.so 통과)
sudo /usr/sbin/dseditgroup -o edit -a <your-shortname> -t user com.apple.access_ssh

# (c) (선택) 본인 home directory 도 SSH 진입 허용 — 본인 personal 작업 시 (첫 등록 시만 install OK)
sudo /usr/bin/install -d -m 700 -o <your-shortname> -g dev /Users/<your-shortname>/.ssh
sudo /usr/bin/install -m 600 -o <your-shortname> -g dev <tmp-key-file> /Users/<your-shortname>/.ssh/authorized_keys
# 두 번째 키 추가 시에는 (a) 처럼 tee -a 로 append.
```

전부 NOPASSWD whitelist 안 (`tee /Users/*` / `install` / `chown` / `dseditgroup`) — 운영자 password 입력 불요.

운영자 ai 회신 받으면 다음 step.

## Step 4: ~/.ssh/config 작성

### Windows (PowerShell)

```powershell
@'
Host axe-macmini
  HostName ssh-axe.axelabs.ai
  User axe
  ProxyCommand "C:\Program Files (x86)\cloudflared\cloudflared.exe" access ssh --hostname %h
  IdentityFile ~/.ssh/id_ed25519
'@ | Out-File -Encoding ascii -FilePath $env:USERPROFILE\.ssh\config
```

### macOS / Linux

```bash
cat <<'EOF' >> ~/.ssh/config

Host axe-macmini
  HostName ssh-axe.axelabs.ai
  User axe
  ProxyCommand cloudflared access ssh --hostname %h
  IdentityFile ~/.ssh/id_ed25519
EOF
```

설명:

- `HostName ssh-axe.axelabs.ai` — Cloudflare Tunnel 의 public hostname. 옛 `ssh.axe.axelabs.ai` 폐기됨 ([D-ops-39](/ops/decisions)).
- `User axe` — OS account = `axe` (AXE platform codebase 공통 위치 `/Users/axe/` 진입). audit 식별은 Cloudflare Access SSO log + ssh key fingerprint + git author 3중.
- `ProxyCommand cloudflared access ssh --hostname %h` — Cloudflare Access SSH 게이트 통과. Windows 는 절대 경로 + 따옴표.

## Step 5: Cloudflare Access 인증 (24h 1회)

```
cloudflared access login https://ssh-axe.axelabs.ai
```

브라우저 자동 열림 → Microsoft 로그인 (본인 email) → 완료. JWT 토큰 발급 (24h 유효, `session_duration: "24h"` per Access app 설정).

## Step 6: 첫 ssh 시도

```
ssh -o StrictHostKeyChecking=accept-new axe-macmini
```

`accept-new` = 첫 시도 시 host fingerprint 자동 등록 (사람 prompt 없이). `axe@AXEs-Mac-mini ~ %` prompt 떨어지면 성공.

함정: 단순 `ssh axe-macmini` 시도 시 `Host key verification failed` — host key prompt 가 non-interactive shell 에서 답을 못 받음. 반드시 `accept-new` 옵션.

## Step 7: AI session 등록 (Claude Code / Cursor / 등)

### Claude Code (Windows) — ProxyCommand 미지원 함정

Claude Code Windows native app 의 SSH backend 는 OpenSSH `ProxyCommand` 를 호출 안 함 → 동일 `~/.ssh/config` 인데 PowerShell 의 `ssh axe-macmini` 는 통과, Claude Code 는 timeout/handshake fail.

**우회 = cloudflared TCP forward** (PC 부팅 후 1회):

```powershell
Start-Process -FilePath "C:\Program Files (x86)\cloudflared\cloudflared.exe" -ArgumentList 'access tcp --hostname ssh-axe.axelabs.ai --url localhost:2222'
```

background 창이 자동으로 뜸 — 그대로 둠 (작업 종료 시까지).

첫 등록 시 known_hosts 추가:

```
ssh -o StrictHostKeyChecking=accept-new -p 2222 axe@localhost
```

Claude Code SSH 연결 다이얼로그:

- 이름: `axe-macmini`
- SSH 호스트: `axe@localhost`
- SSH 포트: `2222`
- Identity File: `~/.ssh/id_ed25519`

저장 → New Session → axe-macmini 선택 → `/Users/axe/` 하위 폴더 (axelabs, axelabs-docs, frame, hive, blueprint, .axe 등) 보임.

### Claude Code (macOS) — ProxyCommand 정상

macOS native ssh backend 라 ProxyCommand 작동. Step 4 의 `~/.ssh/config` 그대로 + Claude Code 다이얼로그에 host = `axe@axe-macmini`. TCP forward 불필요.

### TCP forward 자동화 (Windows, user-context Scheduled Task + VBS wrapper)

cloudflared TCP forward 를 PowerShell 창에서 띄우면 창 종료 시 같이 죽음 (함정 #9).
NSSM 등으로 SYSTEM 서비스화하면 JWT 토큰 격리로 origin 인증 무한 루프 (함정 #10).
PowerShell `-WindowStyle Hidden` 는 초기 깜빡임이 있음 (함정 #13).
Task 의 `-RestartCount` 는 FAILURE 에만 발동 — cloudflared 정상 종료 시 silent 중단 (함정 #14).

해결 = **user-context Scheduled Task + VBScript 무한 루프 wrapper**:

- `wscript.exe` 호스팅 → 깜빡임 0
- VBS 내부 루프 → 어떤 종료 코드든 5초 뒤 재기동
- Hidden + AtLogOn + 본인 계정 → 본인 끌 UI 0

**Setup (관리자 PowerShell 한 번에)**:

```powershell
New-Item -ItemType Directory -Path 'C:\ProgramData\cloudflared-ssh-axe' -Force | Out-Null

# VBScript wrapper — 0-flash + 무한 재시작
@'
Set sh = CreateObject("WScript.Shell")
Do
    sh.Run """C:\Program Files (x86)\cloudflared\cloudflared.exe"" access tcp --hostname ssh-axe.axelabs.ai --url localhost:2222", 0, True
    WScript.Sleep 5000
Loop
'@ | Out-File -FilePath 'C:\ProgramData\cloudflared-ssh-axe\tcp-forward.vbs' -Encoding ascii

# Task 등록
$action    = New-ScheduledTaskAction -Execute 'wscript.exe' `
    -Argument '"C:\ProgramData\cloudflared-ssh-axe\tcp-forward.vbs"'
$trigger   = New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\$env:USERNAME"
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive
$settings  = New-ScheduledTaskSettingsSet -StartWhenAvailable `
    -DontStopIfGoingOnBatteries -AllowStartIfOnBatteries `
    -ExecutionTimeLimit ([TimeSpan]::Zero) -Hidden

Register-ScheduledTask -TaskName 'cloudflared-ssh-axe-tcp' `
    -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force

Start-ScheduledTask -TaskName 'cloudflared-ssh-axe-tcp'
```

**검증**:

```powershell
Start-Sleep -Seconds 5
Get-NetTCPConnection -LocalPort 2222 -State Listen
Get-Process cloudflared, wscript -ErrorAction SilentlyContinue | Select-Object Name, Id, StartTime

# 가시적 창 0 확인
Get-Process cloudflared, wscript -ErrorAction SilentlyContinue |
    Where-Object { $_.MainWindowHandle -ne 0 } |
    Select-Object Name, Id, MainWindowTitle
```

마지막 쿼리가 빈 결과 = 진짜 hidden. 포트 2222 LISTENING + 프로세스 살아있으면 성공.

**작동 특성**:

- PowerShell 창 종료: cloudflared 살아있음 (task scheduler 자식 프로세스 트리)
- cloudflared 정상/비정상 종료: VBS 루프가 5초 뒤 재실행 (`-RestartCount` 의존성 0)
- 로그오프: cloudflared/wscript 죽음 → 재로그인 시 AtLogOn 자동 부활
- PC 재부팅: 로그온 후 자동 시작
- 본인 계정 컨텍스트로 실행 → `~/.cloudflared/` 토큰 캐시 공유 (renewal task 와 동일)
- 가시적 창 0개 — 작업 관리자 "세부 정보" 또는 작업 스케줄러 GUI 까지 가야 보임

## Token 자동 갱신 (Windows)

기존 문서: 24h 마다 사람이 `cloudflared access login` 수동 실행. 이를 자동화하는 self-rescheduling Scheduled Task.

**설계 원칙**:

- **폴링 없음** — 토큰의 실제 `exp` claim 을 파싱해서 그 시점에만 fire (24h 주기 task fire 2~3회)
- **만료 5분 전 미리 갱신** → 다운타임 0
- **Self-rescheduling** — task 가 자기 trigger 를 재예약 (외부 cron 불필요)
- **사람 클릭 0회 (조건부)** — Microsoft "로그인 상태 유지" 켜져 있으면 갱신 시 브라우저 깜빡 → SSO 자동 통과

**중요 — `-RunLevel Highest` 필수** (함정 #11): task 가 자기 자신의 `Set-ScheduledTask` 호출해야 해서 elevated 필요. Interactive 만으로는 `Access is denied`.

### Setup (관리자 PowerShell 한 번에)

```powershell
$dir = 'C:\ProgramData\cloudflared-ssh-axe'
New-Item -ItemType Directory -Path $dir -Force | Out-Null

@'
#Requires -Version 5.1
$ErrorActionPreference = 'Stop'

$Config = @{
    Cloudflared             = 'C:\Program Files (x86)\cloudflared\cloudflared.exe'
    AppUrl                  = 'https://ssh-axe.axelabs.ai'
    LogPath                 = 'C:\ProgramData\cloudflared-ssh-axe\renewal.log'
    TaskName                = 'cloudflared-token-renewal'
    RenewWindowMinutes      = 5
    PostLoginRecheckMinutes = 5
    FallbackRetryMinutes    = 60
}

function Write-Log {
    param([string]$Message)
    "$([DateTime]::Now.ToString('s'))  $Message" | Out-File $Config.LogPath -Append -Encoding utf8
    if ((Get-Item $Config.LogPath).Length -gt 1MB) {
        Get-Content $Config.LogPath -Tail 200 | Set-Content $Config.LogPath -Encoding utf8
    }
}

function Get-JwtExpiry {
    param([string]$Jwt)
    $payload = $Jwt.Trim().Split('.')[1].Replace('-','+').Replace('_','/')
    $payload += '=' * ((4 - ($payload.Length % 4)) % 4)
    $json = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($payload)) | ConvertFrom-Json
    [DateTimeOffset]::FromUnixTimeSeconds($json.exp).LocalDateTime
}

function Get-CurrentToken {
    try {
        $t = & $Config.Cloudflared access token "--app=$($Config.AppUrl)" 2>$null
        if ($LASTEXITCODE -eq 0 -and $t -and $t -notmatch 'Unable to find token|please login') {
            return $t.Trim()
        }
    } catch {}
    return $null
}

function Set-NextRun {
    param([DateTime]$When)
    $triggers = @(
        (New-ScheduledTaskTrigger -Once -At $When),
        (New-ScheduledTaskTrigger -AtLogOn)
    )
    Set-ScheduledTask -TaskName $Config.TaskName -Trigger $triggers | Out-Null
    Write-Log "next run: $($When.ToString('s'))"
}

function Invoke-Login {
    Start-Process $Config.Cloudflared -ArgumentList @('access','login',$Config.AppUrl) -WindowStyle Hidden
    Write-Log 'triggered: cloudflared access login'
}

try {
    $token = Get-CurrentToken
    if ($null -eq $token) {
        Invoke-Login
        Set-NextRun (Get-Date).AddMinutes($Config.PostLoginRecheckMinutes)
        return
    }

    $exp      = Get-JwtExpiry $token
    $minsLeft = [int]($exp - (Get-Date)).TotalMinutes
    Write-Log "token exp=$($exp.ToString('s')) (${minsLeft}min)"

    if ($minsLeft -lt $Config.RenewWindowMinutes) {
        Invoke-Login
        Set-NextRun (Get-Date).AddMinutes($Config.PostLoginRecheckMinutes)
    } else {
        Set-NextRun $exp.AddMinutes(-$Config.RenewWindowMinutes)
    }
}
catch {
    Write-Log "ERROR: $($_.Exception.Message)"
    try { Set-NextRun (Get-Date).AddMinutes($Config.FallbackRetryMinutes) } catch {}
    exit 1
}
'@ | Out-File -FilePath "$dir\renew-token.ps1" -Encoding utf8

$action    = New-ScheduledTaskAction -Execute 'powershell.exe' `
    -Argument '-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "C:\ProgramData\cloudflared-ssh-axe\renew-token.ps1"'
$trigger   = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1)
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" `
    -LogonType Interactive -RunLevel Highest   # ← Highest 필수 (함정 #11)
$settings  = New-ScheduledTaskSettingsSet -StartWhenAvailable `
    -DontStopIfGoingOnBatteries -AllowStartIfOnBatteries

Register-ScheduledTask -TaskName 'cloudflared-token-renewal' `
    -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force
```

### 검증

```powershell
Start-ScheduledTask -TaskName 'cloudflared-token-renewal'
Start-Sleep -Seconds 3
Get-Content C:\ProgramData\cloudflared-ssh-axe\renewal.log -Tail 5
```

`token exp=... (NNNmin)` + `next run: ...` 두 줄 보이면 정상.
`ERROR: Access is denied` 가 보이면 `-RunLevel Highest` 누락 (함정 #11).

### (선택) 갱신 시 사람 클릭 0회

기본 브라우저에서 `https://account.microsoft.com` 로그인 시 **"로그인 상태를 유지하시겠습니까?" → 예**. 그러면 renewal task 가 `cloudflared access login` 트리거해도 Microsoft 가 자동 redirect → 사람 손 안 가는 상태.

조직 Entra 정책이 sticky session 차단하면 갱신 시점에 한 번씩 클릭은 필요 (정책 문제, client 우회 불가).

## (부록) NSSM/Windows 서비스화 시도가 실패하는 이유

직관적으로 cloudflared 를 Windows 서비스화하고 싶을 수 있음 — PowerShell 창 의존성 제거 + 로그오프 후에도 살아있음. 그러나 두 시나리오 모두 실패:

**A. SYSTEM 으로 실행 → 토큰 격리**

`cloudflared access login` 으로 받은 JWT 는 `%USERPROFILE%\.cloudflared\<app-id>-token.json` 에 저장. SYSTEM 서비스는 `C:\Windows\System32\config\systemprofile\.cloudflared\` 를 봄.

증상 (stderr.log):

```
failed to acquire app token lock: timed out waiting for lock file
  C:\Windows\System32\config\systemprofile\.cloudflared\...-token.lock
```

SYSTEM 서비스는 토큰 없으니 자체 login 시도 → SYSTEM 은 desktop session 없어서 브라우저 못 띄움 → 무한 "Waiting for login..." 루프.

**B. 사용자 계정으로 실행 → 패스워드 저장 문제**

`nssm set <svc> ObjectName .\<user> <password>` 또는 services.msc 의 Log On 탭에서 사용자 계정 + 패스워드 입력 필요. 평문 명령행 노출 + Microsoft 계정/Windows Hello 사용자는 패스워드 입력 자체 번거로움 + 계정 패스워드 변경 시 서비스 정지 (운영 부담).

**결론**

SSH 가 사람의 active session 에서만 의미 있음 (SSH 시도 = 본인 로그인 상태). 따라서 user-context Scheduled Task (AtLogOn + VBS wrapper) 가 NSSM 보다 깔끔:

- 패스워드 저장 0
- 토큰 캐시 자동 공유
- 본인 계정 컨텍스트 일관
- 본인 끌 UI 0 (가시적 창 0)
- VBS 무한 루프로 정상/비정상 종료 모두 자동 재기동

## 함정 정리

| # | 증상 | 원인 | 우회 |
|---|---|---|---|
| 1 | `cloudflared not recognized` (Windows) | PATH 미반영 | 새 shell 창 |
| 2 | `tls: handshake failure` (Windows: `SEC_E_ILLEGAL_MESSAGE`, macOS: `sslv3 alert handshake failure`) | hostname 2단 (e.g. `ssh.axe.axelabs.ai`) — Universal SSL 1-level | 평탄 hostname (`ssh-axe.axelabs.ai`), [D-ops-39](/ops/decisions) |
| 3 | pubkey 통과 후 `Connection closed by UNKNOWN port` | SACL `com.apple.access_ssh` 미가입 → PAM `pam_sacl.so` account 거부 | Step 3 (운영자 ai 가 `dseditgroup` 추가) |
| 4 | `Host key verification failed` (prompt 답 못 함) | non-interactive ssh | `-o StrictHostKeyChecking=accept-new` |
| 5 | Claude Code SSH timeout / handshake fail (PowerShell 직접 ssh 는 통과) | Claude Code 의 SSH backend 가 `ProxyCommand` 미지원 | TCP forward (Step 7) |
| 6 | `ssh: connect to host localhost port 2222: Connection refused` | cloudflared TCP forward background 안 띄움 | `Start-Process ... access tcp ...` (Step 7) |
| 7 | Claude Code 폴더 선택 시 axelabs / frame / hive 안 보임 | SSH user = 본인 shortname (본인 home 만 보임). AXE platform codebase 는 `/Users/axe/` | SSH user = `axe` (Step 4 의 config), git author = 본인 email (audit) |
| 8 | 24h 후 `cloudflared access ssh` 에러 / Microsoft 로그인 페이지 prompt | JWT 토큰 exp | `cloudflared access login https://ssh-axe.axelabs.ai` 재실행 (또는 § "Token 자동 갱신") |
| 9 | cloudflared TCP forward 가 PowerShell 창 종료 시 같이 죽음 | `Start-Process` 자식이 부모 종료에 종속 | user-context Scheduled Task + VBS wrapper (§ TCP forward 자동화) |
| 10 | NSSM 등으로 cloudflared 서비스화 후 stderr 에 `failed to acquire app token lock: ...\Windows\System32\config\systemprofile\.cloudflared\...` 무한 루프 | SYSTEM 프로필 토큰 캐시 비어있음. JWT 는 user-scoped 라 SYSTEM 컨텍스트와 격리 | 서비스화 금지. user-context Scheduled Task (§ TCP forward 자동화) |
| 11 | Token renewal task 가 `ERROR: Access is denied` 로 자기 trigger 수정 실패 | `Set-ScheduledTask` 는 elevated 권한 필요 | Principal 에 `-RunLevel Highest` |
| 12 | 24h 마다 SSO 재인증을 사람이 매일 수동 실행해야 함 | docs 기본 흐름이 수동 | Self-rescheduling renewal task (§ Token 자동 갱신) |
| 13 | `powershell.exe -WindowStyle Hidden` 으로 task action 구성 시 매 fire 마다 콘솔 창 깜빡임 | Windows 가 hidden flag 적용하기 전 수십 ms 노출 | TCP forward 같은 daemon 류는 `wscript.exe` + VBS wrapper 사용 (§ TCP forward 자동화) |
| 14 | Task `-RestartCount` 설정해도 cloudflared 정상 종료 (exit 0) 후 재시작 안 됨 → 며칠 후 silently SSH 끊김 | `-RestartCount` 는 FAILURE (non-zero exit) 에만 발동 | VBS wrapper 안에 무한 재시작 루프 (§ TCP forward 자동화) |

## Audit Trail (D-ops-29)

OS user = `axe` 공통이지만 식별 3중:

| Layer | 식별자 |
|---|---|
| Cloudflare Access SSO log | email (Cloudflare 대시보드 — Zero Trust → Logs → Access) |
| sshd auth log | ed25519 fingerprint (`/var/log/system.log`) |
| git commit author | `user.email` per session |

본인 session 의 git config 분리 (axe 의 global `~/.gitconfig` 가 ai@ 으로 되어있을 수 있음):

```bash
# repo 별
cd /Users/axe/<repo>
git config user.email "<your-email>@axellc.com"

# 또는 본인 session 환경변수
export GIT_AUTHOR_EMAIL="<your-email>@axellc.com"
export GIT_COMMITTER_EMAIL="<your-email>@axellc.com"
```

## 매 작업 시 사전 작업

| 빈도 | 작업 | 자동화 |
|---|---|---|
| PC 로그온 시 | cloudflared TCP forward 시작 | ✅ Scheduled task `cloudflared-ssh-axe-tcp` (AtLogOn + VBS 무한 루프) |
| Token 만료 ~5분 전 | `cloudflared access login` | ✅ Scheduled task `cloudflared-token-renewal` (self-rescheduling, RunLevel Highest) |
| 매 SSH 시도 | (없음) | — |
| (선택) Microsoft "로그인 상태 유지" | 갱신 시 사람 클릭 제거 | ⚠️ 본인 브라우저 설정 + Entra 정책에 의존 |

## 참조

- [D-ops-41](/ops/decisions) — SSH client-side automation (NSSM 폐기, user-context Scheduled Task + VBS wrapper, self-rescheduling renewal)
- [D-ops-39](/ops/decisions) — Universal SSL 1-level + flat-hostname 컨벤션
- [D-ops-29](/ops/decisions) — Dual identity (ai@ automation vs soohun.kang human work)
- [B-bp-mcp-teams-admin-consent](/ops/backlog) — Blueprint MCP Teams tools admin consent (미완)
- [/ops/known-gaps](/ops/known-gaps) — Cloudflare Universal SSL 1-level 함정 + 14 trap 분석

_Last updated: 2026-05-27 (Soohun Kang — Windows TCP-forward 자동화 + 24h SSO 자동 갱신 셋업 검증 + 함정 #9~#14 추가)_
