Skip to Content
직원 온보딩SSH 로컬 작업

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 ([email protected]) 에 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

winget install --id Cloudflare.cloudflared

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

cloudflared --version

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

macOS

brew install cloudflare/cloudflare/cloudflared

Linux

Cloudflare 공식 install guide .

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 [email protected]

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

본인 public key 한 줄을 운영자 ai ([email protected]) 에게 전달. 방법 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 완료 후).

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

# (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)

@' 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

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).
  • 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회):

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 한 번에):

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'

검증:

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 한 번에)

$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

검증

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 무한 루프로 정상/비정상 종료 모두 자동 재기동

함정 정리

#증상원인우회
1cloudflared not recognized (Windows)PATH 미반영새 shell 창
2tls: 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
3pubkey 통과 후 Connection closed by UNKNOWN portSACL com.apple.access_ssh 미가입 → PAM pam_sacl.so account 거부Step 3 (운영자 ai 가 dseditgroup 추가)
4Host key verification failed (prompt 답 못 함)non-interactive ssh-o StrictHostKeyChecking=accept-new
5Claude Code SSH timeout / handshake fail (PowerShell 직접 ssh 는 통과)Claude Code 의 SSH backend 가 ProxyCommand 미지원TCP forward (Step 7)
6ssh: connect to host localhost port 2222: Connection refusedcloudflared TCP forward background 안 띄움Start-Process ... access tcp ... (Step 7)
7Claude Code 폴더 선택 시 axelabs / frame / hive 안 보임SSH user = 본인 shortname (본인 home 만 보임). AXE platform codebase 는 /Users/axe/SSH user = axe (Step 4 의 config), git author = 본인 email (audit)
824h 후 cloudflared access ssh 에러 / Microsoft 로그인 페이지 promptJWT 토큰 expcloudflared access login https://ssh-axe.axelabs.ai 재실행 (또는 § “Token 자동 갱신”)
9cloudflared TCP forward 가 PowerShell 창 종료 시 같이 죽음Start-Process 자식이 부모 종료에 종속user-context Scheduled Task + VBS wrapper (§ TCP forward 자동화)
10NSSM 등으로 cloudflared 서비스화 후 stderr 에 failed to acquire app token lock: ...\Windows\System32\config\systemprofile\.cloudflared\... 무한 루프SYSTEM 프로필 토큰 캐시 비어있음. JWT 는 user-scoped 라 SYSTEM 컨텍스트와 격리서비스화 금지. user-context Scheduled Task (§ TCP forward 자동화)
11Token renewal task 가 ERROR: Access is denied 로 자기 trigger 수정 실패Set-ScheduledTask 는 elevated 권한 필요Principal 에 -RunLevel Highest
1224h 마다 SSO 재인증을 사람이 매일 수동 실행해야 함docs 기본 흐름이 수동Self-rescheduling renewal task (§ Token 자동 갱신)
13powershell.exe -WindowStyle Hidden 으로 task action 구성 시 매 fire 마다 콘솔 창 깜빡임Windows 가 hidden flag 적용하기 전 수십 ms 노출TCP forward 같은 daemon 류는 wscript.exe + VBS wrapper 사용 (§ TCP forward 자동화)
14Task -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 logemail (Cloudflare 대시보드 — Zero Trust → Logs → Access)
sshd auth loged25519 fingerprint (/var/log/system.log)
git commit authoruser.email per session

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

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

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

Last updated on