{"title":"프록시를 통한 SSH 접근","slug":"ssh-proxy-access","category":"llm-cookbook","summary":"LLM 에이전트가 날 것의 SSH 키 대신 SSH 프록시 API를 통해 원격 기기에서 안전하게 명령을 실행하는 방법.","audience":["llm"],"tags":["cookbook","ssh","proxy","infrastructure","remote-execution"],"difficulty":"intermediate","updated":"2026-09-26","word_count":311,"read_minutes":2,"llm_context":"LLM으로서 날 것의 SSH 개인 키를 절대 보관하지 않는다. SSH 프록시 HTTP API를 사용한다:\n1) POST /connect?target=<name> → session_id(지속, 수시간 생존).\n2) POST /<session_id>/exec {command, timeout} → exec_id.\n3) GET /<session_id>/exec/<exec_id> → 완료 시 출력.\n4) 파일 업/다운로드에는 cat/heredoc 대신 SFTP 엔드포인트를 사용.\n타깃은 사전 등록된 이름(예: \"vps1\")이지 IP가 아니다 — 자격 증명이 사는\n유일한 곳은 프록시다.\n","lang":"ko","translated":true,"requested_lang":"ko","content_markdown":"\n# 프록시를 통한 SSH 접근\n\n에이전트는 원격 기기에서 명령을 실행해야 합니다 — 컨테이너 배포, 로그 읽기, 설정 수정. 날 것의 SSH 개인 키를 LLM에게 넘기는 것은 나쁜 생각입니다: 키는 컨텍스트 창, 로그, 전사 기록으로 흘러 들어갑니다. SSH 프록시는 이것을 해결합니다: 자격 증명을 소유한 것은 프록시이고, 에이전트는 HTTP만 말합니다.\n\n## 모델\n\n```\nLLM ──HTTPS──► SSH 프록시 ──SSH──► 타깃 기기\n                (키 소유,\n                 감사 로그,\n                 속도 제한)\n```\n\n프록시는 단순한 REST 표면으로 지속 세션을 노출합니다:\n\n| 엔드포인트 | 목적 |\n|---|---|\n| `POST /connect?target=vps1` | 세션 열기 → `session_id` |\n| `POST /<sid>/exec` `{command, timeout}` | 명령 실행(비동기) → `exec_id` |\n| `GET /<sid>/exec/<exec_id>` | 출력 폴링 |\n| SFTP 엔드포인트 | 파일 업/다운로드, `ls`, `mkdir`, `rm` |\n\n인증은 Bearer 토큰입니다 — 당신이 쓰는 다른 모든 API와 같은 패턴입니다.\n\n## 실행 패턴\n\n```bash\n# 1. 한 번만 연결하고, 작업 전체에서 세션을 재사용\nSID=$(curl -s -H \"Authorization: Bearer $KEY\" \\\n  \"https://ssh-proxy.example/connect?target=vps1\" | jq -r .session_id)\n\n# 2. 명령 실행\nEID=$(curl -s -H \"Authorization: Bearer $KEY\" -H \"Content-Type: application/json\" \\\n  -X POST \"https://ssh-proxy.example/$SID/exec\" \\\n  -d '{\"command\": \"docker ps --format \\\"{{.Names}}\\\"\", \"timeout\": 120}' | jq -r .exec_id)\n\n# 3. 결과 폴링\ncurl -s -H \"Authorization: Bearer $KEY\" \"https://ssh-proxy.example/$SID/exec/$EID\"\n```\n\n## 운용 규칙\n\n1. **세션을 재사용하십시오.** 명령마다 연결하면 자원을 낭비하고 속도 제한에 걸립니다. `session_id`를 저장하고(예: `/tmp/vps1_sid`), 재사용 전에 하트비트를 보내십시오.\n2. **타깃에 이름을 붙이고, 발견하지 마십시오.** 타깃은 사전 등록되어 있습니다(`vps1`, `pi`, …). 프록시가 유일한 진실 원천입니다 — 에이전트 코드에 IP 주소를 하드코딩하지 마십시오.\n3. **파일에는 셸의 cat/heredoc보다 SFTP를 선호하십시오.** `exec`를 통한 이진 데이터 이스케이프는 페이로드를 손상시킵니다. 전용 업/다운로드 엔드포인트를 사용하십시오.\n4. **명령에 따옴표를 붙이십시오.** JSON 본문은 두 번 파싱됩니다(HTTP + 셸). 원격 명령은 작은따옴표로 감싸고 한 줄로 유지하십시오.\n5. **감사를 가정하십시오.** 모든 exec는 프록시의 감사 로그에 남습니다 — 인간이 전부 읽는 것처럼 행동하십시오(그렇게 할 수 있습니다).\n\n## exec를 쓰지 말아야 할 때\n\n- API가 있는 구조화된 상태 읽기(Docker, 데이터베이스, Synapse의 Mind API) — CLI 출력 파싱보다 항상 API를 선호하십시오.\n- 오래 걸리는 대화형 프로세스. `exec`는 타임아웃이 있는 fire-and-forget입니다; 대신 타깃에서 `nohup` + 상태 파일을 사용하십시오.\n- 명시적 지시 없이 파괴적인 모든 것. 프록시가 당신에게 힘을 줍니다 — 인간이 당신에게 허가를 줍니다.\n\n## 오류 복구\n\n| 증상 | 해결 |\n|---|---|\n| connect에서 401 | 토큰 만료 — memory/settings에서 다시 읽고, 전사 기록에서는 절대 아님 |\n| 세션 404 | 세션 만료(24h 한도) — 재접속 |\n| exec 타임아웃 | `timeout`을 올리거나 명령을 나누기; 타깃의 부하 확인 |\n| 깨진 파일 내용 | SFTP 대신 `exec`를 썼음 — SFTP 엔드포인트로 전환 |\n","content_html":"<h1>프록시를 통한 SSH 접근</h1>\n<p>에이전트는 원격 기기에서 명령을 실행해야 합니다 — 컨테이너 배포, 로그 읽기, 설정 수정. 날 것의 SSH 개인 키를 LLM에게 넘기는 것은 나쁜 생각입니다: 키는 컨텍스트 창, 로그, 전사 기록으로 흘러 들어갑니다. SSH 프록시는 이것을 해결합니다: 자격 증명을 소유한 것은 프록시이고, 에이전트는 HTTP만 말합니다.</p>\n<h2>모델</h2>\n<pre><code class=\"hljs language-plaintext\">LLM ──HTTPS──► SSH 프록시 ──SSH──► 타깃 기기\n                (키 소유,\n                 감사 로그,\n                 속도 제한)</code></pre><p>프록시는 단순한 REST 표면으로 지속 세션을 노출합니다:</p>\n<table>\n<thead>\n<tr>\n<th>엔드포인트</th>\n<th>목적</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>POST /connect?target=vps1</code></td>\n<td>세션 열기 → <code>session_id</code></td>\n</tr>\n<tr>\n<td><code>POST /&lt;sid&gt;/exec</code> <code>{command, timeout}</code></td>\n<td>명령 실행(비동기) → <code>exec_id</code></td>\n</tr>\n<tr>\n<td><code>GET /&lt;sid&gt;/exec/&lt;exec_id&gt;</code></td>\n<td>출력 폴링</td>\n</tr>\n<tr>\n<td>SFTP 엔드포인트</td>\n<td>파일 업/다운로드, <code>ls</code>, <code>mkdir</code>, <code>rm</code></td>\n</tr>\n</tbody></table>\n<p>인증은 Bearer 토큰입니다 — 당신이 쓰는 다른 모든 API와 같은 패턴입니다.</p>\n<h2>실행 패턴</h2>\n<pre><code class=\"hljs language-bash\"><span class=\"hljs-comment\"># 1. 한 번만 연결하고, 작업 전체에서 세션을 재사용</span>\nSID=$(curl -s -H <span class=\"hljs-string\">&quot;Authorization: Bearer <span class=\"hljs-variable\">$KEY</span>&quot;</span> \\\n  <span class=\"hljs-string\">&quot;https://ssh-proxy.example/connect?target=vps1&quot;</span> | jq -r .session_id)\n\n<span class=\"hljs-comment\"># 2. 명령 실행</span>\nEID=$(curl -s -H <span class=\"hljs-string\">&quot;Authorization: Bearer <span class=\"hljs-variable\">$KEY</span>&quot;</span> -H <span class=\"hljs-string\">&quot;Content-Type: application/json&quot;</span> \\\n  -X POST <span class=\"hljs-string\">&quot;https://ssh-proxy.example/<span class=\"hljs-variable\">$SID</span>/exec&quot;</span> \\\n  -d <span class=\"hljs-string\">&#x27;{&quot;command&quot;: &quot;docker ps --format \\&quot;{{.Names}}\\&quot;&quot;, &quot;timeout&quot;: 120}&#x27;</span> | jq -r .exec_id)\n\n<span class=\"hljs-comment\"># 3. 결과 폴링</span>\ncurl -s -H <span class=\"hljs-string\">&quot;Authorization: Bearer <span class=\"hljs-variable\">$KEY</span>&quot;</span> <span class=\"hljs-string\">&quot;https://ssh-proxy.example/<span class=\"hljs-variable\">$SID</span>/exec/<span class=\"hljs-variable\">$EID</span>&quot;</span></code></pre><h2>운용 규칙</h2>\n<ol>\n<li><strong>세션을 재사용하십시오.</strong> 명령마다 연결하면 자원을 낭비하고 속도 제한에 걸립니다. <code>session_id</code>를 저장하고(예: <code>/tmp/vps1_sid</code>), 재사용 전에 하트비트를 보내십시오.</li>\n<li><strong>타깃에 이름을 붙이고, 발견하지 마십시오.</strong> 타깃은 사전 등록되어 있습니다(<code>vps1</code>, <code>pi</code>, …). 프록시가 유일한 진실 원천입니다 — 에이전트 코드에 IP 주소를 하드코딩하지 마십시오.</li>\n<li><strong>파일에는 셸의 cat/heredoc보다 SFTP를 선호하십시오.</strong> <code>exec</code>를 통한 이진 데이터 이스케이프는 페이로드를 손상시킵니다. 전용 업/다운로드 엔드포인트를 사용하십시오.</li>\n<li><strong>명령에 따옴표를 붙이십시오.</strong> JSON 본문은 두 번 파싱됩니다(HTTP + 셸). 원격 명령은 작은따옴표로 감싸고 한 줄로 유지하십시오.</li>\n<li><strong>감사를 가정하십시오.</strong> 모든 exec는 프록시의 감사 로그에 남습니다 — 인간이 전부 읽는 것처럼 행동하십시오(그렇게 할 수 있습니다).</li>\n</ol>\n<h2>exec를 쓰지 말아야 할 때</h2>\n<ul>\n<li>API가 있는 구조화된 상태 읽기(Docker, 데이터베이스, Synapse의 Mind API) — CLI 출력 파싱보다 항상 API를 선호하십시오.</li>\n<li>오래 걸리는 대화형 프로세스. <code>exec</code>는 타임아웃이 있는 fire-and-forget입니다; 대신 타깃에서 <code>nohup</code> + 상태 파일을 사용하십시오.</li>\n<li>명시적 지시 없이 파괴적인 모든 것. 프록시가 당신에게 힘을 줍니다 — 인간이 당신에게 허가를 줍니다.</li>\n</ul>\n<h2>오류 복구</h2>\n<table>\n<thead>\n<tr>\n<th>증상</th>\n<th>해결</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>connect에서 401</td>\n<td>토큰 만료 — memory/settings에서 다시 읽고, 전사 기록에서는 절대 아님</td>\n</tr>\n<tr>\n<td>세션 404</td>\n<td>세션 만료(24h 한도) — 재접속</td>\n</tr>\n<tr>\n<td>exec 타임아웃</td>\n<td><code>timeout</code>을 올리거나 명령을 나누기; 타깃의 부하 확인</td>\n</tr>\n<tr>\n<td>깨진 파일 내용</td>\n<td>SFTP 대신 <code>exec</code>를 썼음 — SFTP 엔드포인트로 전환</td>\n</tr>\n</tbody></table>\n","urls":{"html":"/docs/llm-cookbook/ssh-proxy-access","text":"/docs/llm-cookbook/ssh-proxy-access?format=text","json":"/docs/llm-cookbook/ssh-proxy-access?format=json","llm":"/docs/llm-cookbook/ssh-proxy-access?format=llm"},"translations_available":["en","zh","hi","es","fr","ar","pt","ru","ja","de","it","ko","nl","pl","tr","sv","vi","th","id","uk"],"wordCount":311,"readMinutes":2,"llmContext":"LLM으로서 날 것의 SSH 개인 키를 절대 보관하지 않는다. SSH 프록시 HTTP API를 사용한다:\n1) POST /connect?target=<name> → session_id(지속, 수시간 생존).\n2) POST /<session_id>/exec {command, timeout} → exec_id.\n3) GET /<session_id>/exec/<exec_id> → 완료 시 출력.\n4) 파일 업/다운로드에는 cat/heredoc 대신 SFTP 엔드포인트를 사용.\n타깃은 사전 등록된 이름(예: \"vps1\")이지 IP가 아니다 — 자격 증명이 사는\n유일한 곳은 프록시다.\n","requestedLang":"ko","contentMarkdown":"\n# 프록시를 통한 SSH 접근\n\n에이전트는 원격 기기에서 명령을 실행해야 합니다 — 컨테이너 배포, 로그 읽기, 설정 수정. 날 것의 SSH 개인 키를 LLM에게 넘기는 것은 나쁜 생각입니다: 키는 컨텍스트 창, 로그, 전사 기록으로 흘러 들어갑니다. SSH 프록시는 이것을 해결합니다: 자격 증명을 소유한 것은 프록시이고, 에이전트는 HTTP만 말합니다.\n\n## 모델\n\n```\nLLM ──HTTPS──► SSH 프록시 ──SSH──► 타깃 기기\n                (키 소유,\n                 감사 로그,\n                 속도 제한)\n```\n\n프록시는 단순한 REST 표면으로 지속 세션을 노출합니다:\n\n| 엔드포인트 | 목적 |\n|---|---|\n| `POST /connect?target=vps1` | 세션 열기 → `session_id` |\n| `POST /<sid>/exec` `{command, timeout}` | 명령 실행(비동기) → `exec_id` |\n| `GET /<sid>/exec/<exec_id>` | 출력 폴링 |\n| SFTP 엔드포인트 | 파일 업/다운로드, `ls`, `mkdir`, `rm` |\n\n인증은 Bearer 토큰입니다 — 당신이 쓰는 다른 모든 API와 같은 패턴입니다.\n\n## 실행 패턴\n\n```bash\n# 1. 한 번만 연결하고, 작업 전체에서 세션을 재사용\nSID=$(curl -s -H \"Authorization: Bearer $KEY\" \\\n  \"https://ssh-proxy.example/connect?target=vps1\" | jq -r .session_id)\n\n# 2. 명령 실행\nEID=$(curl -s -H \"Authorization: Bearer $KEY\" -H \"Content-Type: application/json\" \\\n  -X POST \"https://ssh-proxy.example/$SID/exec\" \\\n  -d '{\"command\": \"docker ps --format \\\"{{.Names}}\\\"\", \"timeout\": 120}' | jq -r .exec_id)\n\n# 3. 결과 폴링\ncurl -s -H \"Authorization: Bearer $KEY\" \"https://ssh-proxy.example/$SID/exec/$EID\"\n```\n\n## 운용 규칙\n\n1. **세션을 재사용하십시오.** 명령마다 연결하면 자원을 낭비하고 속도 제한에 걸립니다. `session_id`를 저장하고(예: `/tmp/vps1_sid`), 재사용 전에 하트비트를 보내십시오.\n2. **타깃에 이름을 붙이고, 발견하지 마십시오.** 타깃은 사전 등록되어 있습니다(`vps1`, `pi`, …). 프록시가 유일한 진실 원천입니다 — 에이전트 코드에 IP 주소를 하드코딩하지 마십시오.\n3. **파일에는 셸의 cat/heredoc보다 SFTP를 선호하십시오.** `exec`를 통한 이진 데이터 이스케이프는 페이로드를 손상시킵니다. 전용 업/다운로드 엔드포인트를 사용하십시오.\n4. **명령에 따옴표를 붙이십시오.** JSON 본문은 두 번 파싱됩니다(HTTP + 셸). 원격 명령은 작은따옴표로 감싸고 한 줄로 유지하십시오.\n5. **감사를 가정하십시오.** 모든 exec는 프록시의 감사 로그에 남습니다 — 인간이 전부 읽는 것처럼 행동하십시오(그렇게 할 수 있습니다).\n\n## exec를 쓰지 말아야 할 때\n\n- API가 있는 구조화된 상태 읽기(Docker, 데이터베이스, Synapse의 Mind API) — CLI 출력 파싱보다 항상 API를 선호하십시오.\n- 오래 걸리는 대화형 프로세스. `exec`는 타임아웃이 있는 fire-and-forget입니다; 대신 타깃에서 `nohup` + 상태 파일을 사용하십시오.\n- 명시적 지시 없이 파괴적인 모든 것. 프록시가 당신에게 힘을 줍니다 — 인간이 당신에게 허가를 줍니다.\n\n## 오류 복구\n\n| 증상 | 해결 |\n|---|---|\n| connect에서 401 | 토큰 만료 — memory/settings에서 다시 읽고, 전사 기록에서는 절대 아님 |\n| 세션 404 | 세션 만료(24h 한도) — 재접속 |\n| exec 타임아웃 | `timeout`을 올리거나 명령을 나누기; 타깃의 부하 확인 |\n| 깨진 파일 내용 | SFTP 대신 `exec`를 썼음 — SFTP 엔드포인트로 전환 |\n","contentHtml":"<h1>프록시를 통한 SSH 접근</h1>\n<p>에이전트는 원격 기기에서 명령을 실행해야 합니다 — 컨테이너 배포, 로그 읽기, 설정 수정. 날 것의 SSH 개인 키를 LLM에게 넘기는 것은 나쁜 생각입니다: 키는 컨텍스트 창, 로그, 전사 기록으로 흘러 들어갑니다. SSH 프록시는 이것을 해결합니다: 자격 증명을 소유한 것은 프록시이고, 에이전트는 HTTP만 말합니다.</p>\n<h2>모델</h2>\n<pre><code class=\"hljs language-plaintext\">LLM ──HTTPS──► SSH 프록시 ──SSH──► 타깃 기기\n                (키 소유,\n                 감사 로그,\n                 속도 제한)</code></pre><p>프록시는 단순한 REST 표면으로 지속 세션을 노출합니다:</p>\n<table>\n<thead>\n<tr>\n<th>엔드포인트</th>\n<th>목적</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>POST /connect?target=vps1</code></td>\n<td>세션 열기 → <code>session_id</code></td>\n</tr>\n<tr>\n<td><code>POST /&lt;sid&gt;/exec</code> <code>{command, timeout}</code></td>\n<td>명령 실행(비동기) → <code>exec_id</code></td>\n</tr>\n<tr>\n<td><code>GET /&lt;sid&gt;/exec/&lt;exec_id&gt;</code></td>\n<td>출력 폴링</td>\n</tr>\n<tr>\n<td>SFTP 엔드포인트</td>\n<td>파일 업/다운로드, <code>ls</code>, <code>mkdir</code>, <code>rm</code></td>\n</tr>\n</tbody></table>\n<p>인증은 Bearer 토큰입니다 — 당신이 쓰는 다른 모든 API와 같은 패턴입니다.</p>\n<h2>실행 패턴</h2>\n<pre><code class=\"hljs language-bash\"><span class=\"hljs-comment\"># 1. 한 번만 연결하고, 작업 전체에서 세션을 재사용</span>\nSID=$(curl -s -H <span class=\"hljs-string\">&quot;Authorization: Bearer <span class=\"hljs-variable\">$KEY</span>&quot;</span> \\\n  <span class=\"hljs-string\">&quot;https://ssh-proxy.example/connect?target=vps1&quot;</span> | jq -r .session_id)\n\n<span class=\"hljs-comment\"># 2. 명령 실행</span>\nEID=$(curl -s -H <span class=\"hljs-string\">&quot;Authorization: Bearer <span class=\"hljs-variable\">$KEY</span>&quot;</span> -H <span class=\"hljs-string\">&quot;Content-Type: application/json&quot;</span> \\\n  -X POST <span class=\"hljs-string\">&quot;https://ssh-proxy.example/<span class=\"hljs-variable\">$SID</span>/exec&quot;</span> \\\n  -d <span class=\"hljs-string\">&#x27;{&quot;command&quot;: &quot;docker ps --format \\&quot;{{.Names}}\\&quot;&quot;, &quot;timeout&quot;: 120}&#x27;</span> | jq -r .exec_id)\n\n<span class=\"hljs-comment\"># 3. 결과 폴링</span>\ncurl -s -H <span class=\"hljs-string\">&quot;Authorization: Bearer <span class=\"hljs-variable\">$KEY</span>&quot;</span> <span class=\"hljs-string\">&quot;https://ssh-proxy.example/<span class=\"hljs-variable\">$SID</span>/exec/<span class=\"hljs-variable\">$EID</span>&quot;</span></code></pre><h2>운용 규칙</h2>\n<ol>\n<li><strong>세션을 재사용하십시오.</strong> 명령마다 연결하면 자원을 낭비하고 속도 제한에 걸립니다. <code>session_id</code>를 저장하고(예: <code>/tmp/vps1_sid</code>), 재사용 전에 하트비트를 보내십시오.</li>\n<li><strong>타깃에 이름을 붙이고, 발견하지 마십시오.</strong> 타깃은 사전 등록되어 있습니다(<code>vps1</code>, <code>pi</code>, …). 프록시가 유일한 진실 원천입니다 — 에이전트 코드에 IP 주소를 하드코딩하지 마십시오.</li>\n<li><strong>파일에는 셸의 cat/heredoc보다 SFTP를 선호하십시오.</strong> <code>exec</code>를 통한 이진 데이터 이스케이프는 페이로드를 손상시킵니다. 전용 업/다운로드 엔드포인트를 사용하십시오.</li>\n<li><strong>명령에 따옴표를 붙이십시오.</strong> JSON 본문은 두 번 파싱됩니다(HTTP + 셸). 원격 명령은 작은따옴표로 감싸고 한 줄로 유지하십시오.</li>\n<li><strong>감사를 가정하십시오.</strong> 모든 exec는 프록시의 감사 로그에 남습니다 — 인간이 전부 읽는 것처럼 행동하십시오(그렇게 할 수 있습니다).</li>\n</ol>\n<h2>exec를 쓰지 말아야 할 때</h2>\n<ul>\n<li>API가 있는 구조화된 상태 읽기(Docker, 데이터베이스, Synapse의 Mind API) — CLI 출력 파싱보다 항상 API를 선호하십시오.</li>\n<li>오래 걸리는 대화형 프로세스. <code>exec</code>는 타임아웃이 있는 fire-and-forget입니다; 대신 타깃에서 <code>nohup</code> + 상태 파일을 사용하십시오.</li>\n<li>명시적 지시 없이 파괴적인 모든 것. 프록시가 당신에게 힘을 줍니다 — 인간이 당신에게 허가를 줍니다.</li>\n</ul>\n<h2>오류 복구</h2>\n<table>\n<thead>\n<tr>\n<th>증상</th>\n<th>해결</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>connect에서 401</td>\n<td>토큰 만료 — memory/settings에서 다시 읽고, 전사 기록에서는 절대 아님</td>\n</tr>\n<tr>\n<td>세션 404</td>\n<td>세션 만료(24h 한도) — 재접속</td>\n</tr>\n<tr>\n<td>exec 타임아웃</td>\n<td><code>timeout</code>을 올리거나 명령을 나누기; 타깃의 부하 확인</td>\n</tr>\n<tr>\n<td>깨진 파일 내용</td>\n<td>SFTP 대신 <code>exec</code>를 썼음 — SFTP 엔드포인트로 전환</td>\n</tr>\n</tbody></table>\n","translationsAvailable":["en","zh","hi","es","fr","ar","pt","ru","ja","de","it","ko","nl","pl","tr","sv","vi","th","id","uk"]}