<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>SSH · 인재덕</title>
    <link>https://jared.lynskey.co.nz/ko/tags/ssh/</link>
    <description>소프트웨어 엔지니어링과 개발 조직 리더십에 대한 이야기. 성과 리뷰, 채용과 팀 확장, CI/CD, 데브옵스 모니터링, 에이전틱 AI. 서울에서 씁니다.</description>
    <generator>Hugo</generator>
    <language>ko</language>
    <copyright>© 2026 인재덕</copyright>
    <lastBuildDate>Sat, 07 Nov 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/ko/tags/ssh/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>제가 매일 Claude Code를 쓰는 방식: tmux, 권한 모드, 그리고 폰</title>
      <link>https://jared.lynskey.co.nz/ko/posts/2026/2026-10-22-claude-code-day-to-day/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ko/posts/2026/2026-10-22-claude-code-day-to-day/</guid>
      <pubDate>Thu, 22 Oct 2026 00:00:00 +1300</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>Claude Code</category>
      <category>AI Agents</category>
      <category>LLM</category>
      <category>Developer Experience</category>
      <category>DevOps</category>
      <category>tmux</category>
      <category>SSH</category>
      <category>Codex</category>
      <category>Automation</category>
      <description>Claude Code는 Anthropic의 코딩 에이전트입니다. Claude 모델이 제 기계에서, 제 파일과 셸을 가지고 일하죠. 제 로그의 숫자와 함께 정리한 제 일상 사용법입니다. tmux 안의 터미널에서 세션 다섯 개, 권한 확인은 거의 건너뛰고, 폰에서는 Remote Control, SSH, 채팅 봇으로 닿습니다. 그리고 Codex와의 비교를 제가 말해 줄 수 없는 이유까지요.</description>
      <content:encoded><![CDATA[<p>Claude Code는 Anthropic의 코딩 에이전트입니다. Claude 앱과 같은 Claude 모델로 돌지만 내 기계에서 돌기 때문에, 파일을 읽고 셸 명령을 실행하고 코드를 고칠 수 있습니다. Claude 유료 플랜에 딸려 나오고요. 저는 VS Code 확장이나 데스크톱 앱이 아니라 터미널에서 씁니다. 노트북의 tmux 세션 하나 안에 세션 다섯 개를 띄워 두고, 거의 전부 권한 확인을 끈 채로 시작하며, 폰에서는 Claude 앱의 Remote Control, SSH, 또는 채팅 봇으로 닿습니다.</p>
<p>아래는 그 셋업을 기억이 아니라 제 기계에서 나온 숫자로 자세히 적은 것입니다. 설정 파일들, Claude Code가 <code>~/.claude/projects</code> 아래에 남기는 세션 기록(제 것은 8월 28일부터 있습니다), 그리고 2월부터 쌓여 프롬프트 7,323개가 들어 있는 프롬프트 기록이 출처입니다.</p>

<h2 class="relative group">Claude와 Claude Code는 다른 물건입니다
    <div id="claude와-claude-code는-다른-물건입니다" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#claude%ec%99%80-claude-code%eb%8a%94-%eb%8b%a4%eb%a5%b8-%eb%ac%bc%ea%b1%b4%ec%9e%85%eb%8b%88%eb%8b%a4" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>Claude는 모델 계열이자, 그 모델과 대화하는 채팅 앱입니다. claude.ai에도, 데스크톱에도, 폰에도 있죠. Claude Code는 그 모델 위에 만든 에이전트입니다. 프로젝트 폴더에서 시작하면 그 자리에서 루프를 돌며 파일을 읽고, 명령을 실행하고, 코드를 고치고, 테스트를 돌립니다. 일이 끝나거나 제가 필요해질 때까지요. Claude에게 뭔가 고치는 법을 물으면 답을 받습니다. Claude Code에게 물으면 고친 결과와, 읽어 볼 diff를 받습니다.</p>
<p>저는 둘 다 씁니다. 제가 만드는 마켓플레이스 겸 커뮤니티 앱은 10월 18일부터 Django 백엔드에서 번역을 Claude API로 바로 보내고 있는데, 그렇게 바꾼 변경도 Claude Code 세션에서 만들었습니다. 제 세션 기록에서 답변의 94%는 Opus 5.5가 했고, 설정의 effort 레벨은 high입니다.</p>
<p>네이티브 설치 프로그램은 한 줄이고, 알아서 최신 상태를 유지합니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -fsSL https://claude.ai/install.sh <span class="p">|</span> bash</span></span></code></pre></div></div>
<p>설치하면 <code>~/.local/bin</code>에 <code>~/.local/share/claude/versions/</code> 안을 가리키는 <code>claude</code> 심볼릭 링크가 생깁니다. 제 것은 10월 21일에 혼자서 2.1.305에서 2.1.306으로 올라갔습니다. Claude Code를 쓰려면 Claude 유료 플랜(Pro, Max, Team, Enterprise)이나 API 계정이 필요합니다. 가격은 꽤 자주 바뀌니 <a href="https://claude.com/pricing"  target="_blank" rel="noreferrer">Anthropic 가격 페이지</a>에 맡기겠습니다.</p>

<h2 class="relative group">tmux 세션 하나, 창 일곱 개
    <div id="tmux-세션-하나-창-일곱-개" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#tmux-%ec%84%b8%ec%85%98-%ed%95%98%eb%82%98-%ec%b0%bd-%ec%9d%bc%ea%b3%b1-%ea%b0%9c" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>이 글을 쓰는 지금 <code>tmux list-panes -a</code>를 치면 노트북에 <code>main</code>이라는 세션 하나가 있고, 창이 일곱 개입니다. 그중 다섯 개가 Claude Code로, 프로젝트마다 하나씩입니다. 제 마켓플레이스 앱과 견적 앱(둘 다 <a href="https://jared.lynskey.co.nz/ko/posts/2026/2026-07-22-my-current-stack/" >Fly.io에서 Django 오토스케일링</a>에서 소개했습니다), 이 블로그, 그리고 자잘한 것 두 개요. 터미널 프로필은 모든 창을 <code>main</code>에 붙는 로그인 셸로 엽니다. 그래서 노트북에서 새 탭을 열든 폰에서 SSH로 로그인하든 같은 세션에 도착합니다.</p>
<p>9월 17일부터 10월 8일까지는 다른 멀티플렉서를 써 봤는데, 현실적인 이유로 tmux로 돌아왔습니다. 폰에서 쓰는 SSH 앱이 tmux로 멀티플렉싱하고, 단축 버튼도 tmux 기본 키를 보내거든요. <code>~/.tmux.conf</code>의 이런 줄들은 폰 때문에 있습니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">set</span> -g prefix C-a                 <span class="c1"># Ctrl-a prefix</span>
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -g prefix2 C-b                <span class="c1"># stock Ctrl-b too, for apps that send it</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Size each window to the client that used it last, not the smallest one,</span>
</span></span><span class="line"><span class="cl"><span class="c1"># so a phone attached to the same session doesn&#39;t shrink it.</span>
</span></span><span class="line"><span class="cl">setw -g window-size latest
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Copy to the clipboard of whichever terminal is attached (OSC 52),</span>
</span></span><span class="line"><span class="cl"><span class="c1"># so over SSH it lands on the phone&#39;s clipboard too.</span>
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -g set-clipboard on</span></span></code></pre></div></div>
<p>tmux 안에서는 두 가지가 달라집니다. Claude Code는 실행 중인 명령을 Ctrl+B로 백그라운드에 보내는데, 이게 이제 제 두 번째 tmux prefix입니다. 그래서 <code>~/.claude/keybindings.json</code>에서 그걸 끄고 대신 Meta+B를 씁니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;bindings&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span> <span class="nt">&#34;context&#34;</span><span class="p">:</span> <span class="s2">&#34;Global&#34;</span><span class="p">,</span> <span class="nt">&#34;bindings&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;ctrl+b&#34;</span><span class="p">:</span> <span class="kc">null</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span> <span class="nt">&#34;context&#34;</span><span class="p">:</span> <span class="s2">&#34;Task&#34;</span><span class="p">,</span> <span class="nt">&#34;bindings&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;ctrl+b&#34;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span> <span class="nt">&#34;meta+b&#34;</span><span class="p">:</span> <span class="s2">&#34;task:background&#34;</span> <span class="p">}</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>다른 하나는 줄바꿈입니다. Enter는 프롬프트를 보냅니다. 문서는 어느 터미널에서나 되는 방법으로 <code>\</code> 다음 Enter, 그리고 Ctrl+J 두 가지를 듭니다. Option+Enter는 터미널이 Option을 Meta로 보내도록 설정하면 되고, Shift+Enter는 일부 터미널에서는 그냥 되지만 tmux는 <code>extended-keys</code>가 켜져 있을 때만 그걸 넘겨줍니다. 제 건 꺼져 있어서, 제 세션에서는 Shift+Enter를 누르면 프롬프트가 보내져 버립니다. 사실 어느 것도 쓸 일이 별로 없습니다. 프롬프트 7,323개 중 제가 직접 친 줄바꿈이 들어간 건 84개이고, 붙여넣기가 들어간 건 279개입니다. 붙여넣은 내용은 Claude Code가 <code>[Pasted text #1 +34 lines]</code> 같은 자리표시자로 접어 두고, 보낼 때는 전부 보냅니다.</p>

<h2 class="relative group">터미널, VS Code 확장, 데스크톱 앱 중 무엇을?
    <div id="터미널-vs-code-확장-데스크톱-앱-중-무엇을" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ed%84%b0%eb%af%b8%eb%84%90-vs-code-%ed%99%95%ec%9e%a5-%eb%8d%b0%ec%8a%a4%ed%81%ac%ed%86%b1-%ec%95%b1-%ec%a4%91-%eb%ac%b4%ec%97%87%ec%9d%84" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>같은 에이전트가 몇 가지 포장으로 나오고, 모두 같은 설정 파일을 읽습니다. Claude Code가 세션 기록마다 남기는 진입점을 기준으로 보면 제 세션은 이렇게 나뉩니다.</p>
<table>
	<thead>
			<tr>
					<th>형태</th>
					<th>무엇인가</th>
					<th>제 사용량, 8월 28일~10월 21일</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>터미널의 CLI</td>
					<td>터미널에서, tmux 안에서 실행하는 <code>claude</code></td>
					<td>세션 128개</td>
			</tr>
			<tr>
					<td>데스크톱 앱의 Code 탭</td>
					<td>Claude 앱 안의 Claude Code, diff 뷰와 패널이 있음</td>
					<td>세션 2개</td>
			</tr>
			<tr>
					<td>VS Code 확장</td>
					<td>에디터 안의 채팅 패널</td>
					<td>없음. 지금은 설치돼 있지 않고 설정만 남아 있습니다</td>
			</tr>
	</tbody>
</table>
<p>Claude 앱과 claude.ai/code는 세션을 시작하는 네 번째 자리가 아닙니다. 노트북에서 이미 돌고 있는 세션(또는 클라우드의 세션)을 조종하는 것이고, 10월 이후 제 세션 대부분이 Remote Control로 그렇게 연결돼 있었습니다.</p>
<p>제 답은 터미널이고, 의리 때문은 아닙니다. 폰이 붙는 tmux가 터미널을 필요로 하고, 외부 이벤트를 세션으로 밀어 넣는 channels도 그렇습니다. 문서에 CLI 기능으로 나와 있거든요.</p>
<p>Claude Code에 git이 필요하냐는 질문도 받습니다. CLI는 필요 없습니다. 제 홈 디렉터리는 저장소가 아닌데, 제 프롬프트 중 208개가 거기서 시작한 세션으로 갔습니다. 데스크톱 앱 문서에는 세션마다 자기 worktree를 받는 세션 격리에 git이 필요하다고 나와 있고, 클라우드 세션을 터미널로 끌어오려면 같은 저장소의 체크아웃이 있어야 합니다. 저는 &ldquo;Git is required&rdquo; 메시지를 직접 본 적이 없습니다. 10월 20일에 쓴 마지막 데스크톱 세션은 아예 저장소가 아닌 폴더에서 돌았습니다.</p>

<h2 class="relative group">권한 모드, 그리고 제가 실제로 쓰는 것
    <div id="권한-모드-그리고-제가-실제로-쓰는-것" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ea%b6%8c%ed%95%9c-%eb%aa%a8%eb%93%9c-%ea%b7%b8%eb%a6%ac%ea%b3%a0-%ec%a0%9c%ea%b0%80-%ec%8b%a4%ec%a0%9c%eb%a1%9c-%ec%93%b0%eb%8a%94-%ea%b2%83" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>권한 모드는 Claude Code가 얼마나 자주 멈춰서 물어볼지를 정하고, Shift+Tab으로 모드를 차례로 바꿉니다. Manual(<code>default</code>)은 파일을 고치거나 명령을 실행하거나 네트워크에 접근하는 대부분의 동작 전에 묻습니다. <code>acceptEdits</code>는 파일 수정과 흔한 파일시스템 명령은 통과시킵니다. <code>plan</code>은 읽고 제안만 할 뿐 고치지 않습니다. <code>auto</code>는 두 번째 모델, 즉 분류기가 위험한 동작을 사람에게 묻는 대신 막아 줍니다. <code>bypassPermissions</code>는 거의 아무것도 묻지 않습니다. 버전 2.1.284부터는 <code>permissions.defaultMode</code>로 따로 정해 두지 않으면 터미널 세션이 auto 모드로 시작합니다.</p>
<p>그래서 &ldquo;Claude Code를 권한 확인 없이 돌리려면 어떻게 하나요&quot;에 대한 제 솔직한 답은, 이렇게 시작한다는 겁니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">claude --dangerously-skip-permissions</span></span></code></pre></div></div>
<p>셸 기록에 남은 <code>claude</code> 명령 69개 중 62개에 이 플래그가 붙어 있고, 지금 돌고 있는 세션 다섯 개도 전부 그렇습니다. 8월 28일부터 10월 21일까지 제가 입력한 프롬프트 1,266개 중 1,179개는 bypass 모드 세션으로, 82개는 auto 모드로, 5개는 Manual이나 accept-edits로 갔습니다.</p>
<p>문서는 bypass 모드가 &ldquo;인터넷 접근이 없는 컨테이너, VM, dev 컨테이너처럼 Claude Code가 호스트 시스템을 망가뜨릴 수 없는 격리된 환경&quot;을 위한 것이라고 분명히 말합니다. 제 노트북은 그 어디에도 해당하지 않고, 제 설정에는 그 경고를 클릭해서 넘겼다는 기록까지 남아 있습니다(<code>&quot;skipDangerousModePermissionPrompt&quot;: true</code>). 같은 길을 가신다면 이건 알아 두세요. bypass 모드에서는 allow 규칙이 아무 효과가 없습니다. 그래서 제 평소 세션에서는 설정 파일 19개에 흩어진 allow 규칙 378개가 하는 일이 전혀 없습니다. deny와 ask 규칙은 여전히 적용되는데, 저는 둘 다 하나도 없습니다.</p>
<p>예외는 다른 사람이 쓴 글을 읽는 세션입니다. 이 세션의 런처는 그 플래그를 넘기지 않아서 auto 모드로 시작합니다. 10월 12일부터 20일 사이에 세션에 도착한 프로덕션 이벤트 27개 전부와 채팅 메시지 43개 중 42개가 auto 모드에서 들어왔고, bypass 모드에서 들어온 건 하나도 없었습니다.</p>
<p>bypass 모드에 실제로 물리지 않게 해 주는 건 몇 가지 습관입니다. 9월의 어느 날 같은 체크아웃에서 돌던 세션 둘이 서로의 작업을 덮어쓰고 그걸 배포할 뻔한 뒤에 Claude 메모리에 적어 둔 것들이죠(자초지종은 <a href="https://jared.lynskey.co.nz/ko/posts/2026/2026-10-22-agentic-ai-security/#격리-가장-그럴듯한-위협은-다른-에이전트" >에이전틱 AI 보안 글</a>에 있습니다). 규칙은 이렇습니다. 작은 수정은 동작하는 순간 커밋한다. 파일은 이름으로 스테이징하고 <code>git add -A</code>는 절대 쓰지 않는다. <code>git status</code>에서 내가 만들지 않은 변경을 살핀다. <code>main</code>에 다른 세션의 배포 안 된 작업이 있으면, 지금 라이브인 커밋으로 worktree를 만들어 거기서 배포한다.</p>

<h2 class="relative group">폰에서 닿기
    <div id="폰에서-닿기" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ed%8f%b0%ec%97%90%ec%84%9c-%eb%8b%bf%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>들어가는 길은 세 가지이고, 각각 닿는 곳이 다릅니다.</p>
<table>
	<thead>
			<tr>
					<th>들어가는 길</th>
					<th>폰에서 얻는 것</th>
					<th>무엇으로 돌아가는지</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Claude 앱의 Remote Control</td>
					<td>세션 하나의 대화: 입력하고, 질문에 답하고, 사진을 첨부</td>
					<td><code>remoteControlAtStartup: true</code>, 그리고 푸시 알림</td>
			</tr>
			<tr>
					<td>폰의 SSH 앱으로 <code>main</code>에 SSH</td>
					<td>터미널 그 자체, 모든 창</td>
					<td>사설 네트워크, 제 사용자로 도는 sshd, 그리고 붙이는 데 쓰는 짧은 alias</td>
			</tr>
			<tr>
					<td>채팅 봇</td>
					<td>프로덕션 오류를 듣고 메시지에 답하는 세션</td>
					<td>Claude Code 공식 채널 플러그인과 제가 만든 이벤트 채널</td>
			</tr>
	</tbody>
</table>
<p>Remote Control은 폰의 Claude 앱이나 브라우저의 claude.ai/code를 노트북에서 계속 돌고 있는 세션에 연결합니다. 파일, 도구, MCP 서버까지 그대로요. 9월 13일부터 10월 20일 사이에 <code>/rc</code>를 26번 쳤습니다. 지금은 <code>remoteControlAtStartup</code>이 켜져 있어서 새 세션이 알아서 연결됩니다. 10월 초부터 제가 시작한 세션 77개 중 67개에 Remote Control 링크가 있었는데, 9월 후반에는 52개 중 3개였습니다.</p>
<p><code>agentPushNotifEnabled</code>를 켜 두면 Claude가 필요하다고 판단할 때 푸시 알림을 보낼 수 있습니다. 문서에 따르면 보통 긴 작업이 끝났거나 결정이 필요할 때입니다. API 키가 아니라 claude.ai 구독이 있어야 하고, 세션이 노트북에서 도니 노트북이 깨어 있어야 합니다. 10월 15일부터 제 노트북은 덮개를 닫아도 잠들지 않습니다(<code>sudo pmset -a disablesleep 1</code>).</p>
<p>반대 방향도 됩니다. <code>/teleport</code>나 <code>claude --teleport</code>는 웹이나 앱에서 시작한 클라우드 세션을 브랜치와 대화째 터미널로 끌어옵니다. 저는 두 번 해 봤고, 둘 다 7월이었습니다.</p>
<p>SSH는 터미널 그 자체가 필요할 때 씁니다. 폰은 <a href="https://jared.lynskey.co.nz/ko/posts/2026/2026-10-01-cloudflare-warp-dev-network/" >사설 네트워크 글</a>에서 다룬 사설 네트워크를 거쳐 노트북에 닿고, alias 하나가 <code>main</code>에 붙여 주니 노트북에 열어 둔 창들 사이를 스와이프로 오갈 수 있습니다. Mac이 폰 앱에 제대로 답하게 만드는 데는 서로 다른 수정이 네 번 들었는데, Claude Code와 관련해 중요한 건 tmux가 절대 저절로 시작돼서는 안 된다는 점입니다. <code>docker ps</code>를 돌리려고 세션을 연 앱이 결과 대신 72 KB짜리 화면 다시 그리기를 돌려받았거든요.</p>
<p>채팅 봇은 앉아서 지켜보고 싶지 않은 세션용입니다. 10월 12일부터 세션 하나가 제가 만든 작은 채널을 달고 돌고 있습니다. 이 채널은 1분에 한 번 앱 서버에서 새 프로덕션 오류와 프로젝트 보드 활동을 확인해 세션에 넘깁니다. 여기에 공식 채팅 플러그인 하나도 붙어 있어서, allowlist에 있는 저희 두 사람이 세션에 메시지를 보낼 수 있습니다. 실행 줄은 이름만 일반적으로 바꾸면 이렇습니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">exec</span> claude <span class="se">\
</span></span></span><span class="line"><span class="cl">  --channels plugin:&lt;chat-plugin&gt;@claude-plugins-official <span class="se">\
</span></span></span><span class="line"><span class="cl">  --dangerously-load-development-channels server:my-events <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span></span></span></code></pre></div></div>
<p>슈퍼바이저 스크립트가 tmux 안에서 이 세션을 계속 살려 둡니다. 재시작하면 같은 대화를 이어 가니, 세션이 백지 상태로 깨어나는 게 아니라 하던 일을 계속하고, 재시작할 때마다 채팅에 알립니다. 이 세션은 작업 트리에서 고치는 데까지만 하고 멈춥니다. 예외가 하나 있는데, 10월 13일에 추가했습니다. 프로덕션에서 실제로 사용자에게 피해를 주고 있는 버그라면 고치고, 테스트하고, 커밋하고, 배포한 다음 무엇을 했는지 저희에게 알려도 됩니다.</p>

<h2 class="relative group">스킬, 메모리, 그리고 /goal
    <div id="스킬-메모리-그리고-goal" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%8a%a4%ed%82%ac-%eb%a9%94%eb%aa%a8%eb%a6%ac-%ea%b7%b8%eb%a6%ac%ea%b3%a0-goal" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>제 설정 파일 어디에도 <code>hooks</code> 키는 없습니다. 가장 가까운 건 <code>/goal</code>로, 7월 11일부터 수도 없이 썼습니다. 조건을 하나 받아서 그 세션에 Stop hook을 거는데, Claude Code의 표현을 빌리면 이 hook은 &ldquo;will block stopping until the condition holds&rdquo;(조건이 충족될 때까지 멈추지 못하게 막는다)입니다. 제 goal은 제가 소리 내어 말하는 그대로 적혀 있습니다. 7월에 실제로 쓴 것 하나는 오타까지 그대로 옮기면 빌드가 &ldquo;still broken, try a deeper fix and dont stop until&rdquo;(아직도 안 돼, 더 근본적으로 고치고 ~까지 멈추지 마) 테스터에게 나갈 때까지라고 했습니다. 긴 실행을 방해하지 않고 상황을 보고 싶을 땐 <code>/btw</code>가 있습니다. 대화에 섞이지 않는 곁가지 질문이죠. 제 건 대부분 &ldquo;hows it going?&quot;(잘 돼 가?)의 변주입니다.</p>
<p>스킬에는 그게 없었으면 매번 설명했을 것들이 들어 있습니다. 사용자 레벨 스킬 하나는 이 블로그를 포함해 제가 관리하는 사이트들의 주간 키워드 보고서를 읽고, 그 내용을 제목, 설명, 태그에 반영합니다. 이 글이 답하는 질문들도 이 스킬이 10월 18일자로 낸 이 블로그의 보고서에서 왔고, 스킬 파일은 저장소에서 코드와 함께 버전 관리됩니다. 마켓플레이스 앱 저장소에도 자체 스킬이 있습니다. 하나는 Docker에서 API를 띄우고 스모크 테스트를 돌리고, 하나는 프로젝트 보드를 다루고, 하나는 회사 경비를 처리합니다. 설치해 둔 플러그인 두 개는 Swift 언어 서버 플러그인과 그 채팅 플러그인입니다.</p>
<p>메모리는 교훈이 쌓이는 곳입니다. 프로젝트 17개에 걸쳐 메모리 파일이 160개 있고, 그중 83개가 마켓플레이스 앱 것이며, 상당수가 뭔가 고장 났던 이야기입니다. 앱의 <code>CLAUDE.md</code>는 이제 6월보다 여섯 배쯤 길어졌습니다. 이 블로그의 <code>CLAUDE.md</code>는 빈 파일입니다. 글이 어떤 목소리여야 하는지는 그 대신 메모리 파일에 들어 있습니다.</p>
<p>큰 일에는 Claude가 서브에이전트를 띄웁니다. 제 메인 세션에서 120번이었고, 그중 40번은 대화를 포크한 것이었습니다. 이 블로그의 일본어, 한국어, 중국어, 베트남어 버전도 그렇게 만들어집니다. 10월 19일과 20일에는 앱 커뮤니티 섹션의 리디자인이 Workflow 도구를 네 번 거쳤습니다. 일곱 가지 관점과 비평가 하나로 감사를 돌려 발견 사항 약 58개를 확정했고, 각자 자기 파일을 맡은 다섯 그룹이 수정한 뒤 그룹마다 리뷰를 받았고, 남은 것들을 한 번 더 훑었고, 마지막으로 모든 발견 사항이 고쳐졌는지 독립적으로 확인했습니다. 리디자인은 20일에 배포됐습니다.</p>

<h2 class="relative group">SSH로 워크스테이션 부리기
    <div id="ssh로-워크스테이션-부리기" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ssh%eb%a1%9c-%ec%9b%8c%ed%81%ac%ec%8a%a4%ed%85%8c%ec%9d%b4%ec%85%98-%eb%b6%80%eb%a6%ac%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h2>
<p><a href="https://jared.lynskey.co.nz/ko/posts/2026/2026-10-01-cloudflare-warp-dev-network/" >사설 네트워크 글</a>은 무거운 일을 GPU가 달린 사무실 워크스테이션 devbox에 맡기고, 어디서든 거기에 닿는 이야기입니다. 그 닿는 일은 대부분 Claude Code가 합니다. 9월 24일부터 10월 19일까지 노트북의 세션 21개가 평범한 <code>ssh</code>로 devbox에서 명령 1,470개를 실행했습니다. 거기서 도는 클러스터에 <code>kubectl</code>을 날리고, 데이터베이스 pod 안에서 <code>psql</code>을 돌리고, 긴 작업이 어떻게 돼 가는지 보려고 <code>tmux capture-pane</code>을 쓰는 식이죠. <code>ssh devbox</code>는 사무실 네트워크 밖에서는 터널로 넘어가니, 같은 세션이 사무실에서도, 집에서도, 카페에서도 동작합니다. 게다가 노트북이 이제 잠들지 않으니, 세션이 devbox를 계속 부리는 동안 저는 폰으로 들여다보면 됩니다.</p>

<h2 class="relative group">Codex와 비교할 수 없는 이유
    <div id="codex와-비교할-수-없는-이유" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#codex%ec%99%80-%eb%b9%84%ea%b5%90%ed%95%a0-%ec%88%98-%ec%97%86%eb%8a%94-%ec%9d%b4%ec%9c%a0" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>Claude Code가 Codex보다 나은지는 제 사용 경험으로는 말씀드릴 수 없습니다. 이유는 표를 보시면 압니다.</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>Codex</th>
					<th>Claude Code</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>언제</td>
					<td>2025년 11월 29일~12월 4일</td>
					<td>적어도 2025년 11월부터. 프롬프트 기록은 2026년 2월부터</td>
			</tr>
			<tr>
					<td>어디서</td>
					<td>VS Code 확장</td>
					<td>터미널(세션 128개), 데스크톱 앱(2개)</td>
			</tr>
			<tr>
					<td>모델과 effort</td>
					<td><code>gpt-5.1-codex</code>, medium</td>
					<td>Opus 5.5, high</td>
			</tr>
			<tr>
					<td>승인</td>
					<td><code>workspace-write</code> 샌드박스, 네트워크 꺼짐, 요청 시 확인</td>
					<td>프롬프트의 93%가 bypass, 채팅 세션은 auto</td>
			</tr>
			<tr>
					<td>사용량</td>
					<td>스레드 4개</td>
					<td>2월부터 프롬프트 7,323개</td>
			</tr>
			<tr>
					<td>프로젝트 노트</td>
					<td><code>AGENTS.md</code>를 읽음</td>
					<td><code>CLAUDE.md</code>와 메모리 파일을 읽음</td>
			</tr>
	</tbody>
</table>
<p>이미지 질문 하나, 웹사이트 수정 두 건, 3D 장면 하나에 쓴 일주일간의 스레드 네 개는 비교가 아니라 맛보기입니다. 그래도 거기서 나온 한 가지는 에이전트를 갈아타는 누구에게나 해당됩니다. 그 3D 프로젝트는 규칙을 <code>claude.md</code>에 두고 있었고, 거기서 Codex에게 던진 첫 질문은 대화를 시작하기 전에 Claude의 파일을 읽느냐는 것이었습니다. 읽지 않는다고 했고, 제가 그 파일을 읽으라고 시키고 나서야 프로젝트 규칙을 파악했습니다. 어느 에이전트로 옮기든, 지시 파일은 직접 챙겨 가지 않으면 그 자리에 남습니다. 그 뒤로 Codex 스레드는 열지 않았습니다. 9월 24일부터 10월 4일 사이 며칠은 제가 아무것도 시작하지 않았는데도 확장이 VS Code에서 로드됐고, 지금은 제거했습니다.</p>
<p>그러니 제가 책임질 수 있는 결론은 좁습니다. 제 셋업은 Claude Code를 중심으로 짜여 있고, Codex가 지금 같은 일을 어떻게 처리하는지는 다시 확인해 보지 않았습니다.</p>
<p>마지막 숫자 하나가 위의 어떤 내용보다 제가 이걸 어떻게 쓰는지 잘 보여 줍니다. 8월 말부터 노트북의 메인 세션에서 Claude Code는 셸 명령을 18,311개 실행했고, 파일을 읽거나 쓰거나 고친 건 2,331번이었습니다. 횟수로 보면 일의 대부분은 원래 제가 쳤을 명령을 대신 실행하는 것입니다. 노트북에서도, SSH 너머에서도, 제가 어디에 있든요. 정작 코드를 쓸 때는 그 코드가 어디서나 환영받지는 않는데, 그건 <a href="https://jared.lynskey.co.nz/ko/posts/2026/2026-10-18-pyhtml2text/" >pyhtml2text 글</a>에서 따로 풀어 둔 이야기입니다.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-10-22-claude-code-day-to-day/featured.jpg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>Cloudflare WARP와 Tunnel로 만든 사설 네트워크: 노트북, 폰, 워크스테이션 연결하기</title>
      <link>https://jared.lynskey.co.nz/ko/posts/2026/2026-10-01-cloudflare-warp-dev-network/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ko/posts/2026/2026-10-01-cloudflare-warp-dev-network/</guid>
      <pubDate>Thu, 01 Oct 2026 00:00:00 +1200</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>Cloudflare</category>
      <category>WARP</category>
      <category>Zero Trust</category>
      <category>Cloudflare Tunnel</category>
      <category>Networking</category>
      <category>SSH</category>
      <category>DevOps</category>
      <category>AI Agents</category>
      <category>Ollama</category>
      <category>Mobile Development</category>
      <category>IPv6</category>
      <description>Cloudflare WARP와 Cloudflare Tunnel 두 개(Zero Trust)로 노트북, 5G에 붙은 폰, 사무실 GPU 워크스테이션을 사설 네트워크 하나로 묶은 방법입니다. 고정 사설 IP, 열린 포트 없음, Access 뒤에 둔 dev 서버까지.</description>
      <content:encoded><![CDATA[<p>Cloudflare WARP를 쓰면 포트를 하나도 열지 않고 내 기계들을 사설 네트워크 하나로 묶을 수 있습니다. 닿고 싶은 기계마다 Cloudflare Tunnel(<code>cloudflared</code>)을 돌리고, 각 터널이 사설 IP 대역을 광고하게 한 다음, 노트북과 폰에 WARP 클라이언트를 include 모드로 설치해 Zero Trust 조직에 등록하면 됩니다. 그때부터는 5G에 붙은 폰에서 <code>198.18.42.2</code>로 평범한 연결을 열면 사무실 워크스테이션에 닿습니다. 아래는 제 셋업이 어떻게 짜여 있는지, 그리고 오는 길에 발목을 잡은 네 가지입니다.</p>
<p>제 작업 환경은 좀처럼 한곳에 모이지 않는 기계 세 대입니다. 어디든 들고 다니는 MacBook Air가 있습니다. 사무실에는 devbox라는 워크스테이션이 있는데, 제대로 된 GPU가 달려 있고 Ollama, 파인튜닝 도구, 자체 컨테이너 레지스트리를 갖춘 작은 Kubernetes 클러스터, 그리고 긴 작업을 붙잡고 씨름하도록 놔둔 에이전트가 무엇이든 돌아갑니다. 그리고 폰이 있습니다. 제가 만드는 앱이 실제로 쓰이는 곳이고, 하루 대부분을 wifi가 아니라 모바일 데이터로 보내죠.</p>
<p>오랫동안 이 셋은 서로 제대로 이야기하지 못했습니다. 노트북은 사무실 LAN에 있을 때만 devbox에 닿았고 그 밖에서는 못 닿았습니다. 폰은 노트북과 같은 wifi에 있을 때만 노트북의 dev 서버에 닿았고, 그것도 제가 <code>0.0.0.0</code>에 바인딩하는 걸 잊지 않고 그날 아침 DHCP가 Mac에 준 주소를 직접 쳐 넣었을 때 얘기였습니다. 워크스테이션에서 재미있는 건 전부, 그러니까 Ollama API, 클러스터 대시보드, 트레이스, 레지스트리는 일부러 localhost나 클러스터 네트워크에 바인딩돼 있었습니다. 어느 것도 인터넷에 있을 물건이 아니니까요.</p>
<p>이 글은 Cloudflare WARP와 Cloudflare Tunnel 한 쌍으로 이것들을 어떻게 이었는지, 그게 AI 기능과 모바일 앱을 만드는 방식을 어떻게 바꿨는지, 그리고 저한테 말도 없이 고장 난 몇 가지에 대한 이야기입니다.</p>

<h2 class="relative group">제가 실제로 원한 것
    <div id="제가-실제로-원한-것" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%a0%9c%ea%b0%80-%ec%8b%a4%ec%a0%9c%eb%a1%9c-%ec%9b%90%ed%95%9c-%ea%b2%83" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>요구사항으로 적으면 짧습니다.</p>
<ol>
<li>노트북은 씬 클라이언트여야 합니다. GPU나 많은 RAM, 몇 시간의 실행 시간이 필요한 일은 전부 devbox에서 돌아야 하고, 노트북은 사무실에서든 집에서든 카페에서든 똑같은 방식으로 거기에 닿아야 합니다.</li>
<li>폰은 어디서든, 모바일 데이터로, 켜고 끄는 걸 기억해야 하는 VPN 없이 제 dev 서버와 셸에 닿아야 합니다.</li>
<li>시스템이 어떻게 돌아가는지 보는 데 쓰는 것들, 즉 대시보드, 트레이스, <code>docker info</code>, 모델 목록은 저만 닿을 수 있어야 하고, 공개 URL을 절대 가져서는 안 됩니다.</li>
<li>인바운드 포트는 없습니다. 사무실 라우터에도, 집 라우터에도, 어디에도요.</li>
</ol>
<p>네 번째 요구사항이 예전의 답, 즉 포트 포워딩과 동적 DNS 이름을 배제합니다. 잔재주도 상당수 배제되죠. devbox와 노트북 둘 다 제가 통제하지 못하는 NAT 뒤에 있고, 노트북은 몇 시간마다 다른 NAT 뒤에 있습니다.</p>

<h2 class="relative group">이전: SSH 터널 저장소
    <div id="이전-ssh-터널-저장소" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%9d%b4%ec%a0%84-ssh-%ed%84%b0%eb%84%90-%ec%a0%80%ec%9e%a5%ec%86%8c" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>첫 버전은 제가 직접 만든 작은 저장소였고, 지금도 바닥층으로 남아 있습니다. 노트북에서 &ldquo;SSH로 닿는 것들에 깔끔한 이름 붙이기&rdquo; 문제만 풀고, 그 이상은 하지 않습니다. dnsmasq가 <code>*.test</code>에 <code>127.0.0.1</code>로 답하고, Caddy가 호스트명에 따라 로컬 포트로 라우팅하면서 <code>tls internal</code>로 브라우저를 만족시키고, autossh launch agent 묶음이 SSH 포워드를 열어 두었다가 끊기면 다시 잇습니다. 호스트명이 그 물건이 어디 사는지 말해 줍니다.</p>
<table>
	<thead>
			<tr>
					<th>접미사</th>
					<th>의미</th>
					<th>실어 나르는 것</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>*.local.test</code></td>
					<td>이 Mac</td>
					<td>없음, dev 서버입니다</td>
			</tr>
			<tr>
					<td><code>*.ssh.test</code></td>
					<td>서버</td>
					<td>autossh 포워드</td>
			</tr>
			<tr>
					<td><code>*.cloud.test</code></td>
					<td>호스팅된 앱</td>
					<td>호스팅 플랫폼 자체 프록시</td>
			</tr>
	</tbody>
</table>
<p>그래서 <code>https://devbox-ollama.ssh.test/api/tags</code>는 워크스테이션의 모델 목록을 보여 주고, <code>https://api.local.test</code>는 제 기계에서 도는, 지금 작업 중인 앱의 API입니다.</p>
<p><code>.ssh</code>나 <code>.cloud</code> 같은 새 최상위 이름이 아니라 <code>.test</code>의 서브도메인이고, <code>.local</code>도 아닙니다. RFC 6761이 바로 이런 용도로 <code>.test</code>를 예약해 두었고, <code>.local</code>은 mDNS 몫입니다. 그걸 가로채면 Mac에서 Bonjour 탐색이 멈춥니다. dnsmasq가 이미 <code>*.test</code> 전체를 와일드카드로 잡고 있으니 라벨 하나 더 붙이는 데 드는 비용은 없습니다. <code>127.0.0.1</code>을 가리키는 <code>/etc/resolver/test</code> 파일 하나면 되고, 그 뒤로 DNS를 더 건드릴 일은 영영 없습니다.</p>

<h3 class="relative group">존재하지 않는 이름에 HTTPS 붙이기
    <div id="존재하지-않는-이름에-https-붙이기" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%a1%b4%ec%9e%ac%ed%95%98%ec%a7%80-%ec%95%8a%eb%8a%94-%ec%9d%b4%eb%a6%84%ec%97%90-https-%eb%b6%99%ec%9d%b4%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>저 이름들은 전부 브라우저가 신뢰하는 인증서로 HTTPS 서비스됩니다. 제 노트북에만 존재하는 호스트명에 그게 된다니 불가능하게 들리죠. Caddy는 자체 내부 인증 기관으로 이걸 해냅니다. 사이트에 <code>tls internal</code>을 넣으면 Caddy는 Let&rsquo;s Encrypt에 요청하는 대신 로컬 CA에서 그 사이트의 인증서를 발급하고, 설치 스크립트가 한 번 실행하는 <code>caddy trust</code>가 그 CA의 루트를 macOS 시스템 키체인(Chrome과 Safari가 읽는 곳)과 Firefox 자체 저장소에 넣습니다. 그다음부터 <code>https://anything.test</code>는 인증서 경고 없이 열리고, 평범한 <code>http://</code>는 그쪽으로 리다이렉트되며, 아무것도 기계 밖으로 나가지 않습니다.</p>
<p>겉모양만의 문제가 아닙니다. 요즘 웹 플랫폼의 상당 부분이 평범한 HTTP에서는 동작을 거부합니다. secure 쿠키, 서비스 워커, 클립보드 API, <code>SameSite=None</code>이 들어간 모든 것이요. 웹 빌드를 평범한 <code>http://localhost</code>에서 테스트하면 실제 HTTPS 도메인에서 나타날 버그가 가려집니다. <code>https://app.local.test</code>에서 테스트하면 그 버그가 제 노트북에서 대신 드러납니다.</p>
<p>사이트 블록 하나입니다. 포트만 빼면 모든 서비스가 똑같습니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-caddyfile" data-lang="caddyfile"><span class="line"><span class="cl"><span class="gh">devbox-ollama.ssh.test</span>, <span class="gh">devbox-ollama.test</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="k">tls</span> <span class="no">internal</span>
</span></span><span class="line"><span class="cl">	<span class="k">import</span> cors
</span></span><span class="line"><span class="cl">	<span class="k">reverse_proxy</span> <span class="n">127.0.0.1</span><span class="p">:</span><span class="mi">11435</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="k">import</span> proxyhdr
</span></span><span class="line"><span class="cl">		<span class="k">transport</span> <span class="s">http</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="k">keepalive</span> <span class="no">off</span>
</span></span><span class="line"><span class="cl">			<span class="k">dial_timeout</span> <span class="mi">5s</span>
</span></span><span class="line"><span class="cl">		<span class="p">}</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>손이 많이 가는 부분은 두 스니펫이 맡습니다. <code>cors</code>는 <code>*</code>를 보내는 대신 호출한 쪽의 <code>Origin</code>을 그대로 돌려줍니다. 자격 증명이 끼는 순간 <code>*</code>는 허용되지 않기 때문입니다. 그리고 <code>OPTIONS</code> 프리플라이트에는 직접 204로 답해서 업스트림이 신경 쓸 필요가 없게 합니다. <code>proxyhdr</code>는 <code>Host</code> 헤더를 다이얼 주소로 바꿔 씁니다. 라우터 관리 페이지나 Chrome의 원격 디버깅 엔드포인트 같은 까다로운 백엔드는 <code>Host</code>를 검사하는데, 이게 없으면 <code>devbox-ollama.ssh.test</code>를 처음 듣는 이름이라며 거부합니다. Ollama의 자체 origin 검사는 포트와 상관없이 루프백을 받아 주니, <code>Host</code>를 <code>127.0.0.1:11435</code>로 바꿔 쓰면 통과합니다.</p>
<p><code>keepalive off</code>는 제가 틀렸을 법한 부분입니다. 업스트림은 네트워크가 바뀔 때마다 autossh가 허물고 다시 세우는 SSH 포워드입니다. keep-alive가 켜져 있으면 Caddy는 이미 죽은 터널에 대한 풀링된 연결을 쥐고 있다가, 리로드할 때까지 영문 모를 404를 돌려줍니다. 끄면 요청마다 새로 다이얼하는데, 루프백에서는 측정할 만한 비용이 없습니다.</p>
<p>각 서비스는 예전의 평평한 이름(<code>devbox-ollama.test</code>)으로도 답하기 때문에, 접미사 체계 이전의 북마크도 여전히 동작합니다.</p>

<h3 class="relative group">메뉴 막대
    <div id="메뉴-막대" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%a9%94%eb%89%b4-%eb%a7%89%eb%8c%80" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>launchd가 포워드 십여 개를 열어 두고 있으니 제가 계속 던진 질문은 &ldquo;살아 있나?&ldquo;였고, 그 답은 세 군데에 흩어져 있었습니다. <code>launchctl list</code>, <code>lsof</code>, 그리고 <code>curl</code>. 그래서 작은 메뉴 막대 플러그인으로 한곳에 모았습니다. 호스트명마다 옆에 점이 하나씩 붙습니다.</p>
<ul>
<li>🟢 서비스가 터널을 통한 요청에 답했습니다</li>
<li>🟡 터널은 열려 있지만 그 뒤의 서비스가 조용합니다</li>
<li>🔴 로컬 포트에서 아무것도 듣고 있지 않으니, 터널 자체가 죽었습니다</li>
</ul>
<p>쓸모 있는 건 노란색입니다. 초록/빨강 검사로는 &ldquo;SSH 포워드가 죽었다&quot;와 &ldquo;반대편에서 Ollama가 뻗었다&quot;를 구분할 수 없고, 둘은 고치는 방법이 전혀 다릅니다. 플러그인은 자체 서비스 목록을 따로 두지 않고 다른 것들과 같은 프록시 설정과 SSH 설정을 읽으니, 거기에 서비스를 추가하면 메뉴에도 추가됩니다.</p>
<p>첫 버전은 욕심이 과했습니다. 10초마다 새로 고치면서 서비스마다 프로세스를 몇 개씩 띄워 한 번 돌 때 70개쯤 됐고, macOS는 곧바로 이 플러그인을 &ldquo;에너지를 많이 사용하는 앱&quot;에 올렸죠. 한 번 돌 때 스냅샷을 한 번만 뜨고 몇 분 간격으로 새로 고치게 바꾸니 코어 하나의 0.1% 정도로 내려왔습니다. 배터리를 갉아먹는 상태 표시는 도움이 안 되니, 처음부터 비용을 따져 둘 만합니다.</p>
<p>구멍이 하나 있었는데, 하필 중요한 구멍이었습니다. devbox에는 공개 이름이 없습니다. 그 SSH 포워드는 <code>192.168.x</code> 주소로 다이얼했으니, 노트북이 사무실 LAN에 있을 때만 동작했습니다. 사무실을 벗어나면 메뉴 막대가 빨개진 채로 그대로였고, 노트북을 아끼려고 워크스테이션으로 옮겨 둔 모든 것이 정확히 제가 그 옆에 앉아 있지 않을 때 닿지 않았습니다.</p>

<h2 class="relative group">방향을 제대로 잡은 터널 두 개
    <div id="방향을-제대로-잡은-터널-두-개" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%b0%a9%ed%96%a5%ec%9d%84-%ec%a0%9c%eb%8c%80%eb%a1%9c-%ec%9e%a1%ec%9d%80-%ed%84%b0%eb%84%90-%eb%91%90-%ea%b0%9c" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>해결책은 세 부분이고, 앞의 둘이 Cloudflare Tunnel입니다.</p>
<p>Cloudflare Tunnel은 <code>cloudflared</code>가 Cloudflare 엣지로 아웃바운드 연결을 붙잡고 있는 것입니다. Cloudflare는 그 연결로 요청을 내려보내니, NAT가 몇 겹이든 그 뒤의 기계가 라우터에서 아무것도 듣지 않고도 닿을 수 있게 됩니다. 노트북을 따라 호텔 wifi로 가고, CGNAT에서도 살아남습니다.</p>
<p>저는 기계마다 하나씩 두 개를 돌리는데, 그 이유는 한 문장 들일 만합니다. 뻔한 지름길은 노트북에 터널 하나를 두고, 사무실 LAN에 있는 devbox의 SSH 포트를 가리키는 ingress 규칙을 하나 더 넣는 것이었습니다. 그러면 devbox에는 아무것도 설치할 필요가 없습니다. 대신 노트북을 거쳐 라우팅되는데, 노트북은 사무실을 떠나는 기계입니다. 고치려던 바로 그 상황에서 고장 났을 겁니다. devbox는 제자리에 있으니, devbox가 엣지로 가는 자기 연결을 쥡니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># config.yml on devbox (runs as a systemd service)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">auto       </span><span class="w"> </span><span class="c"># prefer QUIC, fall back to HTTP/2 over TCP 443</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">ha-connections</span><span class="p">:</span><span class="w"> </span><span class="m">4</span><span class="w">     </span><span class="c"># mains-powered, its job is to stay reachable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">warp-routing</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">connectTimeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">ingress</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">ssh-devbox.example.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">tcp://127.0.0.1:22</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">http_status:404</span></span></span></code></pre></div></div>
<p><code>ssh://</code>가 아니라 <code>tcp://</code>라는 건 헷갈리기 쉬운 부분입니다. <code>ssh://</code>는 Cloudflare의 브라우저 렌더링 터미널을 선택하는데, 이건 스트림을 웹 페이지용으로 감쌉니다. 평범한 <code>ssh</code> 클라이언트를 거기로 통과시키면 배너까지는 받고 키 교환에서 죽습니다. <code>tcp://</code>는 <code>cloudflared access ssh</code>가 기대하는 평범한 바이트 파이프이고, 덤으로 <code>scp</code>, <code>rsync</code>, SSH 위의 git, 포트 포워드까지 실어 나릅니다.</p>
<p>그 마지막 부분이 SSH 계층의 구멍을 막았습니다. 이제 SSH 설정이 연결마다 경로를 고릅니다.</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-sshconfig" data-lang="sshconfig">Match originalhost devbox !exec &#34;nc -z -G 1 &lt;office-lan-ip&gt; 22 &gt;/dev/null 2&gt;&amp;1&#34;
    ProxyCommand cloudflared access ssh --hostname ssh-devbox.example.com ...</code></pre></div>
<p>사무실 LAN에서는 탐침이 성공하고 SSH가 한 홉으로 직접 갑니다. 그 밖의 어디서든 탐침이 실패하고 SSH는 Cloudflare를 거치는데, Access 서비스 토큰으로 인증하니 브라우저 로그인이 끼어들지 않습니다. <code>Match host</code>가 아니라 <code>Match originalhost</code>입니다. <code>host</code>는 <code>HostName</code>이 치환된 뒤에 매칭되니, 그 시점엔 이름이 이미 IP라서 블록이 절대 발동하지 않습니다. 결과적으로 <code>devbox-ollama.ssh.test</code>가 사무실에서도, 집에서도, 기차에서도 동작하고, 메뉴 막대의 점은 초록으로 남습니다.</p>
<p>노트북의 터널은 반대 방향으로 갑니다. Mac의 dev 서버에 진짜 공개 HTTPS 호스트명을 줍니다. 작업 중인 앱마다 API, WebSocket 서버, 웹 빌드요. 전부 Cloudflare Access 뒤에 있는데, 이건 뒤에서 다시 다루겠습니다. 이걸 무모한 게 아니라 안전한 것으로 만드는 부분이니까요.</p>
<p>메뉴 막대도 두 터널을 알게 됐는데, 탐침으로는 할 수 없었습니다. 인바운드 터널에는 확인할 로컬 포트가 없고, 커넥터 둘 중 하나는 노트북에서 돌지도 않으며, 뻔한 테스트인 <code>ssh-devbox.example.com</code>에 curl 날리기는 커넥터가 붙어 있든 말든 Cloudflare 엣지가 답하니, 죽은 터널에서도 초록으로 읽힐 겁니다. launchd도 나을 게 없습니다. <code>cloudflared</code>는 엣지 연결이 전부 끊긴 채로 &ldquo;loaded, exit 0&rdquo; 상태로 앉아 있거든요. 그래서 터널 상태는 각 터널에 건강한 커넥터가 붙어 있는지 Cloudflare API에 물어서 확인합니다. <code>ssh</code>가 닿을지를 결정하는 게 바로 그쪽이니까요. API에 닿지 못하면 빨갛게 그리지 않고 그 부분을 통째로 뺍니다. 메뉴는 힐끗 보고 읽는 것이라, 거기서 잘못된 경보는 빠진 한 줄보다 비쌉니다.</p>

<h2 class="relative group">세 번째 부분: WARP가 이걸 네트워크로 만듭니다
    <div id="세-번째-부분-warp가-이걸-네트워크로-만듭니다" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%84%b8-%eb%b2%88%ec%a7%b8-%eb%b6%80%eb%b6%84-warp%ea%b0%80-%ec%9d%b4%ea%b1%b8-%eb%84%a4%ed%8a%b8%ec%9b%8c%ed%81%ac%eb%a1%9c-%eb%a7%8c%eb%93%ad%eb%8b%88%eb%8b%a4" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>호스트명이 붙은 터널은 HTTPS로 말하는 것, 그리고 <code>cloudflared</code>를 돌릴 수 있는 기계에서의 SSH에는 훌륭합니다. 폰은 못 돌립니다. 게다가 터널 호스트명은 TCP 엔드포인트가 아닙니다. Cloudflare 엣지는 거기서 HTTPS와 WebSocket만 종단하니, <code>ssh.example.com</code>의 22번 포트는 닫혀 있고 Access를 아무리 만지작거려도 열리지 않습니다.</p>
<p>그게 WARP의 일입니다. Cloudflare One Agent(엔터프라이즈용 WARP 클라이언트입니다. 조직에 가입할 수 없는 일반 소비자용 &ldquo;1.1.1.1&rdquo; 앱이 아니고요)를 노트북과 폰에 설치해 제 Zero Trust 조직에 등록했습니다. 각 터널이 사설 경로를 광고하면, WARP를 돌리는 기기는 그 주소로 평범한 소켓을 열어 올바른 기계에 닿을 수 있습니다. &ldquo;이 서비스에 이름으로 닿기&quot;가 아니라 &ldquo;이 기계들이 나와 같은 네트워크에 있다&quot;가 되는 거죠.</p>
<table>
	<thead>
			<tr>
					<th>주소</th>
					<th>기계</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>198.18.42.1</code> / <code>fd12:3456:789a::1</code></td>
					<td>MacBook Air</td>
			</tr>
			<tr>
					<td><code>198.18.42.2</code> / <code>fd12:3456:789a::2</code></td>
					<td>devbox</td>
			</tr>
			<tr>
					<td><code>198.18.42.3</code> / <code>fd12:3456:789a::3</code></td>
					<td>두 번째 Mac</td>
			</tr>
	</tbody>
</table>
<p>이 주소들은 부팅 때 만들어지는 각 기계의 루프백 별칭이라, DHCP나 제가 어떤 라우터 뒤에 있는지와 아무 상관이 없습니다. <code>198.18.42.2</code>는 사무실에서도, 집에서도, 5G에 붙은 폰에서도 devbox입니다. Mac의 진짜 LAN 주소는 옮길 때마다 바뀌지만, 이 주소는 절대 바뀌지 않습니다.</p>
<p>스플릿 터널은 <strong>include 모드</strong>이고, 저 경로들(과 WARP가 스스로 쓰는 Cloudflare 대역)만 들어 있습니다. 이게 클라이언트를 켜 두고 살 수 있게 만드는 부분입니다. 기본값은 exclude 모드인데, WARP가 사설 대역 목록을 뺀 모든 것을 실어 나르니 폰이 보내는 모든 바이트가 Cloudflare를 돌아갑니다. 배터리와 지연 시간 비용이 실제로 있고, 결국 필요할 때만 켜게 됐을 만큼입니다. 그러면 요점이 무너지죠. include 모드에서는 제 기계 세 대로 가는 트래픽만 WARP를 거치고 나머지는 전부 평소 경로로 손대지 않은 채 나갑니다. 그래서 클라이언트는 그냥 켜진 채로 있습니다. 노트북의 <code>warp-cli status</code>는 하루 종일 <code>Connected</code>라고 하고, 저는 신경 쓰지 않습니다.</p>
<p>전송 방식은 UDP 2408 위의 WireGuard가 아니라, UDP 443 위의 HTTP/3에 HTTP/2 폴백이 붙은 MASQUE로 해 두었습니다. 일부 통신사와 게스트 네트워크는 2408을 막지만, 443을 막는 곳은 거의 없습니다. 제 조직 정책에서는 MASQUE에 양자내성 키 합의도 켜져 있는데, 찾아서 켠 건 아니지만 마다할 이유도 없죠.</p>

<h2 class="relative group">왜 198.18이고, 왜 IPv6 주소가 있는지
    <div id="왜-19818이고-왜-ipv6-주소가-있는지" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%99%9c-19818%ec%9d%b4%ea%b3%a0-%ec%99%9c-ipv6-%ec%a3%bc%ec%86%8c%ea%b0%80-%ec%9e%88%eb%8a%94%ec%a7%80" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>주소가 좀 이상해 보이는데, 처음 고른 주소는 배울 게 있는 방식으로 틀렸습니다.</p>
<p>첫 시도는 <code>10.10.0.1</code>이었습니다. 다들 손이 가는 주소니까요. 이건 두 스플릿 터널 모드 모두에서 실패합니다. exclude 모드에서는 WARP의 기본 목록이 RFC 1918 대역 전부와 <code>100.64.0.0/10</code>을 제외하니, 클라이언트가 그 주소로 가는 트래픽을 기기를 떠나기도 전에 버렸을 겁니다. include 모드에서는 제가 어쩌다 붙는 실제 <code>10.x</code> 네트워크와 충돌했을 겁니다. 카페, 코워킹 스페이스, 그리고 꽤 많은 사무실이 딱 그 대역을 씁니다. <code>198.18.0.0/15</code>는 RFC 2544 벤치마킹 대역입니다. 예약돼 있고, 공용 인터넷에서 라우팅되는 일이 없고, WARP의 exclude 목록에 없으며, 평범한 네트워크라면 쓰지 않습니다. IPv6 주소가 ULA(<code>fd00::/8</code>)인 것도 같은 이유입니다.</p>
<p>IPv6가 애초에 왜 있는지가 더 재미있는 이야기입니다. v4 주소가 동작하자 폰에서 wifi로 테스트해 봤고, 잘 됐고, 끝났다고 선언했습니다. 5G에서는 안 됐습니다. Agent는 <em>connected</em>라고 했고, 터널은 멀쩡했고, Access도 괜찮았습니다. wifi 테스트가 그 전부를 증명했으니까요. 문제는 요즘 모바일 네트워크가 대부분 IPv6 전용이라는 점이었습니다. 폰에는 네이티브 IPv4가 아예 없었고, v4 리터럴에는 통신사의 464XLAT 변환기를 통해 닿고 있었는데, include 목록에 v4 접두사만 있으면 그 변환기는 터널 <em>바깥</em>에 놓입니다. 트래픽은 <code>198.18.0.0/15</code>가 라우팅되지 않는 공용 인터넷으로 나갔고, 소리 없이 버려졌습니다. 폰에도 오류가 없고, 커넥터 로그에도 아무것도 없었습니다.</p>
<p>해결책은 모든 기계에 폰이 IPv6로 네이티브하게 닿을 수 있는 두 번째 주소를 주고, 그걸 경로와 include 목록에 추가하는 것입니다. 이제 모바일 데이터에서 폰의 SSH 클라이언트가 실제로 쓰는 건 <code>fd12:3456:789a::</code> 주소입니다.</p>

<h2 class="relative group">오프로딩: 단말기로서의 노트북
    <div id="오프로딩-단말기로서의-노트북" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%98%a4%ed%94%84%eb%a1%9c%eb%94%a9-%eb%8b%a8%eb%a7%90%ea%b8%b0%eb%a1%9c%ec%84%9c%ec%9d%98-%eb%85%b8%ed%8a%b8%eb%b6%81" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>이 셋업 전체가 존재하는 이유가 이 부분입니다.</p>
<p><strong>모델과 파인튜닝.</strong> Ollama는 devbox에서 돌고 <code>127.0.0.1:11434</code>에 바인딩됩니다. 인터넷은 고사하고 사무실 LAN의 다른 기계에서도 보이지 않습니다. 노트북에서는 <code>https://devbox-ollama.ssh.test</code>로 나타나고, 포트 번호만 원하는 도구에는 그냥 포트로 나타납니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">OLLAMA_HOST</span><span class="o">=</span>127.0.0.1:11435 ollama list</span></span></code></pre></div></div>
<p>11434가 아니라 11435인 건 일부러입니다. 11434는 Ollama 자체 기본값이라, 노트북에서도 로컬 Ollama를 돌리게 되면 둘이 포트를 두고 경쟁하고, 어느 쪽이 먼저 떴느냐에 따라 &ldquo;로컬&rdquo; 쪽에서 devbox의 답을 소리 없이 받게 됩니다.</p>
<p>실제 효과는, AI 기능을 만들 때 그 기능이 호출하는 모델이 제가 어디에 있든 노트북이 아니라 워크스테이션 GPU라는 점입니다. 평가 실행, 문서 한 무더기 임베딩, 파인튜닝은 devbox에서 돌고, 노트북 팬은 조용합니다. 에이전트를 프로토타이핑할 때는 워크스테이션의 로컬 모델을 가리키게 해서 API 청구서 없이 툴 호출 반복을 마음껏 태울 수 있고, 루프가 제대로 잡히면 호스팅 모델로 바꾸면 됩니다.</p>
<p><strong>오래 도는 에이전트.</strong> 한 시간 도는 에이전트 세션이 제 노트북 덮개가 열려 있는지에 달려 있어서는 안 됩니다. 대신 devbox에서 돌리면 노트북은 그저 거기로 난 창문입니다. 덮개를 닫고, 나중에 어디서든, 혹은 폰에서 <code>ssh devbox</code>를 하면 여전히 거기 있습니다. 터널 이전에는 이게 사무실에서만 됐는데, 사무실은 정확히 이게 필요 없는 곳이었죠.</p>
<p><strong>빌드와 클러스터.</strong> devbox의 클러스터에는 자체 레지스트리가 있어서, 이미지를 노트북에서 호텔 wifi로 올리는 대신 워크스테이션에서, 실행될 곳 바로 옆에서 빌드하고 푸시할 수 있습니다. 클러스터 대시보드, ingress 대시보드, 레지스트리 UI는 노트북에서 전부 <code>*.ssh.test</code> 호스트명입니다.</p>
<p><strong>모바일 루프.</strong> 폰은 노트북과 같은 사설 네트워크에 있고, 노트북의 dev 서버에는 Access 뒤의 공개 호스트명도 있습니다. 그래서 앱의 네이티브 빌드를 들고 건물 밖으로 나가 5G에서 써도, 제 책상에서 도는 API와 통신합니다. 제가 devbox에 띄워 두었다면 그쪽과 통신하고요. 실제 모바일 네트워크 위의 실제 기기로 피드백 루프를 돌리려면 예전에는 공유 스테이징 서버에 배포해야 했습니다. 지금은 파일을 저장하면 됩니다.</p>
<p>앱 버그로 착각하고 디버깅하기 전에 알아 둘 만한 함정이 하나 있습니다. Access는 WebSocket 호스트명도 보호하는데, 네이티브 WebSocket 클라이언트는 브라우저가 아닙니다. <code>CF_Authorization</code> 쿠키를 들고 있지 않으니, 네이티브 빌드가 WebSocket 서버에 연결하면 엣지에서 403으로 거부되는데, 이게 내 서버가 내는 오류와는 전혀 닮지 않았습니다. 브라우저 탭은 로그인할 때 받은 쿠키를 이미 들고 있으니 괜찮습니다. 브라우저가 아닌 모든 것(네이티브 앱, 스크립트, dev API를 호출하는 에이전트)에 대한 답은 Access <strong>서비스 토큰</strong>이고, 헤더 두 개로 보냅니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">CF-Access-Client-Id:     &lt;client_id&gt;
</span></span><span class="line"><span class="cl">CF-Access-Client-Secret: &lt;client_secret&gt;</span></span></code></pre></div></div>
<p>토큰은 그 자체로는 아무 권한도 주지 않습니다. Access는 정책에 그 토큰이 명시된 앱에서만 받아 주니, 토큰이 닿는 범위는 정확히 dev 호스트명 목록이고 그보다 넓지 않습니다. 저는 그 정책을 <code>allow</code>가 아니라 <code>non_identity</code> 결정으로 분류해 두는데, 그러면 감사 로그에서 로그인한 사람이 아니라 bearer 시크릿으로 보입니다.</p>

<h2 class="relative group">인터넷은 절대 보지 못하는 통계
    <div id="인터넷은-절대-보지-못하는-통계" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%9d%b8%ed%84%b0%eb%84%b7%ec%9d%80-%ec%a0%88%eb%8c%80-%eb%b3%b4%ec%a7%80-%eb%aa%bb%ed%95%98%eb%8a%94-%ed%86%b5%ea%b3%84" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>제가 원한 것의 나머지 절반은 가시성이었습니다. 시스템이 어떻게 돌아가는지 알려 주는 것 대부분은 공개돼 있지 않은 게 맞습니다. devbox에서는 이런 것들입니다.</p>
<ul>
<li>ingress 프록시 대시보드, 그리고 그 트레이스가 담긴 트레이싱 UI</li>
<li>Kubernetes 클러스터용 대시보드</li>
<li>컨테이너 레지스트리 UI(읽기 전용)</li>
<li>Ollama 모델 목록과 지금 로드된 것</li>
<li><code>docker info</code>와 컨테이너 통계</li>
</ul>
<p>이 중 어느 것도 공개 호스트명이 없고, 앞으로도 없을 겁니다. 루프백이나 클러스터 서비스 IP에 바인딩돼 있고, 그 자체가 터널을 타는 SSH 포워드로 닿습니다. 폰의 모니터링 앱은 WARP 너머 SSH로 <code>docker info --format '{{json .}}'</code>를 읽으니, 컨테이너가 떠 있는지 아니면 뭔가 메모리를 다 먹어 치웠는지 어디서든 볼 수 있습니다.</p>
<p>그 앱 덕분에 언급할 만한 작은 버그를 하나 찾았습니다. Mac의 터널용 sshd가 디버깅하던 시절 그대로 <code>LogLevel DEBUG2</code>였습니다. <code>-e</code>를 주면 sshd는 stderr로 로그를 남기는데, 세션에서는 그 stderr가 <em>곧</em> SSH 채널의 stderr입니다. 모든 명령의 출력 끝에 <code>debug2: do_setup_env: set TMPDIR</code>가 붙어서 돌아왔고, 두 스트림을 합치는 폰 앱은 유효한 JSON 뒤에 로그 한 줄이 붙은 걸 받아 파싱에 실패하고는 통째로 오류로 보여 줬습니다. <code>LogLevel INFO</code>로 고쳤습니다. 디버깅이 끝나면 로그 레벨을 되돌려 놓으세요.</p>
<p>이 네트워크에 <em>올리지 않는</em> 것도 똑같이 의도적입니다. 예전 autossh 터널은 제가 관리를 돕는 남의 네트워크의 라우터 몇 대에도 닿습니다. 그것들에는 공개 호스트명도, WARP 경로도 주지 않습니다. 인터넷에 나온 라우터 관리 페이지는 로그인 뒤에 있어도 골칫거리고, 제가 넓힐 수 있는 제 네트워크도 아니니까요.</p>

<h2 class="relative group">Access는 선택이 아닙니다
    <div id="access는-선택이-아닙니다" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#access%eb%8a%94-%ec%84%a0%ed%83%9d%ec%9d%b4-%ec%95%84%eb%8b%99%eb%8b%88%eb%8b%a4" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>Cloudflare Tunnel 호스트명은 DNS 레코드가 생기는 순간 공용 인터넷에 올라갑니다. 앞에 방화벽이 없습니다. 포트를 열지 않는다는 게 애초에 요점이니까요. 보호는 Cloudflare 엣지에서 와야 하고, 그게 Access가 하는 일입니다. 방문자가 누구인지 증명하기 전까지는 요청을 아예 전달하지 않으니, 인증되지 않은 스캐너는 노트북에 절대 닿지 못합니다. 이게 중요한 건 이것들이 dev 서버이기 때문입니다. 설정값이 그대로 드러나는 디버그 모드 트레이스백, 열려 있는 JavaScript 번들러. 이 호스트명들 뒤에 있는 것 중 인터넷을 마주하도록 만들어진 건 하나도 없습니다.</p>
<p>정책은 제 이메일 주소들만 허용하고 나머지는 아무것도 허용하지 않으며, 증명 방법은 이메일 일회용 PIN과 SSO 두 가지입니다. 세션은 8시간인데, 현실적인 위협은 누군가 PIN을 무차별 대입하는 게 아니라 하루짜리 쿠키를 품은 채 카페에 열어 두고 온 노트북이기 때문입니다.</p>
<p>SSH는 서로의 존재를 모르는 두 시스템이 두 번 인증합니다. Access는 TCP 스트림이 sshd에 닿아도 되는지를 정하고, sshd는 그래도 제 키를 요구합니다. 어느 하나를 잃어도 기계가 열리지는 않습니다. Mac의 터널용 sshd는 Cloudflare의 SSH CA도 신뢰해서, Access가 제 SSO 세션으로 발급한 단기 인증서로 저를 로그인시킬 수 있습니다. 그러면서 <code>authorized_keys</code>도 나란히 동작하니, Access 설정이 망가져도 제 노트북에서 제가 쫓겨나는 일은 없습니다.</p>
<p>제일 마음에 드는 건 작은 부분입니다. 노출은 세 가지가 맞아떨어질 때 일어납니다. DNS 레코드, 돌고 있는 커넥터, 활성 상태의 존. 이 셋은 제가 통제하지 못하는 순서로, 서로 다른 시점에 참이 됩니다. 존은 네임서버를 바꾸고 얼마 뒤에 혼자 활성으로 바뀌는데, Mac이 잠들어 있을 때일 수도 있습니다. 그래서 launch agent는 <code>cloudflared</code>를 직접 실행하지 않습니다. 대신 터널 자체 설정에서 호스트명을 읽어 Access API에 어느 것이 보호되고 있는지 묻고, 하나라도 빠져 있으면 시작을 거부하는 사전 점검을 돌립니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">HOSTNAMES</span><span class="o">=</span><span class="k">$(</span>grep -E <span class="s1">&#39;^\s*-\s*hostname:&#39;</span> <span class="s2">&#34;</span><span class="nv">$CONFIG</span><span class="s2">&#34;</span> <span class="p">|</span> sed -E <span class="s1">&#39;s/.*hostname:\s*//&#39;</span> <span class="p">|</span> sort -u<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">PROTECTED</span><span class="o">=</span><span class="k">$(</span>curl -fsS -H <span class="s2">&#34;Authorization: Bearer </span><span class="nv">$CLOUDFLARE_API_TOKEN</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="s2">&#34;https://api.cloudflare.com/client/v4/accounts/</span><span class="nv">$CLOUDFLARE_ACCOUNT_ID</span><span class="s2">/access/apps?per_page=100&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="p">|</span> python3 -c <span class="s1">&#39;import sys,json; print(&#34;\n&#34;.join(a.get(&#34;domain&#34;,&#34;&#34;) for a in json.load(sys.stdin)[&#34;result&#34;]))&#39;</span><span class="k">)</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="o">||</span> <span class="o">{</span> <span class="nb">echo</span> <span class="s2">&#34;could not reach the Access API. refusing (fail closed).&#34;</span><span class="p">;</span> <span class="nb">exit</span> 1<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> h in <span class="nv">$HOSTNAMES</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    grep -qxF <span class="s2">&#34;</span><span class="nv">$h</span><span class="s2">&#34;</span> <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$PROTECTED</span><span class="s2">&#34;</span> <span class="o">||</span> <span class="o">{</span> <span class="nb">echo</span> <span class="s2">&#34;NOT starting: </span><span class="nv">$h</span><span class="s2"> has no Access app&#34;</span><span class="p">;</span> <span class="nb">exit</span> 1<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl"><span class="nb">exec</span> cloudflared tunnel --config <span class="s2">&#34;</span><span class="nv">$CONFIG</span><span class="s2">&#34;</span> run</span></span></code></pre></div></div>
<p>launch agent에는 <code>KeepAlive</code>가 걸려 있어서, 거부되면 30초마다 알아서 재시도하고 Access 앱이 생기면 터널이 혼자 올라옵니다. 닫힌 채로 실패하면 장애를 치르고, 열린 채로 실패하면 노출을 치릅니다. 둘 중 어느 쪽을 해명하고 싶은지는 압니다.</p>

<h2 class="relative group">발목을 잡은 네 가지
    <div id="발목을-잡은-네-가지" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%b0%9c%eb%aa%a9%ec%9d%84-%ec%9e%a1%ec%9d%80-%eb%84%a4-%ea%b0%80%ec%a7%80" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>위의 모든 게 지금은 동작합니다. 여기까지 오는 동안 넷 중 셋은 패턴이 같았습니다. 고장 난 부분은 어디에도 오류를 내지 않았고, 제 눈에 보이는 부분은 전부 정상이라고 보고했습니다.</p>
<p><strong>경로와 include 목록은 서로 다른 목록입니다.</strong> 터널 경로는 Cloudflare가 어떤 주소를 올바른 커넥터로 실어 나를 <em>의향</em>을 갖게 합니다. WARP include 목록은 클라이언트가 애초에 그걸 <em>보내게</em> 만드는 것입니다. devbox가 폰에서 닿지 않았는데, 경로는 있었지만 include 목록에는 여전히 Mac의 주소만 있었기 때문입니다. 폰은 패킷을 로컬에서 버렸고, 커넥터는 흐름을 보지 못했고, 모든 대시보드는 초록이었습니다. 새 기계를 들일 때마다 주소를 양쪽 모두에 추가해야 합니다.</p>
<p><strong>IPv6 전용 모바일 네트워크의 IPv4 리터럴.</strong> 위에서 다뤘습니다. wifi에서는 되고, 5G에서는 안 되고, 오류는 없습니다. WARP 경로가 wifi에서는 되는데 모바일 데이터에서 안 되고 Agent가 connected로 나온다면, 터널이 아니라 라우팅 문제입니다.</p>
<p><strong>cloudflared 뒤의 macOS sshd.</strong> macOS는 sshd를 launchd 소켓 활성화로 띄웁니다. 연결마다 수명이 짧은 <code>sshd -i</code>가 하나씩 뜨는 식이죠. cloudflared 뒤에서는 배너가 클라이언트에 닿은 다음, 클라이언트의 식별 문자열이 돌아오기 전에 스트림이 끊겨서, 모든 로그인이 <code>kex_exchange_identification</code>에서 죽습니다. 양쪽 끝에서 확인했고, 평범한 TCP 릴레이로도 재현해서 Cloudflare 탓이 아님을 확인했습니다. 해결책은 <code>127.0.0.1:2222</code>와 WARP 루프백 주소에서 제 사용자로 도는 두 번째 독립 <code>sshd -D</code>인데, 오직 터널의 origin 노릇을 하려고 존재합니다. 시스템 설정의 원격 로그인은 건드리지 않았습니다. 평범하게 오래 사는 sshd를 쓰는 Linux인 devbox에는 이 문제가 애초에 없었습니다.</p>
<p><strong>더는 존재하지 않는 설정 필드.</strong> 예전 예제에는 터널 설정에 <code>warp-routing: enabled: true</code>가 있습니다. cloudflared 2026.8은 그 필드를 대놓고 거부합니다. WARP 라우팅은 블록이 있고 경로가 터널을 가리키기만 하면 켜집니다. 이건 적어도 오류를 냈는데, 다른 셋을 겪은 뒤라 거의 너그럽게 느껴졌습니다.</p>

<h2 class="relative group">무엇이 바뀌었나
    <div id="무엇이-바뀌었나" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%ac%b4%ec%97%87%ec%9d%b4-%eb%b0%94%eb%80%8c%ec%97%88%eb%82%98" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>솔직하게 요약하면, 하드웨어가 어디 있느냐에 따라 일할 곳을 고르는 걸 그만뒀습니다. 노트북은 배터리가 오래가는 좋은 키보드와 좋은 화면이고, 그 이상일 필요가 없습니다. 워크스테이션은 제가 어디에 있든 무거운 일을 맡습니다. 폰은 사무실 wifi에 묶인 무언가가 아니라 실제 네트워크 위의 실제 테스트 기기입니다. 그리고 시스템이 뭘 하는지 보는 데 쓰는 부분들은 저에게는 클릭 한 번 거리에 있고, 다른 누구에게는 존재하지 않습니다.</p>
<p>이 규모면 Cloudflare의 무료 Zero Trust 플랜에 들어갑니다. 제가 치른 건 시간, 그리고 써 두길 잘했다 싶은 README 하나입니다. 위에 적은 실패는 하나같이 두 번째에도 새로운 미스터리였을 테니까요.</p>
<p>비슷한 걸 고민하고 있다면, 여러분의 셋업에 이런 질문을 던져 보겠습니다.</p>
<ol>
<li>노트북이 언제나 닿을 수 있는 유일한 기계라는 이유만으로 노트북에서 돌고 있는 건 무엇인가요?</li>
<li>폰이 사무실 wifi에서만이 아니라 모바일 데이터로도 dev 서버에 닿나요? wifi 말고 5G에서 테스트해 보세요.</li>
<li>밖에서 보기 쉬운 방법이 그것뿐이라서 공개된 대시보드는 어느 것인가요? 하나하나가 사설 경로로 옮길 후보입니다.</li>
<li>내일 터널 호스트명이 Access 앱 없이 올라온다면, 무엇이 그걸 막나요? 답이 &ldquo;제가 기억하겠죠&quot;라면 사전 점검을 작성하세요.</li>
<li>여러분의 사설 주소는 여러분이 드나드는 네트워크가 절대 쓰지 않을 대역에 있나요?</li>
</ol>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-10-01-cloudflare-warp-dev-network/featured.jpg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>SSH 로그인 알림을 Slack으로 받기: PAM과 웹훅으로 설정하는 법</title>
      <link>https://jared.lynskey.co.nz/ko/posts/2021/2021-12-14-ssh-log/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ko/posts/2021/2021-12-14-ssh-log/</guid>
      <pubDate>Tue, 14 Dec 2021 11:30:03 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>Development</category>
      <category>Slack</category>
      <category>SSH</category>
      <category>Proactive</category>
      <category>DevOps</category>
      <description>SSH 로그인 알림을 Slack으로 받는 방법입니다. PAM과 Slack Incoming Webhook을 이용해 누군가 서버에 SSH 접속을 시도하면 알림이 오게 하고, 로그인 성공 알림도 선택적으로 켤 수 있습니다.</description>
      <content:encoded><![CDATA[
<h2 class="relative group">목적
    <div id="목적" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%aa%a9%ec%a0%81" aria-label="앵커">#</a>
    </span>
    
</h2>
<ul>
<li>SSH는 누군가 로그인을 시도할 때 원하는 명령을 실행하게 만들 수 있습니다. 이 글에서는 Slack과 연동하면서 PAM 실행 순서를 제어하는 방법을 소개합니다.</li>
</ul>

<h2 class="relative group">Slack API 웹훅
    <div id="slack-api-웹훅" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#slack-api-%ec%9b%b9%ed%9b%85" aria-label="앵커">#</a>
    </span>
    
</h2>
<ul>
<li>
<p>앱 만드는 방법은 <a href="https://api.slack.com/messaging/webhooks"  target="_blank" rel="noreferrer">여기</a>에 잘 정리되어 있습니다.</p>
</li>
<li>
<p>Slack 앱을 만들고 웹훅을 활성화한 뒤, SSH 알림을 받을 Incoming Webhook을 하나 만들어 두세요.</p>
</li>
</ul>

<h2 class="relative group">로그인 실패 알림 스크립트 만들기
    <div id="로그인-실패-알림-스크립트-만들기" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%a1%9c%ea%b7%b8%ec%9d%b8-%ec%8b%a4%ed%8c%a8-%ec%95%8c%eb%a6%bc-%ec%8a%a4%ed%81%ac%eb%a6%bd%ed%8a%b8-%eb%a7%8c%eb%93%a4%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h2>
<ul>
<li>스크립트는 아무 데나 둬도 됩니다. 저는 <code>/var/opt/notify-attempt.sh</code>에 뒀습니다.</li>
</ul>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">#!/bin/bash
</span></span><span class="line"><span class="cl">if [ &#34;$PAM_TYPE&#34; != &#34;close_session&#34; ]; then
</span></span><span class="line"><span class="cl">        url=&#34;&lt;YOUR SLACK WEBHOOK&gt;&#34;
</span></span><span class="line"><span class="cl">        channel=&#34;#channel&#34;
</span></span><span class="line"><span class="cl">        host=&#34;$(hostname)&#34;
</span></span><span class="line"><span class="cl">        content=&#34;\&#34;attachments\&#34;: [ { \&#34;mrkdwn_in\&#34;: [\&#34;text\&#34;, \&#34;fallback\&#34;], \&#34;fallback\&#34;: \&#34;SSH login: $PAM_USER connected to \`$host\`\&#34;, \&#34;text\&#34;: \&#34;SSH login to \`$host\`\&#34;, \&#34;fields\&#34;: [ { \&#34;title\&#34;: \&#34;User\&#34;, \&#34;value\&#34;: \&#34;$PAM_USER\&#34;, \&#34;short\&#34;: true }, { \&#34;title\&#34;: \&#34;IP Address\&#34;, \&#34;value\&#34;: \&#34;$PAM_RHOST\&#34;, \&#34;short\&#34;: true } ], \&#34;color\&#34;: \&#34;#F35A00\&#34; } ]&#34;
</span></span><span class="line"><span class="cl">        curl -X POST --data-urlencode &#34;payload={\&#34;channel\&#34;: \&#34;$channel\&#34;, \&#34;mrkdwn\&#34;: true, \&#34;username\&#34;: \&#34;SSH Notifications\&#34;, $content, \&#34;icon_emoji\&#34;: \&#34;:inbox-tray:\&#34;}&#34; &#34;$url&#34; &amp;
</span></span><span class="line"><span class="cl">fi
</span></span><span class="line"><span class="cl">exit</span></span></code></pre></div></div>

<h3 class="relative group">다른 SSH 이벤트도 받고 싶다면
    <div id="다른-ssh-이벤트도-받고-싶다면" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%8b%a4%eb%a5%b8-ssh-%ec%9d%b4%eb%b2%a4%ed%8a%b8%eb%8f%84-%eb%b0%9b%ea%b3%a0-%ec%8b%b6%eb%8b%a4%eb%a9%b4" aria-label="앵커">#</a>
    </span>
    
</h3>
<ul>
<li>로그인 성공 같은 다른 이벤트까지 알림을 받고 싶다면 파일을 두 개 만드는 걸 추천합니다.</li>
<li>각 파일에 Slack으로 보낼 JSON을 넣어두면 됩니다.</li>
<li><code>fallback\&quot;: \&quot;SSH login: $PAM_USER connected to \</code>$host`&quot;` 줄은 이벤트에 맞게 고쳐 주세요.</li>
<li>OpenSSH가 이 스크립트들을 어떻게 쓰는지는 아래 설정을 보면 알 수 있습니다.</li>
</ul>

<h3 class="relative group">ssh 설정
    <div id="ssh-설정" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ssh-%ec%84%a4%ec%a0%95" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>성공 알림은 주석 처리해 뒀습니다. 쓰고 싶다면 주석을 풀면 성공할 때도 Slack으로 알림이 갑니다.
해당 줄: <code># auth  optional            pam_exec.so /var/opt/notify-login.sh</code></p>
<p>[success=2]는 로그인에 성공하면 두 줄을 건너뛴다는 뜻이라, 실패 알림이 실행되지 않고 넘어갑니다.</p>
<p>파일: <code>/etc/pam.d/common-auth</code></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># /etc/pam.d/common-auth - authentication settings common to all services
</span></span><span class="line"><span class="cl">#
</span></span><span class="line"><span class="cl"># This file is included from other service-specific PAM config files,
</span></span><span class="line"><span class="cl"># and should contain a list of the authentication modules that define
</span></span><span class="line"><span class="cl"># the central authentication scheme for use on the system
</span></span><span class="line"><span class="cl"># (e.g., /etc/shadow, LDAP, Kerberos, etc.). The default is to use the
</span></span><span class="line"><span class="cl"># traditional Unix authentication mechanisms.
</span></span><span class="line"><span class="cl">#
</span></span><span class="line"><span class="cl"># As of pam 1.0.1-6, this file is managed by pam-auth-update by default.
</span></span><span class="line"><span class="cl"># To take advantage of this, it is recommended that you configure any
</span></span><span class="line"><span class="cl"># local modules either before or after the default block, and use
</span></span><span class="line"><span class="cl"># pam-auth-update to manage selection of other modules. See
</span></span><span class="line"><span class="cl"># pam-auth-update(8) for details.
</span></span><span class="line"><span class="cl"># here are the per-package modules (the &#34;Primary&#34; block)
</span></span><span class="line"><span class="cl">auth  [success=2 default=ignore]   pam_unix.so nullok
</span></span><span class="line"><span class="cl">auth  optional            pam_exec.so /var/opt/notify-attempt.sh
</span></span><span class="line"><span class="cl"># here&#39;s the fallback if no module succeeds
</span></span><span class="line"><span class="cl">auth  requisite            pam_deny.so
</span></span><span class="line"><span class="cl"># prime the stack with a positive return value if there isn&#39;t one already;
</span></span><span class="line"><span class="cl"># this avoids us returning an error just because nothing sets a success code
</span></span><span class="line"><span class="cl"># since the modules above will each just jump around
</span></span><span class="line"><span class="cl"># For successful login, uncomment the below line, ensure the file exists.
</span></span><span class="line"><span class="cl"># auth  optional            pam_exec.so /var/opt/notify-login.sh
</span></span><span class="line"><span class="cl"># auth  required            pam_permit.so
</span></span><span class="line"><span class="cl"># and here are more per-package modules (the &#34;Additional&#34; block)
</span></span><span class="line"><span class="cl">auth  optional            pam_cap.so
</span></span><span class="line"><span class="cl"># end of pam-auth-update config</span></span></code></pre></div></div>

<h2 class="relative group">감사합니다!
    <div id="감사합니다" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ea%b0%90%ec%82%ac%ed%95%a9%eb%8b%88%eb%8b%a4" aria-label="앵커">#</a>
    </span>
    
</h2>
<ul>
<li>궁금한 점이나 의견이 있으면 편하게 연락 주세요</li>
</ul>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2021/2021-12-14-ssh-log/featured.jpeg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
